Merge remote-tracking branch 'origin/jetty-9.3.x' into jetty-9.4.x

This commit is contained in:
Greg Wilkins 2017-03-06 14:20:23 +11:00
commit 0a2da4822d
17 changed files with 594 additions and 220 deletions

View File

@ -40,6 +40,7 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import org.eclipse.jetty.client.api.ContentProvider;
import org.eclipse.jetty.client.api.ContentResponse;
@ -81,6 +82,7 @@ public class HttpRequest implements Request
private List<HttpCookie> cookies;
private Map<String, Object> attributes;
private List<RequestListener> requestListeners;
private BiFunction<Request, Request, Response.CompleteListener> pushListener;
protected HttpRequest(HttpClient client, HttpConversation conversation, URI uri)
{
@ -567,6 +569,26 @@ public class HttpRequest implements Request
return this;
}
/**
* <p>Sets a listener for pushed resources.</p>
* <p>When resources are pushed from the server, the given {@code listener}
* is invoked for every pushed resource.
* The parameters to the {@code BiFunction} are this request and the
* synthesized request for the pushed resource.
* The {@code BiFunction} should return a {@code CompleteListener} that
* may also implement other listener interfaces to be notified of various
* response events, or {@code null} to signal that the pushed resource
* should be canceled.</p>
*
* @param listener a listener for pushed resource events
* @return this request object
*/
public Request pushListener(BiFunction<Request, Request, Response.CompleteListener> listener)
{
this.pushListener = listener;
return this;
}
@Override
public ContentProvider getContent()
{
@ -698,6 +720,11 @@ public class HttpRequest implements Request
return responseListeners;
}
public BiFunction<Request, Request, Response.CompleteListener> getPushListener()
{
return pushListener;
}
@Override
public boolean abort(Throwable cause)
{

View File

@ -36,7 +36,7 @@ At its most basic, you configure Jetty from two elements:
Instead of editing these directly, Jetty 9.1 introduced more options on how to configure Jetty (these are merely syntactic sugar that eventually resolve into the two basic configuration components).
Jetty 9.1 Startup Features include:
Jetty Startup Features include:
* A separation of the Jetty distribution binaries in `${jetty.home}` and the environment specific configurations (and binaries) found in `${jetty.base}` (detailed in link:#startup-jetty-base-and-jetty-home[Managing Jetty Base and Jetty Home.])
* You can enable a set of libraries and XML configuration files via the newly introduced link:#startup-modules[module system.]

View File

@ -40,6 +40,20 @@ public class QuotedQualityCSV extends QuotedCSV implements Iterable<String>
private final static Double ZERO=new Double(0.0);
private final static Double ONE=new Double(1.0);
/**
* Function to apply a most specific MIME encoding secondary ordering
*/
public static Function<String, Integer> MOST_SPECIFIC = new Function<String, Integer>()
{
@Override
public Integer apply(String s)
{
String[] elements = s.split("/");
return 1000000*elements.length+1000*elements[0].length()+elements[elements.length-1].length();
}
};
private final List<Double> _quality = new ArrayList<>();
private boolean _sorted = false;
private final Function<String, Integer> _secondaryOrdering;
@ -50,7 +64,7 @@ public class QuotedQualityCSV extends QuotedCSV implements Iterable<String>
*/
public QuotedQualityCSV()
{
this((s) -> s.length());
this((s) -> 0);
}
/* ------------------------------------------------------------ */
@ -101,7 +115,14 @@ public class QuotedQualityCSV extends QuotedCSV implements Iterable<String>
@Override
protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue)
{
if (buffer.charAt(paramName)=='q' && paramValue>paramName && buffer.charAt(paramName+1)=='=')
if (paramName<0)
{
if (buffer.charAt(buffer.length()-1)==';')
buffer.setLength(buffer.length()-1);
}
if (paramValue>=0 &&
buffer.charAt(paramName)=='q' && paramValue>paramName &&
buffer.length()>=paramName && buffer.charAt(paramName+1)=='=')
{
Double q;
try
@ -142,7 +163,7 @@ public class QuotedQualityCSV extends QuotedCSV implements Iterable<String>
_sorted=true;
Double last = ZERO;
int lastOrderIndex = Integer.MIN_VALUE;
int lastSecondaryOrder = Integer.MIN_VALUE;
for (int i = _values.size(); i-- > 0;)
{
@ -150,20 +171,20 @@ public class QuotedQualityCSV extends QuotedCSV implements Iterable<String>
Double q = _quality.get(i);
int compare=last.compareTo(q);
if (compare>0 || (compare==0 && _secondaryOrdering.apply(v)<lastOrderIndex))
if (compare>0 || (compare==0 && _secondaryOrdering.apply(v)<lastSecondaryOrder))
{
_values.set(i, _values.get(i + 1));
_values.set(i + 1, v);
_quality.set(i, _quality.get(i + 1));
_quality.set(i + 1, q);
last = ZERO;
lastOrderIndex=0;
lastSecondaryOrder=0;
i = _values.size();
continue;
}
last=q;
lastOrderIndex=_secondaryOrdering.apply(v);
lastSecondaryOrder=_secondaryOrdering.apply(v);
}
int last_element=_quality.size();

View File

@ -433,15 +433,41 @@ public class HttpFieldsTest
fields.add("name", "nothing;q=0");
fields.add("name", "one;q=0.4");
fields.add("name", "three;x=y;q=0.2;a=b,two;q=0.3");
fields.add("name", "first;");
List<String> list = fields.getQualityCSV("name");
assertEquals("zero",HttpFields.valueParameters(list.get(0),null));
assertEquals("one",HttpFields.valueParameters(list.get(1),null));
assertEquals("two",HttpFields.valueParameters(list.get(2),null));
assertEquals("three",HttpFields.valueParameters(list.get(3),null));
assertEquals("four",HttpFields.valueParameters(list.get(4),null));
assertEquals("first",HttpFields.valueParameters(list.get(0),null));
assertEquals("zero",HttpFields.valueParameters(list.get(1),null));
assertEquals("one",HttpFields.valueParameters(list.get(2),null));
assertEquals("two",HttpFields.valueParameters(list.get(3),null));
assertEquals("three",HttpFields.valueParameters(list.get(4),null));
assertEquals("four",HttpFields.valueParameters(list.get(5),null));
}
@Test
public void testGetQualityCSVHeader() throws Exception
{
HttpFields fields = new HttpFields();
fields.put("some", "value");
fields.add("Accept", "zero;q=0.9,four;q=0.1");
fields.put("other", "value");
fields.add("Accept", "nothing;q=0");
fields.add("Accept", "one;q=0.4");
fields.add("Accept", "three;x=y;q=0.2;a=b,two;q=0.3");
fields.add("Accept", "first;");
List<String> list = fields.getQualityCSV(HttpHeader.ACCEPT);
assertEquals("first",HttpFields.valueParameters(list.get(0),null));
assertEquals("zero",HttpFields.valueParameters(list.get(1),null));
assertEquals("one",HttpFields.valueParameters(list.get(2),null));
assertEquals("two",HttpFields.valueParameters(list.get(3),null));
assertEquals("three",HttpFields.valueParameters(list.get(4),null));
assertEquals("four",HttpFields.valueParameters(list.get(5),null));
}
@Test
public void testDateFields() throws Exception

View File

@ -50,6 +50,17 @@ public class QuotedQualityCSVTest
{
QuotedQualityCSV values = new QuotedQualityCSV();
values.addValue("text/*, text/plain, text/plain;format=flowed, */*");
// Note this sort is only on quality and not the most specific type as per 5.3.2
Assert.assertThat(values,Matchers.contains("text/*","text/plain","text/plain;format=flowed","*/*"));
}
@Test
public void test7231_5_3_2_example3_most_specific()
{
QuotedQualityCSV values = new QuotedQualityCSV(QuotedQualityCSV.MOST_SPECIFIC);
values.addValue("text/*, text/plain, text/plain;format=flowed, */*");
Assert.assertThat(values,Matchers.contains("text/plain;format=flowed","text/plain","text/*","*/*"));
}
@ -81,9 +92,9 @@ public class QuotedQualityCSVTest
Assert.assertThat(values,Matchers.contains(
"compress",
"gzip",
"gzip",
"gzip",
"*",
"gzip",
"gzip",
"compress",
"identity"
));
@ -217,4 +228,22 @@ public class QuotedQualityCSVTest
values.addValue("gzip, *");
assertThat(values, contains("*", "gzip"));
}
@Test
public void testSameQuality()
{
QuotedQualityCSV values = new QuotedQualityCSV();
values.addValue("one;q=0.5,two;q=0.5,three;q=0.5");
Assert.assertThat(values.getValues(),Matchers.contains("one","two","three"));
}
@Test
public void testNoQuality()
{
QuotedQualityCSV values = new QuotedQualityCSV();
values.addValue("one,two;,three;x=y");
Assert.assertThat(values.getValues(),Matchers.contains("one","two","three;x=y"));
}
}

