From 6108d0df0c598227c960bc3fffa7efb531eff8d3 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Mon, 29 Oct 2012 11:18:01 -0700 Subject: [PATCH 01/31] 393075 Jetty WebSocket client cannot connect to Tomcat WebSocket Server * Adding testcase to repliate behavior as reported by bug. --- .../websocket/WebSocketClientFactory.java | 6 +- .../websocket/TomcatServerQuirksTest.java | 122 +++++++ .../jetty/websocket/WebSocketClientTest.java | 6 +- .../jetty/websocket/dummy/DummyServer.java | 308 ++++++++++++++++++ .../test/resources/jetty-logging.properties | 4 + 5 files changed, 441 insertions(+), 5 deletions(-) create mode 100644 jetty-websocket/src/test/java/org/eclipse/jetty/websocket/TomcatServerQuirksTest.java create mode 100644 jetty-websocket/src/test/java/org/eclipse/jetty/websocket/dummy/DummyServer.java create mode 100644 jetty-websocket/src/test/resources/jetty-logging.properties diff --git a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketClientFactory.java b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketClientFactory.java index d624428b727..b365ee6dfd5 100644 --- a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketClientFactory.java +++ b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketClientFactory.java @@ -28,6 +28,7 @@ import java.util.Map; import java.util.Queue; import java.util.Random; import java.util.concurrent.ConcurrentLinkedQueue; + import javax.net.ssl.SSLEngine; import org.eclipse.jetty.http.HttpFields; @@ -389,7 +390,7 @@ public class WebSocketClientFactory extends AggregateLifeCycle _accept = value.toString(); } - @Override + @Override // TODO simone says shouldn't be needed public void startRequest(Buffer method, Buffer url, Buffer version) throws IOException { if (_error == null) @@ -397,7 +398,7 @@ public class WebSocketClientFactory extends AggregateLifeCycle _endp.close(); } - @Override + @Override // TODO simone says shouldn't be needed public void content(Buffer ref) throws IOException { if (_error == null) @@ -515,6 +516,7 @@ public class WebSocketClientFactory extends AggregateLifeCycle private WebSocketConnection newWebSocketConnection() throws IOException { + __log.debug("newWebSocketConnection()"); return new WebSocketClientConnection( _future._client.getFactory(), _future.getWebSocket(), diff --git a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/TomcatServerQuirksTest.java b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/TomcatServerQuirksTest.java new file mode 100644 index 00000000000..754a4199db4 --- /dev/null +++ b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/TomcatServerQuirksTest.java @@ -0,0 +1,122 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket; + +import java.io.IOException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.websocket.dummy.DummyServer; +import org.eclipse.jetty.websocket.dummy.DummyServer.ServerConnection; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +public class TomcatServerQuirksTest +{ + /** + * Test for when encountering a "Transfer-Encoding: chunked" on a Upgrade Response header. + * + * @throws IOException + */ + @Test + @Ignore("Bug with Transfer-Encoding") + public void testTomcat7_0_32_WithTransferEncoding() throws Exception { + DummyServer server = new DummyServer(); + int bufferSize = 512; + QueuedThreadPool threadPool = new QueuedThreadPool(); + WebSocketClientFactory factory = new WebSocketClientFactory(threadPool, new ZeroMaskGen(), bufferSize); + + try { + server.start(); + + // Setup Client Factory + threadPool.start(); + factory.start(); + + // Create Client + WebSocketClient client = new WebSocketClient(factory); + + // Create End User WebSocket Class + final CountDownLatch openLatch = new CountDownLatch(1); + final CountDownLatch dataLatch = new CountDownLatch(1); + WebSocket.OnTextMessage websocket = new WebSocket.OnTextMessage() + { + public void onOpen(Connection connection) + { + openLatch.countDown(); + } + + public void onMessage(String data) + { + // System.out.println("data = " + data); + dataLatch.countDown(); + } + + public void onClose(int closeCode, String message) + { + } + }; + + // Open connection + URI wsURI = server.getWsUri(); + client.open(wsURI, websocket); + + // Accept incoming connection + ServerConnection socket = server.accept(); + socket.setSoTimeout(2000); // timeout + + // Issue upgrade + Map extraResponseHeaders = new HashMap(); + extraResponseHeaders.put("Transfer-Encoding", "chunked"); // !! The problem !! + socket.upgrade(extraResponseHeaders); + + // Wait for proper upgrade + Assert.assertTrue("Timed out waiting for Client side WebSocket open event", openLatch.await(1, TimeUnit.SECONDS)); + + // Have server write frame. + int length = bufferSize / 2; + ByteBuffer serverFrame = ByteBuffer.allocate(bufferSize); + serverFrame.put((byte)(0x80 | 0x01)); // FIN + TEXT + serverFrame.put((byte)0x7E); // No MASK and 2 bytes length + serverFrame.put((byte)(length >> 8)); // first length byte + serverFrame.put((byte)(length & 0xFF)); // second length byte + for (int i = 0; i < length; ++i) + serverFrame.put((byte)'x'); + serverFrame.flip(); + byte buf[] = serverFrame.array(); + socket.write(buf,0,buf.length); + socket.flush(); + + Assert.assertTrue(dataLatch.await(1000, TimeUnit.SECONDS)); + } finally { + factory.stop(); + threadPool.stop(); + server.stop(); + } + } +} diff --git a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketClientTest.java b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketClientTest.java index f70c43ca36a..8cd1b467c11 100644 --- a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketClientTest.java +++ b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketClientTest.java @@ -18,6 +18,8 @@ package org.eclipse.jetty.websocket; +import static org.hamcrest.Matchers.*; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -47,9 +49,6 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.is; - public class WebSocketClientTest { private WebSocketClientFactory _factory = new WebSocketClientFactory(); @@ -103,6 +102,7 @@ public class WebSocketClientTest { } }; + client.open(new URI("ws://127.0.0.1:" + _serverPort + "/"), websocket); Socket socket = _server.accept(); diff --git a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/dummy/DummyServer.java b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/dummy/DummyServer.java new file mode 100644 index 00000000000..9b41e51e2fa --- /dev/null +++ b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/dummy/DummyServer.java @@ -0,0 +1,308 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// +package org.eclipse.jetty.websocket.dummy; + +import static org.hamcrest.Matchers.*; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.websocket.WebSocketConnectionRFC6455; +import org.junit.Assert; + +/** + * Simple ServerSocket server used to test oddball server scenarios encountered in the real world. + */ +public class DummyServer +{ + public static class ServerConnection + { + private static final Logger LOG = Log.getLogger(ServerConnection.class); + private final Socket socket; + private InputStream in; + private OutputStream out; + + public ServerConnection(Socket socket) + { + this.socket = socket; + } + + public int read(ByteBuffer buf) throws IOException + { + int len = 0; + while ((in.available() > 0) && (buf.remaining() > 0)) + { + buf.put((byte)in.read()); + len++; + } + return len; + } + + public void disconnect() + { + LOG.debug("disconnect"); + IO.close(in); + IO.close(out); + if (socket != null) + { + try + { + socket.close(); + } + catch (IOException ignore) + { + /* ignore */ + } + } + } + + public InputStream getInputStream() throws IOException + { + if (in == null) + { + in = socket.getInputStream(); + } + return in; + } + + public OutputStream getOutputStream() throws IOException + { + if (out == null) + { + out = socket.getOutputStream(); + } + return out; + } + + public void flush() throws IOException + { + LOG.debug("flush()"); + getOutputStream().flush(); + } + + public String readRequest() throws IOException + { + LOG.debug("Reading client request"); + StringBuilder request = new StringBuilder(); + BufferedReader in = new BufferedReader(new InputStreamReader(getInputStream())); + for (String line = in.readLine(); line != null; line = in.readLine()) + { + if (line.length() == 0) + { + break; + } + request.append(line).append("\r\n"); + LOG.debug("read line: {}",line); + } + + LOG.debug("Client Request:{}{}","\n",request); + return request.toString(); + } + + public void respond(String rawstr) throws IOException + { + LOG.debug("respond(){}{}","\n",rawstr); + getOutputStream().write(rawstr.getBytes()); + flush(); + } + + public void setSoTimeout(int ms) throws SocketException + { + socket.setSoTimeout(ms); + } + + public void upgrade(Map extraResponseHeaders) throws IOException + { + @SuppressWarnings("unused") + Pattern patExts = Pattern.compile("^Sec-WebSocket-Extensions: (.*)$",Pattern.CASE_INSENSITIVE); + Pattern patKey = Pattern.compile("^Sec-WebSocket-Key: (.*)$",Pattern.CASE_INSENSITIVE); + + LOG.debug("(Upgrade) Reading HTTP Request"); + Matcher mat; + String key = "not sent"; + BufferedReader in = new BufferedReader(new InputStreamReader(getInputStream())); + for (String line = in.readLine(); line != null; line = in.readLine()) + { + if (line.length() == 0) + { + break; + } + + // TODO: Check for extensions + // mat = patExts.matcher(line); + // if (mat.matches()) + + // Check for Key + mat = patKey.matcher(line); + if (mat.matches()) + { + key = mat.group(1); + } + } + + LOG.debug("(Upgrade) Writing HTTP Response"); + // TODO: handle extensions? + + // Setup Response + StringBuilder resp = new StringBuilder(); + resp.append("HTTP/1.1 101 Upgrade\r\n"); + resp.append("Upgrade: websocket\r\n"); + resp.append("Connection: upgrade\r\n"); + resp.append("Sec-WebSocket-Accept: "); + resp.append(WebSocketConnectionRFC6455.hashKey(key)).append("\r\n"); + // extra response headers. + if (extraResponseHeaders != null) + { + for (Map.Entry header : extraResponseHeaders.entrySet()) + { + resp.append(header.getKey()); + resp.append(": "); + resp.append(header.getValue()); + resp.append("\r\n"); + } + } + resp.append("\r\n"); + + // Write Response + getOutputStream().write(resp.toString().getBytes()); + flush(); + } + + public void write(byte[] bytes) throws IOException + { + LOG.debug("Writing {} bytes", bytes.length); + getOutputStream().write(bytes); + } + + public void write(byte[] buf, int offset, int length) throws IOException + { + LOG.debug("Writing bytes[{}], offset={}, length={}", buf.length, offset, length); + getOutputStream().write(buf,offset,length); + } + + public void write(int b) throws IOException + { + LOG.debug("Writing int={}", b); + getOutputStream().write(b); + } + } + + private static final Logger LOG = Log.getLogger(DummyServer.class); + private ServerSocket serverSocket; + private URI wsUri; + + public ServerConnection accept() throws IOException + { + LOG.debug(".accept()"); + assertIsStarted(); + Socket socket = serverSocket.accept(); + return new ServerConnection(socket); + } + + private void assertIsStarted() + { + Assert.assertThat("ServerSocket",serverSocket,notNullValue()); + Assert.assertThat("ServerSocket.isBound",serverSocket.isBound(),is(true)); + Assert.assertThat("ServerSocket.isClosed",serverSocket.isClosed(),is(false)); + + Assert.assertThat("WsUri",wsUri,notNullValue()); + } + + public URI getWsUri() + { + return wsUri; + } + + public void respondToClient(Socket connection, String serverResponse) throws IOException + { + InputStream in = null; + InputStreamReader isr = null; + BufferedReader buf = null; + OutputStream out = null; + try + { + in = connection.getInputStream(); + isr = new InputStreamReader(in); + buf = new BufferedReader(isr); + String line; + while ((line = buf.readLine()) != null) + { + // System.err.println(line); + if (line.length() == 0) + { + // Got the "\r\n" line. + break; + } + } + + // System.out.println("[Server-Out] " + serverResponse); + out = connection.getOutputStream(); + out.write(serverResponse.getBytes()); + out.flush(); + } + finally + { + IO.close(buf); + IO.close(isr); + IO.close(in); + IO.close(out); + } + } + + public void start() throws IOException + { + serverSocket = new ServerSocket(); + InetAddress addr = InetAddress.getByName("localhost"); + InetSocketAddress endpoint = new InetSocketAddress(addr,0); + serverSocket.bind(endpoint); + int port = serverSocket.getLocalPort(); + String uri = String.format("ws://%s:%d/",addr.getHostAddress(),port); + wsUri = URI.create(uri); + LOG.debug("Server Started on {} -> {}",endpoint,wsUri); + } + + public void stop() + { + LOG.debug("Stopping Server"); + try + { + serverSocket.close(); + } + catch (IOException ignore) + { + /* ignore */ + } + } + +} diff --git a/jetty-websocket/src/test/resources/jetty-logging.properties b/jetty-websocket/src/test/resources/jetty-logging.properties new file mode 100644 index 00000000000..78d8a9d8123 --- /dev/null +++ b/jetty-websocket/src/test/resources/jetty-logging.properties @@ -0,0 +1,4 @@ +# Setup default logging implementation for during testing +org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog +org.eclipse.jetty.LEVEL=INFO +org.eclipse.jetty.websocket.LEVEL=DEBUG \ No newline at end of file From 1173916da197212e132be53bd5ec223d2cb5ba03 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Mon, 29 Oct 2012 18:57:29 +0100 Subject: [PATCH 02/31] HTTP client: renamed ResponseListener.Timed to Schedulable. --- .../main/java/org/eclipse/jetty/client/HttpClient.java | 4 ++-- .../java/org/eclipse/jetty/client/HttpConnection.java | 4 ++-- .../java/org/eclipse/jetty/client/HttpExchange.java | 4 ++-- .../client/{ResponseListener.java => Schedulable.java} | 10 +++------- .../jetty/client/util/TimedResponseListener.java | 4 ++-- 5 files changed, 11 insertions(+), 15 deletions(-) rename jetty-client/src/main/java/org/eclipse/jetty/client/{ResponseListener.java => Schedulable.java} (77%) 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 1b1f7cffa57..0af29158253 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 @@ -317,8 +317,8 @@ public class HttpClient extends ContainerLifeCycle if (port < 0) port = "https".equals(scheme) ? 443 : 80; - if (listener instanceof ResponseListener.Timed) - ((ResponseListener.Timed)listener).schedule(scheduler); + if (listener instanceof Schedulable) + ((Schedulable)listener).schedule(scheduler); HttpDestination destination = provideDestination(scheme, request.getHost(), port); destination.send(request, listener); 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 c5485984887..3a76303db95 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 @@ -116,8 +116,8 @@ public class HttpConnection extends AbstractConnection implements Connection setExchange(exchange); conversation.getExchanges().offer(exchange); - if (listener instanceof ResponseListener.Timed) - ((ResponseListener.Timed)listener).schedule(client.getScheduler()); + if (listener instanceof Schedulable) + ((Schedulable)listener).schedule(client.getScheduler()); sender.send(exchange); } 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 50ebd35e9a2..022fe6665c8 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 @@ -163,8 +163,8 @@ public class HttpExchange { HttpExchange first = conversation.getExchanges().peekFirst(); Response.Listener listener = first.getResponseListener(); - if (listener instanceof ResponseListener.Timed) - ((ResponseListener.Timed)listener).cancel(); + if (listener instanceof Schedulable) + ((Schedulable)listener).cancel(); conversation.complete(); } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/ResponseListener.java b/jetty-client/src/main/java/org/eclipse/jetty/client/Schedulable.java similarity index 77% rename from jetty-client/src/main/java/org/eclipse/jetty/client/ResponseListener.java rename to jetty-client/src/main/java/org/eclipse/jetty/client/Schedulable.java index dd61fb0bffd..cc97c4aa2fd 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/ResponseListener.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/Schedulable.java @@ -18,15 +18,11 @@ package org.eclipse.jetty.client; -import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.util.thread.Scheduler; -public interface ResponseListener extends Response.Listener +public interface Schedulable { - public interface Timed extends Response.Listener - { - public boolean schedule(Scheduler scheduler); + public boolean schedule(Scheduler scheduler); - public boolean cancel(); - } + public boolean cancel(); } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/util/TimedResponseListener.java b/jetty-client/src/main/java/org/eclipse/jetty/client/util/TimedResponseListener.java index 693feebedcb..6f2cee60697 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/util/TimedResponseListener.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/util/TimedResponseListener.java @@ -22,7 +22,7 @@ import java.nio.ByteBuffer; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import org.eclipse.jetty.client.ResponseListener; +import org.eclipse.jetty.client.Schedulable; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.api.Result; @@ -30,7 +30,7 @@ import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.thread.Scheduler; -public class TimedResponseListener implements ResponseListener.Timed, Runnable +public class TimedResponseListener implements Response.Listener, Schedulable, Runnable { private static final Logger LOG = Log.getLogger(TimedResponseListener.class); From ebb76ecfb93d89cafba2194e1a8b788c88816d19 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Tue, 30 Oct 2012 12:12:54 +0100 Subject: [PATCH 03/31] 392959 - Review HttpClient.getConversation(long). Modified to HttpClient.getConversation(long, boolean) in order to specify whether the conversation must be created or not. --- .../jetty/client/AuthenticationProtocolHandler.java | 2 +- .../org/eclipse/jetty/client/ContinueProtocolHandler.java | 7 ++++--- .../src/main/java/org/eclipse/jetty/client/HttpClient.java | 6 +++--- .../main/java/org/eclipse/jetty/client/HttpConnection.java | 2 +- .../main/java/org/eclipse/jetty/client/HttpRequest.java | 4 ++-- .../src/main/java/org/eclipse/jetty/client/HttpSender.java | 2 +- .../org/eclipse/jetty/client/RedirectProtocolHandler.java | 4 ++-- .../java/org/eclipse/jetty/client/ResponseNotifier.java | 4 ++-- 8 files changed, 16 insertions(+), 15 deletions(-) diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java b/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java index eaeff4bfb45..97599f6f8c3 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java @@ -79,7 +79,7 @@ public class AuthenticationProtocolHandler implements ProtocolHandler public void onComplete(Result result) { Request request = result.getRequest(); - HttpConversation conversation = client.getConversation(request.getConversationID()); + HttpConversation conversation = client.getConversation(request.getConversationID(), false); Response.Listener listener = conversation.getExchanges().peekFirst().getResponseListener(); ContentResponse response = new HttpContentResponse(result.getResponse(), getContent(), getEncoding()); if (result.isFailed()) 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 0e530d049c0..a9dec3af342 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 @@ -41,7 +41,8 @@ public class ContinueProtocolHandler implements ProtocolHandler public boolean accept(Request request, Response response) { boolean expect100 = request.getHeaders().contains(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString()); - boolean handled100 = client.getConversation(request.getConversationID()).getAttribute(ATTRIBUTE) != null; + HttpConversation conversation = client.getConversation(request.getConversationID(), false); + boolean handled100 = conversation != null && conversation.getAttribute(ATTRIBUTE) != null; return expect100 && !handled100; } @@ -60,7 +61,7 @@ public class ContinueProtocolHandler implements ProtocolHandler // Handling of success must be done here and not from onComplete(), // since the onComplete() is not invoked because the request is not completed yet. - HttpConversation conversation = client.getConversation(response.getConversationID()); + HttpConversation conversation = client.getConversation(response.getConversationID(), false); // Mark the 100 Continue response as handled conversation.setAttribute(ATTRIBUTE, Boolean.TRUE); @@ -92,7 +93,7 @@ public class ContinueProtocolHandler implements ProtocolHandler @Override public void onFailure(Response response, Throwable failure) { - HttpConversation conversation = client.getConversation(response.getConversationID()); + HttpConversation conversation = client.getConversation(response.getConversationID(), false); // Mark the 100 Continue response as handled conversation.setAttribute(ATTRIBUTE, Boolean.TRUE); 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 0af29158253..a13d4238c7e 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 @@ -365,10 +365,10 @@ public class HttpClient extends ContainerLifeCycle } } - protected HttpConversation getConversation(long id) + protected HttpConversation getConversation(long id, boolean create) { HttpConversation conversation = conversations.get(id); - if (conversation == null) + if (conversation == null && create) { conversation = new HttpConversation(this, id); HttpConversation existing = conversations.putIfAbsent(id, conversation); @@ -395,7 +395,7 @@ public class HttpClient extends ContainerLifeCycle { for (ProtocolHandler handler : getProtocolHandlers()) { - if (handler.accept(request, response)) + if (handler.accept(request, response)) return handler; } return null; 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 3a76303db95..ee58c05f9de 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 @@ -111,7 +111,7 @@ public class HttpConnection extends AbstractConnection implements Connection idleTimeout = endPoint.getIdleTimeout(); endPoint.setIdleTimeout(request.getIdleTimeout()); - HttpConversation conversation = client.getConversation(request.getConversationID()); + HttpConversation conversation = client.getConversation(request.getConversationID(), true); HttpExchange exchange = new HttpExchange(conversation, this, request, listener); setExchange(exchange); conversation.getExchanges().offer(exchange); 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 d6baccf9b20..7b1b4ce78bf 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 @@ -331,8 +331,8 @@ public class HttpRequest implements Request aborted = true; if (client.provideDestination(getScheme(), getHost(), getPort()).abort(this, reason)) return true; - HttpConversation conversation = client.getConversation(getConversationID()); - return conversation.abort(reason); + HttpConversation conversation = client.getConversation(getConversationID(), false); + return conversation != null && conversation.abort(reason); } @Override 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 a301bc053da..2966cc6aeda 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 @@ -123,7 +123,7 @@ public class HttpSender return; final Request request = exchange.getRequest(); - HttpConversation conversation = client.getConversation(request.getConversationID()); + HttpConversation conversation = exchange.getConversation(); HttpGenerator.RequestInfo requestInfo = null; boolean expect100 = request.getHeaders().contains(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString()); 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 a19f428ea1d..dab9a58c969 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 @@ -105,7 +105,7 @@ public class RedirectProtocolHandler extends Response.Listener.Empty implements private void redirect(Result result, HttpMethod method, String location) { final Request request = result.getRequest(); - HttpConversation conversation = client.getConversation(request.getConversationID()); + HttpConversation conversation = client.getConversation(request.getConversationID(), false); Integer redirects = (Integer)conversation.getAttribute(ATTRIBUTE); if (redirects == null) redirects = 0; @@ -151,7 +151,7 @@ public class RedirectProtocolHandler extends Response.Listener.Empty implements { Request request = result.getRequest(); Response response = result.getResponse(); - HttpConversation conversation = client.getConversation(request.getConversationID()); + HttpConversation conversation = client.getConversation(request.getConversationID(), false); Response.Listener listener = conversation.getExchanges().peekFirst().getResponseListener(); // TODO: should we replay all events, or just the failure ? notifier.notifyFailure(listener, response, failure); diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/ResponseNotifier.java b/jetty-client/src/main/java/org/eclipse/jetty/client/ResponseNotifier.java index 7c7d3304b7f..d56a5b36693 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/ResponseNotifier.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/ResponseNotifier.java @@ -126,7 +126,7 @@ public class ResponseNotifier public void forwardSuccessComplete(Response.Listener listener, Request request, Response response) { - HttpConversation conversation = client.getConversation(request.getConversationID()); + HttpConversation conversation = client.getConversation(request.getConversationID(), false); forwardSuccess(listener, response); conversation.complete(); notifyComplete(listener, new Result(request, response)); @@ -143,7 +143,7 @@ public class ResponseNotifier public void forwardFailureComplete(Response.Listener listener, Request request, Throwable requestFailure, Response response, Throwable responseFailure) { - HttpConversation conversation = client.getConversation(request.getConversationID()); + HttpConversation conversation = client.getConversation(request.getConversationID(), false); forwardFailure(listener, response, responseFailure); conversation.complete(); notifyComplete(listener, new Result(request, requestFailure, response, responseFailure)); From 7f37ddbc254c5df87270c6aa8680c271ce66cf73 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Tue, 30 Oct 2012 12:13:11 +0100 Subject: [PATCH 04/31] HTTP client: added TestTracker rule. --- .../org/eclipse/jetty/client/GZIPContentDecoderTest.java | 5 +++++ .../java/org/eclipse/jetty/client/HttpCookieParserTest.java | 5 +++++ .../java/org/eclipse/jetty/client/HttpCookieStoreTest.java | 5 +++++ .../test/java/org/eclipse/jetty/client/HttpReceiverTest.java | 5 +++++ .../test/java/org/eclipse/jetty/client/HttpSenderTest.java | 5 +++++ 5 files changed, 25 insertions(+) diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/GZIPContentDecoderTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/GZIPContentDecoderTest.java index d70a2ee4907..ca90aee585e 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/GZIPContentDecoderTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/GZIPContentDecoderTest.java @@ -25,6 +25,8 @@ import java.nio.charset.Charset; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; +import org.eclipse.jetty.toolchain.test.TestTracker; +import org.junit.Rule; import org.junit.Test; import static org.junit.Assert.assertEquals; @@ -33,6 +35,9 @@ import static org.junit.Assert.assertTrue; public class GZIPContentDecoderTest { + @Rule + public final TestTracker tracker = new TestTracker(); + @Test public void testStreamNoBlocks() throws Exception { diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieParserTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieParserTest.java index 0b4c57cb020..744fd437451 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieParserTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieParserTest.java @@ -21,11 +21,16 @@ package org.eclipse.jetty.client; import java.util.List; import org.eclipse.jetty.http.HttpCookie; +import org.eclipse.jetty.toolchain.test.TestTracker; import org.junit.Assert; +import org.junit.Rule; import org.junit.Test; public class HttpCookieParserTest { + @Rule + public final TestTracker tracker = new TestTracker(); + @Test public void testParseCookie1() throws Exception { diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieStoreTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieStoreTest.java index b616dd18449..804a7656bda 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieStoreTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieStoreTest.java @@ -23,11 +23,16 @@ import java.util.List; import org.eclipse.jetty.client.api.CookieStore; import org.eclipse.jetty.client.api.Destination; import org.eclipse.jetty.http.HttpCookie; +import org.eclipse.jetty.toolchain.test.TestTracker; import org.junit.Assert; +import org.junit.Rule; import org.junit.Test; public class HttpCookieStoreTest { + @Rule + public final TestTracker tracker = new TestTracker(); + private HttpClient client = new HttpClient(); @Test 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 6618b1ea9e3..6c9487780aa 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 @@ -34,13 +34,18 @@ import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.io.ByteArrayEndPoint; +import org.eclipse.jetty.toolchain.test.TestTracker; import org.junit.After; import org.junit.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; public class HttpReceiverTest { + @Rule + public final TestTracker tracker = new TestTracker(); + private HttpClient client; private HttpDestination destination; private ByteArrayEndPoint endPoint; diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpSenderTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpSenderTest.java index 5f8bbb65dd1..7e24e0fe695 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpSenderTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpSenderTest.java @@ -28,14 +28,19 @@ import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.api.Result; import org.eclipse.jetty.client.util.ByteBufferContentProvider; import org.eclipse.jetty.io.ByteArrayEndPoint; +import org.eclipse.jetty.toolchain.test.TestTracker; import org.eclipse.jetty.toolchain.test.annotation.Slow; import org.junit.After; import org.junit.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; public class HttpSenderTest { + @Rule + public final TestTracker tracker = new TestTracker(); + private HttpClient client; @Before From 33d97b8dd44c2d180a6f94b65828f970e0450951 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Tue, 30 Oct 2012 12:56:21 +0100 Subject: [PATCH 05/31] HTTP client: refactored "last exchange" concept out of HttpConversation into HttpExchange. --- .../jetty/client/HttpConversation.java | 22 ------------------- .../eclipse/jetty/client/HttpExchange.java | 19 +++++++++++++++- .../eclipse/jetty/client/HttpReceiver.java | 2 +- 3 files changed, 19 insertions(+), 24 deletions(-) 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 5e9e6b7d69c..abaa3030b32 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 @@ -35,7 +35,6 @@ public class HttpConversation implements Attributes private final HttpClient client; private final long id; private volatile Response.Listener listener; - private volatile HttpExchange last; public HttpConversation(HttpClient client, long id) { @@ -63,27 +62,6 @@ public class HttpConversation implements Attributes this.listener = listener; } - /** - * @return the exchange that has been identified as the last of this conversation - * @see #last - */ - public HttpExchange getLastExchange() - { - return last; - } - - /** - * Remembers the given {@code exchange} as the last of this conversation. - * - * @param exchange the exchange that is the last of this conversation - * @see #last - */ - public void setLastExchange(HttpExchange exchange) - { - if (last == null) - last = exchange; - } - public void complete() { client.removeConversation(this); 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 022fe6665c8..7dfff3652d7 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 @@ -39,6 +39,7 @@ public class HttpExchange private final Request request; private final Response.Listener listener; private final HttpResponse response; + private volatile boolean last; private volatile Throwable requestFailure; private volatile Throwable responseFailure; @@ -81,6 +82,22 @@ public class HttpExchange return responseFailure; } + /** + * @return whether this exchange is the last in the conversation + */ + public boolean isLast() + { + 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(); @@ -159,7 +176,7 @@ public class HttpExchange { // Request and response completed LOG.debug("{} complete", this); - if (conversation.getLastExchange() == this) + if (isLast()) { HttpExchange first = conversation.getExchanges().peekFirst(); Response.Listener listener = first.getResponseListener(); 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 2a00894edd0..2ae781d525b 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 @@ -146,7 +146,7 @@ public class HttpReceiver implements HttpParser.ResponseHandler Response.Listener handlerListener = protocolHandler == null ? null : protocolHandler.getResponseListener(); if (handlerListener == null) { - conversation.setLastExchange(exchange); + exchange.setLast(true); if (currentListener == initialListener) conversation.setResponseListener(initialListener); else From a1bf37f2a59e3f63cfc21b46b4cfb98c40e88645 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Tue, 30 Oct 2012 08:35:01 -0700 Subject: [PATCH 06/31] Trying to fix intellij scope concern --- tests/test-sessions/test-hash-sessions/pom.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test-sessions/test-hash-sessions/pom.xml b/tests/test-sessions/test-hash-sessions/pom.xml index 0d3797c7b61..ac5c32fe601 100644 --- a/tests/test-sessions/test-hash-sessions/pom.xml +++ b/tests/test-sessions/test-hash-sessions/pom.xml @@ -68,7 +68,8 @@ org.eclipse.jetty.toolchain jetty-test-helper - test + + compile From 1d6ec310b577aab8c9609bb2821790e76315a04d Mon Sep 17 00:00:00 2001 From: Thomas Becker Date: Tue, 30 Oct 2012 16:33:51 +0100 Subject: [PATCH 07/31] 393160: fix connection leaks in NextProtoNegoClientConnection and NextProtoNegoServerConnection --- .../jetty/spdy/client/NextProtoNegoClientConnection.java | 1 + .../jetty/spdy/server/NextProtoNegoServerConnection.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/jetty-spdy/spdy-client/src/main/java/org/eclipse/jetty/spdy/client/NextProtoNegoClientConnection.java b/jetty-spdy/spdy-client/src/main/java/org/eclipse/jetty/spdy/client/NextProtoNegoClientConnection.java index c7fda0a6821..3018c7aad6d 100644 --- a/jetty-spdy/spdy-client/src/main/java/org/eclipse/jetty/spdy/client/NextProtoNegoClientConnection.java +++ b/jetty-spdy/spdy-client/src/main/java/org/eclipse/jetty/spdy/client/NextProtoNegoClientConnection.java @@ -95,6 +95,7 @@ public class NextProtoNegoClientConnection extends AbstractConnection implements catch (IOException x) { LOG.debug(x); + NextProtoNego.remove(engine); getEndPoint().close(); return -1; } diff --git a/jetty-spdy/spdy-server/src/main/java/org/eclipse/jetty/spdy/server/NextProtoNegoServerConnection.java b/jetty-spdy/spdy-server/src/main/java/org/eclipse/jetty/spdy/server/NextProtoNegoServerConnection.java index 40731ad4443..84d361db069 100644 --- a/jetty-spdy/spdy-server/src/main/java/org/eclipse/jetty/spdy/server/NextProtoNegoServerConnection.java +++ b/jetty-spdy/spdy-server/src/main/java/org/eclipse/jetty/spdy/server/NextProtoNegoServerConnection.java @@ -25,7 +25,6 @@ import javax.net.ssl.SSLEngine; import org.eclipse.jetty.io.AbstractConnection; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; -import org.eclipse.jetty.io.ssl.SslConnection.DecryptedEndPoint; import org.eclipse.jetty.npn.NextProtoNego; import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.Connector; @@ -93,6 +92,7 @@ public class NextProtoNegoServerConnection extends AbstractConnection implements catch (IOException x) { LOG.debug(x); + NextProtoNego.remove(engine); getEndPoint().close(); return -1; } From 8d51961516160a75c3f054c36cbe196923d042e5 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Tue, 30 Oct 2012 13:53:30 +0100 Subject: [PATCH 08/31] HTTP client: refactored request listeners to support lambdas. --- .../org/eclipse/jetty/client/HttpClient.java | 2 +- .../org/eclipse/jetty/client/HttpRequest.java | 45 ++++++++++++- .../jetty/client/RedirectProtocolHandler.java | 2 +- .../eclipse/jetty/client/RequestNotifier.java | 20 +++--- .../org/eclipse/jetty/client/api/Request.java | 64 +++++++++++++++++-- .../jetty/client/HttpClientStreamTest.java | 2 +- .../eclipse/jetty/client/HttpClientTest.java | 12 ++-- .../client/HttpConnectionLifecycleTest.java | 4 +- .../jetty/client/HttpRequestAbortTest.java | 4 +- .../eclipse/jetty/client/HttpSenderTest.java | 4 +- 10 files changed, 122 insertions(+), 37 deletions(-) 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 a13d4238c7e..a75d04bd4ca 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 @@ -86,7 +86,7 @@ import org.eclipse.jetty.util.thread.TimerScheduler; * * // Asynchronously * HttpClient client = new HttpClient(); - * client.newRequest("http://localhost:8080").send(new Response.Listener.Adapter() + * client.newRequest("http://localhost:8080").send(new Response.Listener.Empty() * { * @Override * public void onSuccess(Response response) 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 7b1b4ce78bf..1dcb21d5b82 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 @@ -50,7 +50,7 @@ public class HttpRequest implements Request private final HttpFields headers = new HttpFields(); private final Fields params = new Fields(); private final Map attributes = new HashMap<>(); - private final List listeners = new ArrayList<>(); + private final List listeners = new ArrayList<>(); private final HttpClient client; private final long conversation; private final String host; @@ -240,9 +240,13 @@ public class HttpRequest implements Request } @Override - public List getListeners() + public List getListeners(Class type) { - return listeners; + ArrayList result = new ArrayList<>(); + for (RequestListener listener : listeners) + if (type == null || type.isInstance(listener)) + result.add((T)listener); + return result; } @Override @@ -252,6 +256,41 @@ public class HttpRequest implements Request return this; } + @Override + public Request onRequestQueued(QueuedListener listener) + { + this.listeners.add(listener); + return this; + } + + @Override + public Request onRequestBegin(BeginListener listener) + { + this.listeners.add(listener); + return this; + } + + @Override + public Request onRequestHeaders(HeadersListener listener) + { + this.listeners.add(listener); + return this; + } + + @Override + public Request onRequestSuccess(SuccessListener listener) + { + this.listeners.add(listener); + return this; + } + + @Override + public Request onRequestFailure(FailureListener listener) + { + this.listeners.add(listener); + return this; + } + @Override public ContentProvider getContent() { 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 dab9a58c969..b1af9582fc7 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 @@ -129,7 +129,7 @@ public class RedirectProtocolHandler extends Response.Listener.Empty implements // Copy content redirect.content(request.getContent()); - redirect.listener(new Request.Listener.Empty() + redirect.onRequestBegin(new Request.BeginListener() { @Override public void onBegin(Request redirect) diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/RequestNotifier.java b/jetty-client/src/main/java/org/eclipse/jetty/client/RequestNotifier.java index 34518697178..05ac2e642c6 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/RequestNotifier.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/RequestNotifier.java @@ -35,13 +35,13 @@ public class RequestNotifier public void notifyQueued(Request request) { - for (Request.Listener listener : request.getListeners()) + for (Request.QueuedListener listener : request.getListeners(Request.QueuedListener.class)) notifyQueued(listener, request); for (Request.Listener listener : client.getRequestListeners()) notifyQueued(listener, request); } - private void notifyQueued(Request.Listener listener, Request request) + private void notifyQueued(Request.QueuedListener listener, Request request) { try { @@ -56,13 +56,13 @@ public class RequestNotifier public void notifyBegin(Request request) { - for (Request.Listener listener : request.getListeners()) + for (Request.BeginListener listener : request.getListeners(Request.BeginListener.class)) notifyBegin(listener, request); for (Request.Listener listener : client.getRequestListeners()) notifyBegin(listener, request); } - private void notifyBegin(Request.Listener listener, Request request) + private void notifyBegin(Request.BeginListener listener, Request request) { try { @@ -77,13 +77,13 @@ public class RequestNotifier public void notifyHeaders(Request request) { - for (Request.Listener listener : request.getListeners()) + for (Request.HeadersListener listener : request.getListeners(Request.HeadersListener.class)) notifyHeaders(listener, request); for (Request.Listener listener : client.getRequestListeners()) notifyHeaders(listener, request); } - private void notifyHeaders(Request.Listener listener, Request request) + private void notifyHeaders(Request.HeadersListener listener, Request request) { try { @@ -98,13 +98,13 @@ public class RequestNotifier public void notifySuccess(Request request) { - for (Request.Listener listener : request.getListeners()) + for (Request.SuccessListener listener : request.getListeners(Request.SuccessListener.class)) notifySuccess(listener, request); for (Request.Listener listener : client.getRequestListeners()) notifySuccess(listener, request); } - private void notifySuccess(Request.Listener listener, Request request) + private void notifySuccess(Request.SuccessListener listener, Request request) { try { @@ -119,13 +119,13 @@ public class RequestNotifier public void notifyFailure(Request request, Throwable failure) { - for (Request.Listener listener : request.getListeners()) + for (Request.FailureListener listener : request.getListeners(Request.FailureListener.class)) notifyFailure(listener, request, failure); for (Request.Listener listener : client.getRequestListeners()) notifyFailure(listener, request, failure); } - private void notifyFailure(Request.Listener listener, Request request, Throwable failure) + private void notifyFailure(Request.FailureListener listener, Request request, Throwable failure) { try { diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java index b1356b08149..6b271c6856e 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java @@ -20,12 +20,12 @@ package org.eclipse.jetty.client.api; import java.io.IOException; import java.nio.file.Path; +import java.util.EventListener; import java.util.List; import java.util.Map; import java.util.concurrent.Future; import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.client.util.InputStreamResponseListener; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpVersion; @@ -207,9 +207,10 @@ public interface Request Request followRedirects(boolean follow); /** - * @return the listener for request events + * @param listenerClass the class of the listener, or null for all listeners classes + * @return the listeners for request events of the given class */ - List getListeners(); + List getListeners(Class listenerClass); /** * @param listener the listener for request events @@ -217,6 +218,36 @@ public interface Request */ Request listener(Listener listener); + /** + * @param listener the listener for request queued events + * @return this request object + */ + Request onRequestQueued(QueuedListener listener); + + /** + * @param listener the listener for request begin events + * @return this request object + */ + Request onRequestBegin(BeginListener listener); + + /** + * @param listener the listener for request headers events + * @return this request object + */ + Request onRequestHeaders(HeadersListener listener); + + /** + * @param listener the listener for request headers events + * @return this request object + */ + Request onRequestSuccess(SuccessListener listener); + + /** + * @param listener the listener for request headers events + * @return this request object + */ + Request onRequestFailure(FailureListener listener); + /** * Sends this request and returns a {@link Future} that can be used to wait for the * request and the response to be completed (either with a success or a failure). @@ -257,10 +288,11 @@ public interface Request */ boolean aborted(); - /** - * Listener for request events - */ - public interface Listener + public interface RequestListener extends EventListener + { + } + + public interface QueuedListener extends RequestListener { /** * Callback method invoked when the request is queued, waiting to be sent @@ -268,7 +300,10 @@ public interface Request * @param request the request being queued */ public void onQueued(Request request); + } + public interface BeginListener extends RequestListener + { /** * Callback method invoked when the request begins being processed in order to be sent. * This is the last opportunity to modify the request. @@ -276,7 +311,10 @@ public interface Request * @param request the request that begins being processed */ public void onBegin(Request request); + } + public interface HeadersListener extends RequestListener + { /** * Callback method invoked when the request headers (and perhaps small content) have been sent. * The request is now committed, and in transit to the server, and further modifications to the @@ -284,21 +322,33 @@ public interface Request * @param request the request that has been committed */ public void onHeaders(Request request); + } + public interface SuccessListener extends RequestListener + { /** * Callback method invoked when the request has been successfully sent. * * @param request the request sent */ public void onSuccess(Request request); + } + public interface FailureListener extends RequestListener + { /** * Callback method invoked when the request has failed to be sent * @param request the request that failed * @param failure the failure */ public void onFailure(Request request, Throwable failure); + } + /** + * Listener for all request events + */ + public interface Listener extends QueuedListener, BeginListener, HeadersListener, SuccessListener, FailureListener + { /** * An empty implementation of {@link Listener} */ diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientStreamTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientStreamTest.java index ea0cce8cce9..7f744cc22f6 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientStreamTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientStreamTest.java @@ -72,7 +72,7 @@ public class HttpClientStreamTest extends AbstractHttpClientServerTest ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) .scheme(scheme) .file(upload) - .listener(new Request.Listener.Empty() + .onRequestSuccess(new Request.SuccessListener() { @Override public void onSuccess(Request request) diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java index d5b27ae6b75..e525e4a6084 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java @@ -289,7 +289,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest final CountDownLatch successLatch = new CountDownLatch(2); client.newRequest("localhost", connector.getLocalPort()) .scheme(scheme) - .listener(new Request.Listener.Empty() + .onRequestBegin(new Request.BeginListener() { @Override public void onBegin(Request request) @@ -316,7 +316,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest client.newRequest("localhost", connector.getLocalPort()) .scheme(scheme) - .listener(new Request.Listener.Empty() + .onRequestQueued(new Request.QueuedListener() { @Override public void onQueued(Request request) @@ -419,7 +419,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest client.newRequest("localhost", connector.getLocalPort()) .scheme(scheme) .file(file) - .listener(new Request.Listener.Empty() + .onRequestSuccess(new Request.SuccessListener() { @Override public void onSuccess(Request request) @@ -529,7 +529,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest final int port = connector.getLocalPort(); client.newRequest(host, port) .scheme(scheme) - .listener(new Request.Listener.Empty() + .onRequestBegin(new Request.BeginListener() { @Override public void onBegin(Request request) @@ -621,8 +621,4 @@ public class HttpClientTest extends AbstractHttpClientServerTest Assert.assertNotNull(response); Assert.assertEquals(200, response.getStatus()); } - - // TODO: add a test to idle timeout a request that is in the queue... - // TODO: even though "idle timeout" only applies to connections - // TODO: so do we still need a "global" timeout that takes in count queue time + send time + receive time ? } diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java index 46e1870103e..2d08bb590d0 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java @@ -68,7 +68,7 @@ public class HttpConnectionLifecycleTest extends AbstractHttpClientServerTest final CountDownLatch successLatch = new CountDownLatch(3); client.newRequest(host, port) .scheme(scheme) - .listener(new Request.Listener.Empty() + .onRequestSuccess(new Request.SuccessListener() { @Override public void onSuccess(Request request) @@ -308,7 +308,7 @@ public class HttpConnectionLifecycleTest extends AbstractHttpClientServerTest final CountDownLatch failureLatch = new CountDownLatch(2); client.newRequest(host, port) .scheme(scheme) - .listener(new Request.Listener.Empty() + .onRequestFailure(new Request.FailureListener() { @Override public void onFailure(Request request, Throwable failure) diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpRequestAbortTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpRequestAbortTest.java index 5e7d73ab663..337db292b2f 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpRequestAbortTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpRequestAbortTest.java @@ -139,7 +139,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest { ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) .scheme(scheme) - .listener(new Request.Listener.Empty() + .onRequestHeaders(new Request.HeadersListener() { @Override public void onHeaders(Request request) @@ -191,7 +191,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest { client.newRequest("localhost", connector.getLocalPort()) .scheme(scheme) - .listener(new Request.Listener.Empty() + .onRequestHeaders(new Request.HeadersListener() { @Override public void onHeaders(Request request) diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpSenderTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpSenderTest.java index 7e24e0fe695..8a640b50848 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpSenderTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpSenderTest.java @@ -126,7 +126,7 @@ public class HttpSenderTest HttpConnection connection = new HttpConnection(client, endPoint, destination); Request request = client.newRequest(URI.create("http://localhost/")); final CountDownLatch failureLatch = new CountDownLatch(2); - request.listener(new Request.Listener.Empty() + request.onRequestFailure(new Request.FailureListener() { @Override public void onFailure(Request request, Throwable x) @@ -155,7 +155,7 @@ public class HttpSenderTest HttpConnection connection = new HttpConnection(client, endPoint, destination); Request request = client.newRequest(URI.create("http://localhost/")); final CountDownLatch failureLatch = new CountDownLatch(2); - request.listener(new Request.Listener.Empty() + request.onRequestFailure(new Request.FailureListener() { @Override public void onFailure(Request request, Throwable x) From 0d762bcdbc11cd9e5c6d20cad2ec919ba6b060cc Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Tue, 30 Oct 2012 19:21:49 +0100 Subject: [PATCH 09/31] HTTP client: refactored response listeners to support lambdas. --- .../client/AuthenticationProtocolHandler.java | 17 +-- .../jetty/client/ContinueProtocolHandler.java | 20 +++- .../jetty/client/DoubleResponseListener.java | 80 ------------- .../org/eclipse/jetty/client/HttpClient.java | 20 +++- .../eclipse/jetty/client/HttpConnection.java | 15 ++- .../jetty/client/HttpContentResponse.java | 5 +- .../jetty/client/HttpConversation.java | 11 +- .../eclipse/jetty/client/HttpDestination.java | 81 ++++++------- .../eclipse/jetty/client/HttpExchange.java | 20 ++-- .../eclipse/jetty/client/HttpReceiver.java | 45 +++++--- .../org/eclipse/jetty/client/HttpRequest.java | 60 ++++++++-- .../eclipse/jetty/client/HttpResponse.java | 17 ++- .../org/eclipse/jetty/client/HttpSender.java | 21 ++-- .../jetty/client/RedirectProtocolHandler.java | 22 ++-- .../eclipse/jetty/client/RequestNotifier.java | 10 +- .../jetty/client/ResponseNotifier.java | 106 ++++++++++++------ .../eclipse/jetty/client/api/Connection.java | 2 +- .../org/eclipse/jetty/client/api/Request.java | 46 ++++++-- .../eclipse/jetty/client/api/Response.java | 41 +++++-- .../client/util/BlockingResponseListener.java | 1 - .../util/BufferingResponseListener.java | 5 +- .../client/HttpClientAuthenticationTest.java | 15 ++- .../jetty/client/HttpClientLoadTest.java | 6 +- .../eclipse/jetty/client/HttpClientTest.java | 10 +- .../client/HttpConnectionLifecycleTest.java | 6 +- .../jetty/client/HttpReceiverTest.java | 15 +-- .../jetty/client/HttpResponseAbortTest.java | 17 ++- .../eclipse/jetty/client/HttpSenderTest.java | 14 +-- 28 files changed, 424 insertions(+), 304 deletions(-) delete mode 100644 jetty-client/src/main/java/org/eclipse/jetty/client/DoubleResponseListener.java diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java b/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java index 97599f6f8c3..c1e2dd722fa 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java @@ -80,13 +80,13 @@ public class AuthenticationProtocolHandler implements ProtocolHandler { Request request = result.getRequest(); HttpConversation conversation = client.getConversation(request.getConversationID(), false); - Response.Listener listener = conversation.getExchanges().peekFirst().getResponseListener(); + List listeners = conversation.getExchanges().peekFirst().getResponseListeners(); ContentResponse response = new HttpContentResponse(result.getResponse(), getContent(), getEncoding()); if (result.isFailed()) { Throwable failure = result.getFailure(); LOG.debug("Authentication challenge failed {}", failure); - notifier.forwardFailureComplete(listener, request, result.getRequestFailure(), response, result.getResponseFailure()); + notifier.forwardFailureComplete(listeners, request, result.getRequestFailure(), response, result.getResponseFailure()); return; } @@ -94,7 +94,7 @@ public class AuthenticationProtocolHandler implements ProtocolHandler if (wwwAuthenticates.isEmpty()) { LOG.debug("Authentication challenge without WWW-Authenticate header"); - notifier.forwardFailureComplete(listener, request, null, response, new HttpResponseException("HTTP protocol violation: 401 without WWW-Authenticate header", response)); + notifier.forwardFailureComplete(listeners, request, null, response, new HttpResponseException("HTTP protocol violation: 401 without WWW-Authenticate header", response)); return; } @@ -113,7 +113,7 @@ public class AuthenticationProtocolHandler implements ProtocolHandler if (authentication == null) { LOG.debug("No authentication available for {}", request); - notifier.forwardSuccessComplete(listener, request, response); + notifier.forwardSuccessComplete(listeners, request, response); return; } @@ -121,19 +121,20 @@ public class AuthenticationProtocolHandler implements ProtocolHandler LOG.debug("Authentication result {}", authnResult); if (authnResult == null) { - notifier.forwardSuccessComplete(listener, request, response); + notifier.forwardSuccessComplete(listeners, request, response); return; } - authnResult.apply(request); - request.send(new Response.Listener.Empty() + Request newRequest = client.copyRequest(request, request.getURI()); + authnResult.apply(newRequest); + newRequest.onResponseSuccess(new Response.SuccessListener() { @Override public void onSuccess(Response response) { client.getAuthenticationStore().addAuthenticationResult(authnResult); } - }); + }).send(null); } private List parseWWWAuthenticate(Response response) 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 a9dec3af342..8588914571a 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 @@ -18,8 +18,11 @@ package org.eclipse.jetty.client; +import java.util.List; + 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.client.util.BufferingResponseListener; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeaderValue; @@ -67,14 +70,14 @@ public class ContinueProtocolHandler implements ProtocolHandler HttpExchange exchange = conversation.getExchanges().peekLast(); assert exchange.getResponse() == response; - Response.Listener listener = exchange.getResponseListener(); + List listeners = exchange.getResponseListeners(); switch (response.getStatus()) { case 100: { // All good, continue exchange.resetResponse(true); - conversation.setResponseListener(listener); + conversation.setResponseListeners(listeners); exchange.proceed(true); break; } @@ -82,8 +85,8 @@ public class ContinueProtocolHandler implements ProtocolHandler { // Server either does not support 100 Continue, or it does and wants to refuse the request content HttpContentResponse contentResponse = new HttpContentResponse(response, getContent(), getEncoding()); - notifier.forwardSuccess(listener, contentResponse); - conversation.setResponseListener(listener); + notifier.forwardSuccess(listeners, contentResponse); + conversation.setResponseListeners(listeners); exchange.proceed(false); break; } @@ -99,9 +102,14 @@ public class ContinueProtocolHandler implements ProtocolHandler HttpExchange exchange = conversation.getExchanges().peekLast(); assert exchange.getResponse() == response; - Response.Listener listener = exchange.getResponseListener(); + List listeners = exchange.getResponseListeners(); HttpContentResponse contentResponse = new HttpContentResponse(response, getContent(), getEncoding()); - notifier.forwardFailureComplete(listener, exchange.getRequest(), exchange.getRequestFailure(), contentResponse, failure); + notifier.forwardFailureComplete(listeners, exchange.getRequest(), exchange.getRequestFailure(), contentResponse, failure); + } + + @Override + public void onComplete(Result result) + { } } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/DoubleResponseListener.java b/jetty-client/src/main/java/org/eclipse/jetty/client/DoubleResponseListener.java deleted file mode 100644 index ba2587bc203..00000000000 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/DoubleResponseListener.java +++ /dev/null @@ -1,80 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2012 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.client; - -import java.nio.ByteBuffer; - -import org.eclipse.jetty.client.api.Response; -import org.eclipse.jetty.client.api.Result; - -public class DoubleResponseListener implements Response.Listener -{ - private final ResponseNotifier responseNotifier; - private final Response.Listener listener1; - private final Response.Listener listener2; - - public DoubleResponseListener(ResponseNotifier responseNotifier, Response.Listener listener1, Response.Listener listener2) - { - this.responseNotifier = responseNotifier; - this.listener1 = listener1; - this.listener2 = listener2; - } - - @Override - public void onBegin(Response response) - { - responseNotifier.notifyBegin(listener1, response); - responseNotifier.notifyBegin(listener2, response); - } - - @Override - public void onHeaders(Response response) - { - responseNotifier.notifyHeaders(listener1, response); - responseNotifier.notifyHeaders(listener2, response); - } - - @Override - public void onContent(Response response, ByteBuffer content) - { - responseNotifier.notifyContent(listener1, response, content); - responseNotifier.notifyContent(listener2, response, content); - } - - @Override - public void onSuccess(Response response) - { - responseNotifier.notifySuccess(listener1, response); - responseNotifier.notifySuccess(listener2, response); - } - - @Override - public void onFailure(Response response, Throwable failure) - { - responseNotifier.notifyFailure(listener1, response, failure); - responseNotifier.notifyFailure(listener2, response, failure); - } - - @Override - public void onComplete(Result result) - { - responseNotifier.notifyComplete(listener1, result); - responseNotifier.notifyComplete(listener2, result); - } -} 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 a75d04bd4ca..224c16d464c 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 @@ -45,6 +45,7 @@ import org.eclipse.jetty.client.api.CookieStore; import org.eclipse.jetty.client.api.Destination; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.EndPoint; @@ -265,9 +266,15 @@ public class HttpClient extends ContainerLifeCycle return new HttpRequest(this, uri); } - protected Request newRequest(long id, String uri) + protected Request copyRequest(Request oldRequest, String newURI) { - return new HttpRequest(this, id, URI.create(uri)); + Request newRequest = new HttpRequest(this, oldRequest.getConversationID(), URI.create(newURI)); + newRequest.method(oldRequest.getMethod()) + .version(oldRequest.getVersion()) + .content(oldRequest.getContent()); + for (HttpFields.Field header : oldRequest.getHeaders()) + newRequest.header(header.getName(), header.getValue()); + return newRequest; } private String address(String scheme, String host, int port) @@ -307,7 +314,7 @@ public class HttpClient extends ContainerLifeCycle return new ArrayList(destinations.values()); } - protected void send(final Request request, Response.Listener listener) + protected void send(final Request request, List listeners) { String scheme = request.getScheme().toLowerCase(); if (!Arrays.asList("http", "https").contains(scheme)) @@ -317,11 +324,12 @@ public class HttpClient extends ContainerLifeCycle if (port < 0) port = "https".equals(scheme) ? 443 : 80; - if (listener instanceof Schedulable) - ((Schedulable)listener).schedule(scheduler); + for (Response.ResponseListener listener : listeners) + if (listener instanceof Schedulable) + ((Schedulable)listener).schedule(scheduler); HttpDestination destination = provideDestination(scheme, request.getHost(), port); - destination.send(request, listener); + destination.send(request, listeners); } protected void newConnection(HttpDestination destination, Callback callback) 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 ee58c05f9de..aae92bcac98 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 @@ -21,6 +21,7 @@ package org.eclipse.jetty.client; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.UnsupportedCharsetException; +import java.util.Collections; import java.util.Enumeration; import java.util.Iterator; import java.util.List; @@ -102,7 +103,12 @@ public class HttpConnection extends AbstractConnection implements Connection } @Override - public void send(Request request, Response.Listener listener) + public void send(Request request, Response.CompleteListener listener) + { + send(request, Collections.singletonList(listener)); + } + + public void send(Request request, List listeners) { normalizeRequest(request); @@ -112,12 +118,13 @@ public class HttpConnection extends AbstractConnection implements Connection endPoint.setIdleTimeout(request.getIdleTimeout()); HttpConversation conversation = client.getConversation(request.getConversationID(), true); - HttpExchange exchange = new HttpExchange(conversation, this, request, listener); + HttpExchange exchange = new HttpExchange(conversation, this, request, listeners); setExchange(exchange); conversation.getExchanges().offer(exchange); - if (listener instanceof Schedulable) - ((Schedulable)listener).schedule(client.getScheduler()); + for (Response.ResponseListener listener : listeners) + if (listener instanceof Schedulable) + ((Schedulable)listener).schedule(client.getScheduler()); sender.send(exchange); } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpContentResponse.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpContentResponse.java index 97aad12d9ad..aa9dd777eab 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpContentResponse.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpContentResponse.java @@ -20,6 +20,7 @@ package org.eclipse.jetty.client; import java.io.UnsupportedEncodingException; import java.nio.charset.UnsupportedCharsetException; +import java.util.List; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Response; @@ -46,9 +47,9 @@ public class HttpContentResponse implements ContentResponse } @Override - public Listener getListener() + public List getListeners(Class listenerClass) { - return response.getListener(); + return response.getListeners(listenerClass); } @Override 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 abaa3030b32..bacb391e20e 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 @@ -21,6 +21,7 @@ package org.eclipse.jetty.client; import java.util.Collections; 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; @@ -34,7 +35,7 @@ public class HttpConversation implements Attributes private final Deque exchanges = new ConcurrentLinkedDeque<>(); private final HttpClient client; private final long id; - private volatile Response.Listener listener; + private volatile List listeners; public HttpConversation(HttpClient client, long id) { @@ -52,14 +53,14 @@ public class HttpConversation implements Attributes return exchanges; } - public Response.Listener getResponseListener() + public List getResponseListeners() { - return listener; + return listeners; } - public void setResponseListener(Response.Listener listener) + public void setResponseListeners(List listeners) { - this.listener = listener; + this.listeners = listeners; } public void complete() 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 a9a66d2b657..4145d1d6bf2 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 @@ -50,7 +50,7 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable private final String scheme; private final String host; private final int port; - private final Queue requests; + private final Queue requests; private final BlockingQueue idleConnections; private final BlockingQueue activeConnections; private final RequestNotifier requestNotifier; @@ -97,7 +97,7 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable return port; } - public void send(Request request, Response.Listener listener) + public void send(Request request, List listeners) { if (!scheme.equals(request.getScheme())) throw new IllegalArgumentException("Invalid request scheme " + request.getScheme() + " for destination " + this); @@ -107,12 +107,12 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable if (port >= 0 && this.port != port) throw new IllegalArgumentException("Invalid request port " + port + " for destination " + this); - RequestPair requestPair = new RequestPair(request, listener); + RequestContext requestContext = new RequestContext(request, listeners); if (client.isRunning()) { - if (requests.offer(requestPair)) + if (requests.offer(requestContext)) { - if (!client.isRunning() && requests.remove(requestPair)) + if (!client.isRunning() && requests.remove(requestContext)) { throw new RejectedExecutionException(client + " is stopping"); } @@ -202,15 +202,15 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable private void drain(Throwable x) { - RequestPair pair; - while ((pair = requests.poll()) != null) + RequestContext requestContext; + while ((requestContext = requests.poll()) != null) { - Request request = pair.request; + Request request = requestContext.request; requestNotifier.notifyFailure(request, x); - Response.Listener listener = pair.listener; - HttpResponse response = new HttpResponse(request, listener); - responseNotifier.notifyFailure(listener, response, x); - responseNotifier.notifyComplete(listener, new Result(request, x, response, 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)); } } @@ -223,37 +223,40 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable * * @param connection the new connection */ - protected void process(final Connection connection, boolean dispatch) + protected void process(Connection connection, boolean dispatch) { - RequestPair requestPair = requests.poll(); - if (requestPair == null) + // Ugly cast, but lack of generic reification forces it + final HttpConnection httpConnection = (HttpConnection)connection; + + RequestContext requestContext = requests.poll(); + if (requestContext == null) { - LOG.debug("{} idle", connection); - if (!idleConnections.offer(connection)) + LOG.debug("{} idle", httpConnection); + if (!idleConnections.offer(httpConnection)) { LOG.debug("{} idle overflow"); - connection.close(); + httpConnection.close(); } if (!client.isRunning()) { LOG.debug("{} is stopping", client); - remove(connection); - connection.close(); + remove(httpConnection); + httpConnection.close(); } } else { - final Request request = requestPair.request; - final Response.Listener listener = requestPair.listener; + final Request request = requestContext.request; + final List listeners = requestContext.listeners; if (request.aborted()) { - abort(request, listener, "Aborted"); + abort(request, listeners, "Aborted"); LOG.debug("Aborted {} before processing", request); } else { - LOG.debug("{} active", connection); - if (!activeConnections.offer(connection)) + LOG.debug("{} active", httpConnection); + if (!activeConnections.offer(httpConnection)) { LOG.warn("{} active overflow"); } @@ -264,13 +267,13 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable @Override public void run() { - connection.send(request, listener); + httpConnection.send(request, listeners); } }); } else { - connection.send(request, listener); + httpConnection.send(request, listeners); } } } @@ -333,14 +336,14 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable public boolean abort(Request request, String reason) { - for (RequestPair pair : requests) + for (RequestContext requestContext : requests) { - if (pair.request == request) + if (requestContext.request == request) { - if (requests.remove(pair)) + if (requests.remove(requestContext)) { // We were able to remove the pair, so it won't be processed - abort(request, pair.listener, reason); + abort(request, requestContext.listeners, reason); LOG.debug("Aborted {} while queued", request); return true; } @@ -349,13 +352,13 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable return false; } - private void abort(Request request, Response.Listener listener, String reason) + private void abort(Request request, List listeners, String reason) { - HttpResponse response = new HttpResponse(request, listener); + HttpResponse response = new HttpResponse(request, listeners); HttpResponseException responseFailure = new HttpResponseException(reason, response); - responseNotifier.notifyFailure(listener, response, responseFailure); + responseNotifier.notifyFailure(listeners, response, responseFailure); HttpRequestException requestFailure = new HttpRequestException(reason, request); - responseNotifier.notifyComplete(listener, new Result(request, requestFailure, response, responseFailure)); + responseNotifier.notifyComplete(listeners, new Result(request, requestFailure, response, responseFailure)); } @Override @@ -382,15 +385,15 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable return String.format("%s(%s://%s:%d)", HttpDestination.class.getSimpleName(), getScheme(), getHost(), getPort()); } - private static class RequestPair + private static class RequestContext { private final Request request; - private final Response.Listener listener; + private final List listeners; - private RequestPair(Request request, Response.Listener listener) + private RequestContext(Request request, List listeners) { this.request = request; - this.listener = listener; + this.listeners = listeners; } } } 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 7dfff3652d7..8abcd3d2201 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 @@ -18,6 +18,7 @@ package org.eclipse.jetty.client; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicMarkableReference; @@ -37,19 +38,19 @@ public class HttpExchange private final HttpConversation conversation; private final HttpConnection connection; private final Request request; - private final Response.Listener listener; + private final List listeners; private final HttpResponse response; private volatile boolean last; private volatile Throwable requestFailure; private volatile Throwable responseFailure; - public HttpExchange(HttpConversation conversation, HttpConnection connection, Request request, Response.Listener listener) + public HttpExchange(HttpConversation conversation, HttpConnection connection, Request request, List listeners) { this.conversation = conversation; this.connection = connection; this.request = request; - this.listener = listener; - this.response = new HttpResponse(request, listener); + this.listeners = listeners; + this.response = new HttpResponse(request, listeners); } public HttpConversation getConversation() @@ -67,9 +68,9 @@ public class HttpExchange return requestFailure; } - public Response.Listener getResponseListener() + public List getResponseListeners() { - return listener; + return listeners; } public HttpResponse getResponse() @@ -179,9 +180,10 @@ public class HttpExchange if (isLast()) { HttpExchange first = conversation.getExchanges().peekFirst(); - Response.Listener listener = first.getResponseListener(); - if (listener instanceof Schedulable) - ((Schedulable)listener).cancel(); + List listeners = first.getResponseListeners(); + for (Response.ResponseListener listener : listeners) + if (listener instanceof Schedulable) + ((Schedulable)listener).cancel(); conversation.complete(); } } 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 2ae781d525b..b922ef24fa4 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 @@ -20,6 +20,8 @@ package org.eclipse.jetty.client; import java.io.EOFException; import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; import java.util.Enumeration; import java.util.List; import java.util.concurrent.TimeoutException; @@ -139,30 +141,41 @@ public class HttpReceiver implements HttpParser.ResponseHandler response.version(version).status(status).reason(reason); // Probe the protocol handlers - Response.Listener currentListener = exchange.getResponseListener(); - Response.Listener initialListener = conversation.getExchanges().peekFirst().getResponseListener(); + 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 (currentListener == initialListener) - conversation.setResponseListener(initialListener); + if (initialExchange == exchange) + { + conversation.setResponseListeners(exchange.getResponseListeners()); + } else - conversation.setResponseListener(new DoubleResponseListener(responseNotifier, currentListener, initialListener)); + { + List listeners = new ArrayList<>(exchange.getResponseListeners()); + listeners.addAll(initialExchange.getResponseListeners()); + conversation.setResponseListeners(listeners); + } } else { LOG.debug("Found protocol handler {}", protocolHandler); - if (currentListener == initialListener) - conversation.setResponseListener(handlerListener); + if (initialExchange == exchange) + { + conversation.setResponseListeners(Collections.singletonList(handlerListener)); + } else - conversation.setResponseListener(new DoubleResponseListener(responseNotifier, currentListener, handlerListener)); + { + List listeners = new ArrayList<>(exchange.getResponseListeners()); + listeners.add(handlerListener); + conversation.setResponseListeners(listeners); + } } LOG.debug("Receiving {}", response); - responseNotifier.notifyBegin(conversation.getResponseListener(), response); + responseNotifier.notifyBegin(conversation.getResponseListeners(), response); } } return false; @@ -212,7 +225,7 @@ public class HttpReceiver implements HttpParser.ResponseHandler HttpConversation conversation = exchange.getConversation(); HttpResponse response = exchange.getResponse(); LOG.debug("Headers {}", response); - responseNotifier.notifyHeaders(conversation.getResponseListener(), response); + responseNotifier.notifyHeaders(conversation.getResponseListeners(), response); Enumeration contentEncodings = response.getHeaders().getValues(HttpHeader.CONTENT_ENCODING.asString(), ","); if (contentEncodings != null) @@ -254,7 +267,7 @@ public class HttpReceiver implements HttpParser.ResponseHandler LOG.debug("{} {}: {} bytes", decoder, response, buffer.remaining()); } - responseNotifier.notifyContent(conversation.getResponseListener(), response, buffer); + responseNotifier.notifyContent(conversation.getResponseListeners(), response, buffer); } } return false; @@ -287,8 +300,8 @@ public class HttpReceiver implements HttpParser.ResponseHandler exchange.terminateResponse(); HttpResponse response = exchange.getResponse(); - Response.Listener listener = exchange.getConversation().getResponseListener(); - responseNotifier.notifySuccess(listener, response); + List listeners = exchange.getConversation().getResponseListeners(); + responseNotifier.notifySuccess(listeners, response); LOG.debug("Received {}", response); Result result = completion.getReference(); @@ -296,7 +309,7 @@ public class HttpReceiver implements HttpParser.ResponseHandler { connection.complete(exchange, !result.isFailed()); - responseNotifier.notifyComplete(listener, result); + responseNotifier.notifyComplete(listeners, result); } return true; @@ -330,7 +343,7 @@ public class HttpReceiver implements HttpParser.ResponseHandler HttpResponse response = exchange.getResponse(); HttpConversation conversation = exchange.getConversation(); - responseNotifier.notifyFailure(conversation.getResponseListener(), response, failure); + responseNotifier.notifyFailure(conversation.getResponseListeners(), response, failure); LOG.debug("Failed {} {}", response, failure); Result result = completion.getReference(); @@ -338,7 +351,7 @@ public class HttpReceiver implements HttpParser.ResponseHandler { connection.complete(exchange, false); - responseNotifier.notifyComplete(conversation.getResponseListener(), result); + responseNotifier.notifyComplete(conversation.getResponseListeners(), result); } return true; 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 1dcb21d5b82..7dffe62ac6e 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 @@ -50,7 +50,8 @@ public class HttpRequest implements Request private final HttpFields headers = new HttpFields(); private final Fields params = new Fields(); private final Map attributes = new HashMap<>(); - private final List listeners = new ArrayList<>(); + private final List requestListeners = new ArrayList<>(); + private final List responseListeners = new ArrayList<>(); private final HttpClient client; private final long conversation; private final String host; @@ -240,10 +241,10 @@ public class HttpRequest implements Request } @Override - public List getListeners(Class type) + public List getRequestListeners(Class type) { ArrayList result = new ArrayList<>(); - for (RequestListener listener : listeners) + for (RequestListener listener : requestListeners) if (type == null || type.isInstance(listener)) result.add((T)listener); return result; @@ -252,42 +253,77 @@ public class HttpRequest implements Request @Override public Request listener(Request.Listener listener) { - this.listeners.add(listener); + this.requestListeners.add(listener); return this; } @Override public Request onRequestQueued(QueuedListener listener) { - this.listeners.add(listener); + this.requestListeners.add(listener); return this; } @Override public Request onRequestBegin(BeginListener listener) { - this.listeners.add(listener); + this.requestListeners.add(listener); return this; } @Override public Request onRequestHeaders(HeadersListener listener) { - this.listeners.add(listener); + this.requestListeners.add(listener); return this; } @Override public Request onRequestSuccess(SuccessListener listener) { - this.listeners.add(listener); + this.requestListeners.add(listener); return this; } @Override public Request onRequestFailure(FailureListener listener) { - this.listeners.add(listener); + this.requestListeners.add(listener); + return this; + } + + @Override + public Request onResponseBegin(Response.BeginListener listener) + { + this.responseListeners.add(listener); + return this; + } + + @Override + public Request onResponseHeaders(Response.HeadersListener listener) + { + this.responseListeners.add(listener); + return this; + } + + @Override + public Request onResponseContent(Response.ContentListener listener) + { + this.responseListeners.add(listener); + return this; + } + + @Override + public Request onResponseSuccess(Response.SuccessListener listener) + { + this.responseListeners.add(listener); + return this; + } + + @Override + public Request onResponseFailure(Response.FailureListener listener) + { + this.responseListeners.add(listener); return this; } @@ -359,9 +395,11 @@ public class HttpRequest implements Request } @Override - public void send(final Response.Listener listener) + public void send(Response.CompleteListener listener) { - client.send(this, listener); + if (listener != null) + responseListeners.add(listener); + client.send(this, responseListeners); } @Override diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpResponse.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpResponse.java index 62d1f7a845c..0b49f3be92e 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpResponse.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpResponse.java @@ -18,6 +18,9 @@ package org.eclipse.jetty.client; +import java.util.ArrayList; +import java.util.List; + import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.http.HttpFields; @@ -27,15 +30,15 @@ public class HttpResponse implements Response { private final HttpFields headers = new HttpFields(); private final Request request; - private final Listener listener; + private final List listeners; private HttpVersion version; private int status; private String reason; - public HttpResponse(Request request, Listener listener) + public HttpResponse(Request request, List listeners) { this.request = request; - this.listener = listener; + this.listeners = listeners; } public HttpVersion getVersion() @@ -85,9 +88,13 @@ public class HttpResponse implements Response } @Override - public Listener getListener() + public List getListeners(Class type) { - return listener; + ArrayList result = new ArrayList<>(); + for (ResponseListener listener : listeners) + if (type == null || type.isInstance(listener)) + result.add((T)listener); + return result; } @Override 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 2966cc6aeda..bd38c9119f6 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,8 +19,10 @@ 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; @@ -67,12 +69,17 @@ public class HttpSender // Arrange the listeners, so that if there is a request failure the proper listeners are notified HttpConversation conversation = exchange.getConversation(); - Response.Listener currentListener = exchange.getResponseListener(); - Response.Listener initialListener = conversation.getExchanges().peekFirst().getResponseListener(); - if (initialListener == currentListener) - conversation.setResponseListener(initialListener); + HttpExchange initialExchange = conversation.getExchanges().peekFirst(); + if (initialExchange == exchange) + { + conversation.setResponseListeners(exchange.getResponseListeners()); + } else - conversation.setResponseListener(new DoubleResponseListener(responseNotifier, currentListener, initialListener)); + { + List listeners = new ArrayList<>(exchange.getResponseListeners()); + listeners.addAll(initialExchange.getResponseListeners()); + conversation.setResponseListeners(listeners); + } Request request = exchange.getRequest(); if (request.aborted()) @@ -359,7 +366,7 @@ public class HttpSender connection.complete(exchange, !result.isFailed()); HttpConversation conversation = exchange.getConversation(); - responseNotifier.notifyComplete(conversation.getResponseListener(), result); + responseNotifier.notifyComplete(conversation.getResponseListeners(), result); } return true; @@ -405,7 +412,7 @@ public class HttpSender connection.complete(exchange, false); HttpConversation conversation = exchange.getConversation(); - responseNotifier.notifyComplete(conversation.getResponseListener(), result); + responseNotifier.notifyComplete(conversation.getResponseListeners(), result); } return true; 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 b1af9582fc7..82895958ae6 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 @@ -18,10 +18,11 @@ package org.eclipse.jetty.client; +import java.util.List; + 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.HttpFields; import org.eclipse.jetty.http.HttpMethod; public class RedirectProtocolHandler extends Response.Listener.Empty implements ProtocolHandler @@ -115,20 +116,11 @@ public class RedirectProtocolHandler extends Response.Listener.Empty implements ++redirects; conversation.setAttribute(ATTRIBUTE, redirects); - Request redirect = client.newRequest(request.getConversationID(), location); + Request redirect = client.copyRequest(request, location); // Use given method redirect.method(method); - redirect.version(request.getVersion()); - - // Copy headers - for (HttpFields.Field header : request.getHeaders()) - redirect.header(header.getName(), header.getValue()); - - // Copy content - redirect.content(request.getContent()); - redirect.onRequestBegin(new Request.BeginListener() { @Override @@ -139,7 +131,7 @@ public class RedirectProtocolHandler extends Response.Listener.Empty implements } }); - redirect.send(new Response.Listener.Empty()); + redirect.send(null); } else { @@ -152,9 +144,9 @@ public class RedirectProtocolHandler extends Response.Listener.Empty implements Request request = result.getRequest(); Response response = result.getResponse(); HttpConversation conversation = client.getConversation(request.getConversationID(), false); - Response.Listener listener = conversation.getExchanges().peekFirst().getResponseListener(); + List listeners = conversation.getExchanges().peekFirst().getResponseListeners(); // TODO: should we replay all events, or just the failure ? - notifier.notifyFailure(listener, response, failure); - notifier.notifyComplete(listener, new Result(request, response, 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/RequestNotifier.java b/jetty-client/src/main/java/org/eclipse/jetty/client/RequestNotifier.java index 05ac2e642c6..b04949b4852 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/RequestNotifier.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/RequestNotifier.java @@ -35,7 +35,7 @@ public class RequestNotifier public void notifyQueued(Request request) { - for (Request.QueuedListener listener : request.getListeners(Request.QueuedListener.class)) + for (Request.QueuedListener listener : request.getRequestListeners(Request.QueuedListener.class)) notifyQueued(listener, request); for (Request.Listener listener : client.getRequestListeners()) notifyQueued(listener, request); @@ -56,7 +56,7 @@ public class RequestNotifier public void notifyBegin(Request request) { - for (Request.BeginListener listener : request.getListeners(Request.BeginListener.class)) + for (Request.BeginListener listener : request.getRequestListeners(Request.BeginListener.class)) notifyBegin(listener, request); for (Request.Listener listener : client.getRequestListeners()) notifyBegin(listener, request); @@ -77,7 +77,7 @@ public class RequestNotifier public void notifyHeaders(Request request) { - for (Request.HeadersListener listener : request.getListeners(Request.HeadersListener.class)) + for (Request.HeadersListener listener : request.getRequestListeners(Request.HeadersListener.class)) notifyHeaders(listener, request); for (Request.Listener listener : client.getRequestListeners()) notifyHeaders(listener, request); @@ -98,7 +98,7 @@ public class RequestNotifier public void notifySuccess(Request request) { - for (Request.SuccessListener listener : request.getListeners(Request.SuccessListener.class)) + for (Request.SuccessListener listener : request.getRequestListeners(Request.SuccessListener.class)) notifySuccess(listener, request); for (Request.Listener listener : client.getRequestListeners()) notifySuccess(listener, request); @@ -119,7 +119,7 @@ public class RequestNotifier public void notifyFailure(Request request, Throwable failure) { - for (Request.FailureListener listener : request.getListeners(Request.FailureListener.class)) + for (Request.FailureListener listener : request.getRequestListeners(Request.FailureListener.class)) notifyFailure(listener, request, failure); for (Request.Listener listener : client.getRequestListeners()) notifyFailure(listener, request, failure); diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/ResponseNotifier.java b/jetty-client/src/main/java/org/eclipse/jetty/client/ResponseNotifier.java index d56a5b36693..a1b64c6d2ee 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/ResponseNotifier.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/ResponseNotifier.java @@ -19,6 +19,7 @@ package org.eclipse.jetty.client; import java.nio.ByteBuffer; +import java.util.List; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; @@ -37,12 +38,18 @@ public class ResponseNotifier this.client = client; } - public void notifyBegin(Response.Listener listener, Response response) + public void notifyBegin(List listeners, Response response) + { + for (Response.ResponseListener listener : listeners) + if (listener instanceof Response.BeginListener) + notifyBegin((Response.BeginListener)listener, response); + } + + private void notifyBegin(Response.BeginListener listener, Response response) { try { - if (listener != null) - listener.onBegin(response); + listener.onBegin(response); } catch (Exception x) { @@ -50,12 +57,18 @@ public class ResponseNotifier } } - public void notifyHeaders(Response.Listener listener, Response response) + public void notifyHeaders(List listeners, Response response) + { + for (Response.ResponseListener listener : listeners) + if (listener instanceof Response.HeadersListener) + notifyHeaders((Response.HeadersListener)listener, response); + } + + private void notifyHeaders(Response.HeadersListener listener, Response response) { try { - if (listener != null) - listener.onHeaders(response); + listener.onHeaders(response); } catch (Exception x) { @@ -63,12 +76,19 @@ public class ResponseNotifier } } - public void notifyContent(Response.Listener listener, Response response, ByteBuffer buffer) + public void notifyContent(List listeners, Response response, ByteBuffer buffer) + { + for (Response.ResponseListener listener : listeners) + if (listener instanceof Response.ContentListener) + notifyContent((Response.ContentListener)listener, response, buffer); + + } + + private void notifyContent(Response.ContentListener listener, Response response, ByteBuffer buffer) { try { - if (listener != null) - listener.onContent(response, buffer); + listener.onContent(response, buffer); } catch (Exception x) { @@ -76,12 +96,18 @@ public class ResponseNotifier } } - public void notifySuccess(Response.Listener listener, Response response) + public void notifySuccess(List listeners, Response response) + { + for (Response.ResponseListener listener : listeners) + if (listener instanceof Response.SuccessListener) + notifySuccess((Response.SuccessListener)listener, response); + } + + private void notifySuccess(Response.SuccessListener listener, Response response) { try { - if (listener != null) - listener.onSuccess(response); + listener.onSuccess(response); } catch (Exception x) { @@ -89,12 +115,18 @@ public class ResponseNotifier } } - public void notifyFailure(Response.Listener listener, Response response, Throwable failure) + public void notifyFailure(List listeners, Response response, Throwable failure) + { + for (Response.ResponseListener listener : listeners) + if (listener instanceof Response.FailureListener) + notifyFailure((Response.FailureListener)listener, response, failure); + } + + private void notifyFailure(Response.FailureListener listener, Response response, Throwable failure) { try { - if (listener != null) - listener.onFailure(response, failure); + listener.onFailure(response, failure); } catch (Exception x) { @@ -102,12 +134,18 @@ public class ResponseNotifier } } - public void notifyComplete(Response.Listener listener, Result result) + public void notifyComplete(List listeners, Result result) + { + for (Response.ResponseListener listener : listeners) + if (listener instanceof Response.CompleteListener) + notifyComplete((Response.CompleteListener)listener, result); + } + + private void notifyComplete(Response.CompleteListener listener, Result result) { try { - if (listener != null) - listener.onComplete(result); + listener.onComplete(result); } catch (Exception x) { @@ -115,37 +153,37 @@ public class ResponseNotifier } } - public void forwardSuccess(Response.Listener listener, Response response) + public void forwardSuccess(List listeners, Response response) { - notifyBegin(listener, response); - notifyHeaders(listener, response); + notifyBegin(listeners, response); + notifyHeaders(listeners, response); if (response instanceof ContentResponse) - notifyContent(listener, response, ByteBuffer.wrap(((ContentResponse)response).getContent())); - notifySuccess(listener, response); + notifyContent(listeners, response, ByteBuffer.wrap(((ContentResponse)response).getContent())); + notifySuccess(listeners, response); } - public void forwardSuccessComplete(Response.Listener listener, Request request, Response response) + public void forwardSuccessComplete(List listeners, Request request, Response response) { HttpConversation conversation = client.getConversation(request.getConversationID(), false); - forwardSuccess(listener, response); + forwardSuccess(listeners, response); conversation.complete(); - notifyComplete(listener, new Result(request, response)); + notifyComplete(listeners, new Result(request, response)); } - public void forwardFailure(Response.Listener listener, Response response, Throwable failure) + public void forwardFailure(List listeners, Response response, Throwable failure) { - notifyBegin(listener, response); - notifyHeaders(listener, response); + notifyBegin(listeners, response); + notifyHeaders(listeners, response); if (response instanceof ContentResponse) - notifyContent(listener, response, ByteBuffer.wrap(((ContentResponse)response).getContent())); - notifyFailure(listener, response, failure); + notifyContent(listeners, response, ByteBuffer.wrap(((ContentResponse)response).getContent())); + notifyFailure(listeners, response, failure); } - public void forwardFailureComplete(Response.Listener listener, Request request, Throwable requestFailure, Response response, Throwable responseFailure) + public void forwardFailureComplete(List listeners, Request request, Throwable requestFailure, Response response, Throwable responseFailure) { HttpConversation conversation = client.getConversation(request.getConversationID(), false); - forwardFailure(listener, response, responseFailure); + forwardFailure(listeners, response, responseFailure); conversation.complete(); - notifyComplete(listener, new Result(request, requestFailure, response, responseFailure)); + notifyComplete(listeners, new Result(request, requestFailure, response, responseFailure)); } } 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 23d963ca5c8..64b719447e2 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 @@ -37,7 +37,7 @@ public interface Connection extends AutoCloseable * @param request the request to send * @param listener the response listener */ - void send(Request request, Response.Listener listener); + void send(Request request, Response.CompleteListener listener); @Override void close(); diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java index 6b271c6856e..681bfe81423 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java @@ -210,44 +210,74 @@ public interface Request * @param listenerClass the class of the listener, or null for all listeners classes * @return the listeners for request events of the given class */ - List getListeners(Class listenerClass); + List getRequestListeners(Class listenerClass); /** - * @param listener the listener for request events + * @param listener a listener for request events * @return this request object */ Request listener(Listener listener); /** - * @param listener the listener for request queued events + * @param listener a listener for request queued event * @return this request object */ Request onRequestQueued(QueuedListener listener); /** - * @param listener the listener for request begin events + * @param listener a listener for request begin event * @return this request object */ Request onRequestBegin(BeginListener listener); /** - * @param listener the listener for request headers events + * @param listener a listener for request headers event * @return this request object */ Request onRequestHeaders(HeadersListener listener); /** - * @param listener the listener for request headers events + * @param listener a listener for request success event * @return this request object */ Request onRequestSuccess(SuccessListener listener); /** - * @param listener the listener for request headers events + * @param listener a listener for request failure event * @return this request object */ Request onRequestFailure(FailureListener listener); + /** + * @param listener a listener for response begin event + * @return this request object + */ + Request onResponseBegin(Response.BeginListener listener); + + /** + * @param listener a listener for response headers event + * @return this request object + */ + Request onResponseHeaders(Response.HeadersListener listener); + + /** + * @param listener a listener for response content events + * @return this request object + */ + Request onResponseContent(Response.ContentListener listener); + + /** + * @param listener a listener for response success event + * @return this request object + */ + Request onResponseSuccess(Response.SuccessListener listener); + + /** + * @param listener a listener for response failure event + * @return this request object + */ + Request onResponseFailure(Response.FailureListener listener); + /** * Sends this request and returns a {@link Future} that can be used to wait for the * request and the response to be completed (either with a success or a failure). @@ -273,7 +303,7 @@ public interface Request * * @param listener the listener that receives response events */ - void send(Response.Listener listener); + void send(Response.CompleteListener listener); /** * Attempts to abort the send of this request. diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Response.java b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Response.java index 72c97c7c741..951d4853f66 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Response.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Response.java @@ -19,6 +19,8 @@ package org.eclipse.jetty.client.api; import java.nio.ByteBuffer; +import java.util.EventListener; +import java.util.List; import org.eclipse.jetty.client.util.BufferingResponseListener; import org.eclipse.jetty.http.HttpFields; @@ -42,9 +44,9 @@ public interface Response long getConversationID(); /** - * @return the response listener passed to {@link Request#send(Listener)} + * @return the response listener passed to {@link Request#send(CompleteListener)} */ - Listener getListener(); + List getListeners(Class listenerClass); /** * @return the HTTP version of this response, such as "HTTP/1.1" @@ -74,10 +76,11 @@ public interface Response */ boolean abort(String reason); - /** - * Listener for response events - */ - public interface Listener + public interface ResponseListener extends EventListener + { + } + + public interface BeginListener extends ResponseListener { /** * Callback method invoked when the response line containing HTTP version, @@ -88,14 +91,20 @@ public interface Response * @param response the response containing the response line data */ public void onBegin(Response response); + } + public interface HeadersListener extends ResponseListener + { /** * Callback method invoked when the response headers have been received and parsed. * * @param response the response containing the response line data and the headers */ public void onHeaders(Response response); + } + public interface ContentListener extends ResponseListener + { /** * Callback method invoked when the response content has been received. * This method may be invoked multiple times, and the {@code content} buffer must be consumed @@ -105,14 +114,20 @@ public interface Response * @param content the content bytes received */ public void onContent(Response response, ByteBuffer content); + } + public interface SuccessListener extends ResponseListener + { /** * Callback method invoked when the whole response has been successfully received. * * @param response the response containing the response line data and the headers */ public void onSuccess(Response response); + } + public interface FailureListener extends ResponseListener + { /** * Callback method invoked when the response has failed in the process of being received * @@ -120,7 +135,10 @@ public interface Response * @param failure the failure happened */ public void onFailure(Response response, Throwable failure); + } + public interface CompleteListener extends ResponseListener + { /** * Callback method invoked when the request and the response have been processed, * either successfully or not. @@ -129,13 +147,20 @@ public interface Response *

* Requests may complete after response, for example in case of big uploads that are * discarded or read asynchronously by the server. - * This method is always invoked after {@link #onSuccess(Response)} or - * {@link #onFailure(Response, Throwable)}, and only when request indicates that it is completed. + * This method is always invoked after {@link SuccessListener#onSuccess(Response)} or + * {@link FailureListener#onFailure(Response, Throwable)}, and only when request indicates that + * it is completed. * * @param result the result of the request / response exchange */ public void onComplete(Result result); + } + /** + * Listener for response events + */ + public interface Listener extends BeginListener, HeadersListener, ContentListener, SuccessListener, FailureListener, CompleteListener + { /** * An empty implementation of {@link Listener} */ diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/util/BlockingResponseListener.java b/jetty-client/src/main/java/org/eclipse/jetty/client/util/BlockingResponseListener.java index 12ec25d6a68..bef0fbf305e 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/util/BlockingResponseListener.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/util/BlockingResponseListener.java @@ -46,7 +46,6 @@ public class BlockingResponseListener extends BufferingResponseListener implemen @Override public void onComplete(Result result) { - super.onComplete(result); response = new HttpContentResponse(result.getResponse(), getContent(), getEncoding()); failure = result.getFailure(); latch.countDown(); diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/util/BufferingResponseListener.java b/jetty-client/src/main/java/org/eclipse/jetty/client/util/BufferingResponseListener.java index 1c4a7aa5e49..1eba12ed74e 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/util/BufferingResponseListener.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/util/BufferingResponseListener.java @@ -33,7 +33,7 @@ import org.eclipse.jetty.http.HttpHeader; *

The content may be retrieved from {@link #onSuccess(Response)} or {@link #onComplete(Result)} * via {@link #getContent()} or {@link #getContentAsString()}.

*/ -public class BufferingResponseListener extends Response.Listener.Empty +public abstract class BufferingResponseListener extends Response.Listener.Empty { private final int maxLength; private volatile byte[] buffer = new byte[0]; @@ -95,6 +95,9 @@ public class BufferingResponseListener extends Response.Listener.Empty buffer = newBuffer; } + @Override + public abstract void onComplete(Result result); + public String getEncoding() { return encoding; diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientAuthenticationTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientAuthenticationTest.java index 4358138efc5..ac4eac505ac 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientAuthenticationTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientAuthenticationTest.java @@ -99,14 +99,16 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest public void test_BasicAuthentication() throws Exception { startBasic(new EmptyServerHandler()); - test_Authentication(new BasicAuthentication(scheme + "://localhost:" + connector.getLocalPort(), realm, "basic", "basic")); + String uri = scheme + "://localhost:" + connector.getLocalPort(); + test_Authentication(new BasicAuthentication(uri, realm, "basic", "basic")); } @Test public void test_DigestAuthentication() throws Exception { startDigest(new EmptyServerHandler()); - test_Authentication(new DigestAuthentication(scheme + "://localhost:" + connector.getLocalPort(), realm, "digest", "digest")); + String uri = scheme + "://localhost:" + connector.getLocalPort(); + test_Authentication(new DigestAuthentication(uri, realm, "digest", "digest")); } private void test_Authentication(Authentication authentication) throws Exception @@ -189,7 +191,8 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest } }); - client.getAuthenticationStore().addAuthentication(new BasicAuthentication(scheme + "://localhost:" + connector.getLocalPort(), realm, "basic", "basic")); + String uri = scheme + "://localhost:" + connector.getLocalPort(); + client.getAuthenticationStore().addAuthentication(new BasicAuthentication(uri, realm, "basic", "basic")); final CountDownLatch requests = new CountDownLatch(3); Request.Listener.Empty requestListener = new Request.Listener.Empty() @@ -227,7 +230,8 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest } }); - client.getAuthenticationStore().addAuthentication(new BasicAuthentication(scheme + "://localhost:" + connector.getLocalPort(), realm, "basic", "basic")); + String uri = scheme + "://localhost:" + connector.getLocalPort(); + client.getAuthenticationStore().addAuthentication(new BasicAuthentication(uri, realm, "basic", "basic")); final CountDownLatch requests = new CountDownLatch(3); Request.Listener.Empty requestListener = new Request.Listener.Empty() @@ -268,7 +272,8 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest client.getRequestListeners().add(requestListener); AuthenticationStore authenticationStore = client.getAuthenticationStore(); - BasicAuthentication authentication = new BasicAuthentication(scheme + "://localhost:" + connector.getLocalPort(), realm, "basic", "basic"); + String uri = scheme + "://localhost:" + connector.getLocalPort(); + BasicAuthentication authentication = new BasicAuthentication(uri, realm, "basic", "basic"); authenticationStore.addAuthentication(authentication); Request request = client.newRequest("localhost", connector.getLocalPort()).scheme(scheme).path("/secure"); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientLoadTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientLoadTest.java index 5604bc53595..54cc78811de 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientLoadTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientLoadTest.java @@ -67,7 +67,7 @@ public class HttpClientLoadTest extends AbstractHttpClientServerTest client.setMaxQueueSizePerAddress(1024 * 1024); client.setDispatchIO(false); - Random random = new Random(1000L); + Random random = new Random(); int iterations = 500; CountDownLatch latch = new CountDownLatch(iterations); List failures = new ArrayList<>(); @@ -132,15 +132,15 @@ public class HttpClientLoadTest extends AbstractHttpClientServerTest else if (!ssl && random.nextBoolean()) request.header("X-Close", "true"); + int contentLength = random.nextInt(maxContentLength) + 1; switch (method) { case GET: // Randomly ask the server to download data upon this GET request if (random.nextBoolean()) - request.header("X-Download", String.valueOf(random.nextInt(maxContentLength) + 1)); + request.header("X-Download", String.valueOf(contentLength)); break; case POST: - int contentLength = random.nextInt(maxContentLength) + 1; request.header("X-Upload", String.valueOf(contentLength)); request.content(new BytesContentProvider(new byte[contentLength])); break; diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java index e525e4a6084..5ffc74a0679 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java @@ -371,18 +371,19 @@ public class HttpClientTest extends AbstractHttpClientServerTest latch.countDown(); } }) - .send(new Response.Listener.Empty() + .onResponseFailure(new Response.FailureListener() { @Override public void onFailure(Response response, Throwable failure) { latch.countDown(); } - }); + }) + .send(null); client.newRequest("localhost", connector.getLocalPort()) .scheme(scheme) - .send(new Response.Listener.Empty() + .onResponseSuccess(new Response.SuccessListener() { @Override public void onSuccess(Response response) @@ -390,7 +391,8 @@ public class HttpClientTest extends AbstractHttpClientServerTest Assert.assertEquals(200, response.getStatus()); latch.countDown(); } - }); + }) + .send(null); Assert.assertTrue(latch.await(5 * idleTimeout, TimeUnit.MILLISECONDS)); } diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java index 2d08bb590d0..b0aa3b6135e 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java @@ -76,7 +76,7 @@ public class HttpConnectionLifecycleTest extends AbstractHttpClientServerTest successLatch.countDown(); } }) - .send(new Response.Listener.Empty() + .onResponseHeaders(new Response.HeadersListener() { @Override public void onHeaders(Response response) @@ -85,7 +85,9 @@ public class HttpConnectionLifecycleTest extends AbstractHttpClientServerTest Assert.assertEquals(1, activeConnections.size()); headersLatch.countDown(); } - + }) + .send(new Response.Listener.Empty() + { @Override public void onSuccess(Response response) { 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 6c9487780aa..aa7948e925c 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 @@ -22,6 +22,7 @@ import java.io.ByteArrayOutputStream; import java.io.EOFException; import java.net.URI; import java.nio.ByteBuffer; +import java.util.Collections; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -73,7 +74,7 @@ public class HttpReceiverTest { HttpRequest request = new HttpRequest(client, URI.create("http://localhost")); BlockingResponseListener listener = new BlockingResponseListener(request); - HttpExchange exchange = new HttpExchange(conversation, connection, request, listener); + HttpExchange exchange = new HttpExchange(conversation, connection, request, Collections.singletonList(listener)); conversation.getExchanges().offer(exchange); connection.setExchange(exchange); exchange.requestComplete(null); @@ -89,7 +90,7 @@ public class HttpReceiverTest "Content-length: 0\r\n" + "\r\n"); HttpExchange exchange = newExchange(); - BlockingResponseListener listener = (BlockingResponseListener)exchange.getResponseListener(); + BlockingResponseListener listener = (BlockingResponseListener)exchange.getResponseListeners().get(0); exchange.receive(); Response response = listener.get(5, TimeUnit.SECONDS); @@ -113,7 +114,7 @@ public class HttpReceiverTest "\r\n" + content); HttpExchange exchange = newExchange(); - BlockingResponseListener listener = (BlockingResponseListener)exchange.getResponseListener(); + BlockingResponseListener listener = (BlockingResponseListener)exchange.getResponseListeners().get(0); exchange.receive(); Response response = listener.get(5, TimeUnit.SECONDS); @@ -140,7 +141,7 @@ public class HttpReceiverTest "\r\n" + content1); HttpExchange exchange = newExchange(); - BlockingResponseListener listener = (BlockingResponseListener)exchange.getResponseListener(); + BlockingResponseListener listener = (BlockingResponseListener)exchange.getResponseListeners().get(0); exchange.receive(); endPoint.setInputEOF(); exchange.receive(); @@ -164,7 +165,7 @@ public class HttpReceiverTest "Content-length: 1\r\n" + "\r\n"); HttpExchange exchange = newExchange(); - BlockingResponseListener listener = (BlockingResponseListener)exchange.getResponseListener(); + BlockingResponseListener listener = (BlockingResponseListener)exchange.getResponseListeners().get(0); exchange.receive(); // Simulate an idle timeout connection.idleTimeout(); @@ -188,7 +189,7 @@ public class HttpReceiverTest "Content-length: A\r\n" + "\r\n"); HttpExchange exchange = newExchange(); - BlockingResponseListener listener = (BlockingResponseListener)exchange.getResponseListener(); + BlockingResponseListener listener = (BlockingResponseListener)exchange.getResponseListeners().get(0); exchange.receive(); try @@ -219,7 +220,7 @@ public class HttpReceiverTest "Content-Encoding: gzip\r\n" + "\r\n"); HttpExchange exchange = newExchange(); - BlockingResponseListener listener = (BlockingResponseListener)exchange.getResponseListener(); + BlockingResponseListener listener = (BlockingResponseListener)exchange.getResponseListeners().get(0); exchange.receive(); endPoint.reset(); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpResponseAbortTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpResponseAbortTest.java index 4b16b688bc5..74efb0cd14c 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpResponseAbortTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpResponseAbortTest.java @@ -55,14 +55,16 @@ public class HttpResponseAbortTest extends AbstractHttpClientServerTest final CountDownLatch latch = new CountDownLatch(1); client.newRequest("localhost", connector.getLocalPort()) .scheme(scheme) - .send(new Response.Listener.Empty() + .onResponseBegin(new Response.BeginListener() { @Override public void onBegin(Response response) { response.abort(null); } - + }) + .send(new Response.CompleteListener() + { @Override public void onComplete(Result result) { @@ -81,13 +83,16 @@ public class HttpResponseAbortTest extends AbstractHttpClientServerTest final CountDownLatch latch = new CountDownLatch(1); client.newRequest("localhost", connector.getLocalPort()) .scheme(scheme) - .send(new Response.Listener.Empty() + .onResponseHeaders(new Response.HeadersListener() { @Override public void onHeaders(Response response) { response.abort(null); } + }) + .send(new Response.CompleteListener() + { @Override public void onComplete(Result result) @@ -126,14 +131,16 @@ public class HttpResponseAbortTest extends AbstractHttpClientServerTest final CountDownLatch latch = new CountDownLatch(1); client.newRequest("localhost", connector.getLocalPort()) .scheme(scheme) - .send(new Response.Listener.Empty() + .onResponseContent(new Response.ContentListener() { @Override public void onContent(Response response, ByteBuffer content) { response.abort(null); } - + }) + .send(new Response.CompleteListener() + { @Override public void onComplete(Result result) { diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpSenderTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpSenderTest.java index 8a640b50848..25136c830a8 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpSenderTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpSenderTest.java @@ -79,7 +79,7 @@ public class HttpSenderTest successLatch.countDown(); } }); - connection.send(request, null); + connection.send(request, (Response.CompleteListener)null); String requestString = endPoint.takeOutputString(); Assert.assertTrue(requestString.startsWith("GET ")); @@ -96,7 +96,7 @@ public class HttpSenderTest HttpDestination destination = new HttpDestination(client, "http", "localhost", 8080); HttpConnection connection = new HttpConnection(client, endPoint, destination); Request request = client.newRequest(URI.create("http://localhost/")); - connection.send(request, null); + connection.send(request, (Response.CompleteListener)null); // This take will free space in the buffer and allow for the write to complete StringBuilder builder = new StringBuilder(endPoint.takeOutputString()); @@ -126,7 +126,7 @@ public class HttpSenderTest HttpConnection connection = new HttpConnection(client, endPoint, destination); Request request = client.newRequest(URI.create("http://localhost/")); final CountDownLatch failureLatch = new CountDownLatch(2); - request.onRequestFailure(new Request.FailureListener() + request.listener(new Request.Listener.Empty() { @Override public void onFailure(Request request, Throwable x) @@ -155,7 +155,7 @@ public class HttpSenderTest HttpConnection connection = new HttpConnection(client, endPoint, destination); Request request = client.newRequest(URI.create("http://localhost/")); final CountDownLatch failureLatch = new CountDownLatch(2); - request.onRequestFailure(new Request.FailureListener() + request.listener(new Request.Listener.Empty() { @Override public void onFailure(Request request, Throwable x) @@ -207,7 +207,7 @@ public class HttpSenderTest successLatch.countDown(); } }); - connection.send(request, null); + connection.send(request, (Response.CompleteListener)null); String requestString = endPoint.takeOutputString(); Assert.assertTrue(requestString.startsWith("GET ")); @@ -242,7 +242,7 @@ public class HttpSenderTest successLatch.countDown(); } }); - connection.send(request, null); + connection.send(request, (Response.CompleteListener)null); String requestString = endPoint.takeOutputString(); Assert.assertTrue(requestString.startsWith("GET ")); @@ -284,7 +284,7 @@ public class HttpSenderTest successLatch.countDown(); } }); - connection.send(request, null); + connection.send(request, (Response.CompleteListener)null); String requestString = endPoint.takeOutputString(); Assert.assertTrue(requestString.startsWith("GET ")); From b3c1accab922cc70cee687234a4a2e212e066c79 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Tue, 30 Oct 2012 22:37:25 +0100 Subject: [PATCH 10/31] HTTP client: fixed bug in redirects: the new host was overwritten with the old one. --- .../org/eclipse/jetty/client/HttpClient.java | 7 ++ .../jetty/client/ExternalSiteTest.java | 85 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 jetty-client/src/test/java/org/eclipse/jetty/client/ExternalSiteTest.java 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 224c16d464c..d4fc27fb973 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 @@ -46,6 +46,7 @@ import org.eclipse.jetty.client.api.Destination; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.EndPoint; @@ -273,7 +274,13 @@ public class HttpClient extends ContainerLifeCycle .version(oldRequest.getVersion()) .content(oldRequest.getContent()); for (HttpFields.Field header : oldRequest.getHeaders()) + { + // We have a new URI, so skip the host header if present + if (HttpHeader.HOST == header.getHeader()) + continue; + newRequest.header(header.getName(), header.getValue()); + } return newRequest; } diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/ExternalSiteTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ExternalSiteTest.java new file mode 100644 index 00000000000..e21b73f5f13 --- /dev/null +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ExternalSiteTest.java @@ -0,0 +1,85 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 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.client; + +import java.io.IOException; +import java.net.Socket; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.client.api.Result; +import org.eclipse.jetty.toolchain.test.TestTracker; +import org.junit.After; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +public class ExternalSiteTest +{ + @Rule + public final TestTracker tracker = new TestTracker(); + + private HttpClient client; + + @Before + public void prepare() throws Exception + { + client = new HttpClient(); + client.start(); + } + + @After + public void dispose() throws Exception + { + client.stop(); + } + + @Test + public void testExternalSite() throws Exception + { + String host = "wikipedia.org"; + int port = 80; + + // Verify that we have connectivity + try + { + new Socket(host, port); + } + catch (IOException x) + { + Assume.assumeNoException(x); + } + + final CountDownLatch latch = new CountDownLatch(1); + client.newRequest(host, port).send(new Response.CompleteListener() + { + @Override + public void onComplete(Result result) + { + if (!result.isFailed() && result.getResponse().getStatus() == 200) + latch.countDown(); + } + }); + + Assert.assertTrue(latch.await(10, TimeUnit.SECONDS)); + } +} From 61c607076aef6e7eea768f6cbbf926889661a926 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Wed, 24 Oct 2012 10:53:13 -0700 Subject: [PATCH 11/31] Mux interrim work. --- .../org/eclipse/jetty/util/StringUtil.java | 23 + .../jetty/websocket/core/api/Extension.java | 11 + .../core/extensions/mux/MuxChannel.java | 331 ++++++++++++++ .../core/extensions/mux/MuxControlBlock.java | 24 + .../core/extensions/mux/MuxException.java | 40 ++ .../core/extensions/mux/MuxExtension.java | 66 +++ .../core/extensions/mux/MuxGenerator.java | 274 ++++++++++++ .../websocket/core/extensions/mux/MuxOp.java | 28 ++ .../core/extensions/mux/MuxParser.java | 409 ++++++++++++++++++ .../mux/MuxPhysicalConnectionException.java | 44 ++ .../core/extensions/mux/MuxedFrame.java | 73 ++++ .../websocket/core/extensions/mux/Muxer.java | 397 +++++++++++++++++ .../core/extensions/mux/add/MuxAddClient.java | 30 ++ .../core/extensions/mux/add/MuxAddServer.java | 33 ++ .../mux/op/MuxAddChannelRequest.java | 94 ++++ .../mux/op/MuxAddChannelResponse.java | 105 +++++ .../extensions/mux/op/MuxDropChannel.java | 183 ++++++++ .../extensions/mux/op/MuxFlowControl.java | 65 +++ .../extensions/mux/op/MuxNewChannelSlot.java | 76 ++++ .../core/protocol/WebSocketFrame.java | 4 + .../core/extensions/mux/MuxEventCapture.java | 118 +++++ .../mux/MuxGeneratorWrite139SizeTest.java | 96 ++++ .../mux/MuxGeneratorWriteChannelIdTest.java | 100 +++++ .../core/extensions/mux/MuxInjector.java | 60 +++ .../core/extensions/mux/MuxParserRFCTest.java | 232 ++++++++++ .../MuxParserRead139Size_BadEncodingTest.java | 104 +++++ .../mux/MuxParserRead139Size_GoodTest.java | 99 +++++ ...uxParserReadChannelId_BadEncodingTest.java | 106 +++++ .../mux/MuxParserReadChannelId_GoodTest.java | 106 +++++ .../core/extensions/mux/MuxReducer.java | 47 ++ .../extensions/mux/add/DummyMuxAddServer.java | 60 +++ .../mux/add/MuxerAddServerTest.java | 81 ++++ .../core/io/LocalWebSocketConnection.java | 8 +- .../test/resources/jetty-logging.properties | 2 +- 34 files changed, 3527 insertions(+), 2 deletions(-) create mode 100644 jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxChannel.java create mode 100644 jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxControlBlock.java create mode 100644 jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxException.java create mode 100644 jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxExtension.java create mode 100644 jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxGenerator.java create mode 100644 jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxOp.java create mode 100644 jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxParser.java create mode 100644 jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxPhysicalConnectionException.java create mode 100644 jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxedFrame.java create mode 100644 jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/Muxer.java create mode 100644 jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/add/MuxAddClient.java create mode 100644 jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/add/MuxAddServer.java create mode 100644 jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/op/MuxAddChannelRequest.java create mode 100644 jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/op/MuxAddChannelResponse.java create mode 100644 jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/op/MuxDropChannel.java create mode 100644 jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/op/MuxFlowControl.java create mode 100644 jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/op/MuxNewChannelSlot.java create mode 100644 jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxEventCapture.java create mode 100644 jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxGeneratorWrite139SizeTest.java create mode 100644 jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxGeneratorWriteChannelIdTest.java create mode 100644 jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxInjector.java create mode 100644 jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxParserRFCTest.java create mode 100644 jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxParserRead139Size_BadEncodingTest.java create mode 100644 jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxParserRead139Size_GoodTest.java create mode 100644 jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxParserReadChannelId_BadEncodingTest.java create mode 100644 jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxParserReadChannelId_GoodTest.java create mode 100644 jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxReducer.java create mode 100644 jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/add/DummyMuxAddServer.java create mode 100644 jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/add/MuxerAddServerTest.java diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java b/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java index b86794899ec..25661424528 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java @@ -700,4 +700,27 @@ public class StringUtil return minus?(-val):val; throw new NumberFormatException(string); } + + /** + * Truncate a string to a max size. + * + * @param str the string to possibly truncate + * @param maxSize the maximum size of the string + * @return the truncated string. if str param is null, then the returned string will also be null. + */ + public static String truncate(String str, int maxSize) + { + if (str == null) + { + return null; + } + + if (str.length() <= maxSize) + { + return str; + } + + return str.substring(0,maxSize); + } + } diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/api/Extension.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/api/Extension.java index 50a2d13301f..3d54f72943d 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/api/Extension.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/api/Extension.java @@ -38,6 +38,7 @@ public abstract class Extension implements OutgoingFrames, IncomingFrames private ExtensionConfig config; private IncomingFrames nextIncomingFrames; private OutgoingFrames nextOutgoingFrames; + private WebSocketConnection connection; public ByteBufferPool getBufferPool() { @@ -49,6 +50,11 @@ public abstract class Extension implements OutgoingFrames, IncomingFrames return config; } + public WebSocketConnection getConnection() + { + return connection; + } + public String getName() { return config.getName(); @@ -211,6 +217,11 @@ public abstract class Extension implements OutgoingFrames, IncomingFrames this.config = config; } + public void setConnection(WebSocketConnection connection) + { + this.connection = connection; + } + public void setNextIncomingFrames(IncomingFrames nextIncomingFramesHandler) { this.nextIncomingFrames = nextIncomingFramesHandler; diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxChannel.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxChannel.java new file mode 100644 index 00000000000..1f95e2e8427 --- /dev/null +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxChannel.java @@ -0,0 +1,331 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core.extensions.mux; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.FutureCallback; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.websocket.core.api.BaseConnection.SuspendToken; +import org.eclipse.jetty.websocket.core.api.Extension; +import org.eclipse.jetty.websocket.core.api.StatusCode; +import org.eclipse.jetty.websocket.core.api.WebSocketConnection; +import org.eclipse.jetty.websocket.core.api.WebSocketException; +import org.eclipse.jetty.websocket.core.api.WebSocketPolicy; +import org.eclipse.jetty.websocket.core.io.IncomingFrames; +import org.eclipse.jetty.websocket.core.io.OutgoingFrames; +import org.eclipse.jetty.websocket.core.io.WebSocketSession; +import org.eclipse.jetty.websocket.core.protocol.CloseInfo; +import org.eclipse.jetty.websocket.core.protocol.ConnectionState; +import org.eclipse.jetty.websocket.core.protocol.WebSocketFrame; + +/** + * MuxChannel, acts as WebSocketConnection for specific sub-channel. + */ +public class MuxChannel implements WebSocketConnection, IncomingFrames, OutgoingFrames, SuspendToken +{ + private static final Logger LOG = Log.getLogger(MuxChannel.class); + + private final long channelId; + private final Muxer muxer; + private final AtomicBoolean inputClosed; + private final AtomicBoolean outputClosed; + private final AtomicBoolean suspendToken; + private ConnectionState connectionState; + private WebSocketPolicy policy; + private WebSocketSession session; + private IncomingFrames incoming; + private String subProtocol; + + public MuxChannel(long channelId, Muxer muxer) + { + this.channelId = channelId; + this.muxer = muxer; + this.policy = muxer.getPolicy().clonePolicy(); + + this.suspendToken = new AtomicBoolean(false); + this.connectionState = ConnectionState.CONNECTING; + + this.inputClosed = new AtomicBoolean(false); + this.outputClosed = new AtomicBoolean(false); + } + + @Override + public void close() + { + close(StatusCode.NORMAL,null); + } + + @Override + public void close(int statusCode, String reason) + { + CloseInfo close = new CloseInfo(statusCode,reason); + try + { + output("",new FutureCallback<>(),close.asFrame()); + } + catch (IOException e) + { + LOG.warn("Unable to issue Close",e); + disconnect(); + } + } + + @Override + public void disconnect() + { + this.connectionState = ConnectionState.CLOSED; + // TODO: disconnect the virtual end-point? + } + + public long getChannelId() + { + return channelId; + } + + @Override + public WebSocketPolicy getPolicy() + { + return policy; + } + + @Override + public InetSocketAddress getRemoteAddress() + { + return muxer.getRemoteAddress(); + } + + public WebSocketSession getSession() + { + return session; + } + + @Override + public ConnectionState getState() + { + return this.connectionState; + } + + @Override + public String getSubProtocol() + { + return this.subProtocol; + } + + /** + * Incoming exceptions from Muxer. + */ + @Override + public void incoming(WebSocketException e) + { + incoming.incoming(e); + } + + /** + * Incoming frames from Muxer + */ + @Override + public void incoming(WebSocketFrame frame) + { + incoming.incoming(frame); + } + + public boolean isActive() + { + return (getState() != ConnectionState.CLOSED); + } + + @Override + public boolean isInputClosed() + { + return inputClosed.get(); + } + + @Override + public boolean isOpen() + { + return isActive() && muxer.isOpen(); + } + + @Override + public boolean isOutputClosed() + { + return outputClosed.get(); + } + + @Override + public boolean isReading() + { + return true; + } + + public void onClose() + { + this.connectionState = ConnectionState.CLOSED; + } + + @Override + public void onCloseHandshake(boolean incoming, CloseInfo close) + { + boolean in = inputClosed.get(); + boolean out = outputClosed.get(); + if (incoming) + { + in = true; + this.inputClosed.set(true); + } + else + { + out = true; + this.outputClosed.set(true); + } + + LOG.debug("onCloseHandshake({},{}), input={}, output={}",incoming,close,in,out); + + if (in && out) + { + LOG.debug("Close Handshake satisfied, disconnecting"); + this.disconnect(); + } + + if (close.isHarsh()) + { + LOG.debug("Close status code was harsh, disconnecting"); + this.disconnect(); + } + } + + public void onOpen() + { + this.connectionState = ConnectionState.OPEN; + } + + /** + * Frames destined for the Muxer + */ + @Override + public void output(C context, Callback callback, WebSocketFrame frame) throws IOException + { + muxer.output(context,callback,channelId,frame); + } + + /** + * Ping frame destined for the Muxer + */ + @Override + public void ping(C context, Callback callback, byte[] payload) throws IOException + { + output(context,callback,WebSocketFrame.ping().setPayload(payload)); + } + + @Override + public void resume() + { + if (suspendToken.getAndSet(false)) + { + // TODO: Start reading again. (how?) + } + } + + public void setSession(WebSocketSession session) + { + this.session = session; + } + + public void setSubProtocol(String subProtocol) + { + this.subProtocol = subProtocol; + } + + @Override + public SuspendToken suspend() + { + suspendToken.set(true); + // TODO: how to suspend reading? + return this; + } + + public void wireUpExtensions(List extensions) + { + // Start with default routing. + incoming = session; + OutgoingFrames outgoing = this; + + if (extensions != null) + { + Iterator extIter; + // Connect outgoings + extIter = extensions.iterator(); + while (extIter.hasNext()) + { + Extension ext = extIter.next(); + ext.setNextOutgoingFrames(outgoing); + outgoing = ext; + } + + // Connect incomings + Collections.reverse(extensions); + extIter = extensions.iterator(); + while (extIter.hasNext()) + { + Extension ext = extIter.next(); + ext.setNextIncomingFrames(incoming); + incoming = ext; + } + } + + // set outgoing + this.session.setOutgoing(outgoing); + } + + /** + * Generate a binary message, destined for Muxer + */ + @Override + public void write(C context, Callback callback, byte[] buf, int offset, int len) throws IOException + { + output(context,callback,WebSocketFrame.binary().setPayload(buf,offset,len)); + } + + /** + * Generate a binary message, destined for Muxer + */ + @Override + public void write(C context, Callback callback, ByteBuffer buffer) throws IOException + { + output(context,callback,WebSocketFrame.binary().setPayload(buffer)); + } + + /** + * Generate a text message, destined for Muxer + */ + @Override + public void write(C context, Callback callback, String message) throws IOException + { + output(context,callback,WebSocketFrame.text(message)); + } +} diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxControlBlock.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxControlBlock.java new file mode 100644 index 00000000000..b617b545b70 --- /dev/null +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxControlBlock.java @@ -0,0 +1,24 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core.extensions.mux; + +public interface MuxControlBlock +{ + public int getOpCode(); +} diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxException.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxException.java new file mode 100644 index 00000000000..f7b13cc686a --- /dev/null +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxException.java @@ -0,0 +1,40 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core.extensions.mux; + +import org.eclipse.jetty.websocket.core.api.WebSocketException; + +@SuppressWarnings("serial") +public class MuxException extends WebSocketException +{ + public MuxException(String message) + { + super(message); + } + + public MuxException(String message, Throwable cause) + { + super(message,cause); + } + + public MuxException(Throwable cause) + { + super(cause); + } +} diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxExtension.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxExtension.java new file mode 100644 index 00000000000..0b8fc7040a4 --- /dev/null +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxExtension.java @@ -0,0 +1,66 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core.extensions.mux; + +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.websocket.core.api.Extension; +import org.eclipse.jetty.websocket.core.api.WebSocketException; +import org.eclipse.jetty.websocket.core.protocol.WebSocketFrame; + +/** + * Multiplexing Extension for WebSockets. + *

+ * Supporting draft-ietf-hybi-websocket-multiplexing-08 Specification. + */ +public class MuxExtension extends Extension +{ + private Muxer muxer; + + public MuxExtension() + { + super(); + } + + public synchronized Muxer getMuxer() + { + if (this.muxer == null) + { + this.muxer = new Muxer(super.getConnection(),this); + } + return muxer; + } + + @Override + public void incoming(WebSocketException e) + { + getMuxer().incoming(e); + } + + @Override + public void incoming(WebSocketFrame frame) + { + getMuxer().incoming(frame); + } + + @Override + public void output(C context, Callback callback, WebSocketFrame frame) throws java.io.IOException + { + nextOutput(context,callback,frame); + } +} diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxGenerator.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxGenerator.java new file mode 100644 index 00000000000..6d2ae15f7b6 --- /dev/null +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxGenerator.java @@ -0,0 +1,274 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core.extensions.mux; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.eclipse.jetty.io.ArrayByteBufferPool; +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.FutureCallback; +import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxAddChannelRequest; +import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxAddChannelResponse; +import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxDropChannel; +import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxFlowControl; +import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxNewChannelSlot; +import org.eclipse.jetty.websocket.core.io.OutgoingFrames; +import org.eclipse.jetty.websocket.core.protocol.WebSocketFrame; + +/** + * Generate Mux frames destined for the physical connection. + */ +public class MuxGenerator +{ + private static final int CONTROL_BUFFER_SIZE = 2 * 1024; + /** 4 bytes for channel ID + 1 for fin/rsv/opcode */ + private static final int DATA_FRAME_OVERHEAD = 5; + private ByteBufferPool bufferPool; + private OutgoingFrames outgoing; + + public MuxGenerator() + { + this(new ArrayByteBufferPool()); + } + + public MuxGenerator(ByteBufferPool bufferPool) + { + this.bufferPool = bufferPool; + } + + public void generate(long channelId, WebSocketFrame frame) throws IOException + { + output(null, new FutureCallback<>(), channelId, frame); + } + + public void generate(MuxControlBlock... blocks) throws IOException + { + if ((blocks == null) || (blocks.length <= 0)) + { + return; // nothing to do + } + + ByteBuffer payload = bufferPool.acquire(CONTROL_BUFFER_SIZE,false); + BufferUtil.flipToFill(payload); + + writeChannelId(payload,0); // control channel + + for (MuxControlBlock block : blocks) + { + switch (block.getOpCode()) + { + case MuxOp.ADD_CHANNEL_REQUEST: + { + MuxAddChannelRequest op = (MuxAddChannelRequest)block; + byte b = (byte)((op.getOpCode() & 0x07) << 5); // opcode + b |= (byte)((op.getRsv() & 0x07) << 2); // rsv + b |= (op.getEnc() & 0x03); // enc + payload.put(b); // opcode + rsv + enc + writeChannelId(payload,op.getChannelId()); + write139Buffer(payload,op.getHandshake()); + break; + } + case MuxOp.ADD_CHANNEL_RESPONSE: + { + MuxAddChannelResponse op = (MuxAddChannelResponse)block; + byte b = (byte)((op.getOpCode() & 0x07) << 5); // opcode + b |= (op.isFailed()?0x10:0x00); // failure bit + b |= (byte)((op.getRsv() & 0x03) << 2); // rsv + b |= (op.getEnc() & 0x03); // enc + payload.put(b); // opcode + f + rsv + enc + writeChannelId(payload,op.getChannelId()); + if (op.getHandshake() != null) + { + write139Buffer(payload,op.getHandshake()); + } + else + { + // no handshake details + write139Size(payload,0); + } + break; + } + case MuxOp.DROP_CHANNEL: + { + MuxDropChannel op = (MuxDropChannel)block; + byte b = (byte)((op.getOpCode() & 0x07) << 5); // opcode + b |= (byte)(op.getRsv() & 0x1F); // rsv + payload.put(b); // opcode + rsv + writeChannelId(payload,op.getChannelId()); + write139Buffer(payload,op.asReasonBuffer()); + break; + } + case MuxOp.FLOW_CONTROL: + { + MuxFlowControl op = (MuxFlowControl)block; + byte b = (byte)((op.getOpCode() & 0x07) << 5); // opcode + b |= (byte)(op.getRsv() & 0x1F); // rsv + payload.put(b); // opcode + rsv + writeChannelId(payload,op.getChannelId()); + write139Size(payload,op.getSendQuotaSize()); + break; + } + case MuxOp.NEW_CHANNEL_SLOT: + { + MuxNewChannelSlot op = (MuxNewChannelSlot)block; + byte b = (byte)((op.getOpCode() & 0x07) << 5); // opcode + b |= (byte)(op.getRsv() & 0x0F) << 1; // rsv + b |= (byte)(op.isFallback()?0x01:0x00); // fallback bit + payload.put(b); // opcode + rsv + fallback bit + write139Size(payload,op.getNumberOfSlots()); + write139Size(payload,op.getInitialSendQuota()); + break; + } + } + } + BufferUtil.flipToFlush(payload,0); + WebSocketFrame frame = WebSocketFrame.binary(); + frame.setPayload(payload); + outgoing.output(null,new FutureCallback<>(),frame); + } + + public OutgoingFrames getOutgoing() + { + return outgoing; + } + + public void output(C context, Callback callback, long channelId, WebSocketFrame frame) throws IOException + { + ByteBuffer muxPayload = bufferPool.acquire(frame.getPayloadLength() + DATA_FRAME_OVERHEAD,false); + + // start building mux payload + writeChannelId(muxPayload,channelId); + byte b = (byte)(frame.isFin()?0x80:0x00); // fin + b |= (byte)(frame.isRsv1()?0x40:0x00); // rsv1 + b |= (byte)(frame.isRsv2()?0x20:0x00); // rsv2 + b |= (byte)(frame.isRsv3()?0x10:0x00); // rsv3 + b |= (byte)(frame.getOpCode() & 0x0F); // opcode + muxPayload.put(b); + BufferUtil.put(frame.getPayload(),muxPayload); + + // build muxed frame + WebSocketFrame muxFrame = WebSocketFrame.binary(); + muxFrame.setPayload(muxPayload); + // NOTE: the physical connection will handle masking rules for this frame. + + // release original buffer (no longer needed) + bufferPool.release(frame.getPayload()); + + // send muxed frame down to the physical connection. + outgoing.output(context,callback,muxFrame); + } + + public void setOutgoing(OutgoingFrames outgoing) + { + this.outgoing = outgoing; + } + + /** + * Write a 1/3/9 encoded size, then a byte buffer of that size. + * + * @param payload + * @param buffer + */ + public void write139Buffer(ByteBuffer payload, ByteBuffer buffer) + { + write139Size(payload,buffer.remaining()); + writeBuffer(payload,buffer); + } + + /** + * Write a 1/3/9 encoded size. + * + * @param payload + * @param size + */ + public void write139Size(ByteBuffer payload, long size) + { + if (size > 0xFF_FF) + { + // 9 byte encoded + payload.put((byte)0x7F); + payload.putLong(size); + return; + } + + if (size >= 0x7E) + { + // 3 byte encoded + payload.put((byte)0x7E); + payload.put((byte)(size >> 8)); + payload.put((byte)(size & 0xFF)); + return; + } + + // 1 byte (7 bit) encoded + payload.put((byte)(size & 0x7F)); + } + + public void writeBuffer(ByteBuffer payload, ByteBuffer buffer) + { + BufferUtil.put(buffer,payload); + } + + /** + * Write multiplexing channel id, using logical channel id encoding (of 1,2,3, or 4 octets) + * + * @param payload + * @param channelId + */ + public void writeChannelId(ByteBuffer payload, long channelId) + { + if (channelId > 0x1F_FF_FF_FF) + { + throw new MuxException("Illegal Channel ID: too big"); + } + + if (channelId > 0x1F_FF_FF) + { + // 29 bit channel id (4 bytes) + payload.put((byte)(0xE0 | ((channelId >> 24) & 0x1F))); + payload.put((byte)((channelId >> 16) & 0xFF)); + payload.put((byte)((channelId >> 8) & 0xFF)); + payload.put((byte)(channelId & 0xFF)); + return; + } + + if (channelId > 0x3F_FF) + { + // 21 bit channel id (3 bytes) + payload.put((byte)(0xC0 | ((channelId >> 16) & 0x1F))); + payload.put((byte)((channelId >> 8) & 0xFF)); + payload.put((byte)(channelId & 0xFF)); + return; + } + + if (channelId > 0x7F) + { + // 14 bit channel id (2 bytes) + payload.put((byte)(0x80 | ((channelId >> 8) & 0x3F))); + payload.put((byte)(channelId & 0xFF)); + return; + } + + // 7 bit channel id + payload.put((byte)(channelId & 0x7F)); + } +} diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxOp.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxOp.java new file mode 100644 index 00000000000..2b2af55e4df --- /dev/null +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxOp.java @@ -0,0 +1,28 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core.extensions.mux; + +public final class MuxOp +{ + public static final byte ADD_CHANNEL_REQUEST = 0; + public static final byte ADD_CHANNEL_RESPONSE = 1; + public static final byte FLOW_CONTROL = 2; + public static final byte DROP_CHANNEL = 3; + public static final byte NEW_CHANNEL_SLOT = 4; +} diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxParser.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxParser.java new file mode 100644 index 00000000000..4e3c2d29a5f --- /dev/null +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxParser.java @@ -0,0 +1,409 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core.extensions.mux; + +import java.nio.ByteBuffer; + +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxAddChannelRequest; +import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxAddChannelResponse; +import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxDropChannel; +import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxFlowControl; +import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxNewChannelSlot; +import org.eclipse.jetty.websocket.core.protocol.OpCode; +import org.eclipse.jetty.websocket.core.protocol.WebSocketFrame; + +public class MuxParser +{ + public static interface Listener + { + public void onMuxAddChannelRequest(MuxAddChannelRequest request); + + public void onMuxAddChannelResponse(MuxAddChannelResponse response); + + public void onMuxDropChannel(MuxDropChannel drop); + + public void onMuxedFrame(MuxedFrame frame); + + public void onMuxException(MuxException e); + + public void onMuxFlowControl(MuxFlowControl flow); + + public void onMuxNewChannelSlot(MuxNewChannelSlot slot); + } + + private final static Logger LOG = Log.getLogger(MuxParser.class); + + private MuxedFrame muxframe = new MuxedFrame(); + private MuxParser.Listener events; + private long channelId; + + public MuxParser.Listener getEvents() + { + return events; + } + + /** + * Parse the raw {@link WebSocketFrame} payload data for various Mux frames. + * + * @param frame + * the WebSocketFrame to parse for mux payload + */ + public synchronized void parse(WebSocketFrame frame) + { + if (events == null) + { + throw new RuntimeException("No " + MuxParser.Listener.class + " specified"); + } + + if (!frame.hasPayload()) + { + LOG.debug("No payload data, skipping"); + return; // nothing to parse + } + + if (frame.getOpCode() != OpCode.BINARY) + { + LOG.debug("Not a binary opcode (base frame), skipping"); + return; // not a binary opcode + } + + LOG.debug("Parsing Mux Payload of {}",frame); + + try + { + ByteBuffer buffer = frame.getPayload().slice(); + + if (buffer.remaining() <= 0) + { + return; + } + + if (frame.isContinuation()) + { + muxframe.reset(); + muxframe.setFin(frame.isFin()); + muxframe.setFin(frame.isRsv1()); + muxframe.setFin(frame.isRsv2()); + muxframe.setFin(frame.isRsv3()); + muxframe.setContinuation(true); + parseDataFramePayload(buffer); + } + else + { + // new frame + channelId = readChannelId(buffer); + if (channelId == 0) + { + parseControlBlocks(buffer); + } + else + { + parseDataFrame(buffer); + } + } + } + catch (MuxException e) + { + events.onMuxException(e); + } + catch (Throwable t) + { + events.onMuxException(new MuxException(t)); + } + } + + private void parseControlBlocks(ByteBuffer buffer) + { + // process the remaining buffer here. + while (buffer.remaining() > 0) + { + byte b = buffer.get(); + byte opc = (byte)((byte)(b >> 5) & 0xFF); + b = (byte)(b & 0x1F); + + try { + switch (opc) + { + case MuxOp.ADD_CHANNEL_REQUEST: + { + MuxAddChannelRequest op = new MuxAddChannelRequest(); + op.setRsv((byte)((b & 0x1C) >> 2)); + op.setEnc((byte)(b & 0x03)); + op.setChannelId(readChannelId(buffer)); + long handshakeSize = read139EncodedSize(buffer); + op.setHandshake(readBlock(buffer,handshakeSize)); + events.onMuxAddChannelRequest(op); + break; + } + case MuxOp.ADD_CHANNEL_RESPONSE: + { + MuxAddChannelResponse op = new MuxAddChannelResponse(); + op.setFailed((b & 0x10) != 0); + op.setRsv((byte)((byte)(b & 0x0C) >> 2)); + op.setEnc((byte)(b & 0x03)); + op.setChannelId(readChannelId(buffer)); + long handshakeSize = read139EncodedSize(buffer); + op.setHandshake(readBlock(buffer,handshakeSize)); + events.onMuxAddChannelResponse(op); + break; + } + case MuxOp.DROP_CHANNEL: + { + int rsv = (b & 0x1F); + long channelId = readChannelId(buffer); + long reasonSize = read139EncodedSize(buffer); + ByteBuffer reasonBuf = readBlock(buffer,reasonSize); + MuxDropChannel op = MuxDropChannel.parse(channelId,reasonBuf); + op.setRsv(rsv); + events.onMuxDropChannel(op); + break; + } + case MuxOp.FLOW_CONTROL: + { + MuxFlowControl op = new MuxFlowControl(); + op.setRsv((byte)(b & 0x1F)); + op.setChannelId(readChannelId(buffer)); + op.setSendQuotaSize(read139EncodedSize(buffer)); + events.onMuxFlowControl(op); + break; + } + case MuxOp.NEW_CHANNEL_SLOT: + { + MuxNewChannelSlot op = new MuxNewChannelSlot(); + op.setRsv((byte)((b & 0x1E) >> 1)); + op.setFallback((b & 0x01) != 0); + op.setNumberOfSlots(read139EncodedSize(buffer)); + op.setInitialSendQuota(read139EncodedSize(buffer)); + events.onMuxNewChannelSlot(op); + break; + } + default: + { + String err = String.format("Unknown Mux Control Code OPC [0x%X]",opc); + throw new MuxException(err); + } + } + } + catch (Throwable t) + { + LOG.warn(t); + throw new MuxException(t); + } + } + } + + private void parseDataFrame(ByteBuffer buffer) + { + byte b = buffer.get(); + boolean fin = ((b & 0x80) != 0); + boolean rsv1 = ((b & 0x40) != 0); + boolean rsv2 = ((b & 0x20) != 0); + boolean rsv3 = ((b & 0x10) != 0); + byte opcode = (byte)(b & 0x0F); + + if (opcode == OpCode.CONTINUATION) + { + muxframe.setContinuation(true); + } + else + { + muxframe.reset(); + muxframe.setOpCode(opcode); + } + + muxframe.setChannelId(channelId); + muxframe.setFin(fin); + muxframe.setRsv1(rsv1); + muxframe.setRsv2(rsv2); + muxframe.setRsv3(rsv3); + + parseDataFramePayload(buffer); + } + + private void parseDataFramePayload(ByteBuffer buffer) + { + int capacity = buffer.remaining(); + ByteBuffer payload = ByteBuffer.allocate(capacity); + payload.put(buffer); + BufferUtil.flipToFlush(payload,0); + muxframe.setPayload(payload); + try + { + LOG.debug("notifyFrame() - {}",muxframe); + events.onMuxedFrame(muxframe); + } + catch (Throwable t) + { + LOG.warn(t); + } + } + + /** + * Per section 9.1. Number Encoding in Multiplex Control + * Blocks, read the 1/3/9 byte length using Section 5.2 of RFC 6455. + * + * @param buffer + * the buffer to read from + * @return the decoded size + * @throws MuxException + * when the encoding does not make sense per the spec, or it is a value above {@link Long#MAX_VALUE} + */ + public long read139EncodedSize(ByteBuffer buffer) + { + long ret = -1; + long minValue = 0x00; // used to validate minimum # of bytes (per spec) + int cursor = 0; + + byte b = buffer.get(); + ret = (b & 0x7F); + + if (ret == 0x7F) + { + // 9 byte length + ret = 0; + minValue = 0xFF_FF; + cursor = 8; + } + else if (ret == 0x7E) + { + // 3 byte length + ret = 0; + minValue = 0x7F; + cursor = 2; + } + else + { + // 1 byte length + // no validation of minimum bytes needed here + return ret; + } + + // parse multi-byte length + while (cursor > 0) + { + ret = ret << 8; + b = buffer.get(); + ret |= (b & 0xFF); + --cursor; + } + + // validate minimum value per spec. + if (ret <= minValue) + { + String err = String.format("Invalid 1/3/9 length 0x%X (minimum value for chosen encoding is 0x%X)",ret,minValue); + throw new MuxException(err); + } + + return ret; + } + + private ByteBuffer readBlock(ByteBuffer buffer, long size) + { + if (size == 0) + { + return null; + } + + if (size > buffer.remaining()) + { + String err = String.format("Truncated data, expected %,d byte(s), but only %,d byte(s) remain",size,buffer.remaining()); + throw new MuxException(err); + } + + if (size > Integer.MAX_VALUE) + { + String err = String.format("[Int-Sane!] Buffer size %,d is too large to be supported (max allowed is %,d)",size,Integer.MAX_VALUE); + throw new MuxException(err); + } + + ByteBuffer ret = ByteBuffer.allocate((int)size); + BufferUtil.put(buffer,ret); + BufferUtil.flipToFlush(ret,0); + return ret; + } + + /** + * Read Channel ID using Section 7. Framing techniques + * + * @param buffer + * the buffer to parse from. + * @return the channel Id + * @throws MuxException + * when the encoding does not make sense per the spec. + */ + public long readChannelId(ByteBuffer buffer) + { + long id = -1; + long minValue = 0x00; // used to validate minimum # of bytes (per spec) + byte b = buffer.get(); + int cursor = -1; + if ((b & 0x80) == 0) + { + // 7 bit channel id + // no validation of minimum bytes needed here + return (b & 0x7F); + } + else if ((b & 0x40) == 0) + { + // 14 bit channel id + id = (b & 0x3F); + minValue = 0x7F; + cursor = 1; + } + else if ((b & 0x20) == 0) + { + // 21 bit channel id + id = (b & 0x1F); + minValue = 0x3F_FF; + cursor = 2; + } + else + { + // 29 bit channel id + id = (b & 0x1F); + minValue = 0x1F_FF_FF; + cursor = 3; + } + + while (cursor > 0) + { + id = id << 8; + b = buffer.get(); + id |= (b & 0xFF); + --cursor; + } + + // validate minimum value per spec. + if (id <= minValue) + { + String err = String.format("Invalid Channel ID 0x%X (minimum value for chosen encoding is 0x%X)",id,minValue); + throw new MuxException(err); + } + + return id; + } + + public void setEvents(MuxParser.Listener events) + { + this.events = events; + } +} diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxPhysicalConnectionException.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxPhysicalConnectionException.java new file mode 100644 index 00000000000..23567cd2dca --- /dev/null +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxPhysicalConnectionException.java @@ -0,0 +1,44 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core.extensions.mux; + +import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxDropChannel; + +public class MuxPhysicalConnectionException extends MuxException +{ + private static final long serialVersionUID = 1L; + private MuxDropChannel drop; + + public MuxPhysicalConnectionException(MuxDropChannel.Reason code, String phrase) + { + super(phrase); + drop = new MuxDropChannel(0,code,phrase); + } + + public MuxPhysicalConnectionException(MuxDropChannel.Reason code, String phrase, Throwable t) + { + super(phrase,t); + drop = new MuxDropChannel(0,code,phrase); + } + + public MuxDropChannel getMuxDropChannel() + { + return drop; + } +} diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxedFrame.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxedFrame.java new file mode 100644 index 00000000000..4d17dafc7d0 --- /dev/null +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxedFrame.java @@ -0,0 +1,73 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core.extensions.mux; + +import org.eclipse.jetty.websocket.core.protocol.OpCode; +import org.eclipse.jetty.websocket.core.protocol.WebSocketFrame; + +public class MuxedFrame extends WebSocketFrame +{ + private long channelId = -1; + + public MuxedFrame() + { + super(); + } + + public MuxedFrame(MuxedFrame frame) + { + super(frame); + this.channelId = frame.channelId; + } + + public long getChannelId() + { + return channelId; + } + + @Override + public void reset() + { + super.reset(); + this.channelId = -1; + } + + public void setChannelId(long channelId) + { + this.channelId = channelId; + } + + @Override + public String toString() + { + StringBuilder b = new StringBuilder(); + b.append(OpCode.name(getOpCode())); + b.append('['); + b.append("channel=").append(channelId); + b.append(",len=").append(getPayloadLength()); + b.append(",fin=").append(isFin()); + b.append(",rsv="); + b.append(isRsv1()?'1':'.'); + b.append(isRsv2()?'1':'.'); + b.append(isRsv3()?'1':'.'); + b.append(",continuation=").append(isContinuation()); + b.append(']'); + return b.toString(); + } +} diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/Muxer.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/Muxer.java new file mode 100644 index 00000000000..626acee4f52 --- /dev/null +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/Muxer.java @@ -0,0 +1,397 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core.extensions.mux; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.websocket.core.api.StatusCode; +import org.eclipse.jetty.websocket.core.api.WebSocketBehavior; +import org.eclipse.jetty.websocket.core.api.WebSocketConnection; +import org.eclipse.jetty.websocket.core.api.WebSocketException; +import org.eclipse.jetty.websocket.core.api.WebSocketPolicy; +import org.eclipse.jetty.websocket.core.extensions.mux.add.MuxAddClient; +import org.eclipse.jetty.websocket.core.extensions.mux.add.MuxAddServer; +import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxAddChannelRequest; +import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxAddChannelResponse; +import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxDropChannel; +import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxFlowControl; +import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxNewChannelSlot; +import org.eclipse.jetty.websocket.core.io.IncomingFrames; +import org.eclipse.jetty.websocket.core.io.OutgoingFrames; +import org.eclipse.jetty.websocket.core.protocol.WebSocketFrame; + +/** + * Muxer responsible for managing sub-channels. + *

+ * Maintains a 1 (incoming and outgoing mux encapsulated frames) to many (per-channel incoming/outgoing standard websocket frames) relationship, along with + * routing of {@link MuxControlBlock} events. + *

+ * Control Channel events (channel ID == 0) are handled by the Muxer. + */ +public class Muxer implements IncomingFrames, MuxParser.Listener +{ + private static final int CONTROL_CHANNEL_ID = 0; + + private static final Logger LOG = Log.getLogger(Muxer.class); + + /** + * Map of sub-channels, key is the channel Id. + */ + private Map channels = new HashMap(); + + private final WebSocketPolicy policy; + private final WebSocketConnection physicalConnection; + private InetSocketAddress remoteAddress; + /** Parsing frames destined for sub-channels */ + private MuxParser parser; + /** Generating frames destined for physical connection */ + private MuxGenerator generator; + private MuxAddServer addServer; + private MuxAddClient addClient; + + public Muxer(final WebSocketConnection connection, final OutgoingFrames outgoing) + { + this.physicalConnection = connection; + this.policy = connection.getPolicy().clonePolicy(); + this.parser = new MuxParser(); + this.parser.setEvents(this); + this.generator = new MuxGenerator(); + this.generator.setOutgoing(outgoing); + } + + public MuxAddClient getAddClient() + { + return addClient; + } + + public MuxAddServer getAddServer() + { + return addServer; + } + + public WebSocketPolicy getPolicy() + { + return policy; + } + + /** + * Get the remote address of the physical connection. + * + * @return the remote address of the physical connection + */ + public InetSocketAddress getRemoteAddress() + { + return this.remoteAddress; + } + + /** + * Incoming exceptions encountered during parsing of mux encapsulated frames. + */ + @Override + public void incoming(WebSocketException e) + { + // TODO Notify Control Channel 0 + } + + /** + * Incoming mux encapsulated frames. + */ + @Override + public void incoming(WebSocketFrame frame) + { + parser.parse(frame); + } + + /** + * Is the muxer and the physical connection still open? + * + * @return true if open + */ + public boolean isOpen() + { + return physicalConnection.isOpen(); + } + + /** + * Per spec, the physical connection must be failed. + *

+ * Section 18. Fail the Physical Connection. + * + *

To _Fail the Physical Connection_, an endpoint MUST send a DropChannel multiplex control block with objective channel ID of 0 and drop + * reason code in the range of 2000-2999, and then _Fail the WebSocket Connection_ on the physical connection with status code of 1011.
+ */ + private void mustFailPhysicalConnection(MuxPhysicalConnectionException muxe) + { + // TODO: stop muxer from receiving incoming sub-channel traffic. + + MuxDropChannel drop = muxe.getMuxDropChannel(); + LOG.warn(muxe); + try + { + generator.generate(drop); + } + catch (IOException ioe) + { + LOG.warn("Unable to send mux DropChannel",ioe); + } + + String reason = "Mux[MUST FAIL]" + drop.getPhrase(); + reason = StringUtil.truncate(reason,WebSocketFrame.MAX_CONTROL_PAYLOAD); + this.physicalConnection.close(StatusCode.SERVER_ERROR,reason); + + // TODO: trigger abnormal close for all sub-channels. + } + + /** + * Incoming mux control block, destined for the control channel (id 0) + */ + @Override + public void onMuxAddChannelRequest(MuxAddChannelRequest request) + { + if (policy.getBehavior() == WebSocketBehavior.CLIENT) + { + throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_MUX_CONTROL_BLOCK,"AddChannelRequest not allowed per spec"); + } + + if (request.getRsv() != 0) + { + throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_REQUEST_ENCODING,"RSV Not allowed to be set"); + } + + if (request.getChannelId() == CONTROL_CHANNEL_ID) + { + throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_MUX_CONTROL_BLOCK,"Invalid Channel ID"); + } + + // Pre-allocate channel. + long channelId = request.getChannelId(); + MuxChannel channel = new MuxChannel(channelId,this); + this.channels.put(channelId,channel); + + // submit to upgrade handshake process + try + { + MuxAddChannelResponse response = addServer.handshake(physicalConnection,request); + if (response == null) + { + LOG.warn("AddChannelResponse is null"); + // no upgrade possible? + response = new MuxAddChannelResponse(); + response.setChannelId(request.getChannelId()); + response.setFailed(true); + } + // send response + this.generator.generate(response); + } + catch (Throwable t) + { + throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.BAD_REQUEST,"Unable to parse request",t); + } + } + + /** + * Incoming mux control block, destined for the control channel (id 0) + */ + @Override + public void onMuxAddChannelResponse(MuxAddChannelResponse response) + { + if (policy.getBehavior() == WebSocketBehavior.SERVER) + { + throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_MUX_CONTROL_BLOCK,"AddChannelResponse not allowed per spec"); + } + + if (response.getRsv() != 0) + { + throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_RESPONSE_ENCODING,"RSV Not allowed to be set"); + } + + if (response.getChannelId() == CONTROL_CHANNEL_ID) + { + throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_MUX_CONTROL_BLOCK,"Invalid Channel ID"); + } + + // Process channel + long channelId = response.getChannelId(); + MuxChannel channel = this.channels.get(channelId); + if (channel == null) + { + throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_MUX_CONTROL_BLOCK,"Unknown Channel ID"); + } + + // Process Response headers + try + { + // Parse Response + + // TODO: Sec-WebSocket-Accept header + // TODO: Sec-WebSocket-Extensions header + // TODO: Setup extensions + // TODO: Setup sessions + + // Trigger channel open + channel.onOpen(); + } + catch (Throwable t) + { + throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.BAD_RESPONSE,"Unable to parse response",t); + } + } + + /** + * Incoming mux control block, destined for the control channel (id 0) + */ + @Override + public void onMuxDropChannel(MuxDropChannel drop) + { + if (drop.getChannelId() == CONTROL_CHANNEL_ID) + { + throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_MUX_CONTROL_BLOCK,"Invalid Channel ID"); + } + + // Process channel + long channelId = drop.getChannelId(); + MuxChannel channel = this.channels.get(channelId); + if (channel == null) + { + throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_MUX_CONTROL_BLOCK,"Unknown Channel ID"); + } + + String reason = "Mux " + drop.toString(); + if (reason.length() > (WebSocketFrame.MAX_CONTROL_PAYLOAD - 2)) + { + reason = reason.substring(0,WebSocketFrame.MAX_CONTROL_PAYLOAD - 2); + } + + channel.close(StatusCode.PROTOCOL,reason); + // TODO: set channel to inactive? + } + + /** + * Incoming mux-unwrapped frames, destined for a sub-channel + */ + @Override + public void onMuxedFrame(MuxedFrame frame) + { + MuxChannel subchannel = channels.get(frame.getChannelId()); + subchannel.incoming(frame); + } + + @Override + public void onMuxException(MuxException e) + { + if (e instanceof MuxPhysicalConnectionException) + { + mustFailPhysicalConnection((MuxPhysicalConnectionException)e); + } + + LOG.warn(e); + // TODO: handle other mux exceptions? + } + + /** + * Incoming mux control block, destined for the control channel (id 0) + */ + @Override + public void onMuxFlowControl(MuxFlowControl flow) + { + if (flow.getChannelId() == CONTROL_CHANNEL_ID) + { + throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_MUX_CONTROL_BLOCK,"Invalid Channel ID"); + } + + if (flow.getSendQuotaSize() > 0x7F_FF_FF_FF_FF_FF_FF_FFL) + { + throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.SEND_QUOTA_OVERFLOW,"Send Quota Overflow"); + } + + // Process channel + long channelId = flow.getChannelId(); + MuxChannel channel = this.channels.get(channelId); + if (channel == null) + { + throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_MUX_CONTROL_BLOCK,"Unknown Channel ID"); + } + + // TODO: set channel quota + } + + /** + * Incoming mux control block, destined for the control channel (id 0) + */ + @Override + public void onMuxNewChannelSlot(MuxNewChannelSlot slot) + { + if (policy.getBehavior() == WebSocketBehavior.SERVER) + { + throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_MUX_CONTROL_BLOCK,"NewChannelSlot not allowed per spec"); + } + + if (slot.isFallback()) + { + if (slot.getNumberOfSlots() == 0) + { + throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_MUX_CONTROL_BLOCK,"Cannot have 0 number of slots during fallback"); + } + if (slot.getInitialSendQuota() == 0) + { + throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_MUX_CONTROL_BLOCK,"Cannot have 0 initial send quota during fallback"); + } + } + + // TODO: handle channel slot + } + + /** + * Outgoing frame, without mux encapsulated payload. + */ + public void output(C context, Callback callback, long channelId, WebSocketFrame frame) throws IOException + { + generator.output(context,callback,channelId,frame); + } + + public void setAddClient(MuxAddClient addClient) + { + this.addClient = addClient; + } + + public void setAddServer(MuxAddServer addServer) + { + this.addServer = addServer; + } + + /** + * Set the remote address of the physical connection. + *

+ * This address made available to sub-channels. + * + * @param remoteAddress + * the remote address + */ + public void setRemoteAddress(InetSocketAddress remoteAddress) + { + this.remoteAddress = remoteAddress; + } +} diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/add/MuxAddClient.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/add/MuxAddClient.java new file mode 100644 index 00000000000..035c76306db --- /dev/null +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/add/MuxAddClient.java @@ -0,0 +1,30 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core.extensions.mux.add; + +import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxAddChannelResponse; +import org.eclipse.jetty.websocket.core.io.WebSocketSession; + +/** + * Interface for Mux Client to handle receiving a AddChannelResponse + */ +public interface MuxAddClient +{ + WebSocketSession createSession(MuxAddChannelResponse response); +} diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/add/MuxAddServer.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/add/MuxAddServer.java new file mode 100644 index 00000000000..16161630b6d --- /dev/null +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/add/MuxAddServer.java @@ -0,0 +1,33 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core.extensions.mux.add; + +import java.io.IOException; + +import org.eclipse.jetty.websocket.core.api.WebSocketConnection; +import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxAddChannelRequest; +import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxAddChannelResponse; + +/** + * Server interface, for dealing with incoming AddChannelRequest and posting of AddChannelResponse back. + */ +public interface MuxAddServer +{ + MuxAddChannelResponse handshake(WebSocketConnection physicalConnection, MuxAddChannelRequest request) throws IOException; +} diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/op/MuxAddChannelRequest.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/op/MuxAddChannelRequest.java new file mode 100644 index 00000000000..ecba1697307 --- /dev/null +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/op/MuxAddChannelRequest.java @@ -0,0 +1,94 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core.extensions.mux.op; + +import java.nio.ByteBuffer; + +import org.eclipse.jetty.websocket.core.extensions.mux.MuxControlBlock; +import org.eclipse.jetty.websocket.core.extensions.mux.MuxOp; + +public class MuxAddChannelRequest implements MuxControlBlock +{ + private long channelId = -1; + private byte enc; + private ByteBuffer handshake; + private byte rsv; + + public long getChannelId() + { + return channelId; + } + + public byte getEnc() + { + return enc; + } + + public ByteBuffer getHandshake() + { + return handshake; + } + + public long getHandshakeSize() + { + if (handshake == null) + { + return 0; + } + return handshake.remaining(); + } + + @Override + public int getOpCode() + { + return MuxOp.ADD_CHANNEL_REQUEST; + } + + public byte getRsv() + { + return rsv; + } + + public void setChannelId(long channelId) + { + this.channelId = channelId; + } + + public void setEnc(byte enc) + { + this.enc = enc; + } + + public void setHandshake(ByteBuffer handshake) + { + if (handshake == null) + { + this.handshake = null; + } + else + { + this.handshake = handshake.slice(); + } + } + + public void setRsv(byte rsv) + { + this.rsv = rsv; + } +} diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/op/MuxAddChannelResponse.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/op/MuxAddChannelResponse.java new file mode 100644 index 00000000000..9f86fdcd46b --- /dev/null +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/op/MuxAddChannelResponse.java @@ -0,0 +1,105 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core.extensions.mux.op; + +import java.nio.ByteBuffer; + +import org.eclipse.jetty.websocket.core.extensions.mux.MuxControlBlock; +import org.eclipse.jetty.websocket.core.extensions.mux.MuxOp; + +public class MuxAddChannelResponse implements MuxControlBlock +{ + private long channelId; + private byte enc; + private byte rsv; + private boolean failed = false; + private ByteBuffer handshake; + + public long getChannelId() + { + return channelId; + } + + public byte getEnc() + { + return enc; + } + + public ByteBuffer getHandshake() + { + return handshake; + } + + public long getHandshakeSize() + { + if (handshake == null) + { + return 0; + } + return handshake.remaining(); + } + + @Override + public int getOpCode() + { + return MuxOp.ADD_CHANNEL_RESPONSE; + } + + public byte getRsv() + { + return rsv; + } + + public boolean isFailed() + { + return failed; + } + + public void setChannelId(long channelId) + { + this.channelId = channelId; + } + + public void setEnc(byte enc) + { + this.enc = enc; + } + + public void setFailed(boolean failed) + { + this.failed = failed; + } + + public void setHandshake(ByteBuffer handshake) + { + if (handshake == null) + { + this.handshake = null; + } + else + { + this.handshake = handshake.slice(); + } + } + + public void setRsv(byte rsv) + { + this.rsv = rsv; + } +} diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/op/MuxDropChannel.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/op/MuxDropChannel.java new file mode 100644 index 00000000000..a93b13296f2 --- /dev/null +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/op/MuxDropChannel.java @@ -0,0 +1,183 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core.extensions.mux.op; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jetty.websocket.core.extensions.mux.MuxControlBlock; +import org.eclipse.jetty.websocket.core.extensions.mux.MuxOp; + +public class MuxDropChannel implements MuxControlBlock +{ + /** + * Outlined in Section 9.4.1. Drop Reason Codes + */ + public static enum Reason + { + // Normal Close : (1000-1999) + NORMAL_CLOSURE(1000), + + // Failures in Physical Connection : (2000-2999) + PHYSICAL_CONNECTION_FAILED(2000), + INVALID_ENCAPSULATING_MESSAGE(2001), + CHANNEL_ID_TRUNCATED(2002), + ENCAPSULATED_FRAME_TRUNCATED(2003), + UNKNOWN_MUX_CONTROL_OPC(2004), + UNKNOWN_MUX_CONTROL_BLOCK(2005), + CHANNEL_ALREADY_EXISTS(2006), + NEW_CHANNEL_SLOT_VIOLATION(2007), + NEW_CHANNEL_SLOT_OVERFLOW(2008), + BAD_REQUEST(2009), + UNKNOWN_REQUEST_ENCODING(2010), + BAD_RESPONSE(2011), + UNKNOWN_RESPONSE_ENCODING(2012), + + // Failures in Logical Connections : (3000-3999) + LOGICAL_CHANNEL_FAILED(3000), + SEND_QUOTA_VIOLATION(3005), + SEND_QUOTA_OVERFLOW(3006), + IDLE_TIMEOUT(3007), + DROP_CHANNEL_ACK(3008), + + // Other Peer Actions : (4000-4999) + USE_ANOTHER_PHYSICAL_CONNECTION(4001), + BUSY(4002); + + private static final Map codeMap; + + static + { + codeMap = new HashMap<>(); + for (Reason r : values()) + { + codeMap.put(r.getValue(),r); + } + } + + public static Reason valueOf(int code) + { + return codeMap.get(code); + } + + private final int code; + + private Reason(int code) + { + this.code = code; + } + + public int getValue() + { + return code; + } + } + + public static MuxDropChannel parse(long channelId, ByteBuffer payload) + { + // TODO Auto-generated method stub + return null; + } + + private final long channelId; + private final Reason code; + private String phrase; + private int rsv; + + /** + * Normal Drop. no reason Phrase. + * + * @param channelId + * the logical channel Id to perform drop against. + */ + public MuxDropChannel(long channelId) + { + this(channelId,Reason.NORMAL_CLOSURE,null); + } + + /** + * Drop with reason code and optional phrase + * + * @param channelId + * the logical channel Id to perform drop against. + * @param code + * reason code + * @param phrase + * optional human readable phrase + */ + public MuxDropChannel(long channelId, int code, String phrase) + { + this(channelId, Reason.valueOf(code), phrase); + } + + /** + * Drop with reason code and optional phrase + * + * @param channelId + * the logical channel Id to perform drop against. + * @param code + * reason code + * @param phrase + * optional human readable phrase + */ + public MuxDropChannel(long channelId, Reason code, String phrase) + { + this.channelId = channelId; + this.code = code; + this.phrase = phrase; + } + + public ByteBuffer asReasonBuffer() + { + // TODO: convert to reason buffer + return null; + } + + public long getChannelId() + { + return channelId; + } + + public Reason getCode() + { + return code; + } + + @Override + public int getOpCode() + { + return MuxOp.DROP_CHANNEL; + } + + public String getPhrase() + { + return phrase; + } + + public int getRsv() + { + return rsv; + } + + public void setRsv(int rsv) + { + this.rsv = rsv; + } +} diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/op/MuxFlowControl.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/op/MuxFlowControl.java new file mode 100644 index 00000000000..afb8454f70f --- /dev/null +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/op/MuxFlowControl.java @@ -0,0 +1,65 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core.extensions.mux.op; + +import org.eclipse.jetty.websocket.core.extensions.mux.MuxControlBlock; +import org.eclipse.jetty.websocket.core.extensions.mux.MuxOp; + +public class MuxFlowControl implements MuxControlBlock +{ + private long channelId; + private byte rsv; + private long sendQuotaSize; + + public long getChannelId() + { + return channelId; + } + + @Override + public int getOpCode() + { + return MuxOp.FLOW_CONTROL; + } + + public byte getRsv() + { + return rsv; + } + + public long getSendQuotaSize() + { + return sendQuotaSize; + } + + public void setChannelId(long channelId) + { + this.channelId = channelId; + } + + public void setRsv(byte rsv) + { + this.rsv = rsv; + } + + public void setSendQuotaSize(long sendQuotaSize) + { + this.sendQuotaSize = sendQuotaSize; + } +} diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/op/MuxNewChannelSlot.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/op/MuxNewChannelSlot.java new file mode 100644 index 00000000000..903beb9a545 --- /dev/null +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/op/MuxNewChannelSlot.java @@ -0,0 +1,76 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core.extensions.mux.op; + +import org.eclipse.jetty.websocket.core.extensions.mux.MuxControlBlock; +import org.eclipse.jetty.websocket.core.extensions.mux.MuxOp; + +public class MuxNewChannelSlot implements MuxControlBlock +{ + private boolean fallback; + private long initialSendQuota; + private long numberOfSlots; + private byte rsv; + + public long getInitialSendQuota() + { + return initialSendQuota; + } + + public long getNumberOfSlots() + { + return numberOfSlots; + } + + @Override + public int getOpCode() + { + return MuxOp.NEW_CHANNEL_SLOT; + } + + public byte getRsv() + { + return rsv; + } + + public boolean isFallback() + { + return fallback; + } + + public void setFallback(boolean fallback) + { + this.fallback = fallback; + } + + public void setInitialSendQuota(long initialSendQuota) + { + this.initialSendQuota = initialSendQuota; + } + + public void setNumberOfSlots(long numberOfSlots) + { + this.numberOfSlots = numberOfSlots; + } + + public void setRsv(byte rsv) + { + this.rsv = rsv; + } +} diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/protocol/WebSocketFrame.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/protocol/WebSocketFrame.java index 1b00f826a23..e1f0051a1d1 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/protocol/WebSocketFrame.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/protocol/WebSocketFrame.java @@ -556,6 +556,10 @@ public class WebSocketFrame implements Frame b.append('['); b.append("len=").append(payloadLength); b.append(",fin=").append(fin); + b.append(",rsv="); + b.append(rsv1?'1':'.'); + b.append(rsv2?'1':'.'); + b.append(rsv3?'1':'.'); b.append(",masked=").append(masked); b.append(",continuation=").append(continuation); b.append(']'); diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxEventCapture.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxEventCapture.java new file mode 100644 index 00000000000..b6e1e7da15b --- /dev/null +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxEventCapture.java @@ -0,0 +1,118 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core.extensions.mux; + +import static org.hamcrest.Matchers.*; + +import java.util.LinkedList; + +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxAddChannelRequest; +import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxAddChannelResponse; +import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxDropChannel; +import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxFlowControl; +import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxNewChannelSlot; +import org.junit.Assert; + +public class MuxEventCapture implements MuxParser.Listener +{ + private static final Logger LOG = Log.getLogger(MuxEventCapture.class); + + private LinkedList frames = new LinkedList<>(); + private LinkedList ops = new LinkedList<>(); + private LinkedList errors = new LinkedList<>(); + + public void assertFrameCount(int expected) + { + Assert.assertThat("Frame Count",frames.size(), is(expected)); + } + + public void assertHasOp(byte opCode, int expectedCount) + { + int actualCount = 0; + for (MuxControlBlock block : ops) + { + if (block.getOpCode() == opCode) + { + actualCount++; + } + } + Assert.assertThat("Op[" + opCode + "] count",actualCount,is(expectedCount)); + } + + public LinkedList getFrames() + { + return frames; + } + + public LinkedList getOps() + { + return ops; + } + + @Override + public void onMuxAddChannelRequest(MuxAddChannelRequest request) + { + ops.add(request); + } + + @Override + public void onMuxAddChannelResponse(MuxAddChannelResponse response) + { + ops.add(response); + } + + @Override + public void onMuxDropChannel(MuxDropChannel drop) + { + ops.add(drop); + } + + @Override + public void onMuxedFrame(MuxedFrame frame) + { + frames.add(new MuxedFrame(frame)); + } + + @Override + public void onMuxException(MuxException e) + { + LOG.debug(e); + errors.add(e); + } + + @Override + public void onMuxFlowControl(MuxFlowControl flow) + { + ops.add(flow); + } + + @Override + public void onMuxNewChannelSlot(MuxNewChannelSlot slot) + { + ops.add(slot); + } + + public void reset() + { + frames.clear(); + ops.clear(); + } +} diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxGeneratorWrite139SizeTest.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxGeneratorWrite139SizeTest.java new file mode 100644 index 00000000000..281996167b1 --- /dev/null +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxGeneratorWrite139SizeTest.java @@ -0,0 +1,96 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core.extensions.mux; + +import static org.hamcrest.Matchers.*; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.TypeUtil; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class MuxGeneratorWrite139SizeTest +{ + private static MuxGenerator generator = new MuxGenerator(); + + @Parameters + public static Collection data() + { + // Various good 1/3/9 encodings + List data = new ArrayList<>(); + + // @formatter:off + // - 1 byte tests + data.add(new Object[]{ 0L, "00"}); + data.add(new Object[]{ 1L, "01"}); + data.add(new Object[]{ 2L, "02"}); + data.add(new Object[]{ 55L, "37"}); + data.add(new Object[]{125L, "7D"}); + + // - 3 byte tests + data.add(new Object[]{0x00_80L, "7E0080"}); + data.add(new Object[]{0x00_ABL, "7E00AB"}); + data.add(new Object[]{0x00_FFL, "7E00FF"}); + data.add(new Object[]{0x3F_FFL, "7E3FFF"}); + + // - 9 byte tests + data.add(new Object[]{0x00_00_01_FF_FFL, "7F000000000001FFFF"}); + data.add(new Object[]{0x00_00_FF_FF_FFL, "7F0000000000FFFFFF"}); + data.add(new Object[]{0x00_FF_FF_FF_FFL, "7F00000000FFFFFFFF"}); + data.add(new Object[]{0xFF_FF_FF_FF_FFL, "7F000000FFFFFFFFFF"}); + // @formatter:on + + return data; + } + + @Rule + public TestName testname = new TestName(); + + private long value; + private String expectedHex; + + public MuxGeneratorWrite139SizeTest(long value, String expectedHex) + { + this.value = value; + this.expectedHex = expectedHex; + } + + @Test + public void testWrite139Size() + { + System.err.printf("Running %s.%s - value: %,d%n",this.getClass().getName(),testname.getMethodName(),value); + ByteBuffer bbuf = ByteBuffer.allocate(10); + generator.write139Size(bbuf,value); + BufferUtil.flipToFlush(bbuf,0); + byte actual[] = BufferUtil.toArray(bbuf); + String actualHex = TypeUtil.toHexString(actual).toUpperCase(); + Assert.assertThat("1/3/9 encoded size of [" + value + "]",actualHex,is(expectedHex)); + } +} diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxGeneratorWriteChannelIdTest.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxGeneratorWriteChannelIdTest.java new file mode 100644 index 00000000000..f6f10bd2f2b --- /dev/null +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxGeneratorWriteChannelIdTest.java @@ -0,0 +1,100 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core.extensions.mux; + +import static org.hamcrest.Matchers.*; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.TypeUtil; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** + * Tests of valid ChannelID generation + */ +@RunWith(Parameterized.class) +public class MuxGeneratorWriteChannelIdTest +{ + private static MuxGenerator generator = new MuxGenerator(); + + @Parameters + public static Collection data() + { + // Various good Channel IDs + List data = new ArrayList<>(); + + // @formatter:off + // - 1 byte tests + data.add(new Object[]{ 0L, "00"}); + data.add(new Object[]{ 1L, "01"}); + data.add(new Object[]{ 2L, "02"}); + data.add(new Object[]{ 55L, "37"}); + data.add(new Object[]{127L, "7F"}); + + // - 2 byte tests + data.add(new Object[]{0x00_80L, "8080"}); + data.add(new Object[]{0x00_FFL, "80FF"}); + data.add(new Object[]{0x3F_FFL, "BFFF"}); + + // - 3 byte tests + data.add(new Object[]{0x00_FF_FFL, "C0FFFF"}); + data.add(new Object[]{0x1F_FF_FFL, "DFFFFF"}); + + // - 3 byte tests + data.add(new Object[]{0x00_FF_FF_FFL, "E0FFFFFF"}); + data.add(new Object[]{0x1F_FF_FF_FFL, "FFFFFFFF"}); + + // @formatter:on + return data; + } + + @Rule + public TestName testname = new TestName(); + + private long channelId; + private String expectedHex; + + public MuxGeneratorWriteChannelIdTest(long channelId, String expectedHex) + { + this.channelId = channelId; + this.expectedHex = expectedHex; + } + + @Test + public void testReadChannelId() + { + System.err.printf("Running %s.%s - channelId: %,d%n",this.getClass().getName(),testname.getMethodName(),channelId); + ByteBuffer bbuf = ByteBuffer.allocate(10); + generator.writeChannelId(bbuf,channelId); + BufferUtil.flipToFlush(bbuf,0); + byte actual[] = BufferUtil.toArray(bbuf); + String actualHex = TypeUtil.toHexString(actual).toUpperCase(); + Assert.assertThat("Channel ID [" + channelId + "]",actualHex,is(expectedHex)); + } +} diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxInjector.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxInjector.java new file mode 100644 index 00000000000..ef13fed5c93 --- /dev/null +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxInjector.java @@ -0,0 +1,60 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core.extensions.mux; + +import java.io.IOException; + +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.websocket.core.io.IncomingFrames; +import org.eclipse.jetty.websocket.core.io.OutgoingFrames; +import org.eclipse.jetty.websocket.core.protocol.WebSocketFrame; + +/** + * Helpful utility class to send arbitrary mux events into a physical connection's IncomingFrames. + * + * @see MuxReducer + */ +public class MuxInjector implements OutgoingFrames +{ + private IncomingFrames incoming; + private MuxGenerator generator; + + public MuxInjector(IncomingFrames incoming) + { + this.incoming = incoming; + this.generator = new MuxGenerator(); + this.generator.setOutgoing(this); + } + + public void op(long channelId, WebSocketFrame frame) throws IOException + { + this.generator.generate(channelId,frame); + } + + public void op(MuxControlBlock op) throws IOException + { + this.generator.generate(op); + } + + @Override + public void output(C context, Callback callback, WebSocketFrame frame) throws IOException + { + this.incoming.incoming(frame); + } +} diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxParserRFCTest.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxParserRFCTest.java new file mode 100644 index 00000000000..ca1a557089e --- /dev/null +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxParserRFCTest.java @@ -0,0 +1,232 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core.extensions.mux; + +import static org.hamcrest.Matchers.*; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.TypeUtil; +import org.eclipse.jetty.websocket.core.api.WebSocketPolicy; +import org.eclipse.jetty.websocket.core.protocol.IncomingFramesCapture; +import org.eclipse.jetty.websocket.core.protocol.OpCode; +import org.eclipse.jetty.websocket.core.protocol.Parser; +import org.eclipse.jetty.websocket.core.protocol.WebSocketFrame; +import org.junit.Assert; +import org.junit.Test; + +public class MuxParserRFCTest +{ + private LinkedList asFrames(byte[] buf) + { + IncomingFramesCapture capture = new IncomingFramesCapture(); + WebSocketPolicy policy = WebSocketPolicy.newServerPolicy(); + Parser parser = new Parser(policy); + parser.setIncomingFramesHandler(capture); + List muxList = Collections.singletonList(new MuxExtension()); + parser.configureFromExtensions(muxList); + ByteBuffer bbuf = ByteBuffer.wrap(buf); + parser.parse(bbuf); + + return capture.getFrames(); + } + + private boolean isHexOnly(String part) + { + Pattern bytePat = Pattern.compile("(\\s*0x[0-9A-Fa-f]{2}+){1,}+"); + Matcher mat = bytePat.matcher(part); + return mat.matches(); + } + + private MuxEventCapture parseMuxFrames(LinkedList frames) + { + MuxParser parser = new MuxParser(); + MuxEventCapture capture = new MuxEventCapture(); + parser.setEvents(capture); + for(WebSocketFrame frame: frames) { + parser.parse(frame); + } + return capture; + } + + @Test + public void testIsHexOnly() + { + Assert.assertTrue(isHexOnly("0x00")); + Assert.assertTrue(isHexOnly("0x00 0xaF")); + Assert.assertFalse(isHexOnly("Hello World")); + } + + @Test + public void testRFCExample1() throws IOException + { + // Create RFC detailed frames + byte buf[] = toByteArray("0x82 0x0d 0x01 0x81","Hello world"); + LinkedList frames = asFrames(buf); + Assert.assertThat("Frame count",frames.size(),is(1)); + + // Have mux parse frames + MuxEventCapture capture = parseMuxFrames(frames); + capture.assertFrameCount(1); + + MuxedFrame mux; + + mux = capture.getFrames().pop(); + String prefix = "MuxFrame[0]"; + Assert.assertThat(prefix + ".channelId",mux.getChannelId(),is(1L)); + Assert.assertThat(prefix + ".fin",mux.isFin(),is(true)); + Assert.assertThat(prefix + ".rsv1",mux.isRsv1(),is(false)); + Assert.assertThat(prefix + ".rsv2",mux.isRsv2(),is(false)); + Assert.assertThat(prefix + ".rsv3",mux.isRsv3(),is(false)); + Assert.assertThat(prefix + ".masked",mux.isMasked(),is(false)); + Assert.assertThat(prefix + ".opcode",mux.getOpCode(),is(OpCode.TEXT)); + + String payload = mux.getPayloadAsUTF8(); + Assert.assertThat(prefix + ".payload/text",payload,is("Hello world")); + } + + @Test + public void testRFCExample2() throws IOException + { + // Create RFC detailed frames + byte buf[] = toByteArray("0x02 0x07 0x01 0x81","Hello","0x80 0x06"," world"); + LinkedList frames = asFrames(buf); + Assert.assertThat("Frame count",frames.size(),is(2)); + + // Have mux parse frames + MuxEventCapture capture = parseMuxFrames(frames); + capture.assertFrameCount(2); + + MuxedFrame mux; + + // Text Frame + mux = capture.getFrames().get(0); + String prefix = "MuxFrame[0]"; + Assert.assertThat(prefix + ".channelId",mux.getChannelId(),is(1L)); + // (BUG IN DRAFT) Assert.assertThat(prefix + ".fin",mux.isFin(),is(false)); + Assert.assertThat(prefix + ".rsv1",mux.isRsv1(),is(false)); + Assert.assertThat(prefix + ".rsv2",mux.isRsv2(),is(false)); + Assert.assertThat(prefix + ".rsv3",mux.isRsv3(),is(false)); + Assert.assertThat(prefix + ".masked",mux.isMasked(),is(false)); + Assert.assertThat(prefix + ".opcode",mux.getOpCode(),is(OpCode.TEXT)); + + String payload = mux.getPayloadAsUTF8(); + Assert.assertThat(prefix + ".payload/text",payload,is("Hello")); + + // Continuation Frame + mux = capture.getFrames().get(1); + prefix = "MuxFrame[1]"; + // (BUG IN DRAFT) Assert.assertThat(prefix + ".channelId",mux.getChannelId(),is(1L)); + // (BUG IN DRAFT) Assert.assertThat(prefix + ".fin",mux.isFin(),is(true)); + Assert.assertThat(prefix + ".rsv1",mux.isRsv1(),is(false)); + Assert.assertThat(prefix + ".rsv2",mux.isRsv2(),is(false)); + Assert.assertThat(prefix + ".rsv3",mux.isRsv3(),is(false)); + Assert.assertThat(prefix + ".masked",mux.isMasked(),is(false)); + Assert.assertThat(prefix + ".continuation",mux.isContinuation(),is(true)); + // (BUG IN DRAFT) Assert.assertThat(prefix + ".opcode",mux.getOpCode(),is(OpCode.BINARY)); + + payload = mux.getPayloadAsUTF8(); + Assert.assertThat(prefix + ".payload/text",payload,is(" world")); + } + + @Test + public void testRFCExample3() throws IOException + { + // Create RFC detailed frames + byte buf[] = toByteArray("0x82 0x07 0x01 0x01","Hello","0x82 0x05 0x02 0x81","bye","0x82 0x08 0x01 0x80"," world"); + LinkedList frames = asFrames(buf); + Assert.assertThat("Frame count",frames.size(),is(3)); + + // Have mux parse frames + MuxEventCapture capture = parseMuxFrames(frames); + capture.assertFrameCount(3); + + MuxedFrame mux; + + // Text Frame (Message 1) + mux = capture.getFrames().pop(); + String prefix = "MuxFrame[0]"; + Assert.assertThat(prefix + ".channelId",mux.getChannelId(),is(1L)); + Assert.assertThat(prefix + ".fin",mux.isFin(),is(false)); + Assert.assertThat(prefix + ".rsv1",mux.isRsv1(),is(false)); + Assert.assertThat(prefix + ".rsv2",mux.isRsv2(),is(false)); + Assert.assertThat(prefix + ".rsv3",mux.isRsv3(),is(false)); + Assert.assertThat(prefix + ".masked",mux.isMasked(),is(false)); + Assert.assertThat(prefix + ".continuation",mux.isContinuation(),is(false)); + Assert.assertThat(prefix + ".opcode",mux.getOpCode(),is(OpCode.TEXT)); + + String payload = mux.getPayloadAsUTF8(); + Assert.assertThat(prefix + ".payload/text",payload,is("Hello")); + + // Text Frame (Message 2) + mux = capture.getFrames().pop(); + prefix = "MuxFrame[1]"; + Assert.assertThat(prefix + ".channelId",mux.getChannelId(),is(2L)); + Assert.assertThat(prefix + ".fin",mux.isFin(),is(true)); + Assert.assertThat(prefix + ".rsv1",mux.isRsv1(),is(false)); + Assert.assertThat(prefix + ".rsv2",mux.isRsv2(),is(false)); + Assert.assertThat(prefix + ".rsv3",mux.isRsv3(),is(false)); + Assert.assertThat(prefix + ".masked",mux.isMasked(),is(false)); + Assert.assertThat(prefix + ".continuation",mux.isContinuation(),is(false)); + Assert.assertThat(prefix + ".opcode",mux.getOpCode(),is(OpCode.TEXT)); + + payload = mux.getPayloadAsUTF8(); + Assert.assertThat(prefix + ".payload/text",payload,is("bye")); + + // Continuation Frame (Message 1) + mux = capture.getFrames().pop(); + prefix = "MuxFrame[2]"; + Assert.assertThat(prefix + ".channelId",mux.getChannelId(),is(1L)); + Assert.assertThat(prefix + ".fin",mux.isFin(),is(true)); + Assert.assertThat(prefix + ".rsv1",mux.isRsv1(),is(false)); + Assert.assertThat(prefix + ".rsv2",mux.isRsv2(),is(false)); + Assert.assertThat(prefix + ".rsv3",mux.isRsv3(),is(false)); + Assert.assertThat(prefix + ".masked",mux.isMasked(),is(false)); + Assert.assertThat(prefix + ".continuation",mux.isContinuation(),is(true)); + Assert.assertThat(prefix + ".opcode",mux.getOpCode(),is(OpCode.TEXT)); + + payload = mux.getPayloadAsUTF8(); + Assert.assertThat(prefix + ".payload/text",payload,is(" world")); + } + + private byte[] toByteArray(String... parts) throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + for(String part: parts) { + if (isHexOnly(part)) + { + String hexonly = part.replaceAll("\\s*0x",""); + out.write(TypeUtil.fromHexString(hexonly)); + } + else + { + out.write(part.getBytes(StringUtil.__UTF8_CHARSET)); + } + } + return out.toByteArray(); + } +} diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxParserRead139Size_BadEncodingTest.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxParserRead139Size_BadEncodingTest.java new file mode 100644 index 00000000000..cfcf1e89fa6 --- /dev/null +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxParserRead139Size_BadEncodingTest.java @@ -0,0 +1,104 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core.extensions.mux; + +import static org.hamcrest.Matchers.*; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.eclipse.jetty.util.TypeUtil; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** + * Tests for bad 1/3/9 size encoding. + */ +@RunWith(Parameterized.class) +public class MuxParserRead139Size_BadEncodingTest +{ + private static MuxParser parser = new MuxParser(); + + @Parameters + public static Collection data() + { + // Various bad 1/3/9 encodings + // Violating "minimal number of bytes necessary" rule. + List data = new ArrayList<>(); + + // @formatter:off + // - 1 byte tests + // all known 1 byte tests are valid + + // - 3 byte tests + data.add(new Object[]{"7E0000"}); + data.add(new Object[]{"7E0001"}); + data.add(new Object[]{"7E0012"}); + data.add(new Object[]{"7E0059"}); + // extra bytes (not related to 1/3/9 size) + data.add(new Object[]{"7E0012345678"}); + + // - 9 byte tests + data.add(new Object[]{"7F0000000000000000"}); + data.add(new Object[]{"7F0000000000000001"}); + data.add(new Object[]{"7F0000000000000012"}); + data.add(new Object[]{"7F0000000000001234"}); + data.add(new Object[]{"7F000000000000FFFF"}); + + // @formatter:on + return data; + } + + @Rule + public TestName testname = new TestName(); + + private String rawhex; + private byte buf[]; + + public MuxParserRead139Size_BadEncodingTest(String rawhex) + { + this.rawhex = rawhex; + this.buf = TypeUtil.fromHexString(rawhex); + } + + @Test + public void testRead139EncodedSize() + { + System.err.printf("Running %s.%s - hex: %s%n",this.getClass().getName(),testname.getMethodName(),rawhex); + ByteBuffer bbuf = ByteBuffer.wrap(buf); + try + { + parser.read139EncodedSize(bbuf); + // unexpected path + Assert.fail("Should have failed with an invalid parse"); + } + catch (MuxException e) + { + // expected path + Assert.assertThat(e.getMessage(),containsString("Invalid 1/3/9 length")); + } + } +} diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxParserRead139Size_GoodTest.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxParserRead139Size_GoodTest.java new file mode 100644 index 00000000000..b0a0b44932f --- /dev/null +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxParserRead139Size_GoodTest.java @@ -0,0 +1,99 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core.extensions.mux; + +import static org.hamcrest.Matchers.*; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.eclipse.jetty.util.TypeUtil; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class MuxParserRead139Size_GoodTest +{ + private static MuxParser parser = new MuxParser(); + + @Parameters + public static Collection data() + { + // Various good 1/3/9 encodings + List data = new ArrayList<>(); + + // @formatter:off + // - 1 byte tests + data.add(new Object[]{"00", 0L}); + data.add(new Object[]{"01", 1L}); + data.add(new Object[]{"02", 2L}); + data.add(new Object[]{"37", 55L}); + data.add(new Object[]{"7D", 125L}); + // extra bytes (not related to 1/3/9 size) + data.add(new Object[]{"37FF", 55L}); + data.add(new Object[]{"0123456789", 0x01L}); + + // - 3 byte tests + data.add(new Object[]{"7E0080", 0x00_80L}); + data.add(new Object[]{"7E00AB", 0x00_ABL}); + data.add(new Object[]{"7E00FF", 0x00_FFL}); + data.add(new Object[]{"7E3FFF", 0x3F_FFL}); + // extra bytes (not related to 1/3/9 size) + data.add(new Object[]{"7E0123456789", 0x01_23L}); + + // - 9 byte tests + data.add(new Object[]{"7F000000000001FFFF", 0x00_00_01_FF_FFL}); + data.add(new Object[]{"7F0000000000FFFFFF", 0x00_00_FF_FF_FFL}); + data.add(new Object[]{"7F00000000FFFFFFFF", 0x00_FF_FF_FF_FFL}); + data.add(new Object[]{"7F000000FFFFFFFFFF", 0xFF_FF_FF_FF_FFL}); + + // @formatter:on + return data; + } + + @Rule + public TestName testname = new TestName(); + + private String rawhex; + private byte buf[]; + private long expected; + + public MuxParserRead139Size_GoodTest(String rawhex, long expected) + { + this.rawhex = rawhex; + this.buf = TypeUtil.fromHexString(rawhex); + this.expected = expected; + } + + @Test + public void testRead139EncodedSize() + { + System.err.printf("Running %s.%s - hex: %s%n",this.getClass().getName(),testname.getMethodName(),rawhex); + ByteBuffer bbuf = ByteBuffer.wrap(buf); + long actual = parser.read139EncodedSize(bbuf); + Assert.assertThat("1/3/9 size from buffer [" + rawhex + "]",actual,is(expected)); + } +} diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxParserReadChannelId_BadEncodingTest.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxParserReadChannelId_BadEncodingTest.java new file mode 100644 index 00000000000..22fd4dfb738 --- /dev/null +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxParserReadChannelId_BadEncodingTest.java @@ -0,0 +1,106 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core.extensions.mux; + +import static org.hamcrest.Matchers.*; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.eclipse.jetty.util.TypeUtil; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** + * Tests of Invalid ChannelID parsing + */ +@RunWith(Parameterized.class) +public class MuxParserReadChannelId_BadEncodingTest +{ + private static MuxParser parser = new MuxParser(); + + @Parameters + public static Collection data() + { + // Various Invalid Encoded Channel IDs. + // Violating "minimal number of bytes necessary" rule. + List data = new ArrayList<>(); + + // @formatter:off + // - 1 byte tests + // all known 1 byte tests are valid + + // - 2 byte tests + data.add(new Object[]{"8000"}); + data.add(new Object[]{"8001"}); + data.add(new Object[]{"807F"}); + // extra bytes (not related to channelId) + data.add(new Object[]{"8023456789"}); + + // - 3 byte tests + data.add(new Object[]{"C00000"}); + data.add(new Object[]{"C01234"}); + data.add(new Object[]{"C03FFF"}); + + // - 3 byte tests + data.add(new Object[]{"E0000000"}); + data.add(new Object[]{"E0000001"}); + data.add(new Object[]{"E01FFFFF"}); + + // @formatter:on + return data; + } + + @Rule + public TestName testname = new TestName(); + + private String rawhex; + private byte buf[]; + + public MuxParserReadChannelId_BadEncodingTest(String rawhex) + { + this.rawhex = rawhex; + this.buf = TypeUtil.fromHexString(rawhex); + } + + @Test + public void testBadEncoding() + { + System.err.printf("Running %s.%s - hex: %s%n",this.getClass().getName(),testname.getMethodName(),rawhex); + ByteBuffer bbuf = ByteBuffer.wrap(buf); + try + { + parser.readChannelId(bbuf); + // unexpected path + Assert.fail("Should have failed with an invalid parse"); + } + catch (MuxException e) + { + // expected path + Assert.assertThat(e.getMessage(),containsString("Invalid Channel ID")); + } + } +} diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxParserReadChannelId_GoodTest.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxParserReadChannelId_GoodTest.java new file mode 100644 index 00000000000..d8b2b82461e --- /dev/null +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxParserReadChannelId_GoodTest.java @@ -0,0 +1,106 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core.extensions.mux; + +import static org.hamcrest.Matchers.*; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.eclipse.jetty.util.TypeUtil; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** + * Tests of valid ChannelID parsing + */ +@RunWith(Parameterized.class) +public class MuxParserReadChannelId_GoodTest +{ + private static MuxParser parser = new MuxParser(); + + @Parameters + public static Collection data() + { + // Various good Channel IDs + List data = new ArrayList<>(); + + // @formatter:off + // - 1 byte tests + data.add(new Object[]{"00", 0L}); + data.add(new Object[]{"01", 1L}); + data.add(new Object[]{"02", 2L}); + data.add(new Object[]{"7F", 127L}); + // extra bytes (not related to channelId) + data.add(new Object[]{"37FF", 55L}); + data.add(new Object[]{"0123456789", 0x01L}); + + // - 2 byte tests + data.add(new Object[]{"8080", 0x00_80L}); + data.add(new Object[]{"80FF", 0x00_FFL}); + data.add(new Object[]{"BFFF", 0x3F_FFL}); + // extra bytes (not related to channelId) + data.add(new Object[]{"8123456789", 0x01_23L}); + + // - 3 byte tests + data.add(new Object[]{"C0FFFF", 0x00_FF_FFL}); + data.add(new Object[]{"DFFFFF", 0x1F_FF_FFL}); + // extra bytes (not related to channelId) + data.add(new Object[]{"C123456789", 0x01_23_45L}); + + // - 3 byte tests + data.add(new Object[]{"E0FFFFFF", 0x00_FF_FF_FFL}); + data.add(new Object[]{"FFFFFFFF", 0x1F_FF_FF_FFL}); + // extra bytes (not related to channelId) + data.add(new Object[]{"E123456789", 0x01_23_45_67L}); + + // @formatter:on + return data; + } + + @Rule + public TestName testname = new TestName(); + + private String rawhex; + private byte buf[]; + private long expected; + + public MuxParserReadChannelId_GoodTest(String rawhex, long expected) + { + this.rawhex = rawhex; + this.buf = TypeUtil.fromHexString(rawhex); + this.expected = expected; + } + + @Test + public void testReadChannelId() + { + System.err.printf("Running %s.%s - hex: %s%n",this.getClass().getName(),testname.getMethodName(),rawhex); + ByteBuffer bbuf = ByteBuffer.wrap(buf); + long actual = parser.readChannelId(bbuf); + Assert.assertThat("Channel ID from buffer [" + rawhex + "]",actual,is(expected)); + } +} diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxReducer.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxReducer.java new file mode 100644 index 00000000000..6a9b3bd5396 --- /dev/null +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxReducer.java @@ -0,0 +1,47 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core.extensions.mux; + +import java.io.IOException; + +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.websocket.core.io.OutgoingFrames; +import org.eclipse.jetty.websocket.core.protocol.WebSocketFrame; + +/** + * Helpful utility class to parse arbitrary mux events from a physical connection's OutgoingFrames. + * + * @see MuxInjector + */ +public class MuxReducer extends MuxEventCapture implements OutgoingFrames +{ + private MuxParser parser; + + public MuxReducer() + { + parser = new MuxParser(); + parser.setEvents(this); + } + + @Override + public void output(C context, Callback callback, WebSocketFrame frame) throws IOException + { + parser.parse(frame); + } +} diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/add/DummyMuxAddServer.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/add/DummyMuxAddServer.java new file mode 100644 index 00000000000..ee7a0fe4486 --- /dev/null +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/add/DummyMuxAddServer.java @@ -0,0 +1,60 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core.extensions.mux.add; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.websocket.core.api.WebSocketConnection; +import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxAddChannelRequest; +import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxAddChannelResponse; + +/** + * Dummy impl of MuxAddServer + */ +public class DummyMuxAddServer implements MuxAddServer +{ + private static final Logger LOG = Log.getLogger(DummyMuxAddServer.class); + + @Override + public MuxAddChannelResponse handshake(WebSocketConnection physicalConnection, MuxAddChannelRequest request) throws IOException + { + + MuxAddChannelResponse response = new MuxAddChannelResponse(); + response.setChannelId(request.getChannelId()); + response.setEnc((byte)0); + + StringBuilder hresp = new StringBuilder(); + hresp.append("HTTP/1.1 101 Switching Protocols\r\n"); + hresp.append("Connection: upgrade\r\n"); + // not meaningful (per Draft 08) hresp.append("Upgrade: websocket\r\n"); + // not meaningful (per Draft 08) hresp.append("Sec-WebSocket-Accept: Kgo85/8KVE8YPONSeyhgL3GwqhI=\r\n"); + hresp.append("\r\n"); + + ByteBuffer handshake = BufferUtil.toBuffer(hresp.toString()); + LOG.debug("Handshake: {}",BufferUtil.toDetailString(handshake)); + + response.setHandshake(handshake); + + return response; + } +} diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/add/MuxerAddServerTest.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/add/MuxerAddServerTest.java new file mode 100644 index 00000000000..b2ba07356d7 --- /dev/null +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/add/MuxerAddServerTest.java @@ -0,0 +1,81 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.core.extensions.mux.add; + +import static org.hamcrest.Matchers.*; + +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.websocket.core.api.WebSocketPolicy; +import org.eclipse.jetty.websocket.core.extensions.mux.MuxInjector; +import org.eclipse.jetty.websocket.core.extensions.mux.MuxOp; +import org.eclipse.jetty.websocket.core.extensions.mux.MuxReducer; +import org.eclipse.jetty.websocket.core.extensions.mux.Muxer; +import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxAddChannelRequest; +import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxAddChannelResponse; +import org.eclipse.jetty.websocket.core.io.LocalWebSocketConnection; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; + +public class MuxerAddServerTest +{ + @Rule + public TestName testname = new TestName(); + + @Test + public void testAddChannel_Server() throws Exception + { + LocalWebSocketConnection physical = new LocalWebSocketConnection(testname); + physical.setPolicy(WebSocketPolicy.newServerPolicy()); + + MuxReducer reducer = new MuxReducer(); + + Muxer muxer = new Muxer(physical,reducer); + DummyMuxAddServer addServer = new DummyMuxAddServer(); + muxer.setAddServer(addServer); + + MuxInjector inject = new MuxInjector(muxer); + + + // Trigger AddChannel + StringBuilder request = new StringBuilder(); + request.append("GET /echo HTTP/1.1\r\n"); + request.append("Host: localhost\r\n"); + request.append("Upgrade: websocket\r\n"); + request.append("Connection: Upgrade\r\n"); + request.append("Sec-WebSocket-Key: ZDTIRU5vU9xOfkg8JAgN3A==\r\n"); + request.append("Sec-WebSocket-Version: 13\r\n"); + request.append("\r\n"); + + MuxAddChannelRequest req = new MuxAddChannelRequest(); + req.setChannelId(1); + req.setEnc((byte)0); + req.setHandshake(BufferUtil.toBuffer(request.toString())); + + inject.op(req); + + reducer.assertHasOp(MuxOp.ADD_CHANNEL_RESPONSE,1); + MuxAddChannelResponse response = (MuxAddChannelResponse)reducer.getOps().pop(); + Assert.assertThat("AddChannelResponse.channelId",response.getChannelId(),is(1L)); + Assert.assertThat("AddChannelResponse.failed",response.isFailed(),is(false)); + Assert.assertThat("AddChannelResponse.handshake",response.getHandshake(),notNullValue()); + Assert.assertThat("AddChannelResponse.handshakeSize",response.getHandshakeSize(),is(57L)); + } +} diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/io/LocalWebSocketConnection.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/io/LocalWebSocketConnection.java index 64157c5f1b2..6737ace5283 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/io/LocalWebSocketConnection.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/io/LocalWebSocketConnection.java @@ -32,6 +32,7 @@ import org.junit.rules.TestName; public class LocalWebSocketConnection implements WebSocketConnection { private final String id; + private WebSocketPolicy policy = WebSocketPolicy.newServerPolicy(); public LocalWebSocketConnection() { @@ -66,7 +67,7 @@ public class LocalWebSocketConnection implements WebSocketConnection @Override public WebSocketPolicy getPolicy() { - return null; + return policy; } @Override @@ -125,6 +126,11 @@ public class LocalWebSocketConnection implements WebSocketConnection { } + public void setPolicy(WebSocketPolicy policy) + { + this.policy = policy; + } + @Override public SuspendToken suspend() { diff --git a/jetty-websocket/websocket-core/src/test/resources/jetty-logging.properties b/jetty-websocket/websocket-core/src/test/resources/jetty-logging.properties index e7cebead1af..a40bcc9b25c 100644 --- a/jetty-websocket/websocket-core/src/test/resources/jetty-logging.properties +++ b/jetty-websocket/websocket-core/src/test/resources/jetty-logging.properties @@ -1,5 +1,5 @@ org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog -org.eclipse.jetty.websocket.LEVEL=WARN +org.eclipse.jetty.websocket.LEVEL=DEBUG # org.eclipse.jetty.websocket.protocol.Parser.LEVEL=DEBUG # org.eclipse.jetty.websocket.protocol.LEVEL=DEBUG # org.eclipse.jetty.websocket.io.payload.LEVEL=DEBUG From 62f8e13397eb9e7140e3dcfa0d635258adda7813 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Wed, 31 Oct 2012 13:20:52 +0100 Subject: [PATCH 12/31] HTTP client: renamed Request.aborted() to Request.isAborted() to comply with the naming convention. --- .../main/java/org/eclipse/jetty/client/HttpDestination.java | 2 +- .../src/main/java/org/eclipse/jetty/client/HttpRequest.java | 2 +- .../src/main/java/org/eclipse/jetty/client/HttpSender.java | 4 ++-- .../org/eclipse/jetty/client/RedirectProtocolHandler.java | 2 +- .../src/main/java/org/eclipse/jetty/client/api/Request.java | 2 +- .../java/org/eclipse/jetty/client/HttpClientTimeoutTest.java | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) 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 4145d1d6bf2..489826a477b 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 @@ -248,7 +248,7 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable { final Request request = requestContext.request; final List listeners = requestContext.listeners; - if (request.aborted()) + if (request.isAborted()) { abort(request, listeners, "Aborted"); LOG.debug("Aborted {} before processing", request); 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 7dffe62ac6e..537c69966b3 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 @@ -413,7 +413,7 @@ public class HttpRequest implements Request } @Override - public boolean aborted() + public boolean isAborted() { return aborted; } 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 bd38c9119f6..dabd10cbc12 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 @@ -82,7 +82,7 @@ public class HttpSender } Request request = exchange.getRequest(); - if (request.aborted()) + if (request.isAborted()) { exchange.abort(null); } @@ -400,7 +400,7 @@ public class HttpSender Result result = completion.getReference(); boolean notCommitted = current == State.IDLE || current == State.SEND; - if (result == null && notCommitted && !request.aborted()) + if (result == null && notCommitted && !request.isAborted()) { result = exchange.responseComplete(failure).getReference(); exchange.terminateResponse(); 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 82895958ae6..c53b29e4be8 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 @@ -126,7 +126,7 @@ public class RedirectProtocolHandler extends Response.Listener.Empty implements @Override public void onBegin(Request redirect) { - if (request.aborted()) + if (request.isAborted()) redirect.abort(null); } }); diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java index 681bfe81423..1d36b9e8894 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java @@ -316,7 +316,7 @@ public interface Request /** * @return whether {@link #abort(String)} was called */ - boolean aborted(); + boolean isAborted(); public interface RequestListener extends EventListener { 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 700c14e6296..ba3f6488557 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 @@ -151,7 +151,7 @@ public class HttpClientTimeoutTest extends AbstractHttpClientServerTest TimeUnit.MILLISECONDS.sleep(2 * timeout); - Assert.assertFalse(request.aborted()); + Assert.assertFalse(request.isAborted()); } @Slow @@ -208,7 +208,7 @@ public class HttpClientTimeoutTest extends AbstractHttpClientServerTest TimeUnit.MILLISECONDS.sleep(2 * timeout); - Assert.assertFalse(request.aborted()); + Assert.assertFalse(request.isAborted()); } } From b5fd0950550a898058251e22b2b72aec5d0415c0 Mon Sep 17 00:00:00 2001 From: Jesse McConnell Date: Wed, 31 Oct 2012 10:24:47 -0500 Subject: [PATCH 13/31] added test case to validate the existing regex rewrite rule as wired up --- .../jetty/rewrite/handler/RewriteHandlerTest.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/RewriteHandlerTest.java b/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/RewriteHandlerTest.java index 26eaf136fd5..497006c6ab8 100644 --- a/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/RewriteHandlerTest.java +++ b/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/RewriteHandlerTest.java @@ -77,6 +77,20 @@ public class RewriteHandlerTest extends AbstractRuleTestCase @Test public void test() throws Exception { + _response.setStatus(200); + _request.setHandled(false); + _handler.setOriginalPathAttribute("/before"); + _handler.setRewriteRequestURI(true); + _handler.setRewritePathInfo(true); + _request.setRequestURI("/xxx/bar"); + _request.setPathInfo("/xxx/bar"); + _handler.handle("/xxx/bar",_request,_request, _response); + assertEquals(201,_response.getStatus()); + assertEquals("/bar/zzz",_request.getAttribute("target")); + assertEquals("/bar/zzz",_request.getAttribute("URI")); + assertEquals("/bar/zzz",_request.getAttribute("info")); + assertEquals(null,_request.getAttribute("before")); + _response.setStatus(200); _request.setHandled(false); _handler.setOriginalPathAttribute("/before"); From 01c9e650fba6840fdcbc035fe2d289bd0c3f8028 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Wed, 31 Oct 2012 11:07:05 -0700 Subject: [PATCH 14/31] First working TEXT echo thru the muxer (as test case) --- .../core/extensions/mux/MuxChannel.java | 2 + .../core/extensions/mux/MuxGenerator.java | 2 + .../websocket/core/extensions/mux/Muxer.java | 50 +++++++++++++++---- .../core/extensions/mux/add/MuxAddServer.java | 23 +++++++-- .../mux/op/MuxAddChannelRequest.java | 10 ++++ .../mux/op/MuxAddChannelResponse.java | 6 +++ .../core/examples/echo/AdapterEchoSocket.java | 6 +++ .../core/extensions/mux/MuxEventCapture.java | 19 +++++++ .../core/extensions/mux/MuxInjector.java | 6 ++- .../core/extensions/mux/MuxReducer.java | 1 + .../extensions/mux/add/DummyMuxAddServer.java | 50 +++++++++++-------- .../mux/add/MuxerAddServerTest.java | 14 +++++- .../core/io/LocalWebSocketConnection.java | 11 +++- 13 files changed, 162 insertions(+), 38 deletions(-) diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxChannel.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxChannel.java index 1f95e2e8427..fed481e733d 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxChannel.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxChannel.java @@ -254,6 +254,8 @@ public class MuxChannel implements WebSocketConnection, IncomingFrames, Outgoing public void setSession(WebSocketSession session) { this.session = session; + this.incoming = session; + session.setOutgoing(this); } public void setSubProtocol(String subProtocol) diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxGenerator.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxGenerator.java index 6d2ae15f7b6..2950d091c9e 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxGenerator.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxGenerator.java @@ -154,6 +154,7 @@ public class MuxGenerator public void output(C context, Callback callback, long channelId, WebSocketFrame frame) throws IOException { ByteBuffer muxPayload = bufferPool.acquire(frame.getPayloadLength() + DATA_FRAME_OVERHEAD,false); + BufferUtil.flipToFill(muxPayload); // start building mux payload writeChannelId(muxPayload,channelId); @@ -167,6 +168,7 @@ public class MuxGenerator // build muxed frame WebSocketFrame muxFrame = WebSocketFrame.binary(); + BufferUtil.flipToFlush(muxPayload,0); muxFrame.setPayload(muxPayload); // NOTE: the physical connection will handle masking rules for this frame. diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/Muxer.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/Muxer.java index 626acee4f52..64e09293164 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/Muxer.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/Muxer.java @@ -21,8 +21,10 @@ package org.eclipse.jetty.websocket.core.extensions.mux; import java.io.IOException; import java.net.InetSocketAddress; import java.util.HashMap; +import java.util.List; import java.util.Map; +import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.log.Log; @@ -71,6 +73,10 @@ public class Muxer implements IncomingFrames, MuxParser.Listener private MuxGenerator generator; private MuxAddServer addServer; private MuxAddClient addClient; + /** The original request headers, used for delta encoded AddChannelRequest blocks */ + private List physicalRequestHeaders; + /** The original response headers, used for delta encoded AddChannelResponse blocks */ + private List physicalResponseHeaders; public Muxer(final WebSocketConnection connection, final OutgoingFrames outgoing) { @@ -135,6 +141,12 @@ public class Muxer implements IncomingFrames, MuxParser.Listener return physicalConnection.isOpen(); } + public String mergeHeaders(List physicalHeaders, String deltaHeaders) + { + // TODO Auto-generated method stub + return null; + } + /** * Per spec, the physical connection must be failed. *

@@ -194,17 +206,27 @@ public class Muxer implements IncomingFrames, MuxParser.Listener // submit to upgrade handshake process try { - MuxAddChannelResponse response = addServer.handshake(physicalConnection,request); - if (response == null) + String requestHandshake = BufferUtil.toUTF8String(request.getHandshake()); + if (request.isDeltaEncoded()) { - LOG.warn("AddChannelResponse is null"); - // no upgrade possible? - response = new MuxAddChannelResponse(); - response.setChannelId(request.getChannelId()); - response.setFailed(true); + // Merge original request headers out of physical connection. + requestHandshake = mergeHeaders(physicalRequestHeaders,requestHandshake); + } + String responseHandshake = addServer.handshake(channel,requestHandshake); + if (StringUtil.isNotBlank(responseHandshake)) + { + // Upgrade Success + MuxAddChannelResponse response = new MuxAddChannelResponse(); + response.setChannelId(request.getChannelId()); + response.setFailed(false); + response.setHandshake(responseHandshake); + // send response + this.generator.generate(response); + } + else + { + // TODO: trigger error? } - // send response - this.generator.generate(response); } catch (Throwable t) { @@ -369,6 +391,10 @@ public class Muxer implements IncomingFrames, MuxParser.Listener */ public void output(C context, Callback callback, long channelId, WebSocketFrame frame) throws IOException { + if (LOG.isDebugEnabled()) + { + LOG.debug("output({}, {}, {}, {})",context,callback,channelId,frame); + } generator.output(context,callback,channelId,frame); } @@ -394,4 +420,10 @@ public class Muxer implements IncomingFrames, MuxParser.Listener { this.remoteAddress = remoteAddress; } + + @Override + public String toString() + { + return String.format("Muxer[subChannels.size=%d]", channels.size()); + } } diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/add/MuxAddServer.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/add/MuxAddServer.java index 16161630b6d..06a31135f81 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/add/MuxAddServer.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/add/MuxAddServer.java @@ -20,14 +20,27 @@ package org.eclipse.jetty.websocket.core.extensions.mux.add; import java.io.IOException; -import org.eclipse.jetty.websocket.core.api.WebSocketConnection; -import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxAddChannelRequest; -import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxAddChannelResponse; +import org.eclipse.jetty.websocket.core.extensions.mux.MuxChannel; +import org.eclipse.jetty.websocket.core.extensions.mux.MuxException; +import org.eclipse.jetty.websocket.core.io.WebSocketSession; /** - * Server interface, for dealing with incoming AddChannelRequest and posting of AddChannelResponse back. + * Server interface, for dealing with incoming AddChannelRequest / AddChannelResponse flows. */ public interface MuxAddServer { - MuxAddChannelResponse handshake(WebSocketConnection physicalConnection, MuxAddChannelRequest request) throws IOException; + /** + * Perform the handshake. + * + * @param channel + * the channel to attach the {@link WebSocketSession} to. + * @param requestHandshake + * the request handshake (request headers) + * @return the response handshake (the response headers) + * @throws MuxException + * if unable to handshake + * @throws IOException + * if unable to parse request headers + */ + String handshake(MuxChannel channel, String requestHandshake) throws MuxException, IOException; } diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/op/MuxAddChannelRequest.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/op/MuxAddChannelRequest.java index ecba1697307..06374224df7 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/op/MuxAddChannelRequest.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/op/MuxAddChannelRequest.java @@ -65,6 +65,16 @@ public class MuxAddChannelRequest implements MuxControlBlock return rsv; } + public boolean isDeltaEncoded() + { + return (enc == 1); + } + + public boolean isIdentityEncoded() + { + return (enc == 0); + } + public void setChannelId(long channelId) { this.channelId = channelId; diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/op/MuxAddChannelResponse.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/op/MuxAddChannelResponse.java index 9f86fdcd46b..ca8e70f521d 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/op/MuxAddChannelResponse.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/op/MuxAddChannelResponse.java @@ -20,6 +20,7 @@ package org.eclipse.jetty.websocket.core.extensions.mux.op; import java.nio.ByteBuffer; +import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.websocket.core.extensions.mux.MuxControlBlock; import org.eclipse.jetty.websocket.core.extensions.mux.MuxOp; @@ -98,6 +99,11 @@ public class MuxAddChannelResponse implements MuxControlBlock } } + public void setHandshake(String responseHandshake) + { + setHandshake(BufferUtil.toBuffer(responseHandshake)); + } + public void setRsv(byte rsv) { this.rsv = rsv; diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/examples/echo/AdapterEchoSocket.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/examples/echo/AdapterEchoSocket.java index e6ac3699868..8981f653e0c 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/examples/echo/AdapterEchoSocket.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/examples/echo/AdapterEchoSocket.java @@ -20,6 +20,8 @@ package org.eclipse.jetty.websocket.core.examples.echo; import java.io.IOException; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.websocket.core.api.WebSocketAdapter; /** @@ -27,16 +29,20 @@ import org.eclipse.jetty.websocket.core.api.WebSocketAdapter; */ public class AdapterEchoSocket extends WebSocketAdapter { + private static final Logger LOG = Log.getLogger(AdapterEchoSocket.class); + @Override public void onWebSocketText(String message) { if (isNotConnected()) { + LOG.debug("WebSocket Not Connected"); return; } try { + LOG.debug("Echoing back message [{}]",message); // echo the data back getBlockingConnection().write(message); } diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxEventCapture.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxEventCapture.java index b6e1e7da15b..6c3cf12a71d 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxEventCapture.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxEventCapture.java @@ -29,6 +29,7 @@ import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxAddChannelResponse; import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxDropChannel; import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxFlowControl; import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxNewChannelSlot; +import org.eclipse.jetty.websocket.core.protocol.OpCode; import org.junit.Assert; public class MuxEventCapture implements MuxParser.Listener @@ -44,6 +45,24 @@ public class MuxEventCapture implements MuxParser.Listener Assert.assertThat("Frame Count",frames.size(), is(expected)); } + public void assertHasFrame(byte opcode, long channelId, int expectedCount) + { + int actualCount = 0; + + for (MuxedFrame frame : frames) + { + if (frame.getChannelId() == channelId) + { + if (frame.getOpCode() == opcode) + { + actualCount++; + } + } + } + + Assert.assertThat("Expected Count of " + OpCode.name(opcode) + " frames on Channel ID " + channelId,actualCount,is(expectedCount)); + } + public void assertHasOp(byte opCode, int expectedCount) { int actualCount = 0; diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxInjector.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxInjector.java index ef13fed5c93..d3d9170e3fe 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxInjector.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxInjector.java @@ -21,6 +21,8 @@ package org.eclipse.jetty.websocket.core.extensions.mux; import java.io.IOException; import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.websocket.core.io.IncomingFrames; import org.eclipse.jetty.websocket.core.io.OutgoingFrames; import org.eclipse.jetty.websocket.core.protocol.WebSocketFrame; @@ -32,6 +34,7 @@ import org.eclipse.jetty.websocket.core.protocol.WebSocketFrame; */ public class MuxInjector implements OutgoingFrames { + private static final Logger LOG = Log.getLogger(MuxInjector.class); private IncomingFrames incoming; private MuxGenerator generator; @@ -42,7 +45,7 @@ public class MuxInjector implements OutgoingFrames this.generator.setOutgoing(this); } - public void op(long channelId, WebSocketFrame frame) throws IOException + public void frame(long channelId, WebSocketFrame frame) throws IOException { this.generator.generate(channelId,frame); } @@ -55,6 +58,7 @@ public class MuxInjector implements OutgoingFrames @Override public void output(C context, Callback callback, WebSocketFrame frame) throws IOException { + LOG.debug("Injecting {} to {}",frame,incoming); this.incoming.incoming(frame); } } diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxReducer.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxReducer.java index 6a9b3bd5396..f9f46766c26 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxReducer.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/MuxReducer.java @@ -43,5 +43,6 @@ public class MuxReducer extends MuxEventCapture implements OutgoingFrames public void output(C context, Callback callback, WebSocketFrame frame) throws IOException { parser.parse(frame); + callback.completed(context); // let blocked calls know the send is complete. } } diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/add/DummyMuxAddServer.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/add/DummyMuxAddServer.java index ee7a0fe4486..4c5c8599189 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/add/DummyMuxAddServer.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/add/DummyMuxAddServer.java @@ -19,42 +19,52 @@ package org.eclipse.jetty.websocket.core.extensions.mux.add; import java.io.IOException; -import java.nio.ByteBuffer; -import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; -import org.eclipse.jetty.websocket.core.api.WebSocketConnection; -import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxAddChannelRequest; -import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxAddChannelResponse; +import org.eclipse.jetty.websocket.core.api.WebSocketPolicy; +import org.eclipse.jetty.websocket.core.examples.echo.AdapterEchoSocket; +import org.eclipse.jetty.websocket.core.extensions.mux.MuxChannel; +import org.eclipse.jetty.websocket.core.extensions.mux.MuxException; +import org.eclipse.jetty.websocket.core.io.WebSocketSession; +import org.eclipse.jetty.websocket.core.io.event.EventDriver; +import org.eclipse.jetty.websocket.core.io.event.EventDriverFactory; /** * Dummy impl of MuxAddServer */ public class DummyMuxAddServer implements MuxAddServer { + @SuppressWarnings("unused") private static final Logger LOG = Log.getLogger(DummyMuxAddServer.class); + private AdapterEchoSocket echo; + private WebSocketPolicy policy; + private EventDriverFactory eventDriverFactory; + + public DummyMuxAddServer() + { + this.policy = WebSocketPolicy.newServerPolicy(); + this.eventDriverFactory = new EventDriverFactory(policy); + this.echo = new AdapterEchoSocket(); + } @Override - public MuxAddChannelResponse handshake(WebSocketConnection physicalConnection, MuxAddChannelRequest request) throws IOException + public String handshake(MuxChannel channel, String requestHandshake) throws MuxException, IOException { - - MuxAddChannelResponse response = new MuxAddChannelResponse(); - response.setChannelId(request.getChannelId()); - response.setEnc((byte)0); - - StringBuilder hresp = new StringBuilder(); - hresp.append("HTTP/1.1 101 Switching Protocols\r\n"); - hresp.append("Connection: upgrade\r\n"); + StringBuilder response = new StringBuilder(); + response.append("HTTP/1.1 101 Switching Protocols\r\n"); + response.append("Connection: upgrade\r\n"); // not meaningful (per Draft 08) hresp.append("Upgrade: websocket\r\n"); // not meaningful (per Draft 08) hresp.append("Sec-WebSocket-Accept: Kgo85/8KVE8YPONSeyhgL3GwqhI=\r\n"); - hresp.append("\r\n"); + response.append("\r\n"); - ByteBuffer handshake = BufferUtil.toBuffer(hresp.toString()); - LOG.debug("Handshake: {}",BufferUtil.toDetailString(handshake)); + EventDriver websocket = this.eventDriverFactory.wrap(echo); + WebSocketSession session = new WebSocketSession(websocket,channel,channel.getPolicy(),"echo"); + channel.setSession(session); + channel.setSubProtocol("echo"); + channel.onOpen(); + session.onConnect(); - response.setHandshake(handshake); - - return response; + return response.toString(); } } diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/add/MuxerAddServerTest.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/add/MuxerAddServerTest.java index b2ba07356d7..fc8608c2b4a 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/add/MuxerAddServerTest.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/extensions/mux/add/MuxerAddServerTest.java @@ -29,6 +29,8 @@ import org.eclipse.jetty.websocket.core.extensions.mux.Muxer; import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxAddChannelRequest; import org.eclipse.jetty.websocket.core.extensions.mux.op.MuxAddChannelResponse; import org.eclipse.jetty.websocket.core.io.LocalWebSocketConnection; +import org.eclipse.jetty.websocket.core.protocol.OpCode; +import org.eclipse.jetty.websocket.core.protocol.WebSocketFrame; import org.junit.Assert; import org.junit.Rule; import org.junit.Test; @@ -44,16 +46,17 @@ public class MuxerAddServerTest { LocalWebSocketConnection physical = new LocalWebSocketConnection(testname); physical.setPolicy(WebSocketPolicy.newServerPolicy()); + physical.onOpen(); MuxReducer reducer = new MuxReducer(); + // Represents a server side muxer. Muxer muxer = new Muxer(physical,reducer); DummyMuxAddServer addServer = new DummyMuxAddServer(); muxer.setAddServer(addServer); MuxInjector inject = new MuxInjector(muxer); - // Trigger AddChannel StringBuilder request = new StringBuilder(); request.append("GET /echo HTTP/1.1\r\n"); @@ -71,11 +74,20 @@ public class MuxerAddServerTest inject.op(req); + // Make sure we got AddChannelResponse reducer.assertHasOp(MuxOp.ADD_CHANNEL_RESPONSE,1); MuxAddChannelResponse response = (MuxAddChannelResponse)reducer.getOps().pop(); Assert.assertThat("AddChannelResponse.channelId",response.getChannelId(),is(1L)); Assert.assertThat("AddChannelResponse.failed",response.isFailed(),is(false)); Assert.assertThat("AddChannelResponse.handshake",response.getHandshake(),notNullValue()); Assert.assertThat("AddChannelResponse.handshakeSize",response.getHandshakeSize(),is(57L)); + + reducer.reset(); + + // Send simple echo request + inject.frame(1,WebSocketFrame.text("Hello World")); + + // Test for echo response (is there a user echo websocket connected to the sub-channel?) + reducer.assertHasFrame(OpCode.TEXT,1L,1); } } diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/io/LocalWebSocketConnection.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/io/LocalWebSocketConnection.java index 6737ace5283..e69341c1892 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/io/LocalWebSocketConnection.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/io/LocalWebSocketConnection.java @@ -33,6 +33,7 @@ public class LocalWebSocketConnection implements WebSocketConnection { private final String id; private WebSocketPolicy policy = WebSocketPolicy.newServerPolicy(); + private boolean open = false; public LocalWebSocketConnection() { @@ -52,16 +53,19 @@ public class LocalWebSocketConnection implements WebSocketConnection @Override public void close() { + open = false; } @Override public void close(int statusCode, String reason) { + open = false; } @Override public void disconnect() { + open = false; } @Override @@ -98,10 +102,9 @@ public class LocalWebSocketConnection implements WebSocketConnection @Override public boolean isOpen() { - return false; + return open; } - @Override public boolean isOutputClosed() { @@ -121,6 +124,10 @@ public class LocalWebSocketConnection implements WebSocketConnection // TODO Auto-generated method stub } + public void onOpen() { + open = true; + } + @Override public void ping(C context, Callback callback, byte[] payload) throws IOException { From 171fa2db677ce08adde63ce4acbba99952a69547 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Wed, 31 Oct 2012 13:27:21 -0700 Subject: [PATCH 15/31] More use of StringUtil.truncate --- .../jetty/websocket/core/extensions/mux/Muxer.java | 6 +----- .../jetty/websocket/core/io/event/EventDriver.java | 9 +-------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/Muxer.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/Muxer.java index 64e09293164..71b351ace54 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/Muxer.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/extensions/mux/Muxer.java @@ -302,11 +302,7 @@ public class Muxer implements IncomingFrames, MuxParser.Listener } String reason = "Mux " + drop.toString(); - if (reason.length() > (WebSocketFrame.MAX_CONTROL_PAYLOAD - 2)) - { - reason = reason.substring(0,WebSocketFrame.MAX_CONTROL_PAYLOAD - 2); - } - + reason = StringUtil.truncate(reason,(WebSocketFrame.MAX_CONTROL_PAYLOAD - 2)); channel.close(StatusCode.PROTOCOL,reason); // TODO: set channel to inactive? } diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/io/event/EventDriver.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/io/event/EventDriver.java index d02010dd931..00fb6694ea2 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/io/event/EventDriver.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/io/event/EventDriver.java @@ -180,14 +180,7 @@ public abstract class EventDriver implements IncomingFrames protected void terminateConnection(int statusCode, String rawreason) { String reason = rawreason; - if (StringUtil.isNotBlank(reason)) - { - // Trim big exception messages here. - if (reason.length() > (WebSocketFrame.MAX_CONTROL_PAYLOAD - 2)) - { - reason = reason.substring(0,WebSocketFrame.MAX_CONTROL_PAYLOAD - 2); - } - } + reason = StringUtil.truncate(reason,(WebSocketFrame.MAX_CONTROL_PAYLOAD - 2)); LOG.debug("terminateConnection({},{})",statusCode,rawreason); session.close(statusCode,reason); } From e466fa06b9d7933fc419a1a11e1ddb4456db38a4 Mon Sep 17 00:00:00 2001 From: Jan Bartel Date: Thu, 1 Nov 2012 11:44:40 +1100 Subject: [PATCH 16/31] 393291 Confusing log entry about (non) existing webAppSourceDirectory --- .../java/org/eclipse/jetty/maven/plugin/JettyRunMojo.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunMojo.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunMojo.java index acd37629849..4c482127296 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunMojo.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunMojo.java @@ -63,6 +63,8 @@ import org.eclipse.jetty.webapp.WebAppContext; */ public class JettyRunMojo extends AbstractJettyMojo { + public static final String DEFAULT_WEBAPP_SRC = "src"+File.separator+"main"+File.separator+"webapp"; + /** * If true, the <testOutputDirectory> * and the dependencies of <scope>test<scope> @@ -148,8 +150,9 @@ public class JettyRunMojo extends AbstractJettyMojo { if ((getWebAppSourceDirectory() == null) || !getWebAppSourceDirectory().exists()) { - webAppSourceDirectory = new File (project.getBasedir(), "src"+File.separator+"main"+File.separator+"webapp"); - getLog().info("webAppSourceDirectory "+getWebAppSourceDirectory() +" does not exist. Defaulting to "+webAppSourceDirectory.getAbsolutePath()); + File defaultWebAppSrcDir = new File (project.getBasedir(), DEFAULT_WEBAPP_SRC); + getLog().info("webAppSourceDirectory"+(getWebAppSourceDirectory()==null?" not set.":" does not exist.")+" Defaulting to "+defaultWebAppSrcDir.getAbsolutePath()); + webAppSourceDirectory = defaultWebAppSrcDir; } else getLog().info( "Webapp source directory = " + getWebAppSourceDirectory().getCanonicalPath()); From ced624e203adf597cc58a0a22f75d623256a3784 Mon Sep 17 00:00:00 2001 From: Jan Bartel Date: Thu, 1 Nov 2012 11:45:23 +1100 Subject: [PATCH 17/31] Enable jetty-maven-plugin for test webapp. --- test-jetty-webapp/pom.xml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test-jetty-webapp/pom.xml b/test-jetty-webapp/pom.xml index df56c84db86..d729339925f 100644 --- a/test-jetty-webapp/pom.xml +++ b/test-jetty-webapp/pom.xml @@ -93,9 +93,8 @@ - + + + diff --git a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/FileConfigurationManager.java b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/PropertiesConfigurationManager.java similarity index 54% rename from jetty-deploy/src/main/java/org/eclipse/jetty/deploy/FileConfigurationManager.java rename to jetty-deploy/src/main/java/org/eclipse/jetty/deploy/PropertiesConfigurationManager.java index 076dafe93f6..8cb5c344e82 100644 --- a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/FileConfigurationManager.java +++ b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/PropertiesConfigurationManager.java @@ -21,10 +21,15 @@ package org.eclipse.jetty.deploy; import java.io.FileNotFoundException; import java.io.IOException; import java.net.MalformedURLException; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Properties; +import org.eclipse.jetty.util.annotation.ManagedAttribute; +import org.eclipse.jetty.util.annotation.ManagedObject; +import org.eclipse.jetty.util.annotation.ManagedOperation; +import org.eclipse.jetty.util.annotation.Name; import org.eclipse.jetty.util.resource.Resource; /** @@ -32,42 +37,55 @@ import org.eclipse.jetty.util.resource.Resource; * * Supplies properties defined in a file. */ -public class FileConfigurationManager implements ConfigurationManager +@ManagedObject("Configure deployed webapps via properties") +public class PropertiesConfigurationManager implements ConfigurationManager { - private Resource _file; - private Map _map = new HashMap(); + private String _properties; + private final Map _map = new HashMap(); - public FileConfigurationManager() + public PropertiesConfigurationManager(String properties) + { + } + + public PropertiesConfigurationManager() { } - public void setFile(String filename) throws MalformedURLException, IOException + @ManagedAttribute("A file or URL of properties") + public void setFile(String resource) throws MalformedURLException, IOException { - _file = Resource.newResource(filename); + _properties=resource; + _map.clear(); + loadProperties(_properties); } + public String getFile() + { + return _properties; + } + + @ManagedOperation("Set a property") + public void put(@Name("name")String name, @Name("value")String value) + { + _map.put(name,value); + } + /** * @see org.eclipse.jetty.deploy.ConfigurationManager#getProperties() */ + @Override public Map getProperties() { - try - { - loadProperties(); - return _map; - } - catch (Exception e) - { - throw new RuntimeException(e); - } + return new HashMap<>(_map); } - private void loadProperties() throws FileNotFoundException, IOException - { - if (_map.isEmpty() && _file!=null) + private void loadProperties(String resource) throws FileNotFoundException, IOException + { + Resource file=Resource.newResource(resource); + if (file!=null && file.exists()) { Properties properties = new Properties(); - properties.load(_file.getInputStream()); + properties.load(file.getInputStream()); for (Map.Entry entry : properties.entrySet()) _map.put(entry.getKey().toString(),String.valueOf(entry.getValue())); } diff --git a/jetty-deploy/src/test/resources/binding-test-contexts-1.xml b/jetty-deploy/src/test/resources/binding-test-contexts-1.xml index 1614056f430..37f5292ff2e 100644 --- a/jetty-deploy/src/test/resources/binding-test-contexts-1.xml +++ b/jetty-deploy/src/test/resources/binding-test-contexts-1.xml @@ -50,7 +50,7 @@ /webapps 1 - + /xml-configured-jetty.properties diff --git a/jetty-deploy/src/test/resources/jetty-deploymgr-contexts.xml b/jetty-deploy/src/test/resources/jetty-deploymgr-contexts.xml index acf69585ff5..ba38daef80d 100644 --- a/jetty-deploy/src/test/resources/jetty-deploymgr-contexts.xml +++ b/jetty-deploy/src/test/resources/jetty-deploymgr-contexts.xml @@ -17,7 +17,7 @@ /webapps 1 - + /xml-configured-jetty.properties diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/ClasspathPattern.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/ClasspathPattern.java index 63ec5b4ca76..f402b3191c9 100644 --- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/ClasspathPattern.java +++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/ClasspathPattern.java @@ -81,8 +81,6 @@ public class ClasspathPattern /* ------------------------------------------------------------ */ /** - * Initialize the matcher by parsing each classpath pattern in an array - * * @param patterns array of classpath patterns */ private void addPatterns(String[] patterns) @@ -93,7 +91,8 @@ public class ClasspathPattern for (String pattern : patterns) { entry = createEntry(pattern); - if (entry != null) { + if (entry != null) + { _patterns.add(pattern); _entries.add(entry); } @@ -101,6 +100,29 @@ public class ClasspathPattern } } + /* ------------------------------------------------------------ */ + /** + * @param patterns array of classpath patterns + */ + private void prependPatterns(String[] patterns) + { + if (patterns != null) + { + Entry entry = null; + int i=0; + for (String pattern : patterns) + { + entry = createEntry(pattern); + if (entry != null) + { + _patterns.add(i,pattern); + _entries.add(i,entry); + i++; + } + } + } + } + /* ------------------------------------------------------------ */ /** * Create an entry object containing information about @@ -156,9 +178,24 @@ public class ClasspathPattern patterns.add(entries.nextToken()); } - addPatterns((String[])patterns.toArray(new String[patterns.size()])); + addPatterns(patterns.toArray(new String[patterns.size()])); } + + /* ------------------------------------------------------------ */ + public void prependPattern(String classOrPackage) + { + ArrayList patterns = new ArrayList(); + StringTokenizer entries = new StringTokenizer(classOrPackage, ":,"); + while (entries.hasMoreTokens()) + { + patterns.add(entries.nextToken()); + } + + prependPatterns(patterns.toArray(new String[patterns.size()])); + } + + /* ------------------------------------------------------------ */ /** * @return array of classpath patterns @@ -217,4 +254,5 @@ public class ClasspathPattern } return result; } + } diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java index c7aad9f25bd..137dc15e9ec 100644 --- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java +++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java @@ -128,6 +128,7 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL "-org.eclipse.jetty.jndi.", // don't hide naming classes "-org.eclipse.jetty.jaas.", // don't hide jaas classes "-org.eclipse.jetty.websocket.", // WebSocket is a jetty extension + "-org.eclipse.jetty.servlets.", // don't hide jetty servlets "-org.eclipse.jetty.servlet.DefaultServlet", // don't hide default servlet "-org.eclipse.jetty.servlet.listener.", // don't hide useful listeners "org.eclipse.jetty." // hide other jetty classes @@ -650,12 +651,38 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL return _serverClasses.getPatterns(); } - public void addServerClass(String classname) + /* ------------------------------------------------------------ */ + /** Add to the list of Server classes. + * @see #setServerClasses(String[]) + * @param classOrPackage A fully qualified class name (eg com.foo.MyClass) + * or a qualified package name ending with '.' (eg com.foo.). If the class + * or package has '-' it is excluded from the server classes and order is thus + * important when added system class patterns. This argument may also be a comma + * separated list of classOrPackage patterns. + */ + public void addServerClass(String classOrPackage) { if (_serverClasses == null) loadServerClasses(); - _serverClasses.addPattern(classname); + _serverClasses.addPattern(classOrPackage); + } + + /* ------------------------------------------------------------ */ + /** Prepend to the list of Server classes. + * @see #setServerClasses(String[]) + * @param classOrPackage A fully qualified class name (eg com.foo.MyClass) + * or a qualified package name ending with '.' (eg com.foo.). If the class + * or package has '-' it is excluded from the server classes and order is thus + * important when added system class patterns. This argument may also be a comma + * separated list of classOrPackage patterns. + */ + public void prependServerClass(String classOrPackage) + { + if (_serverClasses == null) + loadServerClasses(); + + _serverClasses.prependPattern(classOrPackage); } /* ------------------------------------------------------------ */ @@ -673,12 +700,38 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL } /* ------------------------------------------------------------ */ - public void addSystemClass(String classname) + /** Add to the list of System classes. + * @see #setSystemClasses(String[]) + * @param classOrPackage A fully qualified class name (eg com.foo.MyClass) + * or a qualified package name ending with '.' (eg com.foo.). If the class + * or package has '-' it is excluded from the system classes and order is thus + * important when added system class patterns. This argument may also be a comma + * separated list of classOrPackage patterns. + */ + public void addSystemClass(String classOrPackage) { if (_systemClasses == null) loadSystemClasses(); - _systemClasses.addPattern(classname); + _systemClasses.addPattern(classOrPackage); + } + + + /* ------------------------------------------------------------ */ + /** Prepend to the list of System classes. + * @see #setSystemClasses(String[]) + * @param classOrPackage A fully qualified class name (eg com.foo.MyClass) + * or a qualified package name ending with '.' (eg com.foo.). If the class + * or package has '-' it is excluded from the system classes and order is thus + * important when added system class patterns.This argument may also be a comma + * separated list of classOrPackage patterns. + */ + public void prependSystemClass(String classOrPackage) + { + if (_systemClasses == null) + loadSystemClasses(); + + _systemClasses.prependPattern(classOrPackage); } /* ------------------------------------------------------------ */ diff --git a/test-jetty-webapp/pom.xml b/test-jetty-webapp/pom.xml index d729339925f..90be3ee99aa 100644 --- a/test-jetty-webapp/pom.xml +++ b/test-jetty-webapp/pom.xml @@ -159,6 +159,7 @@ org.eclipse.jetty jetty-servlets ${project.version} + provided org.eclipse.jetty.orbit @@ -169,6 +170,7 @@ org.eclipse.jetty.websocket websocket-server ${project.version} + provided org.eclipse.jetty diff --git a/test-jetty-webapp/src/main/java/com/acme/DispatchServlet.java b/test-jetty-webapp/src/main/java/com/acme/DispatchServlet.java index 38945f6c2a4..2f6434c1fc0 100644 --- a/test-jetty-webapp/src/main/java/com/acme/DispatchServlet.java +++ b/test-jetty-webapp/src/main/java/com/acme/DispatchServlet.java @@ -31,10 +31,6 @@ import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponseWrapper; -import org.eclipse.jetty.util.log.Log; -import org.eclipse.jetty.util.log.Logger; - - /* ------------------------------------------------------------ */ /** Test Servlet RequestDispatcher. * @@ -43,8 +39,6 @@ import org.eclipse.jetty.util.log.Logger; @SuppressWarnings("serial") public class DispatchServlet extends HttpServlet { - private static final Logger LOG = Log.getLogger(DispatchServlet.class); - /* ------------------------------------------------------------ */ String pageType; @@ -165,7 +159,7 @@ public class DispatchServlet extends HttpServlet } catch(IOException e) { - LOG.ignore(e); + // getServletContext().log("ignore",e); } } else diff --git a/test-jetty-webapp/src/main/java/com/acme/Dump.java b/test-jetty-webapp/src/main/java/com/acme/Dump.java index da4c22be2b6..01f9b16cffd 100644 --- a/test-jetty-webapp/src/main/java/com/acme/Dump.java +++ b/test-jetty-webapp/src/main/java/com/acme/Dump.java @@ -53,10 +53,6 @@ import javax.servlet.http.HttpServletResponseWrapper; import org.eclipse.jetty.continuation.Continuation; import org.eclipse.jetty.continuation.ContinuationListener; import org.eclipse.jetty.continuation.ContinuationSupport; -import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.util.StringUtil; -import org.eclipse.jetty.util.log.Log; -import org.eclipse.jetty.util.log.Logger; /** * Dump Servlet Request. @@ -64,8 +60,6 @@ import org.eclipse.jetty.util.log.Logger; @SuppressWarnings("serial") public class Dump extends HttpServlet { - private static final Logger LOG = Log.getLogger(Dump.class); - boolean fixed; Timer _timer; @@ -162,7 +156,7 @@ public class Dump extends HttpServlet try { long s = Long.parseLong(request.getParameter("sleep")); - if (request.getHeader(HttpHeader.EXPECT.asString())!=null && request.getHeader(HttpHeader.EXPECT.asString()).indexOf("102")>=0) + if (request.getHeader("Expect")!=null && request.getHeader("Expect").indexOf("102")>=0) { Thread.sleep(s/2); response.sendError(102); @@ -232,6 +226,7 @@ public class Dump extends HttpServlet continuation.addContinuationListener(new ContinuationListener() { + @Override public void onTimeout(Continuation continuation) { response.addHeader("Dump","onTimeout"); @@ -246,10 +241,11 @@ public class Dump extends HttpServlet } catch (IOException e) { - LOG.ignore(e); + getServletContext().log("",e); } } + @Override public void onComplete(Continuation continuation) { response.addHeader("Dump","onComplete"); @@ -1004,9 +1000,9 @@ public class Dump extends HttpServlet { if (s==null) return "null"; - s=StringUtil.replace(s,"&","&"); - s=StringUtil.replace(s,"<","<"); - s=StringUtil.replace(s,">",">"); + s=s.replaceAll("&","&"); + s=s.replaceAll("<","<"); + s=s.replaceAll(">",">"); return s; } } diff --git a/test-jetty-webapp/src/main/java/com/acme/LoginServlet.java b/test-jetty-webapp/src/main/java/com/acme/LoginServlet.java index e86598d2811..84bd79e3263 100644 --- a/test-jetty-webapp/src/main/java/com/acme/LoginServlet.java +++ b/test-jetty-webapp/src/main/java/com/acme/LoginServlet.java @@ -43,8 +43,6 @@ import org.eclipse.jetty.util.log.Logger; */ public class LoginServlet extends HttpServlet { - private static final Logger LOG = Log.getLogger(SecureModeServlet.class); - /* ------------------------------------------------------------ */ @Override public void init(ServletConfig config) throws ServletException @@ -63,7 +61,6 @@ public class LoginServlet extends HttpServlet @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - response.setContentType("text/html"); ServletOutputStream out = response.getOutputStream(); out.println(""); diff --git a/test-jetty-webapp/src/main/java/com/acme/WebSocketChatServlet.java b/test-jetty-webapp/src/main/java/com/acme/WebSocketChatServlet.java index f0ddb2a5113..46c6196403d 100644 --- a/test-jetty-webapp/src/main/java/com/acme/WebSocketChatServlet.java +++ b/test-jetty-webapp/src/main/java/com/acme/WebSocketChatServlet.java @@ -47,8 +47,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.util.FutureCallback; -import org.eclipse.jetty.util.log.Log; -import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.websocket.core.annotations.OnWebSocketClose; import org.eclipse.jetty.websocket.core.annotations.OnWebSocketConnect; import org.eclipse.jetty.websocket.core.annotations.OnWebSocketMessage; @@ -63,8 +61,6 @@ import org.eclipse.jetty.websocket.server.WebSocketServlet; @SuppressWarnings("serial") public class WebSocketChatServlet extends WebSocketServlet implements WebSocketCreator { - private static final Logger LOG = Log.getLogger(WebSocketChatServlet.class); - /** Holds active sockets to other members of the chat */ private final List members = new CopyOnWriteArrayList(); @@ -131,7 +127,7 @@ public class WebSocketChatServlet extends WebSocketServlet implements WebSocketC } catch (IOException e) { - LOG.warn(e); + getServletContext().log("write failed",e); } } } diff --git a/test-jetty-webapp/src/main/webapp/WEB-INF/jetty-web.xml b/test-jetty-webapp/src/main/webapp/WEB-INF/jetty-web.xml index bbaf1969170..ca48bb7ac9a 100644 --- a/test-jetty-webapp/src/main/webapp/WEB-INF/jetty-web.xml +++ b/test-jetty-webapp/src/main/webapp/WEB-INF/jetty-web.xml @@ -9,6 +9,8 @@ org.eclipse.jetty.servlet.WebApplicationContext object --> + -org.eclipse.jetty.util. + -org.eclipse.jetty.servlets. test webapp is deployed. DO NOT USE IN PRODUCTION! diff --git a/test-jetty-webapp/src/test/java/org/eclipse/jetty/TestServer.java b/test-jetty-webapp/src/test/java/org/eclipse/jetty/TestServer.java index e4372b6f9f9..f138697b3b3 100644 --- a/test-jetty-webapp/src/test/java/org/eclipse/jetty/TestServer.java +++ b/test-jetty-webapp/src/test/java/org/eclipse/jetty/TestServer.java @@ -165,7 +165,7 @@ public class TestServer server.setSendServerVersion(true); WebAppContext webapp = new WebAppContext(); - webapp.setParentLoaderPriority(true); + //webapp.setParentLoaderPriority(true); webapp.setResourceBase("./src/main/webapp"); webapp.setAttribute("testAttribute","testValue"); File sessiondir=File.createTempFile("sessions",null); diff --git a/tests/test-integration/src/test/resources/RFC2616Base.xml b/tests/test-integration/src/test/resources/RFC2616Base.xml index 878b1291129..f791c1af004 100644 --- a/tests/test-integration/src/test/resources/RFC2616Base.xml +++ b/tests/test-integration/src/test/resources/RFC2616Base.xml @@ -95,7 +95,7 @@ /webapp-contexts/RFC2616 0 - + /testable-jetty-server-config.properties From e93a254306bbb27d08dff0d5d7f858fe4996f0df Mon Sep 17 00:00:00 2001 From: Jesse McConnell Date: Thu, 1 Nov 2012 10:57:17 -0500 Subject: [PATCH 20/31] [Bug 393218] add xsd=application/xml mime mapping to defaults --- .../src/main/resources/org/eclipse/jetty/http/mime.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/jetty-http/src/main/resources/org/eclipse/jetty/http/mime.properties b/jetty-http/src/main/resources/org/eclipse/jetty/http/mime.properties index a26891e7b0f..8425ac1931a 100644 --- a/jetty-http/src/main/resources/org/eclipse/jetty/http/mime.properties +++ b/jetty-http/src/main/resources/org/eclipse/jetty/http/mime.properties @@ -171,6 +171,7 @@ xhtml = application/xhtml+xml xls = application/vnd.ms-excel xml = application/xml xpm = image/x-xpixmap +xsd = application/xml xsl = application/xml xslt = application/xslt+xml xul = application/vnd.mozilla.xul+xml From 2ab0ef25bdaed215b735247be365a64977fcfcf3 Mon Sep 17 00:00:00 2001 From: Jesse McConnell Date: Thu, 1 Nov 2012 12:08:07 -0500 Subject: [PATCH 21/31] resolve issues between jetty 8 and jetty 9 maven plugin that jan noticed --- .../jetty/maven/plugin/AbstractJettyMojo.java | 2 +- .../jetty/maven/plugin/ConsoleScanner.java | 53 ++-- .../jetty/maven/plugin/JettyDeployWar.java | 91 +++---- .../maven/plugin/JettyRunForkedMojo.java | 256 +++++++++--------- .../jetty/maven/plugin/JettyRunMojo.java | 12 +- .../maven/plugin/JettyRunWarExplodedMojo.java | 54 ++-- .../jetty/maven/plugin/JettyRunWarMojo.java | 30 +- .../jetty/maven/plugin/JettyServer.java | 51 ++-- .../maven/plugin/JettyWebAppContext.java | 87 ++++-- .../plugin/MavenAnnotationConfiguration.java | 109 ++++++++ .../plugin/MavenWebInfConfiguration.java | 110 ++++---- .../eclipse/jetty/maven/plugin/Monitor.java | 20 +- .../eclipse/jetty/maven/plugin/Starter.java | 66 ++--- 13 files changed, 570 insertions(+), 371 deletions(-) create mode 100644 jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/MavenAnnotationConfiguration.java diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/AbstractJettyMojo.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/AbstractJettyMojo.java index a891f10caac..ac426630987 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/AbstractJettyMojo.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/AbstractJettyMojo.java @@ -483,7 +483,7 @@ public abstract class AbstractJettyMojo extends AbstractMojo if (connectors == null || connectors.length == 0) { //if a SystemProperty -Djetty.port= has been supplied, use that as the default port - this.connectors = new Connector[] { this.server.createDefaultConnector(server, System.getProperty(PORT_SYSPROPERTY, null)) }; + this.connectors = new Connector[] { this.server.createDefaultConnector(System.getProperty(PORT_SYSPROPERTY, null)) }; this.server.setConnectors(this.connectors); } } diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/ConsoleScanner.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/ConsoleScanner.java index cdd769fa5d8..8b3528678f4 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/ConsoleScanner.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/ConsoleScanner.java @@ -16,58 +16,55 @@ // ======================================================================== // -/** - * - */ package org.eclipse.jetty.maven.plugin; import java.io.IOException; -public class ConsoleScanner extends Thread +public class ConsoleScanner extends Thread { - + private final AbstractJettyMojo mojo; - - public ConsoleScanner(AbstractJettyMojo mojo) + + public ConsoleScanner(AbstractJettyMojo mojo) { this.mojo = mojo; setName("Console scanner"); setDaemon(true); } - - public void run() - { - try + + public void run() + { + try { - while (true) + while (true) { checkSystemInput(); getSomeSleep(); } - } - catch (IOException e) + } + catch (IOException e) { mojo.getLog().warn(e); } } - - private void getSomeSleep() + + private void getSomeSleep() { - try + try { Thread.sleep(500); - } - catch (InterruptedException e) + } + catch (InterruptedException e) { mojo.getLog().debug(e); } } - - private void checkSystemInput() throws IOException - { + + private void checkSystemInput() throws IOException + { while (System.in.available() > 0) { int inputByte = System.in.read(); - if (inputByte >= 0) + if (inputByte >= 0) { char c = (char)inputByte; if (c == '\n') { @@ -76,12 +73,12 @@ public class ConsoleScanner extends Thread } } } - - + + /** * Skip buffered bytes of system console. */ - private void clearInputBuffer() + private void clearInputBuffer() { try { @@ -101,9 +98,9 @@ public class ConsoleScanner extends Thread catch (IOException e) { mojo.getLog().warn("Error discarding console input buffer", e); - } + } } - + private void restartWebApp() { try diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyDeployWar.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyDeployWar.java index 8810e6f484e..9010ccff434 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyDeployWar.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyDeployWar.java @@ -1,46 +1,45 @@ -// -// ======================================================================== -// Copyright (c) 1995-2012 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.maven.plugin; - - -/** - *

- * This goal is used to run Jetty with a pre-assembled war. - *

- *

- * It accepts exactly the same options as the run-war goal. - * However, it doesn't assume that the current artifact is a - * webapp and doesn't try to assemble it into a war before its execution. - * So using it makes sense only when used in conjunction with the - * webApp configuration parameter pointing to a pre-built WAR. - *

- *

- * This goal is useful e.g. for launching a web app in Jetty as a target for unit-tested - * HTTP client components. - *

- * - * @goal deploy-war - * @requiresDependencyResolution runtime - * @execute phase="validate" - * @description Deploy a pre-assembled war - * - */ -public class JettyDeployWar extends JettyRunWarMojo -{ -} +// +// ======================================================================== +// Copyright (c) 1995-2012 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.maven.plugin; + +/** + *

+ * This goal is used to run Jetty with a pre-assembled war. + *

+ *

+ * It accepts exactly the same options as the run-war goal. + * However, it doesn't assume that the current artifact is a + * webapp and doesn't try to assemble it into a war before its execution. + * So using it makes sense only when used in conjunction with the + * webApp configuration parameter pointing to a pre-built WAR. + *

+ *

+ * This goal is useful e.g. for launching a web app in Jetty as a target for unit-tested + * HTTP client components. + *

+ * + * @goal deploy-war + * @requiresDependencyResolution runtime + * @execute phase="validate" + * @description Deploy a pre-assembled war + * + */ +public class JettyDeployWar extends JettyRunWarMojo +{ +} diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunForkedMojo.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunForkedMojo.java index 0472850a1af..f37a67ff663 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunForkedMojo.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunForkedMojo.java @@ -18,23 +18,32 @@ package org.eclipse.jetty.maven.plugin; +import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; +import java.io.BufferedWriter; import java.io.File; import java.io.FileOutputStream; +import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.LineNumberReader; import java.net.MalformedURLException; +import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Properties; import java.util.Random; import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.apache.maven.artifact.Artifact; import org.apache.maven.plugin.AbstractMojo; @@ -42,7 +51,10 @@ import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugin.descriptor.PluginDescriptor; import org.apache.maven.project.MavenProject; +import org.codehaus.plexus.component.repository.ComponentDependency; +import org.eclipse.jetty.server.RequestLog; import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.resource.Resource; /** @@ -64,7 +76,7 @@ import org.eclipse.jetty.util.IO; * There is a reference guide to the configuration parameters for this plugin, and more detailed information * with examples in the Configuration Guide. *

- * + * * @goal run-forked * @requiresDependencyResolution compile+runtime * @execute phase="test-compile" @@ -72,17 +84,17 @@ import org.eclipse.jetty.util.IO; * */ public class JettyRunForkedMojo extends AbstractMojo -{ +{ public String PORT_SYSPROPERTY = "jetty.port"; - + /** * Whether or not to include dependencies on the plugin's classpath with <scope>provided</scope> * Use WITH CAUTION as you may wind up with duplicate jars/classes. * @parameter default-value="false" */ protected boolean useProvidedScope; - - + + /** * The maven project. * @@ -91,9 +103,9 @@ public class JettyRunForkedMojo extends AbstractMojo * @readonly */ private MavenProject project; + - - + /** * If true, the <testOutputDirectory> * and the dependencies of <scope>test<scope> @@ -101,27 +113,27 @@ public class JettyRunForkedMojo extends AbstractMojo * @parameter alias="useTestClasspath" default-value="false" */ private boolean useTestScope; - - + + /** * The default location of the web.xml file. Will be used * if <webAppConfig><descriptor> is not set. - * + * * @parameter expression="${basedir}/src/main/webapp/WEB-INF/web.xml" * @readonly */ private String webXml; - + /** * The target directory - * + * * @parameter expression="${project.build.directory}" * @required * @readonly */ protected File target; - - + + /** * The temporary directory to use for the webapp. * Defaults to target/tmp @@ -132,26 +144,26 @@ public class JettyRunForkedMojo extends AbstractMojo */ protected File tmpDirectory; - + /** * The directory containing generated classes. * * @parameter expression="${project.build.outputDirectory}" * @required - * + * */ private File classesDirectory; - - - + + + /** * The directory containing generated test classes. - * + * * @parameter expression="${project.build.testOutputDirectory}" * @required */ private File testClassesDirectory; - + /** * Root directory for all html/jsp etc files * @@ -159,34 +171,34 @@ public class JettyRunForkedMojo extends AbstractMojo * */ private File webAppSourceDirectory; - - + + /** * Directories that contain static resources * for the webapp. Optional. - * + * * @parameter */ private File[] resourceBases; - - + + /** - * If true, the webAppSourceDirectory will be first on the list of - * resources that form the resource base for the webapp. If false, + * If true, the webAppSourceDirectory will be first on the list of + * resources that form the resource base for the webapp. If false, * it will be last. - * + * * @parameter default-value="true" */ private boolean baseAppFirst; - + /** - * Location of jetty xml configuration files whose contents + * Location of jetty xml configuration files whose contents * will be applied before any plugin configuration. Optional. * @parameter */ private String jettyXml; - + /** * The context path for the webapp. Defaults to the * name of the webapp's artifact. @@ -205,71 +217,71 @@ public class JettyRunForkedMojo extends AbstractMojo */ private String contextXml; - - /** + + /** * @parameter expression="${jetty.skip}" default-value="false" */ private boolean skip; /** - * Port to listen to stop jetty on executing -DSTOP.PORT=<stopPort> + * Port to listen to stop jetty on executing -DSTOP.PORT=<stopPort> * -DSTOP.KEY=<stopKey> -jar start.jar --stop * @parameter * @required */ protected int stopPort; - + /** - * Key to provide when stopping jetty on executing java -DSTOP.KEY=<stopKey> + * Key to provide when stopping jetty on executing java -DSTOP.KEY=<stopKey> * -DSTOP.PORT=<stopPort> -jar start.jar --stop * @parameter * @required */ protected String stopKey; - + /** * Arbitrary jvm args to pass to the forked process * @parameter */ private String jvmArgs; - - + + /** * @parameter expression="${plugin.artifacts}" * @readonly */ private List pluginArtifacts; - - + + /** * @parameter expression="${plugin}" * @readonly */ private PluginDescriptor plugin; - - - + + + /** * @parameter expression="true" default-value="true" */ private boolean waitForChild; - + private Process forkedProcess; - + private Random random; - - - + + + public class ShutdownThread extends Thread { public ShutdownThread() { super("RunForkedShutdown"); } - + public void run () { if (forkedProcess != null && waitForChild) @@ -278,7 +290,7 @@ public class JettyRunForkedMojo extends AbstractMojo } } } - + /** * @see org.apache.maven.plugin.Mojo#execute() */ @@ -295,20 +307,20 @@ public class JettyRunForkedMojo extends AbstractMojo random = new Random(); startJettyRunner(); } - - + + public List getProvidedJars() throws MojoExecutionException - { + { //if we are configured to include the provided dependencies on the plugin's classpath //(which mimics being on jetty's classpath vs being on the webapp's classpath), we first //try and filter out ones that will clash with jars that are plugin dependencies, then //create a new classloader that we setup in the parent chain. if (useProvidedScope) { - - List provided = new ArrayList(); + + List provided = new ArrayList(); for ( Iterator iter = project.getArtifacts().iterator(); iter.hasNext(); ) - { + { Artifact artifact = iter.next(); if (Artifact.SCOPE_PROVIDED.equals(artifact.getScope()) && !isPluginArtifact(artifact)) { @@ -322,16 +334,16 @@ public class JettyRunForkedMojo extends AbstractMojo else return null; } - + /* ------------------------------------------------------------ */ public File prepareConfiguration() throws MojoExecutionException { try - { + { //work out the configuration based on what is configured in the pom File propsFile = new File (target, "fork.props"); if (propsFile.exists()) - propsFile.delete(); + propsFile.delete(); propsFile.createNewFile(); //propsFile.deleteOnExit(); @@ -358,9 +370,9 @@ public class JettyRunForkedMojo extends AbstractMojo //sort out base dir of webapp if (webAppSourceDirectory != null) props.put("base.dir", webAppSourceDirectory.getAbsolutePath()); - + //sort out the resource base directories of the webapp - StringBuilder builder = new StringBuilder(); + StringBuilder builder = new StringBuilder(); if (baseAppFirst) { add((webAppSourceDirectory==null?null:webAppSourceDirectory.getAbsolutePath()), builder); @@ -371,7 +383,7 @@ public class JettyRunForkedMojo extends AbstractMojo } } else - { + { if (resourceBases != null) { for (File resDir:resourceBases) @@ -380,7 +392,7 @@ public class JettyRunForkedMojo extends AbstractMojo add((webAppSourceDirectory==null?null:webAppSourceDirectory.getAbsolutePath()), builder); } props.put("res.dirs", builder.toString()); - + //web-inf classes List classDirs = getClassesDirs(); @@ -397,7 +409,7 @@ public class JettyRunForkedMojo extends AbstractMojo { props.put("classes.dir", classesDirectory.getAbsolutePath()); } - + if (useTestScope && testClassesDirectory != null) { props.put("testClasses.dir", testClassesDirectory.getAbsolutePath()); @@ -435,7 +447,7 @@ public class JettyRunForkedMojo extends AbstractMojo throw new MojoExecutionException("Prepare webapp configuration", e); } } - + private void add (String string, StringBuilder builder) { if (string == null) @@ -448,62 +460,62 @@ public class JettyRunForkedMojo extends AbstractMojo private List getClassesDirs () { List classesDirs = new ArrayList(); - + //if using the test classes, make sure they are first //on the list if (useTestScope && (testClassesDirectory != null)) classesDirs.add(testClassesDirectory); - + if (classesDirectory != null) classesDirs.add(classesDirectory); - + return classesDirs; } - - - + + + private List getOverlays() throws MalformedURLException, IOException { List overlays = new ArrayList(); for ( Iterator iter = project.getArtifacts().iterator(); iter.hasNext(); ) { - Artifact artifact = (Artifact) iter.next(); - + Artifact artifact = (Artifact) iter.next(); + if (artifact.getType().equals("war")) overlays.add(artifact.getFile()); } return overlays; } - - - + + + private List getDependencyFiles () { List dependencyFiles = new ArrayList(); - + for ( Iterator iter = project.getArtifacts().iterator(); iter.hasNext(); ) { Artifact artifact = (Artifact) iter.next(); - - if (((!Artifact.SCOPE_PROVIDED.equals(artifact.getScope())) && (!Artifact.SCOPE_TEST.equals( artifact.getScope()))) + + if (((!Artifact.SCOPE_PROVIDED.equals(artifact.getScope())) && (!Artifact.SCOPE_TEST.equals( artifact.getScope()))) || (useTestScope && Artifact.SCOPE_TEST.equals( artifact.getScope()))) { dependencyFiles.add(artifact.getFile()); - getLog().debug( "Adding artifact " + artifact.getFile().getName() + " for WEB-INF/lib " ); + getLog().debug( "Adding artifact " + artifact.getFile().getName() + " for WEB-INF/lib " ); } } - - return dependencyFiles; + + return dependencyFiles; } - + public boolean isPluginArtifact(Artifact artifact) { if (pluginArtifacts == null || pluginArtifacts.isEmpty()) return false; - + boolean isPluginArtifact = false; for (Iterator iter = pluginArtifacts.iterator(); iter.hasNext() && !isPluginArtifact; ) { @@ -512,18 +524,18 @@ public class JettyRunForkedMojo extends AbstractMojo if (pluginArtifact.getGroupId().equals(artifact.getGroupId()) && pluginArtifact.getArtifactId().equals(artifact.getArtifactId())) isPluginArtifact = true; } - + return isPluginArtifact; } - - - + + + private Set getExtraJars() throws Exception { Set extraJars = new HashSet(); - - + + List l = pluginArtifacts; Artifact pluginArtifact = null; @@ -531,7 +543,7 @@ public class JettyRunForkedMojo extends AbstractMojo { Iterator itor = l.iterator(); while (itor.hasNext() && pluginArtifact == null) - { + { Artifact a = (Artifact)itor.next(); if (a.getArtifactId().equals(plugin.getArtifactId())) //get the jetty-maven-plugin jar { @@ -543,18 +555,18 @@ public class JettyRunForkedMojo extends AbstractMojo return extraJars; } - + /* ------------------------------------------------------------ */ public void startJettyRunner() throws MojoExecutionException - { + { try { - + File props = prepareConfiguration(); - + List cmd = new ArrayList(); cmd.add(getJavaBin()); - + if (jvmArgs != null) { String[] args = jvmArgs.split(" "); @@ -564,7 +576,7 @@ public class JettyRunForkedMojo extends AbstractMojo cmd.add(args[i].trim()); } } - + String classPath = getClassPath(); if (classPath != null && classPath.length() > 0) { @@ -572,7 +584,7 @@ public class JettyRunForkedMojo extends AbstractMojo cmd.add(classPath); } cmd.add(Starter.class.getCanonicalName()); - + if (stopPort > 0 && stopKey != null) { cmd.add("--stop-port"); @@ -585,26 +597,26 @@ public class JettyRunForkedMojo extends AbstractMojo cmd.add("--jetty-xml"); cmd.add(jettyXml); } - + if (contextXml != null) { cmd.add("--context-xml"); cmd.add(contextXml); } - + cmd.add("--props"); cmd.add(props.getAbsolutePath()); - + String token = createToken(); cmd.add("--token"); cmd.add(token); - + ProcessBuilder builder = new ProcessBuilder(cmd); builder.directory(project.getBasedir()); - + if (PluginLog.getLog().isDebugEnabled()) PluginLog.getLog().debug(Arrays.toString(cmd.toArray())); - + forkedProcess = builder.start(); PluginLog.getLog().info("Forked process starting"); @@ -612,7 +624,7 @@ public class JettyRunForkedMojo extends AbstractMojo { startPump("STDOUT",forkedProcess.getInputStream()); startPump("STDERR",forkedProcess.getErrorStream()); - int exitcode = forkedProcess.waitFor(); + int exitcode = forkedProcess.waitFor(); PluginLog.getLog().info("Forked execution exit: "+exitcode); } else @@ -652,20 +664,20 @@ public class JettyRunForkedMojo extends AbstractMojo { if (forkedProcess != null && waitForChild) forkedProcess.destroy(); - + throw new MojoExecutionException("Failed to start Jetty within time limit"); } catch (Exception ex) { if (forkedProcess != null && waitForChild) forkedProcess.destroy(); - + throw new MojoExecutionException("Failed to create Jetty process", ex); } } - - - + + + public String getClassPath() throws Exception { StringBuilder classPath = new StringBuilder(); @@ -682,16 +694,16 @@ public class JettyRunForkedMojo extends AbstractMojo } } - + //Any jars that we need from the plugin environment (like the ones containing Starter class) Set extraJars = getExtraJars(); for (Artifact a:extraJars) - { + { classPath.append(File.pathSeparator); classPath.append(a.getFile().getAbsolutePath()); } - - + + //Any jars that we need from the project's dependencies because we're useProvided List providedJars = getProvidedJars(); if (providedJars != null && !providedJars.isEmpty()) @@ -703,7 +715,7 @@ public class JettyRunForkedMojo extends AbstractMojo if (getLog().isDebugEnabled()) getLog().debug("Adding provided jar: "+jar); } } - + return classPath.toString(); } @@ -724,7 +736,7 @@ public class JettyRunForkedMojo extends AbstractMojo return "java"; } - + public static String fileSeparators(String path) { StringBuilder ret = new StringBuilder(); @@ -759,13 +771,13 @@ public class JettyRunForkedMojo extends AbstractMojo return ret.toString(); } - + private String createToken () { return Long.toString(random.nextLong()^System.currentTimeMillis(), 36).toUpperCase(); } - - + + private void startPump(String mode, InputStream inputStream) { ConsoleStreamer pump = new ConsoleStreamer(mode,inputStream); @@ -774,7 +786,7 @@ public class JettyRunForkedMojo extends AbstractMojo thread.start(); } - + /** diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunMojo.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunMojo.java index 4c482127296..f8addd006c6 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunMojo.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunMojo.java @@ -63,7 +63,8 @@ import org.eclipse.jetty.webapp.WebAppContext; */ public class JettyRunMojo extends AbstractJettyMojo { - public static final String DEFAULT_WEBAPP_SRC = "src"+File.separator+"main"+File.separator+"webapp"; + public static final String DEFAULT_WEBAPP_SRC = "src"+File.separator+"main"+File.separator+"webapp"; + /** * If true, the <testOutputDirectory> @@ -136,12 +137,13 @@ public class JettyRunMojo extends AbstractJettyMojo */ private List extraScanTargets; - + + /** * Verify the configuration given in the pom. * - * @see org.eclipse.jetty.maven.plugin.AbstractJettyMojo#checkPomConfiguration() + * @see org.mortbay.jetty.plugin.AbstractJettyMojo#checkPomConfiguration() */ public void checkPomConfiguration () throws MojoExecutionException { @@ -149,7 +151,7 @@ public class JettyRunMojo extends AbstractJettyMojo try { if ((getWebAppSourceDirectory() == null) || !getWebAppSourceDirectory().exists()) - { + { File defaultWebAppSrcDir = new File (project.getBasedir(), DEFAULT_WEBAPP_SRC); getLog().info("webAppSourceDirectory"+(getWebAppSourceDirectory()==null?" not set.":" does not exist.")+" Defaulting to "+defaultWebAppSrcDir.getAbsolutePath()); webAppSourceDirectory = defaultWebAppSrcDir; @@ -444,6 +446,7 @@ public class JettyRunMojo extends AbstractJettyMojo for ( Iterator iter = projectArtifacts.iterator(); iter.hasNext(); ) { Artifact artifact = (Artifact) iter.next(); + // Include runtime and compile time libraries, and possibly test libs too if(artifact.getType().equals("war")) { @@ -451,6 +454,7 @@ public class JettyRunMojo extends AbstractJettyMojo { Resource r=Resource.newResource("jar:"+Resource.toURL(artifact.getFile()).toString()+"!/"); overlays.add(r); + getLog().info("Adding overlay for war project artifact "+artifact.getId()); getExtraScanTargets().add(artifact.getFile()); } catch(Exception e) diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunWarExplodedMojo.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunWarExplodedMojo.java index c1e6a855d21..33e344dfa03 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunWarExplodedMojo.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunWarExplodedMojo.java @@ -25,19 +25,21 @@ import java.util.List; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.eclipse.jetty.util.Scanner; +import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.xml.XmlConfiguration; /** - * + * *

* This goal is used to assemble your webapp into an exploded war and automatically deploy it to Jetty. *

*

- * Once invoked, the plugin can be configured to run continuously, scanning for changes in the pom.xml and - * to WEB-INF/web.xml, WEB-INF/classes or WEB-INF/lib and hot redeploy when a change is detected. + * Once invoked, the plugin can be configured to run continuously, scanning for changes in the pom.xml and + * to WEB-INF/web.xml, WEB-INF/classes or WEB-INF/lib and hot redeploy when a change is detected. *

*

* You may also specify the location of a jetty.xml file whose contents will be applied before any plugin configuration. - * This can be used, for example, to deploy a static webapp that is not part of your maven build. + * This can be used, for example, to deploy a static webapp that is not part of your maven build. *

*

* There is a reference guide to the configuration parameters for this plugin, and more detailed information @@ -51,24 +53,24 @@ import org.eclipse.jetty.util.Scanner; public class JettyRunWarExplodedMojo extends AbstractJettyMojo { - - + + /** * The location of the war file. - * + * * @parameter alias="webApp" expression="${project.build.directory}/${project.build.finalName}" * @required */ private File war; - - - - + + + + /** - * - * @see org.eclipse.jetty.maven.plugin.AbstractJettyMojo#checkPomConfiguration() + * + * @see org.mortbay.jetty.plugin.AbstractJettyMojo#checkPomConfiguration() */ public void checkPomConfiguration() throws MojoExecutionException { @@ -76,7 +78,7 @@ public class JettyRunWarExplodedMojo extends AbstractJettyMojo } /** - * @see org.eclipse.jetty.maven.plugin.AbstractJettyMojo#configureScanner() + * @see org.mortbay.jetty.plugin.AbstractJettyMojo#configureScanner() */ public void configureScanner() throws MojoExecutionException { @@ -93,7 +95,7 @@ public class JettyRunWarExplodedMojo extends AbstractJettyMojo scanList.add(new File(webInfDir, "classes")); scanList.add(new File(webInfDir, "lib")); setScanList(scanList); - + ArrayList listeners = new ArrayList(); listeners.add(new Scanner.BulkListener() { @@ -113,10 +115,10 @@ public class JettyRunWarExplodedMojo extends AbstractJettyMojo setScannerListeners(listeners); } - - - - public void restartWebApp(boolean reconfigureScanner) throws Exception + + + + public void restartWebApp(boolean reconfigureScanner) throws Exception { getLog().info("Restarting webapp"); getLog().debug("Stopping webapp ..."); @@ -152,20 +154,20 @@ public class JettyRunWarExplodedMojo extends AbstractJettyMojo getLog().info("Restart completed."); } - - + + public void configureWebApplication () throws Exception { - super.configureWebApplication(); + super.configureWebApplication(); webApp.setWar(war.getCanonicalPath()); } - + public void execute () throws MojoExecutionException, MojoFailureException { super.execute(); } - - - + + + } diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunWarMojo.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunWarMojo.java index 9c320c968c7..35fc923aebe 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunWarMojo.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunWarMojo.java @@ -21,10 +21,14 @@ package org.eclipse.jetty.maven.plugin; import java.io.File; import java.util.ArrayList; import java.util.List; +import java.util.Set; +import org.apache.maven.artifact.Artifact; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.eclipse.jetty.util.Scanner; +import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.xml.XmlConfiguration; /** *

@@ -32,18 +36,18 @@ import org.eclipse.jetty.util.Scanner; *

*

* Once invoked, the plugin can be configured to run continuously, scanning for changes in the project and to the - * war file and automatically performing a - * hot redeploy when necessary. + * war file and automatically performing a + * hot redeploy when necessary. *

*

* You may also specify the location of a jetty.xml file whose contents will be applied before any plugin configuration. - * This can be used, for example, to deploy a static webapp that is not part of your maven build. + * This can be used, for example, to deploy a static webapp that is not part of your maven build. *

*

* There is a reference guide to the configuration parameters for this plugin, and more detailed information * with examples in the Configuration Guide. *

- * + * * @goal run-war * @requiresDependencyResolution compile+runtime * @execute phase="package" @@ -61,13 +65,13 @@ public class JettyRunWarMojo extends AbstractJettyMojo private File war; - + /** * @see org.apache.maven.plugin.Mojo#execute() */ public void execute() throws MojoExecutionException, MojoFailureException { - super.execute(); + super.execute(); } @@ -75,18 +79,18 @@ public class JettyRunWarMojo extends AbstractJettyMojo public void configureWebApplication () throws Exception { super.configureWebApplication(); - + webApp.setWar(war.getCanonicalPath()); } - + /** - * @see org.eclipse.jetty.maven.plugin.AbstractJettyMojo#checkPomConfiguration() + * @see org.mortbay.jetty.plugin.AbstractJettyMojo#checkPomConfiguration() */ public void checkPomConfiguration() throws MojoExecutionException { - return; + return; } @@ -100,7 +104,7 @@ public class JettyRunWarMojo extends AbstractJettyMojo scanList.add(getProject().getFile()); scanList.add(war); setScanList(scanList); - + ArrayList listeners = new ArrayList(); listeners.add(new Scanner.BulkListener() { @@ -117,11 +121,11 @@ public class JettyRunWarMojo extends AbstractJettyMojo } } }); - setScannerListeners(listeners); + setScannerListeners(listeners); } - public void restartWebApp(boolean reconfigureScanner) throws Exception + public void restartWebApp(boolean reconfigureScanner) throws Exception { getLog().info("Restarting webapp ..."); getLog().debug("Stopping webapp ..."); diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyServer.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyServer.java index 56cbecd035e..b0a38bf4988 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyServer.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyServer.java @@ -23,7 +23,6 @@ import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.RequestLog; import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.ContextHandlerCollection; import org.eclipse.jetty.server.handler.DefaultHandler; import org.eclipse.jetty.server.handler.HandlerCollection; @@ -33,20 +32,20 @@ import org.eclipse.jetty.webapp.WebAppContext; /** * JettyServer - * + * * Maven jetty plugin version of a wrapper for the Server class. - * + * */ public class JettyServer extends org.eclipse.jetty.server.Server { public static int DEFAULT_PORT = 8080; public static int DEFAULT_MAX_IDLE_TIME = 30000; - + private RequestLog requestLog; private ContextHandlerCollection contexts; - - + + public JettyServer() { super(); @@ -55,7 +54,7 @@ public class JettyServer extends org.eclipse.jetty.server.Server Resource.setDefaultUseCaches(false); } - + public void setRequestLog (RequestLog requestLog) { this.requestLog = requestLog; @@ -69,16 +68,16 @@ public class JettyServer extends org.eclipse.jetty.server.Server super.doStart(); } - + /** * @see org.eclipse.jetty.server.handler.HandlerCollection#addHandler(org.eclipse.jetty.server.Handler) */ public void addWebApplication(WebAppContext webapp) throws Exception - { + { contexts.addHandler (webapp); } - + /** * Set up the handler structure to receive a webapp. * Also put in a DefaultHandler so we get a nice page @@ -86,43 +85,43 @@ public class JettyServer extends org.eclipse.jetty.server.Server * context isn't at root. * @throws Exception */ - public void configureHandlers () throws Exception + public void configureHandlers () throws Exception { DefaultHandler defaultHandler = new DefaultHandler(); RequestLogHandler requestLogHandler = new RequestLogHandler(); if (this.requestLog != null) requestLogHandler.setRequestLog(this.requestLog); - + contexts = (ContextHandlerCollection)super.getChildHandlerByClass(ContextHandlerCollection.class); if (contexts==null) - { + { contexts = new ContextHandlerCollection(); HandlerCollection handlers = (HandlerCollection)super.getChildHandlerByClass(HandlerCollection.class); if (handlers==null) { - handlers = new HandlerCollection(); - super.setHandler(handlers); + handlers = new HandlerCollection(); + super.setHandler(handlers); handlers.setHandlers(new Handler[]{contexts, defaultHandler, requestLogHandler}); } else { handlers.addHandler(contexts); } - } + } } - - - - - public Connector createDefaultConnector(Server server, String portnum) throws Exception + + + + + public Connector createDefaultConnector(String portnum) throws Exception { - ServerConnector connector = new ServerConnector(server); + ServerConnector connector = new ServerConnector(this); int port = ((portnum==null||portnum.equals(""))?DEFAULT_PORT:Integer.parseInt(portnum.trim())); connector.setPort(port); - connector.setIdleTimeout(DEFAULT_MAX_IDLE_TIME); - + // connector.setMaxIdleTime(DEFAULT_MAX_IDLE_TIME); + return connector; } - - + + } diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyWebAppContext.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyWebAppContext.java index 328730b43d4..4a2dcd7308a 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyWebAppContext.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyWebAppContext.java @@ -28,7 +28,6 @@ import java.util.Map; import java.util.Set; import java.util.TreeSet; -import org.eclipse.jetty.annotations.AnnotationConfiguration; import org.eclipse.jetty.plus.webapp.EnvConfiguration; import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.log.Log; @@ -39,8 +38,8 @@ import org.eclipse.jetty.webapp.Configuration; import org.eclipse.jetty.webapp.FragmentConfiguration; import org.eclipse.jetty.webapp.JettyWebXmlConfiguration; import org.eclipse.jetty.webapp.MetaInfConfiguration; -import org.eclipse.jetty.webapp.TagLibConfiguration; import org.eclipse.jetty.webapp.WebAppContext; +import org.eclipse.jetty.webapp.WebInfConfiguration; import org.eclipse.jetty.webapp.WebXmlConfiguration; /** @@ -56,6 +55,7 @@ public class JettyWebAppContext extends WebAppContext { private static final Logger LOG = Log.getLogger(JettyWebAppContext.class); + private static final String DEFAULT_CONTAINER_INCLUDE_JAR_PATTERN = ".*/javax.servlet-[^/]*\\.jar$|.*/servlet-api-[^/]*\\.jar$"; private static final String WEB_INF_CLASSES_PREFIX = "/WEB-INF/classes"; private static final String WEB_INF_LIB_PREFIX = "/WEB-INF/lib"; @@ -73,6 +73,19 @@ public class JettyWebAppContext extends WebAppContext * @deprecated The value of this parameter will be ignored by the plugin. Overlays will always be unpacked. */ private boolean unpackOverlays; + + /** + * Set the "org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern" with a pattern for matching jars on + * container classpath to scan. This is analogous to the WebAppContext.setAttribute() call. + */ + private String containerIncludeJarPattern = null; + + /** + * Set the "org.eclipse.jetty.server.webapp.WebInfIncludeJarPattern" with a pattern for matching jars on + * webapp's classpath to scan. This is analogous to the WebAppContext.setAttribute() call. + */ + private String webInfIncludeJarPattern = null; + /** * @deprecated The value of this parameter will be ignored by the plugin. This option will be always disabled. @@ -91,14 +104,34 @@ public class JettyWebAppContext extends WebAppContext new MetaInfConfiguration(), new FragmentConfiguration(), envConfig = new EnvConfiguration(), - new AnnotationConfiguration(), new org.eclipse.jetty.plus.webapp.PlusConfiguration(), - new JettyWebXmlConfiguration(), - new TagLibConfiguration() + new MavenAnnotationConfiguration(), + new JettyWebXmlConfiguration() }); // Turn off copyWebInf option as it is not applicable for plugin. super.setCopyWebInf(false); } + public void setContainerIncludeJarPattern(String pattern) + { + containerIncludeJarPattern = pattern; + } + + public String getContainerIncludeJarPattern() + { + return containerIncludeJarPattern; + } + + + public String getWebInfIncludeJarPattern() + { + return webInfIncludeJarPattern; + } + public void setWebInfIncludeJarPattern(String pattern) + { + webInfIncludeJarPattern = pattern; + } + + public boolean getUnpackOverlays() { @@ -218,17 +251,21 @@ public class JettyWebAppContext extends WebAppContext { //Set up the pattern that tells us where the jars are that need scanning for //stuff like taglibs so we can tell jasper about it (see TagLibConfiguration) - String tmp = (String)getAttribute("org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern"); - - tmp = addPattern(tmp, ".*/.*jsp-api-[^/]*\\.jar$"); - tmp = addPattern(tmp, ".*/.*jsp-[^/]*\\.jar$"); - tmp = addPattern(tmp, ".*/.*taglibs[^/]*\\.jar$"); - tmp = addPattern(tmp, ".*/.*jstl[^/]*\\.jar$"); - tmp = addPattern(tmp, ".*/.*jsf-impl-[^/]*\\.jar$"); // add in 2 most popular jsf impls - tmp = addPattern(tmp, ".*/.*javax.faces-[^/]*\\.jar$"); - tmp = addPattern(tmp, ".*/.*myfaces-impl-[^/]*\\.jar$"); - setAttribute("org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern", tmp); + //Allow user to set up pattern for names of jars from the container classpath + //that will be scanned - note that by default NO jars are scanned + String tmp = containerIncludeJarPattern; + if (tmp==null || "".equals(tmp)) + tmp = (String)getAttribute(WebInfConfiguration.CONTAINER_JAR_PATTERN); + + tmp = addPattern(tmp, DEFAULT_CONTAINER_INCLUDE_JAR_PATTERN); + setAttribute(WebInfConfiguration.CONTAINER_JAR_PATTERN, tmp); + + //Allow user to set up pattern of jar names from WEB-INF that will be scanned. + //Note that by default ALL jars considered to be in WEB-INF will be scanned - setting + //a pattern restricts scanning + if (webInfIncludeJarPattern != null) + setAttribute(WebInfConfiguration.WEBINF_JAR_PATTERN, webInfIncludeJarPattern); //Set up the classes dirs that comprises the equivalent of WEB-INF/classes if (testClasses != null) @@ -241,7 +278,6 @@ public class JettyWebAppContext extends WebAppContext classpathFiles.addAll(webInfClasses); classpathFiles.addAll(webInfJars); - // Initialize map containing all jars in /WEB-INF/lib webInfJarMap.clear(); for (File file : webInfJars) @@ -255,13 +291,28 @@ public class JettyWebAppContext extends WebAppContext if (this.jettyEnvXml != null) envConfig.setJettyEnvXml(Resource.toURL(new File(this.jettyEnvXml))); - //setShutdown(false); + // CHECK setShutdown(false); super.doStart(); } public void doStop () throws Exception { - //setShutdown(true); + if (classpathFiles != null) + classpathFiles.clear(); + classpathFiles = null; + + classes = null; + testClasses = null; + + if (webInfJarMap != null) + webInfJarMap.clear(); + + webInfClasses.clear(); + webInfJars.clear(); + + + + // CHECK setShutdown(true); //just wait a little while to ensure no requests are still being processed Thread.currentThread().sleep(500L); super.doStop(); diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/MavenAnnotationConfiguration.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/MavenAnnotationConfiguration.java new file mode 100644 index 00000000000..566b4af53f4 --- /dev/null +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/MavenAnnotationConfiguration.java @@ -0,0 +1,109 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 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.maven.plugin; + +import java.io.File; + +import org.eclipse.jetty.annotations.AbstractDiscoverableAnnotationHandler; +import org.eclipse.jetty.annotations.AnnotationConfiguration; +import org.eclipse.jetty.annotations.AnnotationParser; +import org.eclipse.jetty.annotations.AnnotationParser.DiscoverableAnnotationHandler; +import org.eclipse.jetty.annotations.ClassNameResolver; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.webapp.MetaData; +import org.eclipse.jetty.webapp.WebAppContext; + +public class MavenAnnotationConfiguration extends AnnotationConfiguration +{ + private static final Logger LOG = Log.getLogger(MavenAnnotationConfiguration.class); + + /* ------------------------------------------------------------ */ + @Override + public void parseWebInfClasses(final WebAppContext context, final AnnotationParser parser) throws Exception + { + JettyWebAppContext jwac = (JettyWebAppContext)context; + if (jwac.getClassPathFiles() == null || jwac.getClassPathFiles().size() == 0) + super.parseWebInfClasses (context, parser); + else + { + LOG.debug("Scanning classes "); + //Look for directories on the classpath and process each one of those + + MetaData metaData = context.getMetaData(); + if (metaData == null) + throw new IllegalStateException ("No metadata"); + + parser.clearHandlers(); + for (DiscoverableAnnotationHandler h:_discoverableAnnotationHandlers) + { + if (h instanceof AbstractDiscoverableAnnotationHandler) + ((AbstractDiscoverableAnnotationHandler)h).setResource(null); // + } + parser.registerHandlers(_discoverableAnnotationHandlers); + parser.registerHandler(_classInheritanceHandler); + parser.registerHandlers(_containerInitializerAnnotationHandlers); + + + for (File f:jwac.getClassPathFiles()) + { + //scan the equivalent of the WEB-INF/classes directory that has been synthesised by the plugin + if (f.isDirectory() && f.exists()) + { + doParse(context, parser, Resource.newResource(f.toURL())); + } + } + + //if an actual WEB-INF/classes directory also exists (eg because of overlayed wars) then scan that + //too + if (context.getWebInf() != null && context.getWebInf().exists()) + { + Resource classesDir = context.getWebInf().addPath("classes/"); + if (classesDir.exists()) + { + doParse(context, parser, classesDir); + } + } + } + } + + + public void doParse (final WebAppContext context, final AnnotationParser parser, Resource resource) + throws Exception + { + parser.parse(resource, new ClassNameResolver() + { + public boolean isExcluded (String name) + { + if (context.isSystemClass(name)) return true; + if (context.isServerClass(name)) return false; + return false; + } + + public boolean shouldOverride (String name) + { + //looking at webapp classpath, found already-parsed class of same name - did it come from system or duplicate in webapp? + if (context.isParentLoaderPriority()) + return false; + return true; + } + }); + } +} diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/MavenWebInfConfiguration.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/MavenWebInfConfiguration.java index 6048cae1d19..e982fdb9571 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/MavenWebInfConfiguration.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/MavenWebInfConfiguration.java @@ -26,6 +26,7 @@ import java.util.Iterator; import java.util.List; import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.LazyList; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.resource.Resource; @@ -40,8 +41,8 @@ public class MavenWebInfConfiguration extends WebInfConfiguration protected Resource _originalResourceBase; protected Resource[] _unpackedOverlays; - - + + public void configure(WebAppContext context) throws Exception { JettyWebAppContext jwac = (JettyWebAppContext)context; @@ -54,11 +55,11 @@ public class MavenWebInfConfiguration extends WebInfConfiguration while (itor.hasNext()) ((WebAppClassLoader)context.getClassLoader()).addClassPath(((File)itor.next()).getCanonicalPath()); - if (LOG.isDebugEnabled()) - LOG.debug("Classpath = "+((URLClassLoader)context.getClassLoader()).getURLs()); + //if (LOG.isDebugEnabled()) + //LOG.debug("Classpath = "+LazyList.array2List(((URLClassLoader)context.getClassLoader()).getURLs())); } super.configure(context); - + // knock out environmental maven and plexus classes from webAppContext String[] existingServerClasses = context.getServerClasses(); String[] newServerClasses = new String[2+(existingServerClasses==null?0:existingServerClasses.length)]; @@ -71,7 +72,7 @@ public class MavenWebInfConfiguration extends WebInfConfiguration for (int i=0;i0) + { + try + { + for (int i=0; i<_unpackedOverlays.length; i++) + { + IO.delete(_unpackedOverlays[i].getFile()); + } + } + catch (IOException e) + { + LOG.ignore(e); + } + } + super.deconfigure(context); + //restore whatever the base resource was before we might have included overlaid wars + context.setBaseResource(_originalResourceBase); + + } + + + + + /** + * @see org.eclipse.jetty.webapp.WebInfConfiguration#unpack(org.eclipse.jetty.webapp.WebAppContext) + */ + @Override + public void unpack(WebAppContext context) throws IOException + { + //Unpack and find base resource as normal + super.unpack(context); + + + //Add in any overlays as a resource collection for the base _originalResourceBase = context.getBaseResource(); JettyWebAppContext jwac = (JettyWebAppContext)context; @@ -102,7 +150,7 @@ public class MavenWebInfConfiguration extends WebInfConfiguration origSize = 1; } } - + int overlaySize = jwac.getOverlays().size(); Resource[] newResources = new Resource[origSize + overlaySize]; @@ -112,7 +160,6 @@ public class MavenWebInfConfiguration extends WebInfConfiguration if (jwac.getBaseAppFirst()) { System.arraycopy(origResources,0,newResources,0,origSize); - offset = origSize; } else @@ -120,53 +167,23 @@ public class MavenWebInfConfiguration extends WebInfConfiguration System.arraycopy(origResources,0,newResources,overlaySize,origSize); } } - + // Overlays are always unpacked _unpackedOverlays = new Resource[overlaySize]; List overlays = jwac.getOverlays(); for (int idx=0; idx0) - { - try - { - for (int i=0; i<_unpackedOverlays.length; i++) - { - IO.delete(_unpackedOverlays[i].getFile()); - } - } - catch (IOException e) - { - LOG.ignore(e); - } - } - super.deconfigure(context); - //restore whatever the base resource was before we might have included overlaid wars - context.setBaseResource(_originalResourceBase); - - } - /** * Get the jars to examine from the files from which we have @@ -175,6 +192,7 @@ public class MavenWebInfConfiguration extends WebInfConfiguration * @param context * @return the list of jars found */ + @Override protected List findJars (WebAppContext context) throws Exception { @@ -203,14 +221,16 @@ public class MavenWebInfConfiguration extends WebInfConfiguration list.addAll(superList); return list; } + + protected Resource unpackOverlay (WebAppContext context, Resource overlay) throws IOException { //resolve if not already resolved resolveTempDirectory(context); - - + + //Get the name of the overlayed war and unpack it to a dir of the //same name in the temporary directory String name = overlay.getName(); @@ -225,6 +245,4 @@ public class MavenWebInfConfiguration extends WebInfConfiguration Resource unpackedOverlay = Resource.newResource(dir.getCanonicalPath()); return unpackedOverlay; } - - } diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/Monitor.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/Monitor.java index 66e6d1529c1..2ef290ea08a 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/Monitor.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/Monitor.java @@ -16,12 +16,12 @@ // ======================================================================== // - package org.eclipse.jetty.maven.plugin; import java.io.IOException; import java.io.InputStreamReader; import java.io.LineNumberReader; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; @@ -41,7 +41,7 @@ import org.eclipse.jetty.server.Server; * by stopping the Server instances. The choice of * behaviour is controlled by either passing true * (exit jvm) or false (stop Servers) in the constructor. - * + * */ public class Monitor extends Thread { @@ -51,7 +51,7 @@ public class Monitor extends Thread ServerSocket _serverSocket; boolean _kill; - public Monitor(int port, String key, Server[] servers, boolean kill) + public Monitor(int port, String key, Server[] servers, boolean kill) throws UnknownHostException, IOException { if (port <= 0) @@ -64,7 +64,7 @@ public class Monitor extends Thread _kill = kill; setDaemon(true); setName("StopJettyPluginMonitor"); - InetSocketAddress address = new InetSocketAddress("127.0.0.1", port); + InetSocketAddress address = new InetSocketAddress("127.0.0.1", port); _serverSocket=new ServerSocket(); _serverSocket.setReuseAddress(true); try @@ -77,7 +77,7 @@ public class Monitor extends Thread throw x; } } - + public void run() { while (_serverSocket != null) @@ -88,7 +88,7 @@ public class Monitor extends Thread socket = _serverSocket.accept(); socket.setSoLinger(false, 0); LineNumberReader lin = new LineNumberReader(new InputStreamReader(socket.getInputStream())); - + String key = lin.readLine(); if (!_key.equals(key)) continue; String cmd = lin.readLine(); @@ -97,13 +97,13 @@ public class Monitor extends Thread try{socket.close();}catch (Exception e){e.printStackTrace();} try{socket.close();}catch (Exception e){e.printStackTrace();} try{_serverSocket.close();}catch (Exception e){e.printStackTrace();} - + _serverSocket = null; - + if (_kill) { System.out.println("Killing Jetty"); - System.exit(0); + System.exit(0); } else { @@ -111,7 +111,7 @@ public class Monitor extends Thread { try { - System.out.println("Stopping server "+i); + System.out.println("Stopping server "+i); _servers[i].stop(); } catch (Exception e) diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/Starter.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/Starter.java index ef2c1092ecf..93eb1e31721 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/Starter.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/Starter.java @@ -23,19 +23,23 @@ import java.io.FileInputStream; import java.util.ArrayList; import java.util.List; import java.util.Properties; +import java.util.StringTokenizer; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.server.handler.ContextHandlerCollection; import org.eclipse.jetty.server.handler.HandlerCollection; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.ResourceCollection; +import org.eclipse.jetty.webapp.WebAppContext; import org.eclipse.jetty.xml.XmlConfiguration; public class Starter -{ +{ public static final String PORT_SYSPROPERTY = "jetty.port"; private static final Logger LOG = Log.getLogger(Starter.class); @@ -45,21 +49,21 @@ public class Starter private JettyServer server; private JettyWebAppContext webApp; private Monitor monitor; - + private int stopPort=0; private String stopKey=null; private Properties props; private String token; - - + + public void configureJetty () throws Exception { LOG.debug("Starting Jetty Server ..."); this.server = new JettyServer(); - //apply any configs from jetty.xml files first + //apply any configs from jetty.xml files first applyJettyXml (); // if the user hasn't configured a connector in the jetty.xml @@ -68,7 +72,7 @@ public class Starter if (connectors == null|| connectors.length == 0) { //if a SystemProperty -Djetty.port= has been supplied, use that as the default port - connectors = new Connector[] { this.server.createDefaultConnector(server, System.getProperty(PORT_SYSPROPERTY, null)) }; + connectors = new Connector[] { this.server.createDefaultConnector(System.getProperty(PORT_SYSPROPERTY, null)) }; this.server.setConnectors(connectors); } @@ -84,15 +88,15 @@ public class Starter this.server.configureHandlers(); webApp = new JettyWebAppContext(); - + //configure webapp from properties file describing unassembled webapp configureWebApp(); - + //set up the webapp from the context xml file provided //NOTE: just like jetty:run mojo this means that the context file can //potentially override settings made in the pom. Ideally, we'd like //the pom to override the context xml file, but as the other mojos all - //configure a WebAppContext in the pom (the element), it is + //configure a WebAppContext in the pom (the element), it is //already configured by the time the context xml file is applied. if (contextXml != null) { @@ -109,30 +113,30 @@ public class Starter monitor = new Monitor(stopPort, stopKey, new Server[]{server}, true); } } - - + + public void configureWebApp () throws Exception { if (props == null) return; - + //apply a properties file that defines the things that we configure in the jetty:run plugin: // - the context path String str = (String)props.get("context.path"); if (str != null) webApp.setContextPath(str); - + // - web.xml str = (String)props.get("web.xml"); if (str != null) webApp.setDescriptor(str); - + // - the tmp directory str = (String)props.getProperty("tmp.dir"); if (str != null) webApp.setTempDirectory(new File(str.trim())); - + // - the base directory str = (String)props.getProperty("base.dir"); if (str != null && !"".equals(str.trim())) @@ -145,7 +149,7 @@ public class Starter ResourceCollection resources = new ResourceCollection(str); webApp.setBaseResource(resources); } - + // - overlays str = (String)props.getProperty("overlay.files"); if (str != null && !"".equals(str.trim())) @@ -163,8 +167,8 @@ public class Starter { webApp.setClasses(new File(str)); } - - str = (String)props.getProperty("testClasses.dir"); + + str = (String)props.getProperty("testClasses.dir"); if (str != null && !"".equals(str.trim())) { webApp.setTestClasses(new File(str)); @@ -181,7 +185,7 @@ public class Starter jars.add(new File(names[j].trim())); webApp.setWebInfLib(jars); } - + } public void getConfiguration (String[] args) @@ -205,7 +209,7 @@ public class Starter for (int j=0; names!= null && j < names.length; j++) { jettyXmls.add(new File(names[j].trim())); - } + } } //--context-xml @@ -221,7 +225,7 @@ public class Starter props = new Properties(); props.load(new FileInputStream(f)); } - + //--token if ("--token".equals(args[i])) { @@ -237,16 +241,16 @@ public class Starter monitor.start(); LOG.info("Started Jetty Server"); - server.start(); + server.start(); } - + public void join () throws Exception { server.join(); } - - + + public void communicateStartupResult (Exception e) { if (token != null) @@ -257,16 +261,16 @@ public class Starter System.out.println(token+"\t"+e.getMessage()); } } - - + + public void applyJettyXml() throws Exception { if (jettyXmls == null) return; - + for ( File xmlFile : jettyXmls ) { - LOG.info( "Configuring Jetty from xml configuration file = " + xmlFile.getCanonicalPath() ); + LOG.info( "Configuring Jetty from xml configuration file = " + xmlFile.getCanonicalPath() ); XmlConfiguration xmlConfiguration = new XmlConfiguration(Resource.toURL(xmlFile)); xmlConfiguration.configure(this.server); } @@ -286,8 +290,8 @@ public class Starter System.arraycopy(existing, 0, children, 1, existing.length); handlers.setHandlers(children); } - - + + public static final void main(String[] args) { if (args == null) From ba06103442f221f1a1e680f25a3de1f91b157683 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Fri, 2 Nov 2012 10:28:23 +1100 Subject: [PATCH 22/31] 393383 delay onClose call until closeOut is done --- .../jetty/websocket/WebSocketConnectionRFC6455.java | 10 +++++----- .../jetty/websocket/WebSocketMessageRFC6455Test.java | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketConnectionRFC6455.java b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketConnectionRFC6455.java index 46c1cbc6a74..d999d924fec 100644 --- a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketConnectionRFC6455.java +++ b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketConnectionRFC6455.java @@ -323,13 +323,13 @@ public class WebSocketConnectionRFC6455 extends AbstractConnection implements We try { - if (tell_app) - _webSocket.onClose(code,message); + if (!closed_out) + closeOut(code,message); } finally { - if (!closed_out) - closeOut(code,message); + if (tell_app) + _webSocket.onClose(code,message); } } @@ -353,7 +353,7 @@ public class WebSocketConnectionRFC6455 extends AbstractConnection implements We } try - { + { if (tell_app) _webSocket.onClose(code,message); } diff --git a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketMessageRFC6455Test.java b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketMessageRFC6455Test.java index 2e1846fde2d..048de59e078 100644 --- a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketMessageRFC6455Test.java +++ b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketMessageRFC6455Test.java @@ -1337,7 +1337,7 @@ public class WebSocketMessageRFC6455Test output.flush(); // Make sure the read times out if there are problems with the implementation - socket.setSoTimeout(1000); + socket.setSoTimeout(10000); InputStream input = socket.getInputStream(); @@ -1347,7 +1347,7 @@ public class WebSocketMessageRFC6455Test skipTo("\r\n\r\n",input); - assertTrue(__serverWebSocket.awaitConnected(1000)); + assertTrue(__serverWebSocket.awaitConnected(10000)); assertNotNull(__serverWebSocket.connection); assertEquals(0x81,input.read()); @@ -1355,7 +1355,7 @@ public class WebSocketMessageRFC6455Test lookFor("sent on connect",input); socket.close(); - assertTrue(__serverWebSocket.awaitDisconnected(500)); + assertTrue(__serverWebSocket.awaitDisconnected(10000)); try { From 65202e9abe2c56a05b45140e46c70a94b4119cbb Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Fri, 2 Nov 2012 11:55:00 +1100 Subject: [PATCH 23/31] 393363 Use Locale.ENGLISH for all toUpperCase and toLowerCase calls --- .../jetty/annotations/AnnotationParser.java | 5 +++-- .../ResourceAnnotationHandler.java | 3 ++- .../jetty/client/SslHttpExchangeTest.java | 4 +++- .../jetty/client/SslSecurityListenerTest.java | 3 ++- .../eclipse/jetty/deploy/WebAppDeployer.java | 3 ++- .../deploy/providers/ContextProvider.java | 3 ++- .../deploy/providers/WebAppProvider.java | 7 ++++--- .../org/eclipse/jetty/deploy/util/FileID.java | 7 ++++--- .../org/eclipse/jetty/http/HttpFields.java | 2 +- .../eclipse/jetty/http/HttpFieldsTest.java | 19 ++++++++++--------- .../jetty/io/nio/SelectChannelEndPoint.java | 3 ++- .../org/eclipse/jetty/jmx/MBeanContainer.java | 3 ++- .../org/eclipse/jetty/jmx/ObjectMBean.java | 2 +- .../jetty/plus/annotation/Injection.java | 3 ++- .../jetty/plus/jaas/spi/LdapLoginModule.java | 9 +++++---- .../plus/security/DataSourceLoginService.java | 7 ++++--- .../jetty/rewrite/handler/ProxyRule.java | 5 +++-- .../authentication/FormAuthenticator.java | 7 ++++--- .../eclipse/jetty/server/CookieCutter.java | 4 +++- .../server/session/JDBCSessionIdManager.java | 7 ++++--- .../server/SelectChannelTimeoutTest.java | 7 ++++--- .../handler/AbstractConnectHandlerTest.java | 3 ++- .../server/handler/IPAccessHandlerTest.java | 3 ++- .../org/eclipse/jetty/servlet/Invoker.java | 3 ++- .../java/org/eclipse/jetty/servlets/CGI.java | 5 +++-- .../eclipse/jetty/servlets/GzipFilter.java | 5 +++-- .../jetty/servlets/MultiPartFilter.java | 3 ++- .../eclipse/jetty/servlets/ProxyServlet.java | 3 ++- .../eclipse/jetty/servlets/PutFilterTest.java | 3 ++- .../org/eclipse/jetty/spdy/api/Headers.java | 17 +++++++++-------- .../spdy/generator/HeadersBlockGenerator.java | 3 ++- .../jetty/spdy/http/ReferrerPushStrategy.java | 3 ++- .../http/ServerHTTPSPDYAsyncConnection.java | 3 ++- .../proxy/ProxyHTTPSPDYAsyncConnection.java | 3 ++- .../java/org/eclipse/jetty/start/Config.java | 9 +++++---- .../java/org/eclipse/jetty/start/Main.java | 9 +++++---- .../jetty/util/RolloverFileOutputStream.java | 5 +++-- .../jetty/util/ajax/JSONObjectConvertor.java | 5 +++-- .../jetty/util/ajax/JSONPojoConvertor.java | 5 +++-- .../org/eclipse/jetty/webapp/JarScanner.java | 3 ++- .../jetty/webapp/MetaInfConfiguration.java | 3 ++- .../webapp/StandardDescriptorProcessor.java | 5 +++-- .../jetty/webapp/TagLibConfiguration.java | 7 ++++--- .../jetty/webapp/WebAppClassLoader.java | 3 ++- .../jetty/webapp/WebInfConfiguration.java | 5 +++-- .../eclipse/jetty/xml/XmlConfiguration.java | 7 ++++--- .../src/main/java/com/acme/Dump.java | 2 +- 47 files changed, 142 insertions(+), 96 deletions(-) diff --git a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/AnnotationParser.java b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/AnnotationParser.java index 781cac429f4..bd362f033c6 100644 --- a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/AnnotationParser.java +++ b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/AnnotationParser.java @@ -29,6 +29,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.jar.JarEntry; @@ -580,7 +581,7 @@ public class AnnotationParser try { String name = entry.getName(); - if (name.toLowerCase().endsWith(".class")) + if (name.toLowerCase(Locale.ENGLISH).endsWith(".class")) { String shortName = name.replace('/', '.').substring(0,name.length()-6); if ((resolver == null) @@ -624,7 +625,7 @@ public class AnnotationParser try { String name = entry.getName(); - if (name.toLowerCase().endsWith(".class")) + if (name.toLowerCase(Locale.ENGLISH).endsWith(".class")) { String shortName = name.replace('/', '.').substring(0,name.length()-6); diff --git a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/ResourceAnnotationHandler.java b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/ResourceAnnotationHandler.java index 4f2dd128574..af2d8aeb0e3 100644 --- a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/ResourceAnnotationHandler.java +++ b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/ResourceAnnotationHandler.java @@ -21,6 +21,7 @@ package org.eclipse.jetty.annotations; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.util.Locale; import javax.annotation.Resource; import javax.naming.InitialContext; @@ -262,7 +263,7 @@ public class ResourceAnnotationHandler extends AbstractIntrospectableAnnotationH //default name is the javabean property name String name = method.getName().substring(3); - name = name.substring(0,1).toLowerCase()+name.substring(1); + name = name.substring(0,1).toLowerCase(Locale.ENGLISH)+name.substring(1); name = clazz.getCanonicalName()+"/"+name; name = (resource.name()!=null && !resource.name().trim().equals("")? resource.name(): name); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/SslHttpExchangeTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/SslHttpExchangeTest.java index 0aa29202d76..93b1c2b7f51 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/SslHttpExchangeTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/SslHttpExchangeTest.java @@ -21,6 +21,8 @@ package org.eclipse.jetty.client; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; +import java.util.Locale; + import org.eclipse.jetty.client.helperClasses.ServerAndClientCreator; import org.eclipse.jetty.client.helperClasses.SslServerAndClientCreator; import org.eclipse.jetty.server.Connector; @@ -51,7 +53,7 @@ public class SslHttpExchangeTest extends HttpExchangeTest { // Use Junit 4.x to flag test as ignored if encountering IBM JVM // Will show up in various junit reports as an ignored test as well. - Assume.assumeThat(System.getProperty("java.vendor").toLowerCase(),not(containsString("ibm"))); + Assume.assumeThat(System.getProperty("java.vendor").toLowerCase(Locale.ENGLISH),not(containsString("ibm"))); } /* ------------------------------------------------------------ */ diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/SslSecurityListenerTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/SslSecurityListenerTest.java index 0e4183ad013..56d73638019 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/SslSecurityListenerTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/SslSecurityListenerTest.java @@ -28,6 +28,7 @@ import java.io.OutputStream; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; +import java.util.Locale; import java.util.Set; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.TimeUnit; @@ -126,7 +127,7 @@ public class SslSecurityListenerTest public void testSslGet() throws Exception { // TODO Resolve problems on IBM JVM https://bugs.eclipse.org/bugs/show_bug.cgi?id=304532 - if (System.getProperty("java.vendor").toLowerCase().indexOf("ibm")>=0) + if (System.getProperty("java.vendor").toLowerCase(Locale.ENGLISH).indexOf("ibm")>=0) { LOG.warn("Skipped SSL testSslGet on IBM JVM"); return; diff --git a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/WebAppDeployer.java b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/WebAppDeployer.java index 759c31cce36..6c841f173b3 100644 --- a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/WebAppDeployer.java +++ b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/WebAppDeployer.java @@ -19,6 +19,7 @@ package org.eclipse.jetty.deploy; import java.util.ArrayList; +import java.util.Locale; import org.eclipse.jetty.deploy.providers.ScanningAppProvider; import org.eclipse.jetty.server.Handler; @@ -223,7 +224,7 @@ public class WebAppDeployer extends AbstractLifeCycle Resource app=r.addPath(r.encode(context)); - if (context.toLowerCase().endsWith(".war")||context.toLowerCase().endsWith(".jar")) + if (context.toLowerCase(Locale.ENGLISH).endsWith(".war")||context.toLowerCase(Locale.ENGLISH).endsWith(".jar")) { context=context.substring(0,context.length()-4); Resource unpacked=r.addPath(context); diff --git a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ContextProvider.java b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ContextProvider.java index f9e0dea5d06..2e4af290988 100644 --- a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ContextProvider.java +++ b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ContextProvider.java @@ -20,6 +20,7 @@ package org.eclipse.jetty.deploy.providers; import java.io.File; import java.io.FilenameFilter; +import java.util.Locale; import org.eclipse.jetty.deploy.App; import org.eclipse.jetty.deploy.ConfigurationManager; @@ -45,7 +46,7 @@ public class ContextProvider extends ScanningAppProvider { if (!dir.exists()) return false; - String lowername = name.toLowerCase(); + String lowername = name.toLowerCase(Locale.ENGLISH); if (lowername.startsWith(".")) return false; diff --git a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/WebAppProvider.java b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/WebAppProvider.java index fbad5325c55..f27a84548a0 100644 --- a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/WebAppProvider.java +++ b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/WebAppProvider.java @@ -22,6 +22,7 @@ import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.net.MalformedURLException; +import java.util.Locale; import org.eclipse.jetty.deploy.App; import org.eclipse.jetty.deploy.util.FileID; @@ -59,7 +60,7 @@ public class WebAppProvider extends ScanningAppProvider { return false; } - String lowername = name.toLowerCase(); + String lowername = name.toLowerCase(Locale.ENGLISH); File file = new File(dir,name); // is it not a directory and not a war ? @@ -279,9 +280,9 @@ public class WebAppProvider extends ScanningAppProvider { context = URIUtil.SLASH; } - else if (context.toLowerCase().startsWith("root-")) + else if (context.toLowerCase(Locale.ENGLISH).startsWith("root-")) { - int dash=context.toLowerCase().indexOf('-'); + int dash=context.toLowerCase(Locale.ENGLISH).indexOf('-'); String virtual = context.substring(dash+1); wah.setVirtualHosts(new String[]{virtual}); context = URIUtil.SLASH; diff --git a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/util/FileID.java b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/util/FileID.java index cda1fe21a88..7df726926f0 100644 --- a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/util/FileID.java +++ b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/util/FileID.java @@ -19,6 +19,7 @@ package org.eclipse.jetty.deploy.util; import java.io.File; +import java.util.Locale; /** * Simple, yet surprisingly common utility methods for identifying various file types commonly seen and worked with in a @@ -38,7 +39,7 @@ public class FileID { if (path.isFile()) { - String name = path.getName().toLowerCase(); + String name = path.getName().toLowerCase(Locale.ENGLISH); return (name.endsWith(".war") || name.endsWith(".jar")); } @@ -62,7 +63,7 @@ public class FileID return false; } - String name = path.getName().toLowerCase(); + String name = path.getName().toLowerCase(Locale.ENGLISH); return (name.endsWith(".war") || name.endsWith(".jar")); } @@ -73,7 +74,7 @@ public class FileID return false; } - String name = path.getName().toLowerCase(); + String name = path.getName().toLowerCase(Locale.ENGLISH); return name.endsWith(".xml"); } } diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java index 89a004f7107..a0a117c6616 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java @@ -959,7 +959,7 @@ public class HttpFields { hasDomain = true; buf.append(";Domain="); - QuotedStringTokenizer.quoteIfNeeded(buf,domain.toLowerCase(),delim); + QuotedStringTokenizer.quoteIfNeeded(buf,domain.toLowerCase(Locale.ENGLISH),delim); } if (maxAge >= 0) diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsTest.java index 3e26687ec4a..20cf89c222e 100644 --- a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsTest.java +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsTest.java @@ -25,6 +25,7 @@ import static org.junit.Assert.assertTrue; import java.util.Enumeration; import java.util.HashSet; +import java.util.Locale; import java.util.Set; import org.eclipse.jetty.io.Buffer; @@ -312,8 +313,8 @@ public class HttpFieldsTest s=enum2set(fields.getFieldNames()); assertEquals(3,s.size()); assertTrue(s.contains("message-id")); - assertEquals("value",fields.getStringField("message-id").toLowerCase()); - assertEquals("value",fields.getStringField("Message-ID").toLowerCase()); + assertEquals("value",fields.getStringField("message-id").toLowerCase(Locale.ENGLISH)); + assertEquals("value",fields.getStringField("Message-ID").toLowerCase(Locale.ENGLISH)); fields.clear(); @@ -323,8 +324,8 @@ public class HttpFieldsTest s=enum2set(fields.getFieldNames()); assertEquals(3,s.size()); assertTrue(s.contains("message-id")); - assertEquals("value",fields.getStringField("Message-ID").toLowerCase()); - assertEquals("value",fields.getStringField("message-id").toLowerCase()); + assertEquals("value",fields.getStringField("Message-ID").toLowerCase(Locale.ENGLISH)); + assertEquals("value",fields.getStringField("message-id").toLowerCase(Locale.ENGLISH)); fields.clear(); @@ -334,8 +335,8 @@ public class HttpFieldsTest s=enum2set(fields.getFieldNames()); assertEquals(3,s.size()); assertTrue(s.contains("message-id")); - assertEquals("value",fields.getStringField("message-id").toLowerCase()); - assertEquals("value",fields.getStringField("Message-ID").toLowerCase()); + assertEquals("value",fields.getStringField("message-id").toLowerCase(Locale.ENGLISH)); + assertEquals("value",fields.getStringField("Message-ID").toLowerCase(Locale.ENGLISH)); fields.clear(); @@ -345,8 +346,8 @@ public class HttpFieldsTest s=enum2set(fields.getFieldNames()); assertEquals(3,s.size()); assertTrue(s.contains("message-id")); - assertEquals("value",fields.getStringField("Message-ID").toLowerCase()); - assertEquals("value",fields.getStringField("message-id").toLowerCase()); + assertEquals("value",fields.getStringField("Message-ID").toLowerCase(Locale.ENGLISH)); + assertEquals("value",fields.getStringField("message-id").toLowerCase(Locale.ENGLISH)); } @Test @@ -458,7 +459,7 @@ public class HttpFieldsTest { Set s=new HashSet(); while(e.hasMoreElements()) - s.add(e.nextElement().toLowerCase()); + s.add(e.nextElement().toLowerCase(Locale.ENGLISH)); return s; } diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SelectChannelEndPoint.java b/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SelectChannelEndPoint.java index bc6ada8bd93..4c09ccd80fb 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SelectChannelEndPoint.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SelectChannelEndPoint.java @@ -23,6 +23,7 @@ import java.nio.channels.ClosedChannelException; import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; +import java.util.Locale; import org.eclipse.jetty.io.AsyncEndPoint; import org.eclipse.jetty.io.Buffer; @@ -42,7 +43,7 @@ public class SelectChannelEndPoint extends ChannelEndPoint implements AsyncEndPo { public static final Logger LOG=Log.getLogger("org.eclipse.jetty.io.nio"); - private final boolean WORK_AROUND_JVM_BUG_6346658 = System.getProperty("os.name").toLowerCase().contains("win"); + private final boolean WORK_AROUND_JVM_BUG_6346658 = System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("win"); private final SelectorManager.SelectSet _selectSet; private final SelectorManager _manager; private SelectionKey _key; diff --git a/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/MBeanContainer.java b/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/MBeanContainer.java index 2bde7f9e30e..dd97554b3e2 100644 --- a/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/MBeanContainer.java +++ b/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/MBeanContainer.java @@ -24,6 +24,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; @@ -251,7 +252,7 @@ public class MBeanContainer extends AbstractLifeCycle implements Container.Liste //no override mbean object name, so make a generic one if (oname == null) { - String type = obj.getClass().getName().toLowerCase(); + String type = obj.getClass().getName().toLowerCase(Locale.ENGLISH); int dot = type.lastIndexOf('.'); if (dot >= 0) type = type.substring(dot + 1); diff --git a/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/ObjectMBean.java b/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/ObjectMBean.java index 0123a4bcb7e..b526599e935 100644 --- a/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/ObjectMBean.java +++ b/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/ObjectMBean.java @@ -564,7 +564,7 @@ public class ObjectMBean implements DynamicMBean } - String uName = name.substring(0, 1).toUpperCase() + name.substring(1); + String uName = name.substring(0, 1).toUpperCase(Locale.ENGLISH) + name.substring(1); Class oClass = onMBean ? this.getClass() : _managed.getClass(); if (LOG.isDebugEnabled()) diff --git a/jetty-plus/src/main/java/org/eclipse/jetty/plus/annotation/Injection.java b/jetty-plus/src/main/java/org/eclipse/jetty/plus/annotation/Injection.java index 68310ee2b79..79ec710e772 100644 --- a/jetty-plus/src/main/java/org/eclipse/jetty/plus/annotation/Injection.java +++ b/jetty-plus/src/main/java/org/eclipse/jetty/plus/annotation/Injection.java @@ -21,6 +21,7 @@ package org.eclipse.jetty.plus.annotation; import java.lang.reflect.Field; import java.lang.reflect.Member; import java.lang.reflect.Method; +import java.util.Locale; import javax.naming.InitialContext; import javax.naming.NamingException; @@ -141,7 +142,7 @@ public class Injection _resourceClass = resourceType; //first look for a javabeans style setter matching the targetName - String setter = "set"+target.substring(0,1).toUpperCase()+target.substring(1); + String setter = "set"+target.substring(0,1).toUpperCase(Locale.ENGLISH)+target.substring(1); try { LOG.debug("Looking for method for setter: "+setter+" with arg "+_resourceClass); diff --git a/jetty-plus/src/main/java/org/eclipse/jetty/plus/jaas/spi/LdapLoginModule.java b/jetty-plus/src/main/java/org/eclipse/jetty/plus/jaas/spi/LdapLoginModule.java index db9e8f9f55e..f2a20b0fe2c 100644 --- a/jetty-plus/src/main/java/org/eclipse/jetty/plus/jaas/spi/LdapLoginModule.java +++ b/jetty-plus/src/main/java/org/eclipse/jetty/plus/jaas/spi/LdapLoginModule.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Hashtable; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Properties; @@ -653,12 +654,12 @@ public class LdapLoginModule extends AbstractLoginModule public static String convertCredentialJettyToLdap(String encryptedPassword) { - if ("MD5:".startsWith(encryptedPassword.toUpperCase())) + if ("MD5:".startsWith(encryptedPassword.toUpperCase(Locale.ENGLISH))) { return "{MD5}" + encryptedPassword.substring("MD5:".length(), encryptedPassword.length()); } - if ("CRYPT:".startsWith(encryptedPassword.toUpperCase())) + if ("CRYPT:".startsWith(encryptedPassword.toUpperCase(Locale.ENGLISH))) { return "{CRYPT}" + encryptedPassword.substring("CRYPT:".length(), encryptedPassword.length()); } @@ -673,12 +674,12 @@ public class LdapLoginModule extends AbstractLoginModule return encryptedPassword; } - if ("{MD5}".startsWith(encryptedPassword.toUpperCase())) + if ("{MD5}".startsWith(encryptedPassword.toUpperCase(Locale.ENGLISH))) { return "MD5:" + encryptedPassword.substring("{MD5}".length(), encryptedPassword.length()); } - if ("{CRYPT}".startsWith(encryptedPassword.toUpperCase())) + if ("{CRYPT}".startsWith(encryptedPassword.toUpperCase(Locale.ENGLISH))) { return "CRYPT:" + encryptedPassword.substring("{CRYPT}".length(), encryptedPassword.length()); } diff --git a/jetty-plus/src/main/java/org/eclipse/jetty/plus/security/DataSourceLoginService.java b/jetty-plus/src/main/java/org/eclipse/jetty/plus/security/DataSourceLoginService.java index a350764ae6d..a6673385ae6 100644 --- a/jetty-plus/src/main/java/org/eclipse/jetty/plus/security/DataSourceLoginService.java +++ b/jetty-plus/src/main/java/org/eclipse/jetty/plus/security/DataSourceLoginService.java @@ -26,6 +26,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import javax.naming.InitialContext; import javax.naming.NameNotFoundException; @@ -414,7 +415,7 @@ public class DataSourceLoginService extends MappedLoginService DatabaseMetaData metaData = connection.getMetaData(); //check if tables exist - String tableName = (metaData.storesLowerCaseIdentifiers()? _userTableName.toLowerCase(): (metaData.storesUpperCaseIdentifiers()?_userTableName.toUpperCase(): _userTableName)); + String tableName = (metaData.storesLowerCaseIdentifiers()? _userTableName.toLowerCase(Locale.ENGLISH): (metaData.storesUpperCaseIdentifiers()?_userTableName.toUpperCase(Locale.ENGLISH): _userTableName)); ResultSet result = metaData.getTables(null, null, tableName, null); if (!result.next()) { @@ -432,7 +433,7 @@ public class DataSourceLoginService extends MappedLoginService result.close(); - tableName = (metaData.storesLowerCaseIdentifiers()? _roleTableName.toLowerCase(): (metaData.storesUpperCaseIdentifiers()?_roleTableName.toUpperCase(): _roleTableName)); + tableName = (metaData.storesLowerCaseIdentifiers()? _roleTableName.toLowerCase(Locale.ENGLISH): (metaData.storesUpperCaseIdentifiers()?_roleTableName.toUpperCase(Locale.ENGLISH): _roleTableName)); result = metaData.getTables(null, null, tableName, null); if (!result.next()) { @@ -449,7 +450,7 @@ public class DataSourceLoginService extends MappedLoginService result.close(); - tableName = (metaData.storesLowerCaseIdentifiers()? _userRoleTableName.toLowerCase(): (metaData.storesUpperCaseIdentifiers()?_userRoleTableName.toUpperCase(): _userRoleTableName)); + tableName = (metaData.storesLowerCaseIdentifiers()? _userRoleTableName.toLowerCase(Locale.ENGLISH): (metaData.storesUpperCaseIdentifiers()?_userRoleTableName.toUpperCase(Locale.ENGLISH): _userRoleTableName)); result = metaData.getTables(null, null, tableName, null); if (!result.next()) { diff --git a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ProxyRule.java b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ProxyRule.java index 67600700fe5..657eead9343 100644 --- a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ProxyRule.java +++ b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ProxyRule.java @@ -24,6 +24,7 @@ import java.io.OutputStream; import java.net.MalformedURLException; import java.util.Enumeration; import java.util.HashSet; +import java.util.Locale; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -229,7 +230,7 @@ public class ProxyRule extends PatternRule @Override protected void onResponseHeader(Buffer name, Buffer value) throws IOException { - String s = name.toString().toLowerCase(); + String s = name.toString().toLowerCase(Locale.ENGLISH); if (!_DontProxyHeaders.contains(s) || (HttpHeaders.CONNECTION_BUFFER.equals(name) && HttpHeaderValues.CLOSE_BUFFER.equals(value))) { if (debug != 0) @@ -348,7 +349,7 @@ public class ProxyRule extends PatternRule String connectionHdr = request.getHeader("Connection"); if (connectionHdr != null) { - connectionHdr = connectionHdr.toLowerCase(); + connectionHdr = connectionHdr.toLowerCase(Locale.ENGLISH); if (connectionHdr.indexOf("keep-alive") < 0 && connectionHdr.indexOf("close") < 0) { connectionHdr = null; diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/FormAuthenticator.java b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/FormAuthenticator.java index 829a9d595b0..26a080dbdb8 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/FormAuthenticator.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/FormAuthenticator.java @@ -21,6 +21,7 @@ package org.eclipse.jetty.security.authentication; import java.io.IOException; import java.util.Collections; import java.util.Enumeration; +import java.util.Locale; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; @@ -390,7 +391,7 @@ public class FormAuthenticator extends LoginAuthenticator @Override public long getDateHeader(String name) { - if (name.toLowerCase().startsWith("if-")) + if (name.toLowerCase(Locale.ENGLISH).startsWith("if-")) return -1; return super.getDateHeader(name); } @@ -398,7 +399,7 @@ public class FormAuthenticator extends LoginAuthenticator @Override public String getHeader(String name) { - if (name.toLowerCase().startsWith("if-")) + if (name.toLowerCase(Locale.ENGLISH).startsWith("if-")) return null; return super.getHeader(name); } @@ -412,7 +413,7 @@ public class FormAuthenticator extends LoginAuthenticator @Override public Enumeration getHeaders(String name) { - if (name.toLowerCase().startsWith("if-")) + if (name.toLowerCase(Locale.ENGLISH).startsWith("if-")) return Collections.enumeration(Collections.EMPTY_LIST); return super.getHeaders(name); } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java b/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java index eb3e39aa373..65c93517404 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java @@ -17,6 +17,8 @@ // package org.eclipse.jetty.server; +import java.util.Locale; + import javax.servlet.http.Cookie; import org.eclipse.jetty.util.LazyList; @@ -286,7 +288,7 @@ public class CookieCutter { if (name.startsWith("$")) { - String lowercaseName = name.toLowerCase(); + String lowercaseName = name.toLowerCase(Locale.ENGLISH); if ("$path".equals(lowercaseName)) { if (cookie!=null) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionIdManager.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionIdManager.java index 5ffb6dd9dad..ecc84b22de7 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionIdManager.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionIdManager.java @@ -34,6 +34,7 @@ import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Random; import java.util.Timer; import java.util.TimerTask; @@ -124,7 +125,7 @@ public class JDBCSessionIdManager extends AbstractSessionIdManager public DatabaseAdaptor (DatabaseMetaData dbMeta) throws SQLException { - _dbName = dbMeta.getDatabaseProductName().toLowerCase(); + _dbName = dbMeta.getDatabaseProductName().toLowerCase(Locale.ENGLISH); LOG.debug ("Using database {}",_dbName); _isLower = dbMeta.storesLowerCaseIdentifiers(); _isUpper = dbMeta.storesUpperCaseIdentifiers(); @@ -140,9 +141,9 @@ public class JDBCSessionIdManager extends AbstractSessionIdManager public String convertIdentifier (String identifier) { if (_isLower) - return identifier.toLowerCase(); + return identifier.toLowerCase(Locale.ENGLISH); if (_isUpper) - return identifier.toUpperCase(); + return identifier.toUpperCase(Locale.ENGLISH); return identifier; } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/SelectChannelTimeoutTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/SelectChannelTimeoutTest.java index d61eb1920ca..e44b9cdaf35 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/SelectChannelTimeoutTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/SelectChannelTimeoutTest.java @@ -24,6 +24,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.Socket; +import java.util.Locale; import org.eclipse.jetty.server.nio.SelectChannelConnector; import org.eclipse.jetty.server.session.SessionHandler; @@ -54,7 +55,7 @@ public class SelectChannelTimeoutTest extends ConnectorTimeoutTest _handler.setSuspendFor(100); _handler.setResumeAfter(25); - assertTrue(process(null).toUpperCase().contains("RESUMED")); + assertTrue(process(null).toUpperCase(Locale.ENGLISH).contains("RESUMED")); } @Test @@ -68,7 +69,7 @@ public class SelectChannelTimeoutTest extends ConnectorTimeoutTest _server.start(); _handler.setSuspendFor(50); - assertTrue(process(null).toUpperCase().contains("TIMEOUT")); + assertTrue(process(null).toUpperCase(Locale.ENGLISH).contains("TIMEOUT")); } @Test @@ -83,7 +84,7 @@ public class SelectChannelTimeoutTest extends ConnectorTimeoutTest _handler.setSuspendFor(100); _handler.setCompleteAfter(25); - assertTrue(process(null).toUpperCase().contains("COMPLETED")); + assertTrue(process(null).toUpperCase(Locale.ENGLISH).contains("COMPLETED")); } private synchronized String process(String content) throws UnsupportedEncodingException, IOException, InterruptedException diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/AbstractConnectHandlerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/AbstractConnectHandlerTest.java index 1711fccce5c..a597e7b801b 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/AbstractConnectHandlerTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/AbstractConnectHandlerTest.java @@ -24,6 +24,7 @@ import java.io.IOException; import java.net.Socket; import java.net.SocketTimeoutException; import java.util.LinkedHashMap; +import java.util.Locale; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -104,7 +105,7 @@ public abstract class AbstractConnectHandlerTest assertTrue(header.lookingAt()); String headerName = header.group(1); String headerValue = header.group(2); - headers.put(headerName.toLowerCase(), headerValue.toLowerCase()); + headers.put(headerName.toLowerCase(Locale.ENGLISH), headerValue.toLowerCase(Locale.ENGLISH)); } StringBuilder body; diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/IPAccessHandlerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/IPAccessHandlerTest.java index dd9750300bc..3bcf058d850 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/IPAccessHandlerTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/IPAccessHandlerTest.java @@ -30,6 +30,7 @@ import java.net.Socket; import java.util.Arrays; import java.util.Collection; import java.util.LinkedHashMap; +import java.util.Locale; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -152,7 +153,7 @@ public class IPAccessHandlerTest assertTrue(header.lookingAt()); String headerName = header.group(1); String headerValue = header.group(2); - headers.put(headerName.toLowerCase(), headerValue.toLowerCase()); + headers.put(headerName.toLowerCase(Locale.ENGLISH), headerValue.toLowerCase(Locale.ENGLISH)); } StringBuilder body = new StringBuilder(); diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/Invoker.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/Invoker.java index 999ebf3286f..98ddb5fc228 100644 --- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/Invoker.java +++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/Invoker.java @@ -21,6 +21,7 @@ package org.eclipse.jetty.servlet; import java.io.IOException; import java.util.Enumeration; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import javax.servlet.ServletContext; @@ -91,7 +92,7 @@ public class Invoker extends HttpServlet { String param=(String)e.nextElement(); String value=getInitParameter(param); - String lvalue=value.toLowerCase(); + String lvalue=value.toLowerCase(Locale.ENGLISH); if ("nonContextServlets".equals(param)) { _nonContextServlets=value.length()>0 && lvalue.startsWith("t"); diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/CGI.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/CGI.java index 7db012ab329..4c625088b7b 100644 --- a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/CGI.java +++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/CGI.java @@ -24,6 +24,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.Enumeration; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import javax.servlet.ServletException; @@ -139,7 +140,7 @@ public class CGI extends HttpServlet if (!_env.envMap.containsKey("SystemRoot")) { String os = System.getProperty("os.name"); - if (os != null && os.toLowerCase().indexOf("windows") != -1) + if (os != null && os.toLowerCase(Locale.ENGLISH).indexOf("windows") != -1) { _env.set("SystemRoot","C:\\WINDOWS"); } @@ -256,7 +257,7 @@ public class CGI extends HttpServlet { String name = (String)enm.nextElement(); String value = req.getHeader(name); - env.set("HTTP_" + name.toUpperCase().replace('-','_'),value); + env.set("HTTP_" + name.toUpperCase(Locale.ENGLISH).replace('-','_'),value); } // these extra ones were from printenv on www.dev.nomura.co.uk diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/GzipFilter.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/GzipFilter.java index 78d47cf2e84..27c930b2045 100644 --- a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/GzipFilter.java +++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/GzipFilter.java @@ -20,6 +20,7 @@ package org.eclipse.jetty.servlets; import java.io.IOException; import java.util.HashSet; +import java.util.Locale; import java.util.Set; import java.util.StringTokenizer; import java.util.regex.Pattern; @@ -278,7 +279,7 @@ public class GzipFilter extends UserAgentFilter { for (int i=0; i< encodings.length; i++) { - if (encodings[i].toLowerCase().contains(GZIP)) + if (encodings[i].toLowerCase(Locale.ENGLISH).contains(GZIP)) { if (isEncodingAcceptable(encodings[i])) { @@ -287,7 +288,7 @@ public class GzipFilter extends UserAgentFilter } } - if (encodings[i].toLowerCase().contains(DEFLATE)) + if (encodings[i].toLowerCase(Locale.ENGLISH).contains(DEFLATE)) { if (isEncodingAcceptable(encodings[i])) { diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/MultiPartFilter.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/MultiPartFilter.java index 2584b2f9f5b..bceb6b5efda 100644 --- a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/MultiPartFilter.java +++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/MultiPartFilter.java @@ -36,6 +36,7 @@ import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Map; import javax.servlet.Filter; @@ -172,7 +173,7 @@ public class MultiPartFilter implements Filter int c=line.indexOf(':',0); if(c>0) { - String key=line.substring(0,c).trim().toLowerCase(); + String key=line.substring(0,c).trim().toLowerCase(Locale.ENGLISH); String value=line.substring(c+1,line.length()).trim(); if(key.equals("content-disposition")) content_disposition=value; diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ProxyServlet.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ProxyServlet.java index b908c5324f0..53eada5e20f 100644 --- a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ProxyServlet.java +++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ProxyServlet.java @@ -30,6 +30,7 @@ import java.util.Collections; import java.util.Enumeration; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.StringTokenizer; @@ -489,7 +490,7 @@ public class ProxyServlet implements Servlet protected void onResponseHeader(Buffer name, Buffer value) throws IOException { String nameString = name.toString(); - String s = nameString.toLowerCase(); + String s = nameString.toLowerCase(Locale.ENGLISH); if (!_DontProxyHeaders.contains(s) || (HttpHeaders.CONNECTION_BUFFER.equals(name) && HttpHeaderValues.CLOSE_BUFFER.equals(value))) { if (debug != 0) diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/PutFilterTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/PutFilterTest.java index 731a9794d21..300430791fb 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/PutFilterTest.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/PutFilterTest.java @@ -28,6 +28,7 @@ import java.net.Socket; import java.net.URL; import java.util.Arrays; import java.util.HashSet; +import java.util.Locale; import java.util.Set; import javax.servlet.http.HttpServletResponse; @@ -61,7 +62,7 @@ public class PutFilterTest FilterHolder holder = tester.addFilter(PutFilter.class,"/*",0); holder.setInitParameter("delAllowed","true"); // Bloody Windows does not allow file renaming - if (!System.getProperty("os.name").toLowerCase().contains("windows")) + if (!System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows")) holder.setInitParameter("putAtomic","true"); tester.start(); } diff --git a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/Headers.java b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/Headers.java index 25d3047efe0..f5d434e1339 100644 --- a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/Headers.java +++ b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/api/Headers.java @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; +import java.util.Locale; import java.util.Map; import java.util.Set; @@ -93,7 +94,7 @@ public class Headers implements Iterable */ public Header get(String name) { - return headers.get(name.trim().toLowerCase()); + return headers.get(name.trim().toLowerCase(Locale.ENGLISH)); } /** @@ -106,7 +107,7 @@ public class Headers implements Iterable { name = name.trim(); Header header = new Header(name, value.trim()); - headers.put(name.toLowerCase(), header); + headers.put(name.toLowerCase(Locale.ENGLISH), header); } /** @@ -117,7 +118,7 @@ public class Headers implements Iterable public void put(Header header) { if (header != null) - headers.put(header.name().toLowerCase(), header); + headers.put(header.name().toLowerCase(Locale.ENGLISH), header); } /** @@ -130,16 +131,16 @@ public class Headers implements Iterable public void add(String name, String value) { name = name.trim(); - Header header = headers.get(name.toLowerCase()); + Header header = headers.get(name.toLowerCase(Locale.ENGLISH)); if (header == null) { header = new Header(name, value.trim()); - headers.put(name.toLowerCase(), header); + headers.put(name.toLowerCase(Locale.ENGLISH), header); } else { header = new Header(header.name(), header.value() + "," + value.trim()); - headers.put(name.toLowerCase(), header); + headers.put(name.toLowerCase(Locale.ENGLISH), header); } } @@ -152,7 +153,7 @@ public class Headers implements Iterable public Header remove(String name) { name = name.trim(); - return headers.remove(name.toLowerCase()); + return headers.remove(name.toLowerCase(Locale.ENGLISH)); } /** @@ -229,7 +230,7 @@ public class Headers implements Iterable @Override public int hashCode() { - int result = name.toLowerCase().hashCode(); + int result = name.toLowerCase(Locale.ENGLISH).hashCode(); result = 31 * result + Arrays.hashCode(values); return result; } diff --git a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/generator/HeadersBlockGenerator.java b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/generator/HeadersBlockGenerator.java index 3f78d1a928d..c273ba35057 100644 --- a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/generator/HeadersBlockGenerator.java +++ b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/generator/HeadersBlockGenerator.java @@ -21,6 +21,7 @@ package org.eclipse.jetty.spdy.generator; import java.io.ByteArrayOutputStream; import java.nio.ByteBuffer; import java.nio.charset.Charset; +import java.util.Locale; import org.eclipse.jetty.spdy.CompressionDictionary; import org.eclipse.jetty.spdy.CompressionFactory; @@ -45,7 +46,7 @@ public class HeadersBlockGenerator writeCount(version, buffer, headers.size()); for (Headers.Header header : headers) { - String name = header.name().toLowerCase(); + String name = header.name().toLowerCase(Locale.ENGLISH); byte[] nameBytes = name.getBytes(iso1); writeNameLength(version, buffer, nameBytes.length); buffer.write(nameBytes, 0, nameBytes.length); diff --git a/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/ReferrerPushStrategy.java b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/ReferrerPushStrategy.java index a1938548412..5fc60853108 100644 --- a/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/ReferrerPushStrategy.java +++ b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/ReferrerPushStrategy.java @@ -23,6 +23,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -205,7 +206,7 @@ public class ReferrerPushStrategy implements PushStrategy if (header == null) return true; - String contentType = header.value().toLowerCase(); + String contentType = header.value().toLowerCase(Locale.ENGLISH); for (String pushContentType : pushContentTypes) if (contentType.startsWith(pushContentType)) return true; diff --git a/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/ServerHTTPSPDYAsyncConnection.java b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/ServerHTTPSPDYAsyncConnection.java index 01a82ca5593..9eb4f2b8a9d 100644 --- a/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/ServerHTTPSPDYAsyncConnection.java +++ b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/ServerHTTPSPDYAsyncConnection.java @@ -24,6 +24,7 @@ import java.io.IOException; import java.io.InterruptedIOException; import java.nio.ByteBuffer; import java.util.LinkedList; +import java.util.Locale; import java.util.Queue; import java.util.Set; import java.util.concurrent.BlockingQueue; @@ -664,7 +665,7 @@ public class ServerHTTPSPDYAsyncConnection extends AbstractHttpConnection implem for (int i = 0; i < fields.size(); ++i) { HttpFields.Field field = fields.getField(i); - String name = field.getName().toLowerCase(); + String name = field.getName().toLowerCase(Locale.ENGLISH); String value = field.getValue(); headers.put(name, value); logger.debug("HTTP < {}: {}", name, value); diff --git a/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/proxy/ProxyHTTPSPDYAsyncConnection.java b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/proxy/ProxyHTTPSPDYAsyncConnection.java index f9e1e0ff130..4274ac64e30 100644 --- a/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/proxy/ProxyHTTPSPDYAsyncConnection.java +++ b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/proxy/ProxyHTTPSPDYAsyncConnection.java @@ -21,6 +21,7 @@ package org.eclipse.jetty.spdy.proxy; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.Locale; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -95,7 +96,7 @@ public class ProxyHTTPSPDYAsyncConnection extends AsyncHttpConnection @Override protected void parsedHeader(Buffer name, Buffer value) throws IOException { - String headerName = name.toString("UTF-8").toLowerCase(); + String headerName = name.toString("UTF-8").toLowerCase(Locale.ENGLISH); String headerValue = value.toString("UTF-8"); switch (headerName) { diff --git a/jetty-start/src/main/java/org/eclipse/jetty/start/Config.java b/jetty-start/src/main/java/org/eclipse/jetty/start/Config.java index 0235a760cfc..f6249ccaf61 100644 --- a/jetty-start/src/main/java/org/eclipse/jetty/start/Config.java +++ b/jetty-start/src/main/java/org/eclipse/jetty/start/Config.java @@ -38,6 +38,7 @@ import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.Set; @@ -269,7 +270,7 @@ public class Config } else { - String name = entry.getName().toLowerCase(); + String name = entry.getName().toLowerCase(Locale.ENGLISH); if (name.endsWith(".jar") || name.endsWith(".zip")) { String jar = entry.getCanonicalPath(); @@ -796,7 +797,7 @@ public class Config } // Add XML configuration - if (subject.toLowerCase().endsWith(".xml")) + if (subject.toLowerCase(Locale.ENGLISH).endsWith(".xml")) { // Config file File f = new File(fixPath(file)); @@ -807,7 +808,7 @@ public class Config } // Set the main class to execute (overrides any previously set) - if (subject.toLowerCase().endsWith(".class")) + if (subject.toLowerCase(Locale.ENGLISH).endsWith(".class")) { // Class String cn = expand(subject.substring(0,subject.length() - 6)); @@ -820,7 +821,7 @@ public class Config } // Add raw classpath entry - if (subject.toLowerCase().endsWith(".path")) + if (subject.toLowerCase(Locale.ENGLISH).endsWith(".path")) { // classpath (jetty.class.path?) to add to runtime classpath String cn = expand(subject.substring(0,subject.length() - 5)); diff --git a/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java b/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java index 9726742630c..dabd5385f6f 100644 --- a/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java +++ b/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java @@ -46,6 +46,7 @@ import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Properties; import java.util.Set; @@ -154,7 +155,7 @@ public class Main { public boolean accept(File dir, String name) { - return name.toLowerCase().endsWith(".ini"); + return name.toLowerCase(Locale.ENGLISH).endsWith(".ini"); } }); Arrays.sort(inis); @@ -385,7 +386,7 @@ public class Main return false; } - String name = path.getName().toLowerCase(); + String name = path.getName(Locale.ENGLISH).toLowerCase(); return (name.startsWith("jetty") && name.endsWith(".xml")); } }); @@ -659,7 +660,7 @@ public class Main private String resolveXmlConfig(String xmlFilename) throws FileNotFoundException { - if (!xmlFilename.toLowerCase().endsWith(".xml")) + if (!xmlFilename.toLowerCase(Locale.ENGLISH).endsWith(".xml")) { // Nothing to resolve. return xmlFilename; @@ -873,7 +874,7 @@ public class Main if (element.isFile()) { - String name = element.getName().toLowerCase(); + String name = element.getName().toLowerCase(Locale.ENGLISH); if (name.endsWith(".jar")) { return JarVersion.getVersion(element); diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/RolloverFileOutputStream.java b/jetty-util/src/main/java/org/eclipse/jetty/util/RolloverFileOutputStream.java index a4d7ee7a13e..fffada8bd33 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/RolloverFileOutputStream.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/RolloverFileOutputStream.java @@ -27,6 +27,7 @@ import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; +import java.util.Locale; import java.util.TimeZone; import java.util.Timer; import java.util.TimerTask; @@ -221,7 +222,7 @@ public class RolloverFileOutputStream extends FilterOutputStream // Is this a rollover file? String filename=file.getName(); - int i=filename.toLowerCase().indexOf(YYYY_MM_DD); + int i=filename.toLowerCase(Locale.ENGLISH).indexOf(YYYY_MM_DD); if (i>=0) { file=new File(dir, @@ -258,7 +259,7 @@ public class RolloverFileOutputStream extends FilterOutputStream File file= new File(_filename); File dir = new File(file.getParent()); String fn=file.getName(); - int s=fn.toLowerCase().indexOf(YYYY_MM_DD); + int s=fn.toLowerCase(Locale.ENGLISH).indexOf(YYYY_MM_DD); if (s<0) return; String prefix=fn.substring(0,s); diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/ajax/JSONObjectConvertor.java b/jetty-util/src/main/java/org/eclipse/jetty/util/ajax/JSONObjectConvertor.java index 70e73e97786..f2e823deab9 100755 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/ajax/JSONObjectConvertor.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/ajax/JSONObjectConvertor.java @@ -22,6 +22,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.HashSet; +import java.util.Locale; import java.util.Map; import java.util.Set; @@ -89,9 +90,9 @@ public class JSONObjectConvertor implements JSON.Convertor { String name=m.getName(); if (name.startsWith("is")) - name=name.substring(2,3).toLowerCase()+name.substring(3); + name=name.substring(2,3).toLowerCase(Locale.ENGLISH)+name.substring(3); else if (name.startsWith("get")) - name=name.substring(3,4).toLowerCase()+name.substring(4); + name=name.substring(3,4).toLowerCase(Locale.ENGLISH)+name.substring(4); else continue; diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/ajax/JSONPojoConvertor.java b/jetty-util/src/main/java/org/eclipse/jetty/util/ajax/JSONPojoConvertor.java index d49d4cfe8ec..ca2f5e93be6 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/ajax/JSONPojoConvertor.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/ajax/JSONPojoConvertor.java @@ -26,6 +26,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.Locale; import java.util.Map; import java.util.Set; @@ -122,9 +123,9 @@ public class JSONPojoConvertor implements JSON.Convertor if(m.getReturnType()!=null) { if (name.startsWith("is") && name.length()>2) - name=name.substring(2,3).toLowerCase()+name.substring(3); + name=name.substring(2,3).toLowerCase(Locale.ENGLISH)+name.substring(3); else if (name.startsWith("get") && name.length()>3) - name=name.substring(3,4).toLowerCase()+name.substring(4); + name=name.substring(3,4).toLowerCase(Locale.ENGLISH)+name.substring(4); else break; if(includeField(name, m)) diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/JarScanner.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/JarScanner.java index 4528e162923..0dfa1eefb9e 100644 --- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/JarScanner.java +++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/JarScanner.java @@ -23,6 +23,7 @@ import java.io.InputStream; import java.net.URI; import java.net.URL; import java.net.URLClassLoader; +import java.util.Locale; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; import java.util.regex.Pattern; @@ -144,7 +145,7 @@ public abstract class JarScanner extends org.eclipse.jetty.util.PatternMatcher throws Exception { LOG.debug("Search of {}",uri); - if (uri.toString().toLowerCase().endsWith(".jar")) + if (uri.toString().toLowerCase(Locale.ENGLISH).endsWith(".jar")) { InputStream in = Resource.newResource(uri).getInputStream(); diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/MetaInfConfiguration.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/MetaInfConfiguration.java index 4b9be461ceb..2190c30050c 100644 --- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/MetaInfConfiguration.java +++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/MetaInfConfiguration.java @@ -22,6 +22,7 @@ package org.eclipse.jetty.webapp; import java.net.URI; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.jar.JarEntry; import org.eclipse.jetty.util.log.Log; @@ -125,7 +126,7 @@ public class MetaInfConfiguration extends AbstractConfiguration } else { - String lcname = name.toLowerCase(); + String lcname = name.toLowerCase(Locale.ENGLISH); if (lcname.endsWith(".tld")) { addResource(context,METAINF_TLDS,Resource.newResource("jar:"+jarUri+"!/"+name)); diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/StandardDescriptorProcessor.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/StandardDescriptorProcessor.java index af737d78ab2..cc308ed30f3 100644 --- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/StandardDescriptorProcessor.java +++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/StandardDescriptorProcessor.java @@ -27,6 +27,7 @@ import java.util.EnumSet; import java.util.EventListener; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Map; import javax.servlet.ServletException; @@ -320,7 +321,7 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor XmlParser.Node startup = node.get("load-on-startup"); if (startup != null) { - String s = startup.toString(false, true).toLowerCase(); + String s = startup.toString(false, true).toLowerCase(Locale.ENGLISH); int order = 0; if (s.startsWith("t")) { @@ -916,7 +917,7 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor if (data != null) { data = data.get("transport-guarantee"); - String guarantee = data.toString(false, true).toUpperCase(); + String guarantee = data.toString(false, true).toUpperCase(Locale.ENGLISH); if (guarantee == null || guarantee.length() == 0 || "NONE".equals(guarantee)) scBase.setDataConstraint(Constraint.DC_NONE); else if ("INTEGRAL".equals(guarantee)) diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/TagLibConfiguration.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/TagLibConfiguration.java index 580f363f813..da47a9ab62f 100644 --- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/TagLibConfiguration.java +++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/TagLibConfiguration.java @@ -28,6 +28,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; @@ -217,7 +218,7 @@ public class TagLibConfiguration extends AbstractConfiguration while(iter.hasNext()) { String location = iter.next(); - if (location!=null && location.toLowerCase().endsWith(".tld")) + if (location!=null && location.toLowerCase(Locale.ENGLISH).endsWith(".tld")) { if (!location.startsWith("/")) location="/WEB-INF/"+location; @@ -234,7 +235,7 @@ public class TagLibConfiguration extends AbstractConfiguration String[] contents = web_inf.list(); for (int i=0;contents!=null && i Date: Thu, 1 Nov 2012 19:12:36 -0700 Subject: [PATCH 24/31] Fixing compile issue. File.getName(Locale) is not allowed. --- jetty-start/src/main/java/org/eclipse/jetty/start/Main.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java b/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java index dabd5385f6f..7530f013420 100644 --- a/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java +++ b/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java @@ -386,7 +386,7 @@ public class Main return false; } - String name = path.getName(Locale.ENGLISH).toLowerCase(); + String name = path.getName().toLowerCase(); return (name.startsWith("jetty") && name.endsWith(".xml")); } }); From 13f6940fc3d19ef11ac2b5307fb5282f0d0d901e Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Fri, 2 Nov 2012 13:13:21 +1100 Subject: [PATCH 25/31] 393363 Use Locale.ENGLISH for all toUpperCase and toLowerCase calls --- jetty-start/src/main/java/org/eclipse/jetty/start/Main.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java b/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java index 7530f013420..3aa0483d23d 100644 --- a/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java +++ b/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java @@ -386,7 +386,7 @@ public class Main return false; } - String name = path.getName().toLowerCase(); + String name = path.getName().toLowerCase(Locale.ENGLISH); return (name.startsWith("jetty") && name.endsWith(".xml")); } }); From e076e8a8f0474fe0f539fc53297fa57056ab4cc8 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Fri, 2 Nov 2012 13:19:07 +1100 Subject: [PATCH 26/31] 393363 Use Locale.ENGLISH for all toUpperCase and toLowerCase calls --- .../boot/internal/webapp/LibExtClassLoaderHelper.java | 3 ++- .../eclipse/jetty/overlays/OverlayedAppProvider.java | 11 ++++++----- .../org/eclipse/jetty/rewrite/handler/ProxyRule.java | 2 +- .../java/org/eclipse/jetty/servlets/ProxyServlet.java | 4 ++-- .../src/main/java/org/eclipse/jetty/start/Main.java | 2 +- .../org/eclipse/jetty/util/MultiPartInputStream.java | 6 +++--- .../eclipse/jetty/util/ajax/JSONPojoConvertor.java | 2 +- .../src/main/java/org/eclipse/jetty/nested/Dump.java | 2 +- 8 files changed, 17 insertions(+), 15 deletions(-) diff --git a/jetty-osgi/jetty-osgi-boot/src/main/java/org/eclipse/jetty/osgi/boot/internal/webapp/LibExtClassLoaderHelper.java b/jetty-osgi/jetty-osgi-boot/src/main/java/org/eclipse/jetty/osgi/boot/internal/webapp/LibExtClassLoaderHelper.java index 6a3010db277..6e1f7b9b5af 100644 --- a/jetty-osgi/jetty-osgi-boot/src/main/java/org/eclipse/jetty/osgi/boot/internal/webapp/LibExtClassLoaderHelper.java +++ b/jetty-osgi/jetty-osgi-boot/src/main/java/org/eclipse/jetty/osgi/boot/internal/webapp/LibExtClassLoaderHelper.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; @@ -109,7 +110,7 @@ public class LibExtClassLoaderHelper for (File f : jettyResources.listFiles()) { jettyResFiles.put(f.getName(), f); - if (f.getName().toLowerCase().startsWith("readme")) + if (f.getName().toLowerCase(Locale.ENGLISH).startsWith("readme")) { continue; } diff --git a/jetty-overlay-deployer/src/main/java/org/eclipse/jetty/overlays/OverlayedAppProvider.java b/jetty-overlay-deployer/src/main/java/org/eclipse/jetty/overlays/OverlayedAppProvider.java index 0a4cfb52cd4..1042a97bc79 100644 --- a/jetty-overlay-deployer/src/main/java/org/eclipse/jetty/overlays/OverlayedAppProvider.java +++ b/jetty-overlay-deployer/src/main/java/org/eclipse/jetty/overlays/OverlayedAppProvider.java @@ -32,6 +32,7 @@ import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.Set; @@ -389,7 +390,7 @@ public class OverlayedAppProvider extends AbstractLifeCycle implements AppProvid List libs = new ArrayList(); for (String jar :instance_lib.list()) { - if (!jar.toLowerCase().endsWith(".jar")) + if (!jar.toLowerCase(Locale.ENGLISH).endsWith(".jar")) continue; libs.add(instance_lib.addPath(jar).getURL()); } @@ -610,7 +611,7 @@ public class OverlayedAppProvider extends AbstractLifeCycle implements AppProvid { for (String jar :lib.list()) { - if (!jar.toLowerCase().endsWith(".jar")) + if (!jar.toLowerCase(Locale.ENGLISH).endsWith(".jar")) continue; libs.add(lib.addPath(jar).getURL()); } @@ -832,12 +833,12 @@ public class OverlayedAppProvider extends AbstractLifeCycle implements AppProvid File origin = new File(new URI(_scanDir.toURI()+ruri)); String name=origin.getName(); - Monitor monitor = Monitor.valueOf(origin.getParentFile().getName().toUpperCase()); + Monitor monitor = Monitor.valueOf(origin.getParentFile().getName().toUpperCase(Locale.ENGLISH)); String ext=".war"; // check directory vs archive - if (origin.isDirectory() || !origin.exists() && !ruri.toLowerCase().endsWith(ext)) + if (origin.isDirectory() || !origin.exists() && !ruri.toLowerCase(Locale.ENGLISH).endsWith(ext)) { // directories have priority over archives directory=origin; @@ -846,7 +847,7 @@ public class OverlayedAppProvider extends AbstractLifeCycle implements AppProvid else { // check extension name - if (!ruri.toLowerCase().endsWith(ext)) + if (!ruri.toLowerCase(Locale.ENGLISH).endsWith(ext)) continue; name=name.substring(0,name.length()-4); diff --git a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ProxyRule.java b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ProxyRule.java index 657eead9343..f5a1598cf4c 100644 --- a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ProxyRule.java +++ b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ProxyRule.java @@ -371,7 +371,7 @@ public class ProxyRule extends PatternRule { // TODO could be better than this! String hdr = (String)enm.nextElement(); - String lhdr = hdr.toLowerCase(); + String lhdr = hdr.toLowerCase(Locale.ENGLISH); if (_DontProxyHeaders.contains(lhdr)) continue; diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ProxyServlet.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ProxyServlet.java index 53eada5e20f..46fc4823a59 100644 --- a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ProxyServlet.java +++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ProxyServlet.java @@ -561,7 +561,7 @@ public class ProxyServlet implements Servlet String connectionHdr = request.getHeader("Connection"); if (connectionHdr != null) { - connectionHdr = connectionHdr.toLowerCase(); + connectionHdr = connectionHdr.toLowerCase(Locale.ENGLISH); if (connectionHdr.indexOf("keep-alive") < 0 && connectionHdr.indexOf("close") < 0) connectionHdr = null; } @@ -579,7 +579,7 @@ public class ProxyServlet implements Servlet { // TODO could be better than this! String hdr = (String)enm.nextElement(); - String lhdr = hdr.toLowerCase(); + String lhdr = hdr.toLowerCase(Locale.ENGLISH); if (_DontProxyHeaders.contains(lhdr)) continue; diff --git a/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java b/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java index dabd5385f6f..3aa0483d23d 100644 --- a/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java +++ b/jetty-start/src/main/java/org/eclipse/jetty/start/Main.java @@ -386,7 +386,7 @@ public class Main return false; } - String name = path.getName(Locale.ENGLISH).toLowerCase(); + String name = path.getName().toLowerCase(Locale.ENGLISH); return (name.startsWith("jetty") && name.endsWith(".xml")); } }); diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStream.java b/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStream.java index 4077ed28528..84944306319 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStream.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStream.java @@ -181,7 +181,7 @@ public class MultiPartInputStream { if (name == null) return null; - return (String)_headers.getValue(name.toLowerCase(), 0); + return (String)_headers.getValue(name.toLowerCase(Locale.ENGLISH), 0); } /** @@ -505,7 +505,7 @@ public class MultiPartInputStream int c=line.indexOf(':',0); if(c>0) { - String key=line.substring(0,c).trim().toLowerCase(); + String key=line.substring(0,c).trim().toLowerCase(Locale.ENGLISH); String value=line.substring(c+1,line.length()).trim(); headers.put(key, value); if (key.equalsIgnoreCase("content-disposition")) @@ -531,7 +531,7 @@ public class MultiPartInputStream while(tok.hasMoreTokens()) { String t=tok.nextToken().trim(); - String tl=t.toLowerCase(); + String tl=t.toLowerCase(Locale.ENGLISH); if(t.startsWith("form-data")) form_data=true; else if(tl.startsWith("name=")) diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/ajax/JSONPojoConvertor.java b/jetty-util/src/main/java/org/eclipse/jetty/util/ajax/JSONPojoConvertor.java index ca2f5e93be6..b0196fc49b5 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/ajax/JSONPojoConvertor.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/ajax/JSONPojoConvertor.java @@ -135,7 +135,7 @@ public class JSONPojoConvertor implements JSON.Convertor case 1: if (name.startsWith("set") && name.length()>3) { - name=name.substring(3,4).toLowerCase()+name.substring(4); + name=name.substring(3,4).toLowerCase(Locale.ENGLISH)+name.substring(4); if(includeField(name, m)) addSetter(name, m); } diff --git a/test-jetty-nested/src/main/java/org/eclipse/jetty/nested/Dump.java b/test-jetty-nested/src/main/java/org/eclipse/jetty/nested/Dump.java index cd6f9217a69..7a1a1558189 100644 --- a/test-jetty-nested/src/main/java/org/eclipse/jetty/nested/Dump.java +++ b/test-jetty-nested/src/main/java/org/eclipse/jetty/nested/Dump.java @@ -101,7 +101,7 @@ public class Dump extends HttpServlet final boolean flush= request.getParameter("flush")!=null?Boolean.parseBoolean(request.getParameter("flush")):false; - if(request.getPathInfo()!=null && request.getPathInfo().toLowerCase().indexOf("script")!=-1) + if(request.getPathInfo()!=null && request.getPathInfo().toLowerCase(Locale.ENGLISH).indexOf("script")!=-1) { response.sendRedirect(response.encodeRedirectURL(getServletContext().getContextPath() + "/dump/info")); return; From bca34dd87b33f53a560ffa88f004eaa060cf50c6 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Fri, 2 Nov 2012 14:27:33 +1100 Subject: [PATCH 27/31] jetty-9 cleanups for doco fetches --- .../java/org/eclipse/jetty/embedded/OneContext.java | 11 ++--------- .../src/main/resources/webapps/javadoc.xml | 9 +-------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/OneContext.java b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/OneContext.java index 44dfbcc986d..6d04aaccd24 100644 --- a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/OneContext.java +++ b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/OneContext.java @@ -21,13 +21,6 @@ package org.eclipse.jetty.embedded; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.ContextHandler; -/* ------------------------------------------------------------ */ -/** - * A {@link ContextHandler} provides a common environment for multiple Handlers, - * such as: URI context path, class loader, static resource base. - * - * Typically a ContextHandler is used only when multiple contexts are likely. - */ public class OneContext { public static void main(String[] args) throws Exception @@ -38,10 +31,10 @@ public class OneContext context.setContextPath("/"); context.setResourceBase("."); context.setClassLoader(Thread.currentThread().getContextClassLoader()); - server.setHandler(context); - context.setHandler(new HelloHandler()); + server.setHandler(context); + server.start(); server.join(); } diff --git a/jetty-distribution/src/main/resources/webapps/javadoc.xml b/jetty-distribution/src/main/resources/webapps/javadoc.xml index 3fec5c07feb..71567efd934 100644 --- a/jetty-distribution/src/main/resources/webapps/javadoc.xml +++ b/jetty-distribution/src/main/resources/webapps/javadoc.xml @@ -1,13 +1,7 @@ - - + /javadoc /javadoc/ @@ -21,6 +15,5 @@ to serve static html files and images. max-age=3600,public - From 949996931a089589de6005b2e0bc2f4c44aa1820 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Thu, 1 Nov 2012 21:25:25 -0700 Subject: [PATCH 28/31] Fixing license (for check plugin) --- .../test/java/org/eclipse/jetty/websocket/dummy/DummyServer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/dummy/DummyServer.java b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/dummy/DummyServer.java index 9b41e51e2fa..1a9ff16fcd4 100644 --- a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/dummy/DummyServer.java +++ b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/dummy/DummyServer.java @@ -15,6 +15,7 @@ // You may elect to redistribute this code under either of these licenses. // ======================================================================== // + package org.eclipse.jetty.websocket.dummy; import static org.hamcrest.Matchers.*; From 94c05e6c2ecf840887defb54461102dfab561d56 Mon Sep 17 00:00:00 2001 From: Jan Bartel Date: Fri, 2 Nov 2012 15:41:58 +1100 Subject: [PATCH 29/31] Porting test webapps from codehaus to jetty-9 --- jetty-distribution/pom.xml | 11 +- jetty-jaas/pom.xml | 2 - .../src/main/config/etc/jetty-jaas.xml | 13 - tests/pom.xml | 4 + tests/test-webapps/pom.xml | 3 + .../src/main/assembly/config.xml | 2 +- .../{contexts => webapps}/test-jaas.xml | 3 +- .../src/main/webapp/index.html | 17 +- tests/test-webapps/test-jndi-webapp/pom.xml | 153 +++++++++ .../src/main/assembly/config.xml | 34 ++ .../src/main/java/com/acme/JNDITest.java | 161 +++++++++ .../src/main/templates/env-definitions.xml | 47 +++ .../main/templates/jetty-test-jndi-header.xml | 39 +++ .../main/templates/plugin-context-header.xml | 23 ++ .../src/main/webapp/WEB-INF/jetty-env.xml | 34 ++ .../src/main/webapp/WEB-INF/web.xml | 61 ++++ .../src/main/webapp/images/jetty_banner.gif | Bin 0 -> 72262 bytes .../main/webapp/images/small_powered_by.gif | Bin 0 -> 4787 bytes .../src/main/webapp/index.html | 62 ++++ .../test-webapps/test-mock-resources/pom.xml | 35 ++ .../main/java/com/acme/MockDataSource.java | 101 ++++++ .../java/com/acme/MockUserTransaction.java | 82 +++++ tests/test-webapps/test-servlet-spec/pom.xml | 30 ++ .../test-container-initializer/pom.xml | 30 ++ .../src/main/java/com/acme/Foo.java | 15 + .../main/java/com/acme/FooInitializer.java | 20 ++ .../javax.servlet.ServletContainerInitializer | 1 + .../test-spec-webapp/pom.xml | 165 +++++++++ .../test-spec-webapp/src/etc/realm.properties | 8 + .../src/main/assembly/config.xml | 34 ++ .../main/java/com/acme/AnnotationTest.java | 319 ++++++++++++++++++ .../src/main/java/com/acme/Bar.java | 25 ++ .../main/java/com/acme/MockDataSource.java | 109 ++++++ .../java/com/acme/MockUserTransaction.java | 90 +++++ .../src/main/java/com/acme/MultiPartTest.java | 122 +++++++ .../java/com/acme/RoleAnnotationTest.java | 93 +++++ .../main/java/com/acme/SecuredServlet.java | 41 +++ .../src/main/java/com/acme/TestListener.java | 146 ++++++++ .../templates/annotations-context-header.xml | 47 +++ .../src/main/templates/env-definitions.xml | 19 ++ .../main/templates/plugin-context-header.xml | 18 + .../src/main/webapp/META-INF/MANIFEST.MF | 3 + .../src/main/webapp/WEB-INF/jetty-env.xml | 17 + .../src/main/webapp/WEB-INF/web.xml | 100 ++++++ .../src/main/webapp/authfail.html | 6 + .../src/main/webapp/images/jetty_banner.gif | Bin 0 -> 72262 bytes .../main/webapp/images/small_powered_by.gif | Bin 0 -> 4787 bytes .../src/main/webapp/index.html | 69 ++++ .../src/main/webapp/login.html | 15 + .../src/main/webapp/logout.jsp | 21 ++ .../src/test/jetty-plugin-env.xml | 43 +++ .../test-web-fragment/pom.xml | 30 ++ .../main/java/com/acme/FragmentServlet.java | 82 +++++ .../META-INF/resources/fragmentA/index.html | 8 + .../main/resources/META-INF/web-fragment.xml | 39 +++ 55 files changed, 2627 insertions(+), 25 deletions(-) rename {tests/test-webapps/test-jaas-webapp => jetty-jaas}/src/main/config/etc/jetty-jaas.xml (62%) rename tests/test-webapps/test-jaas-webapp/src/main/config/{contexts => webapps}/test-jaas.xml (93%) create mode 100644 tests/test-webapps/test-jndi-webapp/pom.xml create mode 100644 tests/test-webapps/test-jndi-webapp/src/main/assembly/config.xml create mode 100644 tests/test-webapps/test-jndi-webapp/src/main/java/com/acme/JNDITest.java create mode 100644 tests/test-webapps/test-jndi-webapp/src/main/templates/env-definitions.xml create mode 100644 tests/test-webapps/test-jndi-webapp/src/main/templates/jetty-test-jndi-header.xml create mode 100644 tests/test-webapps/test-jndi-webapp/src/main/templates/plugin-context-header.xml create mode 100644 tests/test-webapps/test-jndi-webapp/src/main/webapp/WEB-INF/jetty-env.xml create mode 100644 tests/test-webapps/test-jndi-webapp/src/main/webapp/WEB-INF/web.xml create mode 100644 tests/test-webapps/test-jndi-webapp/src/main/webapp/images/jetty_banner.gif create mode 100644 tests/test-webapps/test-jndi-webapp/src/main/webapp/images/small_powered_by.gif create mode 100644 tests/test-webapps/test-jndi-webapp/src/main/webapp/index.html create mode 100644 tests/test-webapps/test-mock-resources/pom.xml create mode 100644 tests/test-webapps/test-mock-resources/src/main/java/com/acme/MockDataSource.java create mode 100644 tests/test-webapps/test-mock-resources/src/main/java/com/acme/MockUserTransaction.java create mode 100644 tests/test-webapps/test-servlet-spec/pom.xml create mode 100644 tests/test-webapps/test-servlet-spec/test-container-initializer/pom.xml create mode 100644 tests/test-webapps/test-servlet-spec/test-container-initializer/src/main/java/com/acme/Foo.java create mode 100644 tests/test-webapps/test-servlet-spec/test-container-initializer/src/main/java/com/acme/FooInitializer.java create mode 100644 tests/test-webapps/test-servlet-spec/test-container-initializer/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer create mode 100644 tests/test-webapps/test-servlet-spec/test-spec-webapp/pom.xml create mode 100644 tests/test-webapps/test-servlet-spec/test-spec-webapp/src/etc/realm.properties create mode 100644 tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/assembly/config.xml create mode 100644 tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/AnnotationTest.java create mode 100644 tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/Bar.java create mode 100644 tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/MockDataSource.java create mode 100644 tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/MockUserTransaction.java create mode 100644 tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/MultiPartTest.java create mode 100644 tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/RoleAnnotationTest.java create mode 100644 tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/SecuredServlet.java create mode 100644 tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/TestListener.java create mode 100644 tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/templates/annotations-context-header.xml create mode 100644 tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/templates/env-definitions.xml create mode 100644 tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/templates/plugin-context-header.xml create mode 100644 tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/META-INF/MANIFEST.MF create mode 100644 tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/WEB-INF/jetty-env.xml create mode 100644 tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/WEB-INF/web.xml create mode 100644 tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/authfail.html create mode 100644 tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/images/jetty_banner.gif create mode 100644 tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/images/small_powered_by.gif create mode 100644 tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/index.html create mode 100644 tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/login.html create mode 100644 tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/logout.jsp create mode 100644 tests/test-webapps/test-servlet-spec/test-spec-webapp/src/test/jetty-plugin-env.xml create mode 100644 tests/test-webapps/test-servlet-spec/test-web-fragment/pom.xml create mode 100644 tests/test-webapps/test-servlet-spec/test-web-fragment/src/main/java/com/acme/FragmentServlet.java create mode 100644 tests/test-webapps/test-servlet-spec/test-web-fragment/src/main/resources/META-INF/resources/fragmentA/index.html create mode 100644 tests/test-webapps/test-servlet-spec/test-web-fragment/src/main/resources/META-INF/web-fragment.xml diff --git a/jetty-distribution/pom.xml b/jetty-distribution/pom.xml index 2d4e4756318..20f8027a298 100644 --- a/jetty-distribution/pom.xml +++ b/jetty-distribution/pom.xml @@ -364,6 +364,12 @@ ${project.version} + + org.eclipse.jetty + jetty-jaas + ${project.version} + + org.eclipse.jetty jetty-annotations @@ -381,11 +387,6 @@ jetty-jndi ${project.version} - - org.eclipse.jetty - jetty-plus - ${project.version} - org.eclipse.jetty jetty-client diff --git a/jetty-jaas/pom.xml b/jetty-jaas/pom.xml index 748e8b62751..9128322745f 100644 --- a/jetty-jaas/pom.xml +++ b/jetty-jaas/pom.xml @@ -45,7 +45,6 @@ - diff --git a/tests/test-webapps/test-jaas-webapp/src/main/config/etc/jetty-jaas.xml b/jetty-jaas/src/main/config/etc/jetty-jaas.xml similarity index 62% rename from tests/test-webapps/test-jaas-webapp/src/main/config/etc/jetty-jaas.xml rename to jetty-jaas/src/main/config/etc/jetty-jaas.xml index 7b083ffdcd0..0d9613dfbeb 100644 --- a/tests/test-webapps/test-jaas-webapp/src/main/config/etc/jetty-jaas.xml +++ b/jetty-jaas/src/main/config/etc/jetty-jaas.xml @@ -14,17 +14,4 @@ /etc/login.conf - - - - - - - - Test JAAS Realm - xyz - - - - diff --git a/tests/pom.xml b/tests/pom.xml index ea72f4de254..5d2065c0eb9 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -40,9 +40,13 @@ + test-webapps test-sessions + diff --git a/tests/test-webapps/pom.xml b/tests/test-webapps/pom.xml index 50493194414..ea792ab117c 100644 --- a/tests/test-webapps/pom.xml +++ b/tests/test-webapps/pom.xml @@ -40,5 +40,8 @@ test-webapp-rfc2616 + test-mock-resources + test-servlet-spec + test-jaas-webapp diff --git a/tests/test-webapps/test-jaas-webapp/src/main/assembly/config.xml b/tests/test-webapps/test-jaas-webapp/src/main/assembly/config.xml index ca9e00e028e..04effeab037 100644 --- a/tests/test-webapps/test-jaas-webapp/src/main/assembly/config.xml +++ b/tests/test-webapps/test-jaas-webapp/src/main/assembly/config.xml @@ -10,7 +10,7 @@ src/main/config - contexts/** + webapps/test-jaas.xml etc/** diff --git a/tests/test-webapps/test-jaas-webapp/src/main/config/contexts/test-jaas.xml b/tests/test-webapps/test-jaas-webapp/src/main/config/webapps/test-jaas.xml similarity index 93% rename from tests/test-webapps/test-jaas-webapp/src/main/config/contexts/test-jaas.xml rename to tests/test-webapps/test-jaas-webapp/src/main/config/webapps/test-jaas.xml index 5b71dadbb16..8a94b929245 100644 --- a/tests/test-webapps/test-jaas-webapp/src/main/config/contexts/test-jaas.xml +++ b/tests/test-webapps/test-jaas-webapp/src/main/config/webapps/test-jaas.xml @@ -7,7 +7,8 @@ /test-jaas - /webapps/test-jaas + /webapps/test-jaas.war + true diff --git a/tests/test-webapps/test-jaas-webapp/src/main/webapp/index.html b/tests/test-webapps/test-jaas-webapp/src/main/webapp/index.html index 4c6e0d12edf..62b744fa650 100644 --- a/tests/test-webapps/test-jaas-webapp/src/main/webapp/index.html +++ b/tests/test-webapps/test-jaas-webapp/src/main/webapp/index.html @@ -1,4 +1,15 @@ -Welcome to the JAAS Authentication and Authorization Test + + + JAAS Authentication and Authorization Test + + + + + @@ -6,6 +17,7 @@ Home

+ Test Web Application Only - Do NOT Deploy in Production

JAAS Authentication and Authorization Demo

@@ -20,7 +32,6 @@
  • Unjar the test-jaas-webapp-<version>-config.jar inside $JETTY_HOME. The following files will be added:
    -       etc/jetty-jaas.xml
            etc/login.conf
            etc/login.properties
            contexts/test-jaas.xml
    @@ -37,7 +48,7 @@
       Click on the following link to test JAAS authentication and role-based web security constraint authorization.
       

    - This demo uses a simple login module that stores its configuration in a properties file. There are other types of login module provided with the jetty distro. For full information, please refer to the jetty documentation: http://www.eclipse.org/jetty/documentation/current/. + This demo uses a simple login module that stores its configuration in a properties file. There are other types of login module provided with the jetty distro. For full information, please refer to the Jetty 9 documentation.

    To authenticate successfully with this demonstration, you must use username="me" with password="me". All other usernames, passwords should result in authentication failure. diff --git a/tests/test-webapps/test-jndi-webapp/pom.xml b/tests/test-webapps/test-jndi-webapp/pom.xml new file mode 100644 index 00000000000..49cfa087650 --- /dev/null +++ b/tests/test-webapps/test-jndi-webapp/pom.xml @@ -0,0 +1,153 @@ + + + 4.0.0 + + org.eclipse.jetty.tests + test-webapps-parent + 9.0.0-SNAPSHOT + + test-jndi-webapp + Jetty Tests :: WebApp :: JNDI + war + + + + org.apache.maven.plugins + maven-deploy-plugin + + + true + + + + maven-antrun-plugin + + + generate-xml-files + process-resources + + + + + + + + + + + + run + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + package + + copy + + + + + org.eclipse.jetty.tests + test-mock-resources + ${project.version} + jar + ** + true + ${project.build.directory}/lib/jndi + + + + + + + + maven-resources-plugin + + + copy-transaction-properties + process-resources + + copy-resources + + + ${project.build.directory}/resources + + + src/main/config/resources + + **/transactions.properties + + true + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 2.2-beta-3 + + + package + + single + + + + src/main/assembly/config.xml + + + + + + + + org.apache.maven.plugins + maven-war-plugin + + + + + org.eclipse.jetty + jetty-maven-plugin + ${project.version} + + ${project.build.directory}/plugin-context.xml + + src/main/webapp + src/main/webapp/WEB-INF/web.xml + /test-jndi + + + + + + + + org.eclipse.jetty.orbit + javax.transaction + 1.1.1.v201105210645 + provided + + + org.eclipse.jetty.orbit + javax.servlet + provided + + + org.eclipse.jetty.orbit + javax.mail.glassfish + provided + 1.4.1.v201005082020 + + + diff --git a/tests/test-webapps/test-jndi-webapp/src/main/assembly/config.xml b/tests/test-webapps/test-jndi-webapp/src/main/assembly/config.xml new file mode 100644 index 00000000000..e307ad4d0a7 --- /dev/null +++ b/tests/test-webapps/test-jndi-webapp/src/main/assembly/config.xml @@ -0,0 +1,34 @@ + + + config + false + + jar + + + + src/main/config + + + ** + + + **/resources/** + + + + target + webapps + + test-jndi.xml + + + + target + + + lib/jndi/** + + + + diff --git a/tests/test-webapps/test-jndi-webapp/src/main/java/com/acme/JNDITest.java b/tests/test-webapps/test-jndi-webapp/src/main/java/com/acme/JNDITest.java new file mode 100644 index 00000000000..021a21152c6 --- /dev/null +++ b/tests/test-webapps/test-jndi-webapp/src/main/java/com/acme/JNDITest.java @@ -0,0 +1,161 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 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 com.acme; + +import java.io.IOException; +import java.lang.reflect.Method; + +import javax.mail.Session; +import javax.naming.InitialContext; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.sql.DataSource; +import javax.transaction.UserTransaction; + +/** + * JNDITest + * + * Use JNDI from within Jetty. + * + * Also, use servlet spec 2.5 resource injection and lifecycle callbacks from within the web.xml + * to set up some of the JNDI resources. + * + */ +public class JNDITest extends HttpServlet +{ + private DataSource myDS; + + private Session myMailSession; + private Double wiggle; + private Integer woggle; + private Double gargle; + + private String resourceNameMappingInjectionResult; + private String envEntryOverrideResult; + private String postConstructResult = "PostConstruct method called: FALSE"; + private String preDestroyResult = "PreDestroy method called: NOT YET"; + private String envEntryGlobalScopeResult; + private String envEntryWebAppScopeResult; + private String userTransactionResult; + private String mailSessionResult; + + + public void setMyDatasource(DataSource ds) + { + myDS=ds; + } + + + private void postConstruct () + { + String tmp = (myDS == null?"":myDS.toString()); + resourceNameMappingInjectionResult= "Injection of resource to locally mapped name (java:comp/env/mydatasource as java:comp/env/mydatasource1): "+String.valueOf(myDS); + envEntryOverrideResult = "Override of EnvEntry in jetty-env.xml (java:comp/env/wiggle): "+(wiggle==55.0?"PASS":"FAIL(expected 55.0, got "+wiggle+")"); + postConstructResult = "PostConstruct method called: PASS"; + } + + private void preDestroy() + { + preDestroyResult = "PreDestroy method called: PASS"; + } + + + public void init(ServletConfig config) throws ServletException + { + super.init(config); + try + { + InitialContext ic = new InitialContext(); + woggle = (Integer)ic.lookup("java:comp/env/woggle"); + envEntryGlobalScopeResult = "EnvEntry defined in context xml lookup result (java:comp/env/woggle): "+(woggle==4000?"PASS":"FAIL(expected 4000, got "+woggle+")"); + gargle = (Double)ic.lookup("java:comp/env/gargle"); + envEntryWebAppScopeResult = "EnvEntry defined in jetty-env.xml lookup result (java:comp/env/gargle): "+(gargle==100.0?"PASS":"FAIL(expected 100, got "+gargle+")"); + UserTransaction utx = (UserTransaction)ic.lookup("java:comp/UserTransaction"); + userTransactionResult = "UserTransaction lookup result (java:comp/UserTransaction): "+(utx!=null?"PASS":"FAIL"); + myMailSession = (Session)ic.lookup("java:comp/env/mail/Session"); + mailSessionResult = "Mail Session lookup result (java:comp/env/mail/Session): "+(myMailSession!=null?"PASS": "FAIL"); + } + catch (Exception e) + { + throw new ServletException(e); + } + } + + + + /* ------------------------------------------------------------ */ + public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + doGet(request, response); + } + + /* ------------------------------------------------------------ */ + public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + String mailTo = request.getParameter("mailto"); + String mailFrom = request.getParameter("mailfrom"); + + if (mailTo != null) + mailTo = mailTo.trim(); + + if (mailFrom != null) + mailFrom = mailFrom.trim(); + + try + { + response.setContentType("text/html"); + ServletOutputStream out = response.getOutputStream(); + out.println(""); + out.println("

    Jetty JNDI Tests

    "); + out.println(""); + + out.println("

    Injection and JNDI Lookup Results

    "); + out.println("

    "+resourceNameMappingInjectionResult+"

    "); + out.println("

    "+envEntryOverrideResult+"

    "); + out.println("

    "+postConstructResult+"

    "); + out.println("

    "+preDestroyResult+"

    "); + out.println("

    "+envEntryGlobalScopeResult+"

    "); + out.println("

    "+envEntryWebAppScopeResult+"

    "); + out.println("

    "+userTransactionResult+"

    "); + out.println("

    "+mailSessionResult+"

    "); + + + out.println(""); + out.println(""); + out.flush(); + } + catch (Exception e) + { + throw new ServletException(e); + } + } + + + + public void destroy () + { + } +} diff --git a/tests/test-webapps/test-jndi-webapp/src/main/templates/env-definitions.xml b/tests/test-webapps/test-jndi-webapp/src/main/templates/env-definitions.xml new file mode 100644 index 00000000000..b1b8f0e3224 --- /dev/null +++ b/tests/test-webapps/test-jndi-webapp/src/main/templates/env-definitions.xml @@ -0,0 +1,47 @@ + + + + + woggle + 4000 + false + + + + + + wiggle + 100 + true + + + + + + mail/Session + + + CHANGE-ME + CHANGE-ME + + + false + CHANGE-ME + CHANGE-ME + false + + + + + + + + + + jdbc/mydatasource + + + + + + diff --git a/tests/test-webapps/test-jndi-webapp/src/main/templates/jetty-test-jndi-header.xml b/tests/test-webapps/test-jndi-webapp/src/main/templates/jetty-test-jndi-header.xml new file mode 100644 index 00000000000..477c9ca2e82 --- /dev/null +++ b/tests/test-webapps/test-jndi-webapp/src/main/templates/jetty-test-jndi-header.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + org.eclipse.jetty.webapp.WebInfConfiguration + org.eclipse.jetty.webapp.WebXmlConfiguration + org.eclipse.jetty.webapp.MetaInfConfiguration + org.eclipse.jetty.webapp.FragmentConfiguration + org.eclipse.jetty.plus.webapp.EnvConfiguration + org.eclipse.jetty.plus.webapp.PlusConfiguration + org.eclipse.jetty.annotations.AnnotationConfiguration + org.eclipse.jetty.webapp.JettyWebXmlConfiguration + org.eclipse.jetty.webapp.TagLibConfiguration + + + + + /test-jndi + /webapps/test-jndi.war + true + false + true + diff --git a/tests/test-webapps/test-jndi-webapp/src/main/templates/plugin-context-header.xml b/tests/test-webapps/test-jndi-webapp/src/main/templates/plugin-context-header.xml new file mode 100644 index 00000000000..cdda3f1d6e1 --- /dev/null +++ b/tests/test-webapps/test-jndi-webapp/src/main/templates/plugin-context-header.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + derby.system.home + + + + + diff --git a/tests/test-webapps/test-jndi-webapp/src/main/webapp/WEB-INF/jetty-env.xml b/tests/test-webapps/test-jndi-webapp/src/main/webapp/WEB-INF/jetty-env.xml new file mode 100644 index 00000000000..8adbfae71c3 --- /dev/null +++ b/tests/test-webapps/test-jndi-webapp/src/main/webapp/WEB-INF/jetty-env.xml @@ -0,0 +1,34 @@ + + + + + + + + + gargle + 100 + true + + + + + + wiggle + 55.0 + true + + + + + + jdbc/mydatasource1 + jdbc/mydatasource + + + + + + + + diff --git a/tests/test-webapps/test-jndi-webapp/src/main/webapp/WEB-INF/web.xml b/tests/test-webapps/test-jndi-webapp/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..3c434672a24 --- /dev/null +++ b/tests/test-webapps/test-jndi-webapp/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,61 @@ + + + + Test JNDI WebApp + + + JNDITest + com.acme.JNDITest + 1 + + + + JNDITest + /test/* + + + + wiggle + 99.99 + java.lang.Double + + com.acme.JNDITest + wiggle + + + + + mail/Session + javax.mail.Session + Container + + + + jdbc/mydatasource1 + javax.sql.DataSource + Container + + com.acme.JNDITest + myDatasource + + + + + + com.acme.JNDITest + postConstruct + + + + com.acme.JNDITest + preDestroy + + + + + diff --git a/tests/test-webapps/test-jndi-webapp/src/main/webapp/images/jetty_banner.gif b/tests/test-webapps/test-jndi-webapp/src/main/webapp/images/jetty_banner.gif new file mode 100644 index 0000000000000000000000000000000000000000..0a9b1e019bafbfd2c80f59898ca602c32edc79fa GIT binary patch literal 72262 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ℑ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 + + JNDI Test WebApp + + + + + + + +

     

    + Home +
    +
    + Test Web Application Only - Do NOT Deploy in Production +
    + +

    +

    JNDI Test WebApp

    + +

    +This example shows how to configure and lookup resources such as DataSources, a JTA transaction manager and a java.mail.Session in JNDI. +

    + +

    Preparation

    +

    +

      +
    1. Ensure that you have downloaded and unpacked the test-jndi-webapp-<version>-config.jar, where <version> is replaced by the desired version number. The following files will be created: +
      +      lib/jndi/test-mock-resources-<version>.jar (where <version> is replaced by the particular version number)
      +      webapps/test-jndi.xml
      +     
      +
    2. +
    3. Edit your $JETTY_HOME/start.ini file and add the following line: +
      +      OPTIONS=plus
      +      
      +
    4. +
    +

    + +

    Execution

    +

    +Click Test to check the runtime lookup of the JNDI resources. +

    +
    + +
    + + +
    +
    + +
    + + + diff --git a/tests/test-webapps/test-mock-resources/pom.xml b/tests/test-webapps/test-mock-resources/pom.xml new file mode 100644 index 00000000000..0ee6b5b7673 --- /dev/null +++ b/tests/test-webapps/test-mock-resources/pom.xml @@ -0,0 +1,35 @@ + + 4.0.0 + + org.eclipse.jetty.tests + test-webapps-parent + 9.0.0-SNAPSHOT + + Jetty Tests :: WebApp :: Mock Resources + test-mock-resources + jar + + + + maven-compiler-plugin + + 1.6 + 1.6 + false + + + + + + + org.eclipse.jetty.orbit + javax.transaction + 1.1.1.v201105210645 + provided + + + org.eclipse.jetty.orbit + javax.servlet + + + diff --git a/tests/test-webapps/test-mock-resources/src/main/java/com/acme/MockDataSource.java b/tests/test-webapps/test-mock-resources/src/main/java/com/acme/MockDataSource.java new file mode 100644 index 00000000000..4fbdf8b71c4 --- /dev/null +++ b/tests/test-webapps/test-mock-resources/src/main/java/com/acme/MockDataSource.java @@ -0,0 +1,101 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 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 com.acme; + +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.logging.Logger; + +import javax.sql.DataSource; + +/** + * MockDataSource + * + * + */ +public class MockDataSource implements DataSource +{ + + /** + * NOTE: JDK7+ new feature + */ + public Logger getParentLogger() + { + return null; + } + + /** + * @see javax.sql.DataSource#getConnection() + */ + public Connection getConnection() throws SQLException + { + return null; + } + + /** + * @see javax.sql.DataSource#getConnection(java.lang.String, java.lang.String) + */ + public Connection getConnection(String username, String password) + throws SQLException + { + return null; + } + + /** + * @see javax.sql.DataSource#getLogWriter() + */ + public PrintWriter getLogWriter() throws SQLException + { + return null; + } + + /** + * @see javax.sql.DataSource#getLoginTimeout() + */ + public int getLoginTimeout() throws SQLException + { + return 0; + } + + /** + * @see javax.sql.DataSource#setLogWriter(java.io.PrintWriter) + */ + public void setLogWriter(PrintWriter out) throws SQLException + { + } + + /** + * @see javax.sql.DataSource#setLoginTimeout(int) + */ + public void setLoginTimeout(int seconds) throws SQLException + { + } + + public boolean isWrapperFor(Class iface) throws SQLException + { + return false; + } + + public T unwrap(Class iface) throws SQLException + { + return null; + } + +} diff --git a/tests/test-webapps/test-mock-resources/src/main/java/com/acme/MockUserTransaction.java b/tests/test-webapps/test-mock-resources/src/main/java/com/acme/MockUserTransaction.java new file mode 100644 index 00000000000..cabf3a103ce --- /dev/null +++ b/tests/test-webapps/test-mock-resources/src/main/java/com/acme/MockUserTransaction.java @@ -0,0 +1,82 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 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 com.acme; + +import javax.transaction.HeuristicMixedException; +import javax.transaction.HeuristicRollbackException; +import javax.transaction.NotSupportedException; +import javax.transaction.RollbackException; +import javax.transaction.SystemException; +import javax.transaction.UserTransaction; + +/** + * MockUserTransaction + * + * + */ +public class MockUserTransaction implements UserTransaction +{ + + /** + * @see javax.transaction.UserTransaction#begin() + */ + public void begin() throws NotSupportedException, SystemException + { + } + + /** + * @see javax.transaction.UserTransaction#commit() + */ + public void commit() throws HeuristicMixedException, + HeuristicRollbackException, IllegalStateException, + RollbackException, SecurityException, SystemException + { + } + + /** + * @see javax.transaction.UserTransaction#getStatus() + */ + public int getStatus() throws SystemException + { + return 0; + } + + /** + * @see javax.transaction.UserTransaction#rollback() + */ + public void rollback() throws IllegalStateException, SecurityException, + SystemException + { + } + + /** + * @see javax.transaction.UserTransaction#setRollbackOnly() + */ + public void setRollbackOnly() throws IllegalStateException, SystemException + { + } + + /** + * @see javax.transaction.UserTransaction#setTransactionTimeout(int) + */ + public void setTransactionTimeout(int arg0) throws SystemException + { + } + +} diff --git a/tests/test-webapps/test-servlet-spec/pom.xml b/tests/test-webapps/test-servlet-spec/pom.xml new file mode 100644 index 00000000000..9e835334689 --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + org.eclipse.jetty.tests + test-webapps-parent + 9.0.0-SNAPSHOT + + test-servlet-spec-parent + Jetty Tests :: Spec Test WebApp :: Parent + pom + + + + org.apache.maven.plugins + maven-deploy-plugin + + + true + + + + + + test-web-fragment + test-container-initializer + test-spec-webapp + + + diff --git a/tests/test-webapps/test-servlet-spec/test-container-initializer/pom.xml b/tests/test-webapps/test-servlet-spec/test-container-initializer/pom.xml new file mode 100644 index 00000000000..fe9bf936443 --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-container-initializer/pom.xml @@ -0,0 +1,30 @@ + + 4.0.0 + + org.eclipse.jetty.tests + test-servlet-spec-parent + 9.0.0-SNAPSHOT + + test-container-initializer + jar + Jetty Tests :: WebApp :: Servlet Spec :: ServletContainerInitializer Test Jar + + + + maven-compiler-plugin + + 1.6 + 1.6 + false + + + + + + + org.eclipse.jetty.orbit + javax.servlet + provided + + + diff --git a/tests/test-webapps/test-servlet-spec/test-container-initializer/src/main/java/com/acme/Foo.java b/tests/test-webapps/test-servlet-spec/test-container-initializer/src/main/java/com/acme/Foo.java new file mode 100644 index 00000000000..c9b4d89e8d7 --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-container-initializer/src/main/java/com/acme/Foo.java @@ -0,0 +1,15 @@ +package com.acme; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD}) +public @interface Foo +{ + int value(); +} + diff --git a/tests/test-webapps/test-servlet-spec/test-container-initializer/src/main/java/com/acme/FooInitializer.java b/tests/test-webapps/test-servlet-spec/test-container-initializer/src/main/java/com/acme/FooInitializer.java new file mode 100644 index 00000000000..f80409ebf0d --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-container-initializer/src/main/java/com/acme/FooInitializer.java @@ -0,0 +1,20 @@ +package com.acme; + +import java.util.Set; +import java.util.ArrayList; +import javax.servlet.ServletRegistration; +import javax.servlet.ServletContext; +import javax.servlet.annotation.HandlesTypes; +import javax.servlet.ServletContainerInitializer; + +@HandlesTypes ({javax.servlet.Servlet.class, Foo.class}) +public class FooInitializer implements ServletContainerInitializer +{ + + public void onStartup(Set> classes, ServletContext context) + { + context.setAttribute("com.acme.Foo", new ArrayList(classes)); + ServletRegistration.Dynamic reg = context.addServlet("AnnotationTest", "com.acme.AnnotationTest"); + context.setAttribute("com.acme.AnnotationTest.complete", (reg == null)); + } +} diff --git a/tests/test-webapps/test-servlet-spec/test-container-initializer/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer b/tests/test-webapps/test-servlet-spec/test-container-initializer/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer new file mode 100644 index 00000000000..264910bf918 --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-container-initializer/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer @@ -0,0 +1 @@ +com.acme.FooInitializer diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/pom.xml b/tests/test-webapps/test-servlet-spec/test-spec-webapp/pom.xml new file mode 100644 index 00000000000..3df1ae18e72 --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/pom.xml @@ -0,0 +1,165 @@ + + + 4.0.0 + + org.eclipse.jetty.tests + test-servlet-spec-parent + 9.0.0-SNAPSHOT + + Jetty Tests :: Webapps :: Spec Webapp + test-spec-webapp + war + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + maven-antrun-plugin + + + generate-xml-files + process-resources + + + + + + + + + + + + run + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + package + + copy + + + + + org.eclipse.jetty.tests + test-mock-resources + ${project.version} + jar + ** + true + ${project.build.directory}/lib/jndi + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 2.2-beta-3 + + + package + + single + + + + ${basedir}/src/main/assembly/config.xml + + + + + + + org.eclipse.jetty + jetty-maven-plugin + ${project.version} + + 10 + true + ${project.build.directory}/plugin-context.xml + + src/main/webapp + src/main/webapp/WEB-INF/web.xml + /test-annotations + .*/javax.servlet-[^/]*\.jar$ + true + ${basedir}/src/main/webapp/WEB-INF/jetty-env.xml + + + + Test Realm + src/etc/realm.properties + + + + + + org.eclipse.jetty.tests + test-mock-resources + ${project.version} + + + + + + + + org.eclipse.jetty.orbit + javax.transaction + 1.1.1.v201105210645 + provided + + + org.eclipse.jetty + jetty-server + ${project.version} + provided + + + org.eclipse.jetty.orbit + javax.mail.glassfish + 1.4.1.v201005082020 + provided + + + org.eclipse.jetty.orbit + javax.servlet + provided + + + org.eclipse.jetty.orbit + javax.annotation + 1.1.0.v201108011116 + provided + + + org.eclipse.jetty.tests + test-web-fragment + ${project.version} + + + org.eclipse.jetty.tests + test-container-initializer + ${project.version} + + + org.eclipse.jetty + jetty-util + ${project.version} + + + diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/etc/realm.properties b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/etc/realm.properties new file mode 100644 index 00000000000..ca4cfc5be5e --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/etc/realm.properties @@ -0,0 +1,8 @@ +jetty: MD5:164c88b302622e17050af52c89945d44,user +admin: CRYPT:adpexzg3FUZAk,server-administrator,content-administrator,admin +other: OBF:1xmk1w261u9r1w1c1xmq,user +plain: plain,user +user: password,user + +# This entry is for digest auth. The credential is a MD5 hash of username:realmname:password +digest: MD5:6e120743ad67abfbc385bc2bb754e297,user diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/assembly/config.xml b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/assembly/config.xml new file mode 100644 index 00000000000..9d920fc3a02 --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/assembly/config.xml @@ -0,0 +1,34 @@ + + + config + false + + jar + + + + src/main/config + + + ** + + + **/resources/** + + + + target + webapps + + test-annotations.xml + + + + target + + + lib/jndi/** + + + + diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/AnnotationTest.java b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/AnnotationTest.java new file mode 100644 index 00000000000..587f551378e --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/AnnotationTest.java @@ -0,0 +1,319 @@ +//======================================================================== +//Copyright 2004-2009 Mort Bay Consulting Pty. Ltd. +//------------------------------------------------------------------------ +//Licensed under the Apache License, Version 2.0 (the "License"); +//you may not use this file except in compliance with the License. +//You may obtain a copy of the License at +//http://www.apache.org/licenses/LICENSE-2.0 +//Unless required by applicable law or agreed to in writing, software +//distributed under the License is distributed on an "AS IS" BASIS, +//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//See the License for the specific language governing permissions and +//limitations under the License. +//======================================================================== + +/** + * + */ +package com.acme; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; + +import javax.naming.InitialContext; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.sql.DataSource; +import javax.transaction.UserTransaction; +import javax.annotation.Resource; +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.annotation.security.RunAs; +import javax.servlet.annotation.WebServlet; +import javax.servlet.annotation.WebInitParam; +import javax.annotation.security.DeclareRoles; + +/** + * AnnotationTest + * + * Use Annotations from within Jetty. + * + * Also, use servlet spec 2.5 resource injection and lifecycle callbacks from within the web.xml + * to set up some of the JNDI resources. + * + */ + +@RunAs("special") +@WebServlet(urlPatterns = {"/","/test/*"}, name="AnnotationTest", initParams={@WebInitParam(name="fromAnnotation", value="xyz")}) +@DeclareRoles({"user","client"}) +public class AnnotationTest extends HttpServlet +{ + static List __HandlesTypes; + private String postConstructResult = ""; + private String dsResult = ""; + private String envResult = ""; + private String envLookupResult = ""; + private String envResult2 =""; + private String envLookupResult2 = ""; + private String envResult3 = ""; + private String envLookupResult3 = ""; + private String dsLookupResult = ""; + private String txResult = ""; + private String txLookupResult = ""; + private DataSource myDS; + private ServletConfig config; + + @Resource(mappedName="UserTransaction") + private UserTransaction myUserTransaction; + + + @Resource(mappedName="maxAmount") + private Double maxAmount; + + @Resource(name="someAmount") + private Double minAmount; + + @Resource + private Double avgAmount; + + + @Resource(mappedName="jdbc/mydatasource") + public void setMyDatasource(DataSource ds) + { + myDS=ds; + } + + + @PostConstruct + private void myPostConstructMethod () + { + postConstructResult = "Called"; + try + { + dsResult = (myDS==null?"FAIL":"myDS="+myDS.toString()); + } + catch (Exception e) + { + dsResult = "FAIL: "+e; + } + + + envResult = (maxAmount==null?"FAIL":"maxAmount="+maxAmount.toString()); + + try + { + InitialContext ic = new InitialContext(); + envLookupResult = "java:comp/env/com.acme.AnnotationTest/maxAmount="+ic.lookup("java:comp/env/com.acme.AnnotationTest/maxAmount"); + } + catch (Exception e) + { + envLookupResult = "FAIL: "+e; + } + + envResult2 = (minAmount==null?"FAIL":"minAmount="+minAmount.toString()); + try + { + InitialContext ic = new InitialContext(); + envLookupResult2 = "java:comp/env/someAmount="+ic.lookup("java:comp/env/someAmount"); + } + catch (Exception e) + { + envLookupResult2 = "FAIL: "+e; + } + envResult3 = (minAmount==null?"FAIL":"avgAmount="+avgAmount.toString()); + try + { + InitialContext ic = new InitialContext(); + envLookupResult3 = "java:comp/env/com.acme.AnnotationTest/avgAmount="+ic.lookup("java:comp/env/com.acme.AnnotationTest/avgAmount"); + } + catch (Exception e) + { + envLookupResult3 = "FAIL: "+e; + } + + + + try + { + InitialContext ic = new InitialContext(); + dsLookupResult = "java:comp/env/com.acme.AnnotationTest/myDatasource="+ic.lookup("java:comp/env/com.acme.AnnotationTest/myDatasource"); + } + catch (Exception e) + { + dsLookupResult = "FAIL: "+e; + } + + txResult = (myUserTransaction==null?"FAIL":"myUserTransaction="+myUserTransaction); + try + { + InitialContext ic = new InitialContext(); + txLookupResult = "java:comp/env/com.acme.AnnotationTest/myUserTransaction="+ic.lookup("java:comp/env/com.acme.AnnotationTest/myUserTransaction"); + } + catch (Exception e) + { + txLookupResult = "FAIL: "+e; + } + } + + @PreDestroy + private void myPreDestroyMethod() + { + } + + public void init(ServletConfig config) throws ServletException + { + super.init(config); + this.config = config; + } + + + + /* ------------------------------------------------------------ */ + public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + doGet(request, response); + } + + /* ------------------------------------------------------------ */ + public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + try + { + response.setContentType("text/html"); + ServletOutputStream out = response.getOutputStream(); + out.println(""); + out.println(""); + out.println("

    Results

    "); + + out.println("

    Init Params from Annotation

    "); + out.println("
    ");
    +            out.println("initParams={@WebInitParam(name=\"fromAnnotation\", value=\"xyz\")}");
    +            out.println("
    "); + out.println("
    Result: "+("xyz".equals(config.getInitParameter("fromAnnotation"))? "PASS": "FAIL")); + + out.println("

    Init Params from web-fragment

    "); + out.println("
    ");
    +            out.println("extra1=123, extra2=345");
    +            out.println("
    "); + boolean fragInitParamResult = "123".equals(config.getInitParameter("extra1")) && "345".equals(config.getInitParameter("extra2")); + out.println("
    Result: "+(fragInitParamResult? "PASS": "FAIL")); + + + __HandlesTypes = Arrays.asList( "javax.servlet.GenericServlet", + "javax.servlet.http.HttpServlet", + "com.acme.AnnotationTest", + "com.acme.RoleAnnotationTest", + "com.acme.MultiPartTest", + "com.acme.FragmentServlet", + "com.acme.TestListener", + "com.acme.SecuredServlet", + "com.acme.Bar"); + out.println("

    @ContainerInitializer

    "); + out.println("
    ");
    +             out.println("@HandlesTypes({javax.servlet.Servlet.class, Foo.class})");
    +             out.println("
    "); + out.print("
    Result: "); + List classes = (List)config.getServletContext().getAttribute("com.acme.Foo"); + List classNames = new ArrayList(); + if (classes != null) + { + for (Class c: classes) + { + classNames.add(c.getName()); + out.print(c.getName()+" "); + } + + if (classNames.size() != __HandlesTypes.size()) + out.println("
    FAIL"); + else if (!classNames.containsAll(__HandlesTypes)) + out.println("
    FAIL"); + else + out.println("
    PASS"); + } + else + out.print("
    FAIL (No such attribute com.acme.Foo)"); + out.println("
    "); + + out.println("

    Complete Servlet Registration

    "); + Boolean complete = (Boolean)config.getServletContext().getAttribute("com.acme.AnnotationTest.complete"); + out.println("
    Result: "+(complete.booleanValue()?"PASS":"FAIL")+""); + + out.println("

    @PostConstruct Callback

    "); + out.println("
    ");
    +            out.println("@PostConstruct");
    +            out.println("private void myPostConstructMethod ()");
    +            out.println("{}"); 
    +            out.println("
    "); + out.println("
    Result: "+postConstructResult+""); + + + out.println("

    @Resource Injection for DataSource

    "); + out.println("
    ");         
    +            out.println("@Resource(mappedName=\"jdbc/mydatasource\");");
    +            out.println("public void setMyDatasource(DataSource ds)");
    +            out.println("{");
    +            out.println("myDS=ds;");
    +            out.println("}");
    +            out.println("
    "); + out.println("
    Result: "+dsResult+""); + out.println("
    JNDI Lookup Result: "+dsLookupResult+""); + + + out.println("

    @Resource Injection for env-entry

    "); + out.println("
    ");
    +            out.println("@Resource(mappedName=\"maxAmount\")");
    +            out.println("private Double maxAmount;");
    +            out.println("@Resource(name=\"minAmount\")");
    +            out.println("private Double minAmount;");
    +            out.println("
    "); + out.println("
    Result: "+envResult+": "+(maxAmount.compareTo(new Double(55))==0?" PASS":" FAIL")+""); + out.println("
    JNDI Lookup Result: "+envLookupResult+""); + out.println("
    Result: "+envResult2+": "+(minAmount.compareTo(new Double("0.99"))==0?" PASS":" FAIL")+""); + out.println("
    JNDI Lookup Result: "+envLookupResult2+""); + out.println("
    Result: "+envResult3+": "+(avgAmount.compareTo(new Double("1.25"))==0?" PASS":" FAIL")+""); + out.println("
    JNDI Lookup Result: "+envLookupResult3+""); + out.println("

    @Resource Injection for UserTransaction

    "); + out.println("
    ");
    +            out.println("@Resource(mappedName=\"UserTransaction\")");
    +            out.println("private UserTransaction myUserTransaction;");
    +            out.println("
    "); + out.println("
    Result: "+txResult+""); + out.println("
    JNDI Lookup Result: "+txLookupResult+""); + out.println("

    DeclaresRoles

    "); + out.println("

    Login as user \"admin\" with password \"admin\" when prompted after clicking the button below to test @DeclareRoles annotation

    "); + String context = request.getContextPath(); + if (!context.endsWith("/")) + context += "/"; + context += "role/"; + out.println("
    "); + + out.println("

    ServletSecurity

    "); + out.println("

    Login as user \"admin\" with password \"admin\" when prompted after clicking the button below to test @ServletSecurity annotation

    "); + context = request.getContextPath(); + if (!context.endsWith("/")) + context += "/"; + context += "sec/foo"; + out.println("
    "); + + + out.println(""); + out.println(""); + out.flush(); + } + catch (Exception e) + { + throw new ServletException(e); + } + } + + + + +} diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/Bar.java b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/Bar.java new file mode 100644 index 00000000000..264d5359c38 --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/Bar.java @@ -0,0 +1,25 @@ +//======================================================================== +//Copyright 2010 Mort Bay Consulting Pty. Ltd. +//------------------------------------------------------------------------ +//Licensed under the Apache License, Version 2.0 (the "License"); +//you may not use this file except in compliance with the License. +//You may obtain a copy of the License at +//http://www.apache.org/licenses/LICENSE-2.0 +//Unless required by applicable law or agreed to in writing, software +//distributed under the License is distributed on an "AS IS" BASIS, +//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//See the License for the specific language governing permissions and +//limitations under the License. +//======================================================================== + +package com.acme; + +import com.acme.Foo; + +public class Bar { + + @Foo(2) + public void someMethod () { + } + +} diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/MockDataSource.java b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/MockDataSource.java new file mode 100644 index 00000000000..c64319a4b23 --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/MockDataSource.java @@ -0,0 +1,109 @@ +//======================================================================== +//$Id: MockDataSource.java 3317 2008-07-19 08:08:24Z gregw $ +//Copyright 2006 Mort Bay Consulting Pty. Ltd. +//------------------------------------------------------------------------ +//Licensed under the Apache License, Version 2.0 (the "License"); +//you may not use this file except in compliance with the License. +//You may obtain a copy of the License at +//http://www.apache.org/licenses/LICENSE-2.0 +//Unless required by applicable law or agreed to in writing, software +//distributed under the License is distributed on an "AS IS" BASIS, +//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//See the License for the specific language governing permissions and +//limitations under the License. +//======================================================================== + +package com.acme; + +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.logging.Logger; + +import javax.sql.DataSource; + +/** + * MockDataSource + * + * + */ +public class MockDataSource implements DataSource +{ + + /** + * NOTE: JDK7+ new feature + */ + public Logger getParentLogger() + { + // TODO Auto-generated method stub + return null; + } + + /** + * @see javax.sql.DataSource#getConnection() + */ + public Connection getConnection() throws SQLException + { + // TODO Auto-generated method stub + return null; + } + + /** + * @see javax.sql.DataSource#getConnection(java.lang.String, java.lang.String) + */ + public Connection getConnection(String username, String password) + throws SQLException + { + // TODO Auto-generated method stub + return null; + } + + /** + * @see javax.sql.DataSource#getLogWriter() + */ + public PrintWriter getLogWriter() throws SQLException + { + // TODO Auto-generated method stub + return null; + } + + /** + * @see javax.sql.DataSource#getLoginTimeout() + */ + public int getLoginTimeout() throws SQLException + { + // TODO Auto-generated method stub + return 0; + } + + /** + * @see javax.sql.DataSource#setLogWriter(java.io.PrintWriter) + */ + public void setLogWriter(PrintWriter out) throws SQLException + { + // TODO Auto-generated method stub + + } + + /** + * @see javax.sql.DataSource#setLoginTimeout(int) + */ + public void setLoginTimeout(int seconds) throws SQLException + { + // TODO Auto-generated method stub + + } + + public boolean isWrapperFor(Class iface) throws SQLException + { + // TODO Auto-generated method stub + return false; + } + + public T unwrap(Class iface) throws SQLException + { + // TODO Auto-generated method stub + return null; + } + +} diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/MockUserTransaction.java b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/MockUserTransaction.java new file mode 100644 index 00000000000..4c21e2d637a --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/MockUserTransaction.java @@ -0,0 +1,90 @@ +//======================================================================== +//$Id: MockUserTransaction.java 1692 2007-03-23 04:33:07Z janb $ +//Copyright 2006 Mort Bay Consulting Pty. Ltd. +//------------------------------------------------------------------------ +//Licensed under the Apache License, Version 2.0 (the "License"); +//you may not use this file except in compliance with the License. +//You may obtain a copy of the License at +//http://www.apache.org/licenses/LICENSE-2.0 +//Unless required by applicable law or agreed to in writing, software +//distributed under the License is distributed on an "AS IS" BASIS, +//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//See the License for the specific language governing permissions and +//limitations under the License. +//======================================================================== + +package com.acme; + +import javax.transaction.HeuristicMixedException; +import javax.transaction.HeuristicRollbackException; +import javax.transaction.NotSupportedException; +import javax.transaction.RollbackException; +import javax.transaction.SystemException; +import javax.transaction.UserTransaction; + +/** + * MockUserTransaction + * + * + */ +public class MockUserTransaction implements UserTransaction +{ + + /** + * @see javax.transaction.UserTransaction#begin() + */ + public void begin() throws NotSupportedException, SystemException + { + // TODO Auto-generated method stub + + } + + /** + * @see javax.transaction.UserTransaction#commit() + */ + public void commit() throws HeuristicMixedException, + HeuristicRollbackException, IllegalStateException, + RollbackException, SecurityException, SystemException + { + // TODO Auto-generated method stub + + } + + /** + * @see javax.transaction.UserTransaction#getStatus() + */ + public int getStatus() throws SystemException + { + // TODO Auto-generated method stub + return 0; + } + + /** + * @see javax.transaction.UserTransaction#rollback() + */ + public void rollback() throws IllegalStateException, SecurityException, + SystemException + { + // TODO Auto-generated method stub + + } + + /** + * @see javax.transaction.UserTransaction#setRollbackOnly() + */ + public void setRollbackOnly() throws IllegalStateException, SystemException + { + // TODO Auto-generated method stub + + } + + /** + * @see javax.transaction.UserTransaction#setTransactionTimeout(int) + */ + public void setTransactionTimeout(int arg0) throws SystemException + { + // TODO Auto-generated method stub + + } + +} diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/MultiPartTest.java b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/MultiPartTest.java new file mode 100644 index 00000000000..a8f35594efb --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/MultiPartTest.java @@ -0,0 +1,122 @@ +//======================================================================== +//Copyright 2010 Mort Bay Consulting Pty. Ltd. +//------------------------------------------------------------------------ +//Licensed under the Apache License, Version 2.0 (the "License"); +//you may not use this file except in compliance with the License. +//You may obtain a copy of the License at +//http://www.apache.org/licenses/LICENSE-2.0 +//Unless required by applicable law or agreed to in writing, software +//distributed under the License is distributed on an "AS IS" BASIS, +//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//See the License for the specific language governing permissions and +//limitations under the License. +//======================================================================== + +/** + * + */ +package com.acme; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.Part; +import javax.servlet.annotation.MultipartConfig; + +import org.eclipse.jetty.util.IO; +/** + * MultiPartTest + * + * Test Servlet 3.0 MultiPart Mime handling. + * + * + */ + +@MultipartConfig(location="foo/bar", maxFileSize=10240, maxRequestSize=-1, fileSizeThreshold=2048) +public class MultiPartTest extends HttpServlet +{ + private ServletConfig config; + + + public void init(ServletConfig config) throws ServletException + { + super.init(config); + this.config = config; + } + + + + /* ------------------------------------------------------------ */ + public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + + try + { + response.setContentType("text/html"); + ServletOutputStream out = response.getOutputStream(); + out.println(""); + out.println("

    Results

    "); + out.println(""); + out.println("

    "); + + Collection parts = request.getParts(); + out.println("Parts: "+parts.size()); + for (Part p: parts) + { + out.println("

    "+p.getName()+"

    "); + out.println("Size: "+p.getSize()); + if (p.getContentType() == null || p.getContentType().startsWith("text/plain")) + { + out.println("

    "); + IO.copy(p.getInputStream(),out); + out.println("

    "); + } + } + out.println(""); + out.println(""); + out.flush(); + } + catch (ServletException e) + { + throw e; + } + catch (Exception e) + { + throw new ServletException(e); + } + } + + /* ------------------------------------------------------------ */ + public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + try + { + response.setContentType("text/html"); + ServletOutputStream out = response.getOutputStream(); + out.println(""); + out.println(""); + out.println("

    Use a POST Instead

    "); + out.println(""); + out.println(""); + out.flush(); + } + catch (Exception e) + { + throw new ServletException(e); + } + } + + + + +} diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/RoleAnnotationTest.java b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/RoleAnnotationTest.java new file mode 100644 index 00000000000..34ef9389430 --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/RoleAnnotationTest.java @@ -0,0 +1,93 @@ +//======================================================================== +//Copyright 2010 Mort Bay Consulting Pty. Ltd. +//------------------------------------------------------------------------ +//Licensed under the Apache License, Version 2.0 (the "License"); +//you may not use this file except in compliance with the License. +//You may obtain a copy of the License at +//http://www.apache.org/licenses/LICENSE-2.0 +//Unless required by applicable law or agreed to in writing, software +//distributed under the License is distributed on an "AS IS" BASIS, +//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//See the License for the specific language governing permissions and +//limitations under the License. +//======================================================================== + +/** + * + */ +package com.acme; + +import java.io.IOException; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.annotation.security.DeclareRoles; + +/** + * RoleAnnotationTest + * + * Use DeclareRolesAnnotations from within Jetty. + * + * + */ + + +@DeclareRoles({"server-administrator","user"}) +public class RoleAnnotationTest extends HttpServlet +{ + private ServletConfig _config; + + public void init(ServletConfig config) throws ServletException + { + super.init(config); + _config = config; + } + + + + /* ------------------------------------------------------------ */ + public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + doGet(request, response); + } + + /* ------------------------------------------------------------ */ + public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + try + { + response.setContentType("text/html"); + ServletOutputStream out = response.getOutputStream(); + out.println(""); + out.println("

    Jetty DeclareRoles Annotation Results

    "); + out.println(""); + + out.println("

    Roles

    "); + boolean result = request.isUserInRole("other"); + out.println("
    Result: isUserInRole(\"other\")="+result+":"+ (result==false?" PASS":" FAIL")+""); + + result = request.isUserInRole("manager"); + out.println("
    Result: isUserInRole(\"manager\")="+result+":"+ (result?" PASS":" FAIL")+""); + result = request.isUserInRole("user"); + out.println("
    Result: isUserInRole(\"user\")="+result+":"+ (result==false?" PASS":" FAIL")+""); + String context = _config.getServletContext().getContextPath(); + if (!context.endsWith("/")) + context += "/"; + + out.println("

    Logout

    "); + + out.println(""); + out.println(""); + out.flush(); + } + catch (Exception e) + { + throw new ServletException(e); + } + } + +} diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/SecuredServlet.java b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/SecuredServlet.java new file mode 100644 index 00000000000..8479905fe19 --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/SecuredServlet.java @@ -0,0 +1,41 @@ +package com.acme; + +import java.io.IOException; +import java.io.PrintWriter; + +import javax.servlet.ServletException; +import javax.servlet.annotation.HttpConstraint; +import javax.servlet.annotation.ServletSecurity; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@WebServlet(urlPatterns="/sec/*") +@ServletSecurity(@HttpConstraint(rolesAllowed="admin")) +public class SecuredServlet extends HttpServlet +{ + + + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException + { + PrintWriter writer = resp.getWriter(); + writer.println( ""); + writer.println( ""); + writer.println("

    @ServletSecurity

    "); + writer.println("
    ");
    +        writer.println("@ServletSecurity");
    +        writer.println("public class SecuredServlet");
    +        writer.println("
    "); + writer.println("
    Result: "+true+""); + String context = getServletConfig().getServletContext().getContextPath(); + if (!context.endsWith("/")) + context += "/"; + writer.println("

    Logout

    "); + writer.println( ""); + writer.println( ""); + writer.flush(); + writer.close(); + } +} diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/TestListener.java b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/TestListener.java new file mode 100644 index 00000000000..ea2d3470dc0 --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/TestListener.java @@ -0,0 +1,146 @@ +//======================================================================== +//$Id: TestListener.java 3363 2008-07-22 13:40:59Z janb $ +//Copyright 2004-2005 Mort Bay Consulting Pty. Ltd. +//------------------------------------------------------------------------ +//Licensed under the Apache License, Version 2.0 (the "License"); +//you may not use this file except in compliance with the License. +//You may obtain a copy of the License at +//http://www.apache.org/licenses/LICENSE-2.0 +//Unless required by applicable law or agreed to in writing, software +//distributed under the License is distributed on an "AS IS" BASIS, +//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//See the License for the specific language governing permissions and +//limitations under the License. +//======================================================================== + +package com.acme; +import javax.annotation.Resource; +import javax.servlet.annotation.WebListener; +import javax.servlet.ServletContextAttributeEvent; +import javax.servlet.ServletContextAttributeListener; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.servlet.ServletRequestAttributeEvent; +import javax.servlet.ServletRequestAttributeListener; +import javax.servlet.ServletRequestEvent; +import javax.servlet.ServletRequestListener; +import javax.servlet.http.HttpSessionActivationListener; +import javax.servlet.http.HttpSessionAttributeListener; +import javax.servlet.http.HttpSessionBindingEvent; +import javax.servlet.http.HttpSessionEvent; +import javax.servlet.http.HttpSessionListener; +import com.acme.Foo; + + +@Foo(1) +@WebListener +public class TestListener implements HttpSessionListener, HttpSessionAttributeListener, HttpSessionActivationListener, ServletContextListener, ServletContextAttributeListener, ServletRequestListener, ServletRequestAttributeListener +{ + + @Resource(mappedName="maxAmount") + private Double maxAmount; + + public void attributeAdded(HttpSessionBindingEvent se) + { + // System.err.println("attributedAdded "+se); + } + + public void attributeRemoved(HttpSessionBindingEvent se) + { + // System.err.println("attributeRemoved "+se); + } + + public void attributeReplaced(HttpSessionBindingEvent se) + { + // System.err.println("attributeReplaced "+se); + } + + public void sessionWillPassivate(HttpSessionEvent se) + { + // System.err.println("sessionWillPassivate "+se); + } + + public void sessionDidActivate(HttpSessionEvent se) + { + // System.err.println("sessionDidActivate "+se); + } + + public void contextInitialized(ServletContextEvent sce) + { + //System.err.println("contextInitialized, maxAmount injected as "+maxAmount); + } + + public void contextDestroyed(ServletContextEvent sce) + { + // System.err.println("contextDestroyed "+sce); + } + + public void attributeAdded(ServletContextAttributeEvent scab) + { + // System.err.println("attributeAdded "+scab); + } + + public void attributeRemoved(ServletContextAttributeEvent scab) + { + // System.err.println("attributeRemoved "+scab); + } + + public void attributeReplaced(ServletContextAttributeEvent scab) + { + // System.err.println("attributeReplaced "+scab); + } + + public void requestDestroyed(ServletRequestEvent sre) + { + // System.err.println("requestDestroyed "+sre); + } + + public void requestInitialized(ServletRequestEvent sre) + { + // System.err.println("requestInitialized "+sre); + } + + public void attributeAdded(ServletRequestAttributeEvent srae) + { + // System.err.println("attributeAdded "+srae); + } + + public void attributeRemoved(ServletRequestAttributeEvent srae) + { + // System.err.println("attributeRemoved "+srae); + } + + public void attributeReplaced(ServletRequestAttributeEvent srae) + { + // System.err.println("attributeReplaced "+srae); + } + + public void sessionCreated(HttpSessionEvent se) + { + // System.err.println("sessionCreated "+se); + } + + public void sessionDestroyed(HttpSessionEvent se) + { + // System.err.println("sessionDestroyed "+se); + } + + public void requestCompleted(ServletRequestEvent rre) + { + // TODO Auto-generated method stub + + } + + public void requestResumed(ServletRequestEvent rre) + { + // TODO Auto-generated method stub + + } + + public void requestSuspended(ServletRequestEvent rre) + { + // TODO Auto-generated method stub + + } + +} diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/templates/annotations-context-header.xml b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/templates/annotations-context-header.xml new file mode 100644 index 00000000000..06a1f7e6fd7 --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/templates/annotations-context-header.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + org.eclipse.jetty.webapp.WebInfConfiguration + org.eclipse.jetty.webapp.WebXmlConfiguration + org.eclipse.jetty.webapp.MetaInfConfiguration + org.eclipse.jetty.webapp.FragmentConfiguration + org.eclipse.jetty.plus.webapp.EnvConfiguration + org.eclipse.jetty.plus.webapp.PlusConfiguration + org.eclipse.jetty.annotations.AnnotationConfiguration + org.eclipse.jetty.webapp.JettyWebXmlConfiguration + org.eclipse.jetty.webapp.TagLibConfiguration + + + + /test-annotations + /webapps/test-annotations + true + + + + + Test Realm + /etc/realm.properties + + + + diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/templates/env-definitions.xml b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/templates/env-definitions.xml new file mode 100644 index 00000000000..b08559b9e75 --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/templates/env-definitions.xml @@ -0,0 +1,19 @@ + + + + maxAmount + 100 + true + + + + + + jdbc/mydatasource + + + + + + + diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/templates/plugin-context-header.xml b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/templates/plugin-context-header.xml new file mode 100644 index 00000000000..7326e8ef0b9 --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/templates/plugin-context-header.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/META-INF/MANIFEST.MF b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/META-INF/MANIFEST.MF new file mode 100644 index 00000000000..5e9495128c0 --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Class-Path: + diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/WEB-INF/jetty-env.xml b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/WEB-INF/jetty-env.xml new file mode 100644 index 00000000000..01d28cd860c --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/WEB-INF/jetty-env.xml @@ -0,0 +1,17 @@ + + + + + + + + + + maxAmount + 55.0 + true + + + + + diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/WEB-INF/web.xml b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..6d894ee6a1e --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,100 @@ + + + + Test Annotations WebApp + + + com.acme.TestListener + + + + + AnnotationTest + 1 + + + + AnnotationTest + /test/* + + + + RoleAnnotationTest + com.acme.RoleAnnotationTest + 1 + + manager + server-administrator + + + + + RoleAnnotationTest + /role/* + + + + Multi + com.acme.MultiPartTest + 2 + + + + Multi + /multi/* + + + + com.acme.AnnotationTest/avgAmount + 1.25 + java.lang.Double + + + + someAmount + 0.99 + java.lang.Double + + + + + Admin Role + /role/* + + + admin + + + + + admin + + + + + + + FORM + Test Realm + + + /login.html + + + /authfail.html + + + + + + diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/authfail.html b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/authfail.html new file mode 100644 index 00000000000..17dc9670a56 --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/authfail.html @@ -0,0 +1,6 @@ + + Authentication Failure</title</head> + <body> + <h1>Authentication Failure</h1> + <p>Sorry, either your login or password were incorrect, please try again.</p> +</html> diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/images/jetty_banner.gif b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/images/jetty_banner.gif new file mode 100644 index 0000000000000000000000000000000000000000..0a9b1e019bafbfd2c80f59898ca602c32edc79fa GIT binary patch literal 72262 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ℑ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 literal 0 HcmV?d00001 diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/images/small_powered_by.gif b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/images/small_powered_by.gif new file mode 100644 index 0000000000000000000000000000000000000000..c5dd44319f0aa17ea93b15fbcc8a18e14ca397d5 GIT binary patch literal 4787 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 literal 0 HcmV?d00001 diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/index.html b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/index.html new file mode 100644 index 00000000000..514eb8e2575 --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/index.html @@ -0,0 +1,69 @@ +<HTML> + <HEAD> + <TITLE>Test Specification WebApp + + + + + + + + +

     

    + Home +
    +
    + Test Web Application Only - Do NOT Deploy in Production +
    +

    +

    Advanced Servlet Specification Test WebApp

    + +

    +This example uses and tests newer aspects of the servlet specification such as annotations, web-fragments and servlet container initializers. +

    + +

    Preparation

    +

    +

      +
    1. Ensure that you have downloaded and unpacked the test-spec-webapp-<version>-config.jar, where is replaced by the desired version number. The following files will be created: +
      +      lib/jndi/test-mock-resources-<version>.jar (where <version> is replaced by the particular version number)
      +      webapps/test-annotations.xml
      +      
      +
    2. +
    3. Edit your $JETTY_HOME/start.ini file and add the following lines: +
      +      OPTIONS=plus
      +      OPTIONS=annotations
      +      
      +
    4. +
    + +

    + +

    The Tests

    + +

    Test Servlet 2.5/3.0 Annotations, Fragments and Initializers

    +
    + +
    + +

    Test Servlet 3.0 Multipart Mime

    +
    + File to upload: + +
    + + +
    +
    + +
    + + + diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/login.html b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/login.html new file mode 100644 index 00000000000..1b44dea40ad --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/login.html @@ -0,0 +1,15 @@ + +Annotation Test + +

    Enter your username and password to login

    + Enter login=admin and password=admin in order to authenticate successfully +
    + Login: +

    + Password: +

    + +

    +

    + + diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/logout.jsp b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/logout.jsp new file mode 100644 index 00000000000..0a001c3745f --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/logout.jsp @@ -0,0 +1,21 @@ +<%@ page contentType="text/html; charset=UTF-8" %> +<%@ page import="java.util.*"%> +<%@ page import="javax.servlet.*" %> +<%@ page import="javax.servlet.http.*" %> + + +Logout + + + + <% + HttpSession s = request.getSession(false); + s.invalidate(); + %> +

    Logout

    + +

    You are now logged out.

    + Home + + + diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/test/jetty-plugin-env.xml b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/test/jetty-plugin-env.xml new file mode 100644 index 00000000000..8eacd850837 --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/test/jetty-plugin-env.xml @@ -0,0 +1,43 @@ + + + + + + + + + + maxAmount + 55.0 + true + + + + + + + + + + + + + + + + + + + + + + + jdbc/mydatasource + + + + + + + + diff --git a/tests/test-webapps/test-servlet-spec/test-web-fragment/pom.xml b/tests/test-webapps/test-servlet-spec/test-web-fragment/pom.xml new file mode 100644 index 00000000000..f28b2443579 --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-web-fragment/pom.xml @@ -0,0 +1,30 @@ + + 4.0.0 + + org.eclipse.jetty.tests + test-servlet-spec-parent + 9.0.0-SNAPSHOT + + Jetty Tests :: WebApp :: Servlet Spec :: Fragment Jar + org.eclipse.jetty.tests + test-web-fragment + jar + + + + maven-compiler-plugin + + 1.6 + 1.6 + false + + + + + + + org.eclipse.jetty.orbit + javax.servlet + + + diff --git a/tests/test-webapps/test-servlet-spec/test-web-fragment/src/main/java/com/acme/FragmentServlet.java b/tests/test-webapps/test-servlet-spec/test-web-fragment/src/main/java/com/acme/FragmentServlet.java new file mode 100644 index 00000000000..498197741ec --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-web-fragment/src/main/java/com/acme/FragmentServlet.java @@ -0,0 +1,82 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 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 com.acme; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * FragmentServlet + * + * A web fragment jar. + */ + +public class FragmentServlet extends HttpServlet +{ + private ServletConfig config; + + + public void init(ServletConfig config) throws ServletException + { + super.init(config); + this.config = config; + } + + + + /* ------------------------------------------------------------ */ + public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + doGet(request, response); + } + + /* ------------------------------------------------------------ */ + public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + try + { + response.setContentType("text/html"); + ServletOutputStream out = response.getOutputStream(); + out.println(""); + out.println("

    Jetty Fragment Servlet

    "); + out.println(""); + out.println(""); + out.println(""); + out.flush(); + } + catch (Exception e) + { + throw new ServletException(e); + } + } + + + + +} diff --git a/tests/test-webapps/test-servlet-spec/test-web-fragment/src/main/resources/META-INF/resources/fragmentA/index.html b/tests/test-webapps/test-servlet-spec/test-web-fragment/src/main/resources/META-INF/resources/fragmentA/index.html new file mode 100644 index 00000000000..0b686ef9276 --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-web-fragment/src/main/resources/META-INF/resources/fragmentA/index.html @@ -0,0 +1,8 @@ +

    Welcome to a Fragment

    + +

    +This index.html file was included in a fragment's META-INF/resources directory. +

    + +Now hit a servlet added by a fragment + diff --git a/tests/test-webapps/test-servlet-spec/test-web-fragment/src/main/resources/META-INF/web-fragment.xml b/tests/test-webapps/test-servlet-spec/test-web-fragment/src/main/resources/META-INF/web-fragment.xml new file mode 100644 index 00000000000..c4b334a1469 --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-web-fragment/src/main/resources/META-INF/web-fragment.xml @@ -0,0 +1,39 @@ + + + + + FragmentA + + + + + + + AnnotationTest + com.acme.AnnotationTest + + extra1123 + + + extra2345 + + + + + Fragment + com.acme.FragmentServlet + + + + Fragment + /fragment/* + + + + + + From 2f735cf5a9185cb365d3ded57d97463ce3024312 Mon Sep 17 00:00:00 2001 From: Jan Bartel Date: Fri, 2 Nov 2012 15:43:59 +1100 Subject: [PATCH 30/31] Adding jetty-jaas module to build for jetty-9 --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index 28c77397899..b66f74042c6 100644 --- a/pom.xml +++ b/pom.xml @@ -403,6 +403,7 @@ jetty-annotations jetty-jndi jetty-jsp + jetty-jaas jetty-distribution jetty-spring jetty-client From 4e7932f5192d31910fe9a70e0ea6609fbb2e0d68 Mon Sep 17 00:00:00 2001 From: Jan Bartel Date: Fri, 2 Nov 2012 16:56:17 +1100 Subject: [PATCH 31/31] Add production warning; rename test-annotation.war to test-spec.war for jetty-9 --- .../src/main/webapp/WEB-INF/jetty-web.xml | 8 ++++++++ .../main/templates/plugin-context-header.xml | 17 +++++++---------- .../src/main/webapp/WEB-INF/jetty-web.xml | 8 ++++++++ .../test-jndi-webapp/src/main/webapp/index.html | 3 ++- .../test-servlet-spec/test-spec-webapp/pom.xml | 2 +- .../src/main/assembly/config.xml | 2 +- .../templates/annotations-context-header.xml | 2 +- .../main/templates/plugin-context-header.xml | 6 ++++-- .../src/main/webapp/WEB-INF/jetty-web.xml | 8 ++++++++ .../test-spec-webapp/src/main/webapp/index.html | 1 + 10 files changed, 41 insertions(+), 16 deletions(-) create mode 100644 tests/test-webapps/test-jaas-webapp/src/main/webapp/WEB-INF/jetty-web.xml create mode 100644 tests/test-webapps/test-jndi-webapp/src/main/webapp/WEB-INF/jetty-web.xml create mode 100644 tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/WEB-INF/jetty-web.xml diff --git a/tests/test-webapps/test-jaas-webapp/src/main/webapp/WEB-INF/jetty-web.xml b/tests/test-webapps/test-jaas-webapp/src/main/webapp/WEB-INF/jetty-web.xml new file mode 100644 index 00000000000..c469f742375 --- /dev/null +++ b/tests/test-webapps/test-jaas-webapp/src/main/webapp/WEB-INF/jetty-web.xml @@ -0,0 +1,8 @@ + + + + + + test-jaas webapp is deployed. DO NOT USE IN PRODUCTION! + + diff --git a/tests/test-webapps/test-jndi-webapp/src/main/templates/plugin-context-header.xml b/tests/test-webapps/test-jndi-webapp/src/main/templates/plugin-context-header.xml index cdda3f1d6e1..252251b16e6 100644 --- a/tests/test-webapps/test-jndi-webapp/src/main/templates/plugin-context-header.xml +++ b/tests/test-webapps/test-jndi-webapp/src/main/templates/plugin-context-header.xml @@ -6,18 +6,15 @@ + + + + + + - - - - - - - - derby.system.home - - + diff --git a/tests/test-webapps/test-jndi-webapp/src/main/webapp/WEB-INF/jetty-web.xml b/tests/test-webapps/test-jndi-webapp/src/main/webapp/WEB-INF/jetty-web.xml new file mode 100644 index 00000000000..f7207c25b02 --- /dev/null +++ b/tests/test-webapps/test-jndi-webapp/src/main/webapp/WEB-INF/jetty-web.xml @@ -0,0 +1,8 @@ + + + + + + test-jndi webapp is deployed. DO NOT USE IN PRODUCTION! + + diff --git a/tests/test-webapps/test-jndi-webapp/src/main/webapp/index.html b/tests/test-webapps/test-jndi-webapp/src/main/webapp/index.html index ee892149b26..ddb4ac7908b 100644 --- a/tests/test-webapps/test-jndi-webapp/src/main/webapp/index.html +++ b/tests/test-webapps/test-jndi-webapp/src/main/webapp/index.html @@ -36,9 +36,10 @@ This example shows how to configure and lookup resources such as DataSources, a webapps/test-jndi.xml
  • -
  • Edit your $JETTY_HOME/start.ini file and add the following line: +
  • Edit your $JETTY_HOME/start.ini file and add the following lines:
           OPTIONS=plus
    +      OPTIONS=jta
           
  • diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/pom.xml b/tests/test-webapps/test-servlet-spec/test-spec-webapp/pom.xml index 3df1ae18e72..2ee913bfb87 100644 --- a/tests/test-webapps/test-servlet-spec/test-spec-webapp/pom.xml +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/pom.xml @@ -29,7 +29,7 @@ - + diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/assembly/config.xml b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/assembly/config.xml index 9d920fc3a02..d1c38d25ac2 100644 --- a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/assembly/config.xml +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/assembly/config.xml @@ -20,7 +20,7 @@ target webapps - test-annotations.xml + test-spec.xml diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/templates/annotations-context-header.xml b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/templates/annotations-context-header.xml index 06a1f7e6fd7..79c365f5095 100644 --- a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/templates/annotations-context-header.xml +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/templates/annotations-context-header.xml @@ -33,7 +33,7 @@
    /test-annotations - /webapps/test-annotations + /webapps/test-spec.war true diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/templates/plugin-context-header.xml b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/templates/plugin-context-header.xml index 7326e8ef0b9..a757cba4c17 100644 --- a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/templates/plugin-context-header.xml +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/templates/plugin-context-header.xml @@ -10,9 +10,11 @@ - + - + + + diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/WEB-INF/jetty-web.xml b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/WEB-INF/jetty-web.xml new file mode 100644 index 00000000000..1b2a0df056d --- /dev/null +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/WEB-INF/jetty-web.xml @@ -0,0 +1,8 @@ + + + + + + test-spec webapp is deployed. DO NOT USE IN PRODUCTION! + + diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/index.html b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/index.html index 514eb8e2575..b2ba7cd6efa 100644 --- a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/index.html +++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/index.html @@ -40,6 +40,7 @@ This example uses and tests newer aspects of the servlet specification such as a
           OPTIONS=plus
           OPTIONS=annotations
    +      OPTIONS=jta