package com.mastfrog.netty.http.test.harness;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Sets;
import com.google.common.net.MediaType;
import com.google.inject.Binder;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Singleton;
import com.mastfrog.acteur.util.ErrorInterceptor;
import com.mastfrog.acteur.headers.HeaderValueType;
import com.mastfrog.acteur.headers.Headers;
import com.mastfrog.acteur.headers.Method;
import com.mastfrog.acteur.util.Server;
import com.mastfrog.acteur.util.ServerControl;
import com.mastfrog.giulius.ShutdownHookRegistry;
import com.mastfrog.netty.http.client.CookieStore;
import com.mastfrog.netty.http.client.HttpClient;
import com.mastfrog.netty.http.client.HttpRequestBuilder;
import com.mastfrog.netty.http.client.ResponseFuture;
import com.mastfrog.netty.http.client.ResponseHandler;
import com.mastfrog.netty.http.client.State;
import com.mastfrog.netty.http.client.StateType;
import static com.mastfrog.netty.http.client.StateType.Closed;
import static com.mastfrog.netty.http.client.StateType.FullContentReceived;
import static com.mastfrog.netty.http.client.StateType.HeadersReceived;
import com.mastfrog.settings.Settings;
import com.mastfrog.url.Protocol;
import com.mastfrog.url.URL;
import com.mastfrog.util.thread.Receiver;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufInputStream;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Array;
import java.net.DatagramSocket;
import java.net.ServerSocket;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.joda.time.Duration;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import com.mastfrog.util.Exceptions;
import io.netty.handler.codec.http.Cookie;
import java.util.Date;
import java.util.EnumMap;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import static org.junit.Assert.fail;
/**
* A general purpose test harness for Web applications. Note: Your module should
* <pre>
* bind(ErrorHandler.class).to(TestHarness.class);
* </pre> to ensure that server-side exceptions are thrown when you call
* <code>CallHandler.throwIfError()</code>
*
* @author Tim Boudreau
*/
@Singleton
public class TestHarness implements ErrorInterceptor {
private final Server server;
private final HttpClient client;
private final int port;
@Inject
public TestHarness(Server server, Settings settings, ShutdownHookRegistry reg, HttpClient client) throws IOException {
this.server = server;
port = settings.getInt("testPort", findPort());
this.client = client;
if (reg != null) {
reg.add(new Shutdown());
}
}
/**
* Constructor for manual construction
*
* @param port The port
* @param client An http client
*/
public TestHarness(int port, HttpClient client) {
this.server = new Fake(port);
this.port = port;
this.client = client;
}
public Server getServer() {
return server;
}
private int findPort() {
Random r = new Random(System.currentTimeMillis());
int port;
do {
port = r.nextInt(4000) + 4000;
} while (!available(port));
return port;
}
private boolean available(int port) {
ServerSocket ss = null;
DatagramSocket ds = null;
try {
ss = new ServerSocket(port);
ss.setReuseAddress(true);
ds = new DatagramSocket(port);
ds.setReuseAddress(true);
return true;
} catch (IOException e) {
} finally {
if (ds != null) {
ds.close();
}
if (ss != null) {
try {
ss.close();
} catch (IOException e) {
/* should not be thrown */
}
}
}
return false;
}
public int getPort() {
return port;
}
private static Throwable err;
@Override
public void onError(Throwable err) {
this.err = err;
}
public static void throwIfError() throws Throwable {
Throwable old = err;
err = null;
if (old != null) {
throw old;
}
}
private class Shutdown implements Runnable {
@Override
public void run() {
client.shutdown();
try {
if (serverStart != null) {
serverStart.shutdown(true);
}
} catch (Exception ex) {
Exceptions.printStackTrace(ex);
}
}
}
public TestRequestBuilder get(String... pathElements) {
return request(Method.GET, pathElements);
}
public TestRequestBuilder put(String... pathElements) {
return request(Method.PUT, pathElements);
}
public TestRequestBuilder post(String... pathElements) {
return request(Method.POST, pathElements);
}
public TestRequestBuilder delete(String... pathElements) {
return request(Method.DELETE, pathElements);
}
public TestRequestBuilder options(String... pathElements) {
return request(Method.OPTIONS, pathElements);
}
public TestRequestBuilder head(String... pathElements) {
return request(Method.HEAD, pathElements);
}
public TestRequestBuilder trace(String... pathElements) {
return request(Method.TRACE, pathElements);
}
private volatile ServerControl serverStart;
public TestRequestBuilder request(Method m, String... pathElements) {
if (serverStart == null) {
synchronized (this) {
if (serverStart == null) {
try {
serverStart = server.start(port);
} catch (IOException ex) {
Exceptions.chuck(ex);
}
}
}
}
TestRequestBuilder result = new TestRequestBuilder(client.request(m).setHost("localhost").setPort(server.getPort()));
for (String el : pathElements) {
String[] parts = el.split("/");
for (String part : parts) {
if (part.isEmpty()) {
continue;
}
result.addPathElement(part);
}
}
return result;
}
public static class TestRequestBuilder implements HttpRequestBuilder {
private final HttpRequestBuilder bldr;
private Duration timeout = Duration.standardSeconds(10);
TestRequestBuilder(HttpRequestBuilder bldr) {
this.bldr = bldr;
}
public TestRequestBuilder setTimeout(Duration dur) {
assertNotNull(dur);
this.timeout = dur;
return this;
}
public TestRequestBuilder setCookieStore(CookieStore store) {
bldr.setCookieStore(store);
return this;
}
@Override
public <T> TestRequestBuilder addHeader(HeaderValueType<T> type, T value) {
bldr.addHeader(type, value);
return this;
}
@Override
public TestRequestBuilder addPathElement(String element) {
bldr.addPathElement(element);
return this;
}
@Override
public TestRequestBuilder addQueryPair(String key, String value) {
bldr.addQueryPair(key, value);
return this;
}
@Override
public TestRequestBuilder setAnchor(String anchor) {
bldr.setAnchor(anchor);
return this;
}
@Override
public TestRequestBuilder setHost(String host) {
bldr.setHost(host);
return this;
}
@Override
public TestRequestBuilder setPath(String path) {
bldr.setPath(path);
return this;
}
@Override
public TestRequestBuilder setPort(int port) {
bldr.setPort(port);
return this;
}
@Override
public TestRequestBuilder setProtocol(Protocol protocol) {
bldr.setProtocol(protocol);
return this;
}
@Override
public TestRequestBuilder setURL(URL url) {
bldr.setURL(url);
return this;
}
@Override
public TestRequestBuilder setURL(String url) {
bldr.setURL(url);
return this;
}
@Override
public TestRequestBuilder setUserName(String userName) {
bldr.setUserName(userName);
return this;
}
@Override
public TestRequestBuilder setPassword(String password) {
bldr.setPassword(password);
return this;
}
@Override
public TestRequestBuilder basicAuthentication(String username, String password) {
bldr.basicAuthentication(username, password);
return this;
}
private ResponseFuture future;
@Override
public ResponseFuture execute(ResponseHandler<?> response) {
return future = bldr.execute(response);
}
@Override
public ResponseFuture execute() {
return future = bldr.execute();
}
@Override
public TestRequestBuilder setBody(Object o, MediaType contentType) throws IOException {
bldr.setBody(o, contentType);
return this;
}
@Override
public <T> TestRequestBuilder on(Class<? extends State<T>> event, Receiver<T> r) {
bldr.on(event, r);
return this;
}
@Override
public <T> TestRequestBuilder on(StateType event, Receiver<T> r) {
bldr.on(event, r);
return this;
}
@Override
public TestRequestBuilder onEvent(Receiver<State<?>> r) {
bldr.onEvent(r);
return this;
}
@Override
public URL toURL() {
return bldr.toURL();
}
private boolean log;
public TestRequestBuilder log() {
this.log = true;
return this;
}
public CallResult go() {
CallResultImpl impl = new CallResultImpl(toURL(), timeout, log);
onEvent(impl);
impl.future = execute();
return impl;
}
@Override
public HttpRequestBuilder noHostHeader() {
bldr.noHostHeader();
return this;
}
@Override
public HttpRequestBuilder noConnectionHeader() {
bldr.noConnectionHeader();
return this;
}
@Override
public HttpRequestBuilder noDateHeader() {
bldr.noDateHeader();
return this;
}
}
private static final class CallResultImpl extends Receiver<State<?>> implements CallResult, Runnable {
private final URL url;
private final Set<StateType> states = Sets.newCopyOnWriteArraySet();
private final AtomicReference<HttpResponseStatus> status = new AtomicReference<>();
private final AtomicReference<HttpHeaders> headers = new AtomicReference<>();
private final AtomicReference<ByteBuf> content = new AtomicReference<>();
private volatile ResponseFuture future;
private Throwable err;
private final Map<StateType, CountDownLatch> latches = Collections.synchronizedMap(new EnumMap<StateType, CountDownLatch>(StateType.class));
private final Duration timeout;
private CallResultImpl(URL toURL, Duration timeout, boolean log) {
this.log = log;
this.url = toURL;
for (StateType type : StateType.values()) {
latches.put(type, new NamedLatch(type.name()));
}
this.timeout = timeout;
}
private String headersToString(HttpHeaders hdrs) {
if (headers == null) {
return "[null]";
}
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> e : hdrs.entries()) {
sb.append(e.getKey()).append(':').append(' ').append(e.getValue()).append('\n');
}
return sb.toString();
}
public void cancel() {
assertNotNull("Http call never made", future);
future.cancel();
}
public StateType state() {
return future == null ? null : future.lastState();
}
private final Thread mainThread = Thread.currentThread();
public void run() {
try {
Thread.sleep(timeout.getMillis());
if (!states.contains(StateType.Closed)) {
System.out.println("Cancelling request for timeout "
+ timeout + " " + url.getPathAndQuery());
if (future != null) {
future.cancel();
}
mainThread.interrupt();
}
} catch (InterruptedException ex) {
Exceptions.printStackTrace(ex);
}
}
private boolean log;
public void log() {
log = true;
}
@Override
public void receive(State<?> state) {
if (log) {
System.out.println(url.getPathAndQuery() + " - " + state.name() + " - " + state.get());
}
states.add(state.stateType());
latches.get(state.stateType()).countDown();
boolean updateState = true;
switch (state.stateType()) {
case Connected:
Thread t = new Thread(this);
t.setDaemon(true);
t.setName("Timeout thread for " + url.getPathAndQuery());
t.setPriority(Thread.NORM_PRIORITY - 1);
t.start();
break;
case SendRequest:
State.SendRequest sr = (State.SendRequest) state;
if (log) {
System.out.println("SENT REQUEST " + headersToString(sr.get().headers()));
}
break;
case Closed:
for (CountDownLatch latch : latches.values()) {
latch.countDown();
}
break;
case Finished:
case HeadersReceived:
HttpResponse hr = (HttpResponse) state.get();
HttpResponseStatus st = hr.getStatus();
if (HttpResponseStatus.CONTINUE.equals(st)) {
updateState = false;
}
setStatus(st);
setHeaders(hr.headers());
break;
case FullContentReceived:
State.FullContentReceived full = (State.FullContentReceived) state;
setContent(full.get());
break;
case Error:
this.err = (Throwable) state.get();
this.err.printStackTrace();
break;
}
if (updateState) {
latches.get(state.stateType()).countDown();
}
}
void await(StateType state) throws InterruptedException {
await(latches.get(state));
}
void await(CountDownLatch latch) throws InterruptedException {
if (log) {
System.out.println("WAIT ON " + latch);
}
latch.await(timeout.getMillis(), TimeUnit.MILLISECONDS);
}
@Override
public CallResult assertStateSeen(StateType type) throws InterruptedException {
await(latches.get(type));
assertTrue(type + " not in " + states, states.contains(type));
return this;
}
@Override
public CallResult assertCode(int code) throws Throwable {
await(HeadersReceived);
// if (code != 100 && HttpResponseStatus.CONTINUE.equals(getStatus())) {
await(Closed);
// }
assertNotNull("Status is null, not " + code, getStatus());
assertEquals(code, getStatus().code());
return this;
}
private String contentAsString() throws UnsupportedEncodingException {
ByteBuf buf = getContent();
assertNotNull(buf);
if (!buf.isReadable()) {
return null;
}
buf.resetReaderIndex();
byte[] b = new byte[getContent().readableBytes()];
buf.readBytes(b);
buf.resetReaderIndex();
return new String(b, "UTF-8");
}
public String content() throws UnsupportedEncodingException, InterruptedException {
await(FullContentReceived);
return contentAsString();
}
private ByteBuf getContent() {
return content.get();
}
@Override
public CallResult assertContentContains(String expected) throws Throwable {
await(FullContentReceived);
String s = contentAsString();
assertNotNull("Content buffer not readable", s);
assertFalse("0 bytes content", s.isEmpty());
assertTrue("Content does not contain '" + expected + "'", s.contains(expected));
return this;
}
@Override
public CallResult assertContent(String expected) throws Throwable {
await(FullContentReceived);
await(Closed);
String s = contentAsString();
assertNotNull("Content buffer not readable", s);
assertFalse("0 bytes content", s.isEmpty());
assertEquals(expected, s);
return this;
}
@Override
public CallResult assertStatus(HttpResponseStatus status) throws Throwable {
await(HeadersReceived);
if (HttpResponseStatus.CONTINUE != status && HttpResponseStatus.CONTINUE.equals(this.getStatus()) || getStatus() == null) {
await(Closed);
}
assertNotNull("Status never sent, expected " + status, this.getStatus());
HttpResponseStatus st = this.getStatus();
if (!status.equals(st)) {
throw new AssertionError("Expected " + status + " got " + st + ": " + content());
}
return this;
}
@Override
public CallResult throwIfError() throws Throwable {
TestHarness.throwIfError();
await(latches.get(StateType.Error));
if (err != null) {
throw err;
}
if (future != null) {
future.throwIfError();
}
return this;
}
private String headersToString() {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> e : getHeaders().entries()) {
sb.append(e.getKey()).append(": ").append(e.getValue()).append("\n");
}
return sb.toString();
}
@Override
public CallResult assertHasHeader(String name) throws Throwable {
await(HeadersReceived);
assertNotNull("Headers never sent", getHeaders());
String val = getHeaders().get(name);
assertNotNull("No value for '" + name + "' in \n" + headersToString(), val);
return this;
}
public <T> CallResult assertHeader(HeaderValueType<T> hdr, T value) throws Throwable {
waitForHeaders(hdr.name());
assertNotNull("Headers never sent", getHeaders());
String val = getHeaders().get(hdr.name());
assertNotNull("No value for '" + hdr.name() + "' in \n" + headersToString(), val);
T obj = hdr.toValue(val);
assertEquals(value, obj);
return this;
}
private HttpHeaders waitForHeaders(String lookingFor) throws InterruptedException {
await(HeadersReceived);
HttpHeaders h = getHeaders();
if (h == null) {
await(Closed);
h = getHeaders();
} else {
String s = h.get(lookingFor);
if (s == null) {
await(Closed);
}
}
return h;
}
public <T> CallResult assertHeaderNotEquals(HeaderValueType<T> hdr, T value) throws Throwable {
HttpHeaders h = waitForHeaders(hdr.name());
assertNotNull("Headers never arrived", h);
T obj = hdr.toValue(h.get(hdr.name()));
assertNotEquals(value, obj);
return this;
}
public <T> Iterable<T> getHeaders(HeaderValueType<T> hdr) throws InterruptedException {
HttpHeaders h = waitForHeaders(hdr.name());
List<String> all = h.getAll(hdr.name());
List<T> result = new LinkedList<>();
if (all != null) {
for (String s : all) {
result.add(hdr.toValue(s));
}
}
return result;
}
public <T> T getHeader(HeaderValueType<T> hdr) throws InterruptedException {
HttpHeaders h = waitForHeaders(hdr.name());
assertNotNull("Headers never sent", h);
String result = h.get(hdr.name());
if (result != null) {
return hdr.toValue(result);
}
return null;
}
@Override
public CallResult await() throws Throwable {
await(Closed);
return this;
}
@Override
public <T> T content(Class<T> type) throws Throwable {
await(FullContentReceived);
assertHasContent();
ByteBuf buf = getContent();
assertTrue("Content not readable", buf.isReadable());
getContent().resetReaderIndex();
if (type == byte[].class) {
byte[] b = new byte[buf.readableBytes()];
buf.readBytes(b);
return type.cast(b);
} else if (type == ByteBuf.class) {
return type.cast(buf);
} else if (type == String.class || type == CharSequence.class) {
byte[] b = new byte[buf.readableBytes()];
buf.readBytes(b);
return type.cast(new String(b, "UTF-8"));
} else {
ObjectMapper m = new ObjectMapper();
try {
return m.readValue(new ByteBufInputStream(buf), type);
} catch (JsonParseException | JsonMappingException ex) {
buf.resetReaderIndex();
String data = bufToString(buf);
throw new IOException(ex.getMessage() + " - data: " + data, ex);
}
}
}
public <T> CallResult assertContentNotEquals(Class<T> type, T compareTo) throws Throwable {
assertHasContent();
T obj = this.content(type);
if (obj != null && compareTo != null && obj.getClass().isArray() && compareTo.getClass().isArray()) {
assertFalse("Should not be equal: " + Objects.toString(compareTo) + " and "
+ Objects.toString(obj), Objects.equals(compareTo, obj));
} else {
assertNotEquals(compareTo, obj);
}
return this;
}
private String arrayToString(Object o) {
StringBuilder sb = new StringBuilder();
int count = Array.getLength(o);
for (int i = 0; i < count; i++) {
Object elem = Array.get(o, i);
sb.append("" + elem);
if (i != count - 1) {
sb.append(",");
}
}
return sb.toString();
}
public <T> CallResult assertContent(Class<T> type, T compareTo) throws Throwable {
assertHasContent();
T obj = this.content(type);
if (obj != null && compareTo != null && obj.getClass().isArray() && compareTo.getClass().isArray()) {
assertTrue("Not equal: " + arrayToString(compareTo) + " and "
+ arrayToString(obj), Objects.deepEquals(compareTo, obj));
} else {
assertEquals(compareTo, obj);
}
return this;
}
public <T> CallResult assertHasContent() throws Throwable {
await(FullContentReceived);
if (getContent() == null) {
await(Closed);
}
assertNotNull("No content received", getContent());
return this;
}
/**
* @return the status
*/
public HttpResponseStatus getStatus() {
return status.get();
}
/**
* @param status the status to set
*/
public void setStatus(HttpResponseStatus status) {
if (status == null) {
return;
}
HttpResponseStatus st = getStatus();
if (st != null) {
if (status.code() > st.code()) {
this.status.set(status);
}
} else {
this.status.set(status);
}
}
/**
* @return the headers
*/
public HttpHeaders getHeaders() {
return headers.get();
}
/**
* @param headers the headers to set
*/
public void setHeaders(HttpHeaders headers) {
if (headers == null) {
return;
}
HttpHeaders curr = getHeaders();
if (curr != null) {
DefaultHttpHeaders hdrs = new DefaultHttpHeaders();
for (Map.Entry<String, String> e : headers) {
hdrs.add(e.getKey(), e.getValue());
}
for (Map.Entry<String, String> e : curr) {
hdrs.add(e.getKey(), e.getValue());
}
this.headers.set(hdrs);
} else {
this.headers.set(headers);
}
}
private String bufToString(ByteBuf buf) {
buf.resetReaderIndex();
if (buf.readableBytes() <= 0) {
return "[no bytes]";
}
byte[] b = new byte[buf.readableBytes()];
buf.readBytes(b);
return new String(b);
}
/**
* @param content the content to set
*/
public void setContent(ByteBuf content) {
if (content == null) {
return;
}
if (this.content.get() != null && log) {
// throw new Error("Replace content? Old: " + bufToString(this.content.get())
// + " NEW " + bufToString(content));
System.out.println("Replacing old content: " + bufToString(this.content.get()));
}
this.content.set(content);
}
public CallResult assertHasCookie(String name) throws Throwable {
for (Cookie ck : getHeaders(Headers.SET_COOKIE)) {
if (name.equals(ck.getName())) {
return this;
}
}
fail("No cookie named '" + name + "' in " + getHeaders(Headers.SET_COOKIE));
return this;
}
public CallResult assertCookieValue(String name, String val) throws Throwable {
for (Cookie ck : getHeaders(Headers.SET_COOKIE)) {
if (name.equals(ck.getName())) {
assertEquals(val, ck.getValue());
}
}
return this;
}
}
public interface CallResult {
CallResult assertCookieValue(String name, String value) throws Throwable;
CallResult assertHasCookie(String name) throws Throwable;
CallResult assertStateSeen(StateType type) throws Throwable;
CallResult assertContentContains(String expected) throws Throwable;
CallResult assertContent(String expected) throws Throwable;
CallResult assertCode(int code) throws Throwable;
CallResult assertStatus(HttpResponseStatus status) throws Throwable;
CallResult throwIfError() throws Throwable;
<T> CallResult assertHeader(HeaderValueType<T> hdr, T value) throws Throwable;
<T> CallResult assertHeaderNotEquals(HeaderValueType<T> hdr, T value) throws Throwable;
CallResult await() throws Throwable;
String content() throws UnsupportedEncodingException, InterruptedException;
<T> T content(Class<T> type) throws Throwable;
void cancel();
StateType state();
CallResult assertHasHeader(String name) throws Throwable;
<T> T getHeader(HeaderValueType<T> hdr) throws InterruptedException;
<T> CallResult assertContent(Class<T> type, T compareTo) throws Throwable;
<T> CallResult assertContentNotEquals(Class<T> type, T compareTo) throws Throwable;
<T> CallResult assertHasContent() throws Throwable;
<T> Iterable<T> getHeaders(HeaderValueType<T> hdr) throws Throwable;
}
private static class NamedLatch extends CountDownLatch {
private final String name;
public NamedLatch(String name) {
super(1);
this.name = name;
}
@Override
public void await() throws InterruptedException {
String old = Thread.currentThread().getName();
Thread.currentThread().setName("Waiting " + this + " (was " + old + ")");
try {
super.await();
} finally {
Thread.currentThread().setName(old);
}
}
@Override
public boolean await(long l, TimeUnit tu) throws InterruptedException {
String old = Thread.currentThread().getName();
Thread.currentThread().setName("Waiting " + this + " (was " + old + ")");
try {
return super.await(l, tu);
} finally {
Thread.currentThread().setName(old);
}
}
public String toString() {
return name + " (" + getCount() + ")";
}
}
private static class Fake implements Server, ServerControl {
private final int port;
Fake(int port) {
this.port = port;
}
@Override
public int getPort() {
return port;
}
@Override
public ServerControl start() throws IOException {
return this;
}
@Override
public ServerControl start(int port) throws IOException {
return this;
}
@Override
public ServerControl start(boolean ssl) throws IOException {
return this;
}
@Override
public ServerControl start(int port, boolean ssl) throws IOException {
return this;
}
@Override
public void shutdown(boolean immediately, long timeout, TimeUnit unit) throws InterruptedException {
//do nothing
}
@Override
public void shutdown(boolean immediately) throws InterruptedException {
//do nothing
}
@Override
public void await() throws InterruptedException {
synchronized(this) {
wait();
}
}
@Override
public void awaitUninterruptibly() {
synchronized(this) {
for (;;) {
try {
wait();
return;
} catch (InterruptedException ex) {
org.openide.util.Exceptions.printStackTrace(ex);
}
}
}
}
@Override
public long awaitNanos(long l) throws InterruptedException {
//do nothing
return 0L;
}
@Override
public boolean await(long l, TimeUnit tu) throws InterruptedException {
return true;
}
@Override
public boolean awaitUntil(Date date) throws InterruptedException {
return true;
}
@Override
public void signal() {
synchronized (this) {
notifyAll();
}
}
@Override
public void signalAll() {
synchronized (this) {
notifyAll();
}
}
}
}