View File

@ -34,19 +34,26 @@ public class HttpChannelOverHTTP2 extends HttpChannel
{
private final HttpConnectionOverHTTP2 connection;
private final Session session;
private final boolean push;
private final HttpSenderOverHTTP2 sender;
private final HttpReceiverOverHTTP2 receiver;
private Stream stream;
public HttpChannelOverHTTP2(HttpDestination destination, HttpConnectionOverHTTP2 connection, Session session)
public HttpChannelOverHTTP2(HttpDestination destination, HttpConnectionOverHTTP2 connection, Session session, boolean push)
{
super(destination);
this.connection = connection;
this.session = session;
this.push = push;
this.sender = new HttpSenderOverHTTP2(this);
this.receiver = new HttpReceiverOverHTTP2(this);
}
protected HttpConnectionOverHTTP2 getHttpConnection()
{
return connection;
}
public Session getSession()
{
return session;
@ -110,6 +117,7 @@ public class HttpChannelOverHTTP2 extends HttpChannel
public void exchangeTerminated(HttpExchange exchange, Result result)
{
super.exchangeTerminated(exchange, result);
release();
if (!push)
release();
}
}

View File

@ -61,15 +61,15 @@ public class HttpConnectionOverHTTP2 extends HttpConnection implements Sweeper.S
normalizeRequest(exchange.getRequest());
// One connection maps to N channels, so for each exchange we create a new channel.
HttpChannel channel = newHttpChannel();
HttpChannel channel = newHttpChannel(false);
channels.add(channel);
return send(channel, exchange);
}
protected HttpChannelOverHTTP2 newHttpChannel()
protected HttpChannelOverHTTP2 newHttpChannel(boolean push)
{
return new HttpChannelOverHTTP2(getHttpDestination(), this, getSession());
return new HttpChannelOverHTTP2(getHttpDestination(), this, getSession(), push);
}
protected void release(HttpChannel channel)

