package org.webpieces.httpclient.impl; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentLinkedQueue; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.webpieces.data.api.DataWrapper; import org.webpieces.data.api.DataWrapperGenerator; import org.webpieces.data.api.DataWrapperGeneratorFactory; import org.webpieces.httpclient.api.HttpChunkWriter; import org.webpieces.httpclient.api.HttpResponseListener; import org.webpieces.httpclient.api.HttpSocket; import org.webpieces.httpparser.api.HttpParser; import org.webpieces.httpparser.api.Memento; import org.webpieces.httpparser.api.dto.HttpChunk; import org.webpieces.httpparser.api.dto.HttpPayload; import org.webpieces.httpparser.api.dto.HttpRequest; import org.webpieces.httpparser.api.dto.HttpResponse; import org.webpieces.nio.api.ChannelManager; import org.webpieces.nio.api.channels.Channel; import org.webpieces.nio.api.channels.TCPChannel; import org.webpieces.nio.api.exceptions.NioClosedChannelException; import org.webpieces.nio.api.handlers.DataListener; import org.webpieces.nio.api.handlers.RecordingDataListener; public class HttpSocketImpl implements HttpSocket { private static final Logger log = LoggerFactory.getLogger(HttpSocketImpl.class); private static DataWrapperGenerator wrapperGen = DataWrapperGeneratorFactory.createDataWrapperGenerator(); private TCPChannel channel; private CompletableFuture<HttpSocket> connectFuture; private boolean isClosed; private boolean connected; private ConcurrentLinkedQueue<PendingRequest> pendingRequests = new ConcurrentLinkedQueue<>(); private HttpParser parser; private Memento memento; private ConcurrentLinkedQueue<HttpResponseListener> responsesToComplete = new ConcurrentLinkedQueue<>(); private DataListener dataListener = new MyDataListener(); private boolean isRecording = false; public HttpSocketImpl(TCPChannel channel, HttpParser parser) { this.channel = channel; this.parser = parser; memento = parser.prepareToParse(); } public HttpSocketImpl(ChannelManager mgr, String idForLogging, HttpParser parser2, Object object) { } @Override public CompletableFuture<HttpSocket> connect(InetSocketAddress addr) { if(isRecording ) { dataListener = new RecordingDataListener("httpSock-", dataListener); } connectFuture = channel.connect(addr, dataListener).thenApply(channel -> connected()); return connectFuture; } @Override public CompletableFuture<HttpResponse> send(HttpRequest request) { CompletableFuture<HttpResponse> future = new CompletableFuture<HttpResponse>(); HttpResponseListener l = new CompletableListener(future); send(request, l); return future; } private synchronized HttpSocket connected() { connected = true; while(!pendingRequests.isEmpty()) { PendingRequest req = pendingRequests.remove(); actuallySendRequest(req.getFuture(), req.getRequest(), req.getListener()); } return this; } @Override public CompletableFuture<HttpChunkWriter> send(HttpRequest request, HttpResponseListener listener) { if(connectFuture == null) throw new IllegalArgumentException("You must at least call httpSocket.connect first(it " + "doesn't have to complete...you just have to call it before caling send)"); CompletableFuture<HttpChunkWriter> future = new CompletableFuture<>(); boolean wasConnected = false; synchronized (this) { if(!connected) { pendingRequests.add(new PendingRequest(future, request, listener)); } else wasConnected = true; } if(wasConnected) actuallySendRequest(future, request, listener); return future; } private void actuallySendRequest(CompletableFuture<HttpChunkWriter> future, HttpRequest request, HttpResponseListener listener) { HttpResponseListener l = new CatchResponseListener(listener); ByteBuffer wrap = parser.marshalToByteBuffer(request); //put this on the queue before the write to be completed from the listener below responsesToComplete.offer(l); log.info("sending request now. req="+request.getRequestLine().getUri()); CompletableFuture<Channel> write = channel.write(wrap); write.handle((c, t) -> chainToFuture(c, t, future)); } private Void chainToFuture(Channel c, Throwable t, CompletableFuture<HttpChunkWriter> future) { if(t != null) { future.completeExceptionally(new RuntimeException(t)); return null; } HttpChunkWriterImpl impl = new HttpChunkWriterImpl(channel, parser); future.complete(impl); return null; } @Override public CompletableFuture<HttpSocket> close() { if(isClosed) { return CompletableFuture.completedFuture(this); } cleanUpPendings("You closed the socket"); CompletableFuture<Channel> future = channel.close(); return future.thenApply(chan -> { isClosed = true; return this; }); } private void cleanUpPendings(String msg) { //do we need an isClosing state and cache that future? (I don't think so but time will tell) while(!responsesToComplete.isEmpty()) { HttpResponseListener listener = responsesToComplete.poll(); if(listener != null) { listener.failure(new NioClosedChannelException(msg+" before responses were received")); } } synchronized (this) { while(!pendingRequests.isEmpty()) { PendingRequest pending = pendingRequests.poll(); pending.getListener().failure(new NioClosedChannelException(msg+" before requests were sent")); } } } private class MyDataListener implements DataListener { private boolean processingChunked = false; @Override public void incomingData(Channel channel, ByteBuffer b) { log.info("size="+b.remaining()); DataWrapper wrapper = wrapperGen.wrapByteBuffer(b); memento = parser.parse(memento, wrapper); List<HttpPayload> parsedMessages = memento.getParsedMessages(); for(HttpPayload msg : parsedMessages) { if(processingChunked) { HttpChunk chunk = (HttpChunk) msg; HttpResponseListener listener = responsesToComplete.peek(); if(chunk.isLastChunk()) { processingChunked = false; responsesToComplete.poll(); } listener.incomingChunk(chunk, chunk.isLastChunk()); } else if(!msg.isHasChunkedTransferHeader()) { HttpResponse resp = (HttpResponse) msg; HttpResponseListener listener = responsesToComplete.poll(); listener.incomingResponse(resp, true); } else { processingChunked = true; HttpResponse resp = (HttpResponse) msg; HttpResponseListener listener = responsesToComplete.peek(); listener.incomingResponse(resp, false); } } } @Override public void farEndClosed(Channel channel) { log.info("far end closed"); isClosed = true; cleanUpPendings("Remote end closed"); } @Override public void failure(Channel channel, ByteBuffer data, Exception e) { log.error("Failure on channel="+channel, e); while(!responsesToComplete.isEmpty()) { HttpResponseListener listener = responsesToComplete.poll(); if(listener != null) { listener.failure(e); } } } @Override public void applyBackPressure(Channel channel) { } @Override public void releaseBackPressure(Channel channel) { } } }