View File

@ -21,13 +21,19 @@ package org.eclipse.jetty.http2.client.http;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Queue;
import java.util.function.BiFunction;
import org.eclipse.jetty.client.HttpChannel;
import org.eclipse.jetty.client.HttpExchange;
import org.eclipse.jetty.client.HttpReceiver;
import org.eclipse.jetty.client.HttpRequest;
import org.eclipse.jetty.client.HttpResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpStatus;
@ -91,7 +97,32 @@ public class HttpReceiverOverHTTP2 extends HttpReceiver implements Stream.Listen
@Override
public Stream.Listener onPush(Stream stream, PushPromiseFrame frame)
{
// Not supported.
HttpExchange exchange = getHttpExchange();
if (exchange == null)
return null;
HttpRequest request = exchange.getRequest();
MetaData.Request metaData = (MetaData.Request)frame.getMetaData();
HttpRequest pushRequest = (HttpRequest)getHttpDestination().getHttpClient().newRequest(metaData.getURIString());
BiFunction<Request, Request, Response.CompleteListener> pushListener = request.getPushListener();
if (pushListener != null)
{
Response.CompleteListener listener = pushListener.apply(request, pushRequest);
if (listener != null)
{
HttpChannelOverHTTP2 pushChannel = getHttpChannel().getHttpConnection().newHttpChannel(true);
List<Response.ResponseListener> listeners = Collections.singletonList(listener);
HttpExchange pushExchange = new HttpExchange(getHttpDestination(), pushRequest, listeners);
pushChannel.associate(pushExchange);
pushChannel.setStream(stream);
// TODO: idle timeout ?
pushExchange.requestComplete(null);
pushExchange.terminateRequest();
return pushChannel.getStreamListener();
}
}
stream.reset(new ResetFrame(stream.getId(), ErrorCode.REFUSED_STREAM_ERROR.code), Callback.NOOP);
return null;
}

View File

@ -247,9 +247,9 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest
return new HttpConnectionOverHTTP2(destination, session)
{
@Override
protected HttpChannelOverHTTP2 newHttpChannel()
protected HttpChannelOverHTTP2 newHttpChannel(boolean push)
{
return new HttpChannelOverHTTP2(getHttpDestination(), this, getSession())
return new HttpChannelOverHTTP2(getHttpDestination(), this, getSession(), push)
{
@Override
public void setStream(Stream stream)

View File

@ -0,0 +1,213 @@
//
// ========================================================================
// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.http2.client.http;
import java.io.IOException;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.client.HttpRequest;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.BufferingResponseListener;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http2.api.Stream;
import org.eclipse.jetty.http2.api.server.ServerSessionListener;
import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.frames.PushPromiseFrame;
import org.eclipse.jetty.http2.frames.ResetFrame;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Promise;
import org.junit.Assert;
import org.junit.Test;
public class PushedResourcesTest extends AbstractTest
{
@Test
public void testPushedResourceCancelled() throws Exception
{
String pushPath = "/secondary";
CountDownLatch latch = new CountDownLatch(1);
start(new ServerSessionListener.Adapter()
{
@Override
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
{
HttpURI pushURI = new HttpURI("http://localhost:" + connector.getLocalPort() + pushPath);
MetaData.Request pushRequest = new MetaData.Request(HttpMethod.GET.asString(), pushURI, HttpVersion.HTTP_2, new HttpFields());
stream.push(new PushPromiseFrame(stream.getId(), 0, pushRequest), new Promise.Adapter<Stream>()
{
@Override
public void succeeded(Stream pushStream)
{
// Just send the normal response and wait for the reset.
MetaData.Response response = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, new HttpFields());
stream.headers(new HeadersFrame(stream.getId(), response, null, true), Callback.NOOP);
}
}, new Stream.Listener.Adapter()
{
@Override
public void onReset(Stream stream, ResetFrame frame)
{
latch.countDown();
}
});
return null;
}
});
HttpRequest request = (HttpRequest)client.newRequest("localhost", connector.getLocalPort());
ContentResponse response = request
.pushListener((mainRequest, pushedRequest) -> null)
.timeout(5, TimeUnit.SECONDS)
.send();
Assert.assertEquals(HttpStatus.OK_200, response.getStatus());
Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));
}
@Test
public void testPushedResources() throws Exception
{
Random random = new Random();
byte[] bytes = new byte[512];
random.nextBytes(bytes);
byte[] pushBytes1 = new byte[1024];
random.nextBytes(pushBytes1);
byte[] pushBytes2 = new byte[2048];
random.nextBytes(pushBytes2);
String path1 = "/secondary1";
String path2 = "/secondary2";
start(new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
baseRequest.setHandled(true);
if (target.equals(path1))
{
response.getOutputStream().write(pushBytes1);
}
else if (target.equals(path2))
{
response.getOutputStream().write(pushBytes2);
}
else
{
baseRequest.getPushBuilder()
.path(path1)
.push();
baseRequest.getPushBuilder()
.path(path2)
.push();
response.getOutputStream().write(bytes);
}
}
});
CountDownLatch latch1 = new CountDownLatch(1);
CountDownLatch latch2 = new CountDownLatch(1);
HttpRequest request = (HttpRequest)client.newRequest("localhost", connector.getLocalPort());
ContentResponse response = request
.pushListener((mainRequest, pushedRequest) -> new BufferingResponseListener()
{
@Override
public void onComplete(Result result)
{
Assert.assertTrue(result.isSucceeded());
if (pushedRequest.getPath().equals(path1))
{
Assert.assertArrayEquals(pushBytes1, getContent());
latch1.countDown();
}
else if (pushedRequest.getPath().equals(path2))
{
Assert.assertArrayEquals(pushBytes2, getContent());
latch2.countDown();
}
}
})
.timeout(5, TimeUnit.SECONDS)
.send();
Assert.assertEquals(HttpStatus.OK_200, response.getStatus());
Assert.assertArrayEquals(bytes, response.getContent());
Assert.assertTrue(latch1.await(5, TimeUnit.SECONDS));
Assert.assertTrue(latch2.await(5, TimeUnit.SECONDS));
}
@Test
public void testPushedResourceRedirect() throws Exception
{
Random random = new Random();
byte[] pushBytes = new byte[512];
random.nextBytes(pushBytes);
String oldPath = "/old";
String newPath = "/new";
start(new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
baseRequest.setHandled(true);
if (target.equals(oldPath))
response.sendRedirect(newPath);
else if (target.equals(newPath))
response.getOutputStream().write(pushBytes);
else
baseRequest.getPushBuilder().path(oldPath).push();
}
});
CountDownLatch latch = new CountDownLatch(1);
HttpRequest request = (HttpRequest)client.newRequest("localhost", connector.getLocalPort());
ContentResponse response = request
.pushListener((mainRequest, pushedRequest) -> new BufferingResponseListener()
{
@Override
public void onComplete(Result result)
{
Assert.assertTrue(result.isSucceeded());
Assert.assertEquals(oldPath, pushedRequest.getPath());
Assert.assertEquals(newPath, result.getRequest().getPath());
Assert.assertArrayEquals(pushBytes, getContent());
latch.countDown();
}
})
.timeout(5, TimeUnit.SECONDS)
.send();
Assert.assertEquals(HttpStatus.OK_200, response.getStatus());
Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));
}
}

View File

@ -111,7 +111,6 @@ public class HttpChannelState
{
_interested = interest;
}
private boolean isInterested() { return _interested;}
}

View File

@ -24,6 +24,7 @@ import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Date;
@ -176,20 +177,43 @@ public class RolloverFileOutputStream extends FilterOutputStream
_rollTask=new RollTask();
midnight = ZonedDateTime.now().toLocalDate().atStartOfDay(zone.toZoneId());
midnight = toMidnight(ZonedDateTime.now(), zone.toZoneId());
scheduleNextRollover();
}
}
private void scheduleNextRollover()
/**
* Get the "start of day" for the provided DateTime at the zone specified.
*
* @param dateTime the date time to calculate from
* @param zone the zone to return the date in
* @return start of the day of the date provided
*/
public static ZonedDateTime toMidnight(ZonedDateTime dateTime, ZoneId zone)
{
return dateTime.toLocalDate().atStartOfDay(zone);
}
/**
* Get the next "start of day" for the provided date.
*
* @param dateTime the date to calculate from
* @return the start of the next day
*/
public static ZonedDateTime nextMidnight(ZonedDateTime dateTime)
{
// Increment to next day.
// Using Calendar.add(DAY, 1) takes in account Daylights Savings
// differences, and still maintains the "midnight" settings for
// Hour, Minute, Second, Milliseconds
midnight = midnight.toLocalDate().plus(1, ChronoUnit.DAYS).atStartOfDay(midnight.getZone());
__rollover.schedule(_rollTask,midnight.toInstant().toEpochMilli());
return dateTime.toLocalDate().plus(1, ChronoUnit.DAYS).atStartOfDay(dateTime.getZone());
}
private void scheduleNextRollover()
{
midnight = nextMidnight(midnight);
__rollover.schedule(_rollTask,midnight.toInstant().toEpochMilli() - System.currentTimeMillis());
}
/* ------------------------------------------------------------ */

View File

@ -0,0 +1,147 @@
//
// ========================================================================
// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.util;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.TimeZone;
import org.junit.Test;
public class RolloverFileOutputStreamTest
{
private static ZoneId toZoneId(String timezoneId)
{
ZoneId zone = TimeZone.getTimeZone(timezoneId).toZoneId();
// System.out.printf(".toZoneId(\"%s\") = [id=%s,normalized=%s]%n", timezoneId, zone.getId(), zone.normalized());
return zone;
}
private static ZonedDateTime toDateTime(String timendate, ZoneId zone)
{
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd-hh:mm:ss.S a z")
.withZone(zone);
return ZonedDateTime.parse(timendate, formatter);
}
private static String toString(TemporalAccessor date)
{
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd-hh:mm:ss.S a z");
return formatter.format(date);
}
private void assertSequence(ZonedDateTime midnight, Object[][] expected)
{
ZonedDateTime nextEvent = midnight;
for (int i = 0; i < expected.length; i++)
{
long lastMs = nextEvent.toInstant().toEpochMilli();
nextEvent = RolloverFileOutputStream.nextMidnight(nextEvent);
assertThat("Next Event", toString(nextEvent), is(expected[i][0]));
long duration = (nextEvent.toInstant().toEpochMilli() - lastMs);
assertThat("Duration to next event", duration, is((long) expected[i][1]));
}
}
@Test
public void testMidnightRolloverCalc_PST_DST_Start()
{
ZoneId zone = toZoneId("PST");
ZonedDateTime initialDate = toDateTime("2016.03.11-01:23:45.0 PM PST", zone);
ZonedDateTime midnight = RolloverFileOutputStream.toMidnight(initialDate, zone);
assertThat("Midnight", toString(midnight), is("2016.03.11-12:00:00.0 AM PST"));
Object expected[][] = {
{"2016.03.12-12:00:00.0 AM PST", 86_400_000L},
{"2016.03.13-12:00:00.0 AM PST", 86_400_000L},
{"2016.03.14-12:00:00.0 AM PDT", 82_800_000L}, // the short day
{"2016.03.15-12:00:00.0 AM PDT", 86_400_000L},
{"2016.03.16-12:00:00.0 AM PDT", 86_400_000L},
};
assertSequence(midnight, expected);
}
@Test
public void testMidnightRolloverCalc_PST_DST_End()
{
ZoneId zone = toZoneId("PST");
ZonedDateTime initialDate = toDateTime("2016.11.04-11:22:33.0 AM PDT", zone);
ZonedDateTime midnight = RolloverFileOutputStream.toMidnight(initialDate, zone);
assertThat("Midnight", toString(midnight), is("2016.11.04-12:00:00.0 AM PDT"));
Object expected[][] = {
{"2016.11.05-12:00:00.0 AM PDT", 86_400_000L},
{"2016.11.06-12:00:00.0 AM PDT", 86_400_000L},
{"2016.11.07-12:00:00.0 AM PST", 90_000_000L}, // the long day
{"2016.11.08-12:00:00.0 AM PST", 86_400_000L},
{"2016.11.09-12:00:00.0 AM PST", 86_400_000L},
};
assertSequence(midnight, expected);
}
@Test
public void testMidnightRolloverCalc_Sydney_DST_Start()
{
ZoneId zone = toZoneId("Australia/Sydney");
ZonedDateTime initialDate = toDateTime("2016.10.01-01:23:45.0 PM AEST", zone);
ZonedDateTime midnight = RolloverFileOutputStream.toMidnight(initialDate, zone);
assertThat("Midnight", toString(midnight), is("2016.10.01-12:00:00.0 AM AEST"));
Object expected[][] = {
{"2016.10.02-12:00:00.0 AM AEST", 86_400_000L},
{"2016.10.03-12:00:00.0 AM AEDT", 82_800_000L}, // the short day
{"2016.10.04-12:00:00.0 AM AEDT", 86_400_000L},
{"2016.10.05-12:00:00.0 AM AEDT", 86_400_000L},
{"2016.10.06-12:00:00.0 AM AEDT", 86_400_000L},
};
assertSequence(midnight, expected);
}
@Test
public void testMidnightRolloverCalc_Sydney_DST_End()
{
ZoneId zone = toZoneId("Australia/Sydney");
ZonedDateTime initialDate = toDateTime("2016.04.02-11:22:33.0 AM AEDT", zone);
ZonedDateTime midnight = RolloverFileOutputStream.toMidnight(initialDate, zone);
assertThat("Midnight", toString(midnight), is("2016.04.02-12:00:00.0 AM AEDT"));
Object expected[][] = {
{"2016.04.03-12:00:00.0 AM AEDT", 86_400_000L},
{"2016.04.04-12:00:00.0 AM AEST", 90_000_000L}, // The long day
{"2016.04.05-12:00:00.0 AM AEST", 86_400_000L},
{"2016.04.06-12:00:00.0 AM AEST", 86_400_000L},
{"2016.04.07-12:00:00.0 AM AEST", 86_400_000L},
};
assertSequence(midnight, expected);
}
}

View File

@ -21,14 +21,15 @@ package org.eclipse.jetty.websocket.client;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
@ -40,6 +41,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP;
@ -72,9 +74,7 @@ import org.eclipse.jetty.websocket.common.test.IncomingFramesCapture;
import org.eclipse.jetty.websocket.common.test.RawFrameBuilder;
import org.hamcrest.Matcher;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
@ -91,14 +91,15 @@ public class ClientCloseTest
public CountDownLatch closeLatch = new CountDownLatch(1);
public AtomicInteger closeCount = new AtomicInteger(0);
public CountDownLatch openLatch = new CountDownLatch(1);
public CountDownLatch errorLatch = new CountDownLatch(1);
public EventQueue<String> messageQueue = new EventQueue<>();
public EventQueue<Throwable> errorQueue = new EventQueue<>();
public AtomicReference<Throwable> error = new AtomicReference<>();
public void assertNoCloseEvent()
{
Assert.assertThat("Client Close Event",closeLatch.getCount(),is(1L));
Assert.assertThat("Client Close Event Status Code ",closeCode,is(-1));
assertThat("Client Close Event",closeLatch.getCount(),is(1L));
assertThat("Client Close Event Status Code ",closeCode,is(-1));
}
public void assertReceivedCloseEvent(int clientTimeoutMs, Matcher<Integer> statusCodeMatcher, Matcher<String> reasonMatcher)
@ -106,39 +107,22 @@ public class ClientCloseTest
{
long maxTimeout = clientTimeoutMs * 2;
Assert.assertThat("Client Close Event Occurred",closeLatch.await(maxTimeout,TimeUnit.MILLISECONDS),is(true));
Assert.assertThat("Client Close Event Count",closeCount.get(),is(1));
Assert.assertThat("Client Close Event Status Code",closeCode,statusCodeMatcher);
assertThat("Client Close Event Occurred",closeLatch.await(maxTimeout,TimeUnit.MILLISECONDS),is(true));
assertThat("Client Close Event Count",closeCount.get(),is(1));
assertThat("Client Close Event Status Code",closeCode,statusCodeMatcher);
if (reasonMatcher == null)
{
Assert.assertThat("Client Close Event Reason",closeReason,nullValue());
assertThat("Client Close Event Reason",closeReason,nullValue());
}
else
{
Assert.assertThat("Client Close Event Reason",closeReason,reasonMatcher);
}
}
public void assertReceivedError(Class<? extends Throwable> expectedThrownClass, Matcher<String> messageMatcher) throws TimeoutException,
InterruptedException
{
errorQueue.awaitEventCount(1,30,TimeUnit.SECONDS);
Throwable actual = errorQueue.poll();
Assert.assertThat("Client Error Event",actual,instanceOf(expectedThrownClass));
if (messageMatcher == null)
{
Assert.assertThat("Client Error Event Message",actual.getMessage(),nullValue());
}
else
{
Assert.assertThat("Client Error Event Message",actual.getMessage(),messageMatcher);
assertThat("Client Close Event Reason",closeReason,reasonMatcher);
}
}
public void clearQueues()
{
messageQueue.clear();
errorQueue.clear();
}
@Override
@ -164,7 +148,8 @@ public class ClientCloseTest
public void onWebSocketError(Throwable cause)
{
LOG.debug("onWebSocketError",cause);
Assert.assertThat("Error capture",errorQueue.offer(cause),is(true));
assertThat("Unique Error Event", error.compareAndSet(null, cause), is(true));
errorLatch.countDown();
}
@Override
@ -177,15 +162,15 @@ public class ClientCloseTest
public EndPoint getEndPoint() throws Exception
{
Session session = getSession();
Assert.assertThat("Session type",session,instanceOf(WebSocketSession.class));
assertThat("Session type",session,instanceOf(WebSocketSession.class));
WebSocketSession wssession = (WebSocketSession)session;
Field fld = wssession.getClass().getDeclaredField("connection");
fld.setAccessible(true);
Assert.assertThat("Field: connection",fld,notNullValue());
assertThat("Field: connection",fld,notNullValue());
Object val = fld.get(wssession);
Assert.assertThat("Connection type",val,instanceOf(AbstractWebSocketConnection.class));
assertThat("Connection type",val,instanceOf(AbstractWebSocketConnection.class));
@SuppressWarnings("resource")
AbstractWebSocketConnection wsconn = (AbstractWebSocketConnection)val;
return wsconn.getEndPoint();
@ -204,7 +189,7 @@ public class ClientCloseTest
clientFuture.get(30,TimeUnit.SECONDS);
// Wait for client connect via client websocket
Assert.assertThat("Client WebSocket is Open",clientSocket.openLatch.await(30,TimeUnit.SECONDS),is(true));
assertThat("Client WebSocket is Open",clientSocket.openLatch.await(30,TimeUnit.SECONDS),is(true));
try
{
@ -220,8 +205,8 @@ public class ClientCloseTest
serverCapture.assertNoErrors();
serverCapture.assertFrameCount(1);
WebSocketFrame frame = serverCapture.getFrames().poll();
Assert.assertThat("Server received frame",frame.getOpCode(),is(OpCode.TEXT));
Assert.assertThat("Server received frame payload",frame.getPayloadAsUTF8(),is(echoMsg));
assertThat("Server received frame",frame.getOpCode(),is(OpCode.TEXT));
assertThat("Server received frame payload",frame.getPayloadAsUTF8(),is(echoMsg));
// Server send echo reply
serverConns.write(new TextFrame().setPayload(echoMsg));
@ -231,10 +216,10 @@ public class ClientCloseTest
// Verify received message
String recvMsg = clientSocket.messageQueue.poll();
Assert.assertThat("Received message",recvMsg,is(echoMsg));
assertThat("Received message",recvMsg,is(echoMsg));
// Verify that there are no errors
Assert.assertThat("Error events",clientSocket.errorQueue,empty());
assertThat("Error events",clientSocket.error.get(),nullValue());
}
finally
{
@ -250,16 +235,16 @@ public class ClientCloseTest
serverCapture.assertFrameCount(1);
serverCapture.assertHasFrame(OpCode.CLOSE,1);
WebSocketFrame frame = serverCapture.getFrames().poll();
Assert.assertThat("Server received close frame",frame.getOpCode(),is(OpCode.CLOSE));
assertThat("Server received close frame",frame.getOpCode(),is(OpCode.CLOSE));
CloseInfo closeInfo = new CloseInfo(frame);
Assert.assertThat("Server received close code",closeInfo.getStatusCode(),is(expectedCloseCode));
assertThat("Server received close code",closeInfo.getStatusCode(),is(expectedCloseCode));
if (closeReasonMatcher == null)
{
Assert.assertThat("Server received close reason",closeInfo.getReason(),nullValue());
assertThat("Server received close reason",closeInfo.getReason(),nullValue());
}
else
{
Assert.assertThat("Server received close reason",closeInfo.getReason(),closeReasonMatcher);
assertThat("Server received close reason",closeInfo.getReason(),closeReasonMatcher);
}
}
@ -364,12 +349,12 @@ public class ClientCloseTest
// Verify received messages
String recvMsg = clientSocket.messageQueue.poll();
Assert.assertThat("Received message 1",recvMsg,is("Hello"));
assertThat("Received message 1",recvMsg,is("Hello"));
recvMsg = clientSocket.messageQueue.poll();
Assert.assertThat("Received message 2",recvMsg,is("World"));
assertThat("Received message 2",recvMsg,is("World"));
// Verify that there are no errors
Assert.assertThat("Error events",clientSocket.errorQueue,empty());
assertThat("Error events",clientSocket.error.get(),nullValue());
// client close event on ws-endpoint
clientSocket.assertReceivedCloseEvent(timeout,is(StatusCode.NORMAL),containsString("From Server"));
@ -399,7 +384,7 @@ public class ClientCloseTest
// when write is congested, client enqueue close frame
// client initiate write, but write never completes
EndPoint endp = clientSocket.getEndPoint();
Assert.assertThat("EndPoint is testable",endp,instanceOf(TestEndPoint.class));
assertThat("EndPoint is testable",endp,instanceOf(TestEndPoint.class));
TestEndPoint testendp = (TestEndPoint)endp;
char msg[] = new char[10240];
@ -418,7 +403,7 @@ public class ClientCloseTest
LOG.debug("Wrote {} frames totalling {} bytes of payload before congestion kicked in",writeCount,writeSize);
// Verify that there are no errors
Assert.assertThat("Error events",clientSocket.errorQueue,empty());
assertThat("Error events",clientSocket.error.get(),nullValue());
// client idle timeout triggers close event on client ws-endpoint
// client close event on ws-endpoint
@ -462,7 +447,9 @@ public class ClientCloseTest
serverConn.write(bad);
// client should have noticed the error
clientSocket.assertReceivedError(ProtocolException.class,containsString("Invalid control frame"));
assertThat("OnError Latch", clientSocket.errorLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat("OnError", clientSocket.error.get(), instanceOf(ProtocolException.class));
assertThat("OnError", clientSocket.error.get().getMessage(), containsString("Invalid control frame"));
// client parse invalid frame, notifies server of close (protocol error)
confirmServerReceivedCloseFrame(serverConn,StatusCode.PROTOCOL,allOf(containsString("Invalid control frame"),containsString("length")));
@ -512,8 +499,6 @@ public class ClientCloseTest
}
@Test
// TODO work out why this test is failing
@Ignore
public void testServerNoCloseHandshake() throws Exception
{
// Set client timeout
@ -545,7 +530,9 @@ public class ClientCloseTest
// server sits idle
// client idle timeout triggers close event on client ws-endpoint
clientSocket.assertReceivedCloseEvent(timeout,is(StatusCode.SHUTDOWN),containsString("Timeout"));
assertThat("OnError Latch", clientSocket.errorLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat("OnError", clientSocket.error.get(), instanceOf(SocketTimeoutException.class));
assertThat("OnError", clientSocket.error.get().getMessage(), containsString("Timeout on Read"));
}
@Test(timeout = 5000L)
@ -617,8 +604,9 @@ public class ClientCloseTest
// client write failure
final String origCloseReason = "Normal Close";
clientSocket.getSession().close(StatusCode.NORMAL,origCloseReason);
clientSocket.assertReceivedError(EofException.class,null);
assertThat("OnError Latch", clientSocket.errorLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat("OnError", clientSocket.error.get(), instanceOf(EofException.class));
// client triggers close event on client ws-endpoint
// assert - close code==1006 (abnormal)

View File

@ -1,58 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.common.io;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.websocket.api.WriteCallback;
/**
* Wraps the exposed {@link WriteCallback} WebSocket API with a Jetty {@link Callback}.
* <p>
* We don't expose the jetty {@link Callback} object to the webapp, as that makes things complicated for the WebAppContext's Classloader.
*/
public class WriteCallbackWrapper implements Callback
{
public static Callback wrap(WriteCallback callback)
{
if (callback == null)
{
return null;
}
return new WriteCallbackWrapper(callback);
}
private final WriteCallback callback;
public WriteCallbackWrapper(WriteCallback callback)
{
this.callback = callback;
}
@Override
public void failed(Throwable x)
{
callback.writeFailed(x);
}
@Override
public void succeeded()
{
callback.writeSuccess();
}
}

View File

@ -1,81 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.websocket.common.io;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.websocket.api.WriteCallback;
/**
* Tracking Callback for testing how the callbacks are used.
*/
public class TrackingCallback implements Callback, WriteCallback
{
private AtomicInteger called = new AtomicInteger();
private boolean success = false;
private Throwable failure = null;
@Override
public void failed(Throwable x)
{
this.called.incrementAndGet();
this.success = false;
this.failure = x;
}
@Override
public void succeeded()
{
this.called.incrementAndGet();
this.success = false;
}
public Throwable getFailure()
{
return failure;
}
public boolean isSuccess()
{
return success;
}
public boolean isCalled()
{
return called.get() >= 1;
}
public int getCallCount()
{
return called.get();
}
@Override
public void writeFailed(Throwable x)
{
failed(x);
}
@Override
public void writeSuccess()
{
succeeded();
}
}

View File

@ -146,9 +146,9 @@ public class HttpChannelAssociationTest extends AbstractTest
return new HttpConnectionOverHTTP2(destination, session)
{
@Override
protected HttpChannelOverHTTP2 newHttpChannel()
protected HttpChannelOverHTTP2 newHttpChannel(boolean push)
{
return new HttpChannelOverHTTP2(getHttpDestination(), this, getSession())
return new HttpChannelOverHTTP2(getHttpDestination(), this, getSession(), push)
{
@Override
public boolean associate(HttpExchange exchange)