/** * */ package xapi.jre.io; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.SocketException; import java.net.URL; import java.net.URLConnection; import java.util.List; import java.util.Map.Entry; import javax.inject.Provider; import xapi.annotation.inject.InstanceDefault; import xapi.annotation.inject.SingletonDefault; import xapi.collect.X_Collect; import xapi.collect.api.StringDictionary; import xapi.collect.api.StringTo; import xapi.collect.api.StringTo.Many; import xapi.inject.impl.SingletonProvider; import xapi.io.IOConstants; import xapi.io.X_IO; import xapi.io.api.CancelledException; import xapi.io.api.IOCallback; import xapi.io.api.IOMessage; import xapi.io.api.IORequest; import xapi.io.api.LineReader; import xapi.io.api.StringReader; import xapi.io.impl.AbstractIOService; import xapi.io.service.IOService; import xapi.log.X_Log; import xapi.log.api.LogLevel; import xapi.time.X_Time; import xapi.time.api.Moment; import xapi.util.X_Runtime; import xapi.util.X_Util; import xapi.util.api.ReceivesValue; import xapi.util.impl.RunUnsafe; /** * @author James X. Nelson (james@wetheinter.net, @james) * */ @InstanceDefault(implFor=IOService.class) @SingletonDefault(implFor=IOService.class) public class IOServiceDefault extends AbstractIOService <URLConnection> { /** * @author James X. Nelson (james@wetheinter.net, @james) * */ public class IORequestDefault extends AbstractIORequest { private Thread connectionThread; @Override public void cancel() { super.cancel(); if (connectionThread != null) { connectionThread.interrupt(); connectionThread = null; synchronized (this) { notifyAll(); } } } @Override public String response() { if (!super.isStarted()) { synchronized (this) { try { wait(); } catch (final InterruptedException e) { cancel(); Thread.currentThread().interrupt(); } } } if (connectionThread != null) { synchronized (connectionThread) { try { if (connectionThread != null) { connectionThread.join(); } } catch (final InterruptedException ignored) { cancel(); Thread.currentThread().interrupt(); } connectionThread = null; } } return getValue(); } public void setConnectionThread(final Thread thread) { this.connectionThread = thread; } } @Override public IORequest<String> get(final String uri, final StringDictionary<String> headers, final IOCallback<IOMessage<String>> callback) { final String url = normalize(uri); if (callback.isCancelled()) { return cancelled; } final Moment startUp = X_Time.now(); try { final URL asUrl = new URL(url); final URLConnection connect = asUrl.openConnection(); connect.setDoInput(true); connect.setDoOutput(false); if (headers != null) { headers.forKeys(new ReceivesValue<String>() { @Override public void set(final String key) { final String value = headers.getValue(key); connect.setRequestProperty(key, value); } }); } applySettings(connect, IOConstants.METHOD_GET); final LogLevel logLevel = logLevel(); if (X_Log.loggable(logLevel)) { X_Log.log(getClass(), logLevel, "Startup time for ",url,"took",X_Time.difference(startUp)); } final IORequestDefault request = createRequest(); sendRequest(connect, request, callback, url, headers, null); return request; } catch (final Throwable e) { callback.onError(e); if (X_Runtime.isDebug()) { X_Log.warn("IO Error", e); } return cancelled; } } /** * @see xapi.io.service.IOService#post(java.lang.String, java.lang.String, xapi.collect.api.StringDictionary, xapi.io.api.IOCallback) */ @Override public IORequest<String> post(final String uri, final String body, final StringDictionary<String> headers, final IOCallback<IOMessage<String>> callback) { final String url = normalize(uri); if (callback.isCancelled()) { return cancelled; } final Moment startUp = X_Time.now(); try { final URL asUrl = new URL(url); final URLConnection connect = asUrl.openConnection(); connect.setDoInput(true); connect.setDoOutput(true); if (headers != null) { headers.forKeys(new ReceivesValue<String>() { @Override public void set(final String key) { final String value = headers.getValue(key); connect.setRequestProperty(key, value); } }); } applySettings(connect, IOConstants.METHOD_POST); final LogLevel logLevel = logLevel(); if (X_Log.loggable(logLevel)) { X_Log.log(getClass(), logLevel, "Startup time for ",url,"took",X_Time.difference(startUp)); } final IORequestDefault request = createRequest(); sendRequest(connect, request, callback, url, headers, body); return request; } catch (final Throwable e) { callback.onError(e); if (X_Runtime.isDebug()) { X_Log.warn("IO Error", e); } return cancelled; } } protected void sendRequest(final URLConnection connect, final IORequestDefault request, final IOCallback<IOMessage<String>> callback, final String url, final StringDictionary<String> headers, final String body) { final LogLevel logLevel = logLevel(); final Moment before = X_Time.now(); X_Time.runUnsafe(new RunUnsafe() { @Override protected void doRun() throws Throwable { if (X_Log.loggable(logLevel)) { X_Log.log(getClass(), logLevel, "Starting IO for ",url,"took",X_Time.difference(before)); } if (request.isCancelled()) { callback.onError(new CancelledException(request)); return; } request.setConnectionThread(Thread.currentThread()); synchronized (request) { request.start(); request.notifyAll(); } InputStream in; String res; try { if (body != null) { // We need to send data on the output stream first final Moment start = X_Time.now(); try( final OutputStream out = connect.getOutputStream(); ) { in = toStream(body, headers); X_IO.drain(out, in); if (X_Log.loggable(logLevel)) { X_Log.log(getClass(), logLevel, "Sending data for ",url,"took",X_Time.difference(start)); } } } final Moment start = X_Time.now(); try { in = connect.getInputStream(); try { res = drainInput(in, callback); } finally { in.close(); } } catch (final SocketException e) { in = connect.getInputStream(); if (request.isCancelled()) { callback.onError(new CancelledException(request)); return; } try { res = drainInput(in, callback); } finally { in.close(); } } if (X_Log.loggable(logLevel)) { X_Log.log(getClass(), logLevel, "Receiving data for ",url,"took",X_Time.difference(start)); } if (request.isCancelled()) { callback.onError(new CancelledException(request)); return; } final Provider<Many<String>> resultHeaders = new SingletonProvider<Many<String>>() { @Override protected Many<String> initialValue() { final Many<String> headers = X_Collect.newStringMultiMap(String.class); for (final Entry<String, List<String>> entry : connect.getHeaderFields().entrySet()) { for (final String value : entry.getValue()) { headers.add(entry.getKey(), value); } } return headers; } }; request.setValue(res); request.setResultHeaders(resultHeaders); if (connect instanceof HttpURLConnection) { final int status = ((HttpURLConnection)connect).getResponseCode(); final String message = ((HttpURLConnection)connect).getResponseMessage(); request.setStatus(status, message); } else { request.setStatus(IORequest.STATUS_NOT_HTTP, "Request not using http: "+connect.getClass()); } final Moment callbackTime = X_Time.now(); try { callback.onSuccess(new IOMessage<String>() { @Override public String body() { return request.getValue(); } @Override public int modifier() { return IOConstants.METHOD_GET; } @Override public String url() { return url; } @Override public StringTo.Many<String> headers() { return resultHeaders.get(); } @Override public int statusCode() { return request.getStatusCode(); } @Override public String statusMessage() { return request.getStatusText(); } }); } catch (final Throwable t) { X_Log.error("Error invoking IO callback on",callback,"for request",url, t); callback.onError(X_Util.unwrap(t)); } if (X_Log.loggable(logLevel)) { X_Log.log(getClass(), logLevel, "Callback time for ",url,"took",X_Time.difference(callbackTime)); } } catch (final Throwable t) { request.cancel(); callback.onError(X_Util.unwrap(t)); } finally { request.connectionThread = null; } } }); } private InputStream toStream(final String body, final StringDictionary<String> headers) { return X_IO.toStreamUtf8(body); } protected IORequestDefault createRequest() { return new IORequestDefault(); } protected String drainInput ( final InputStream in, final IOCallback<IOMessage<String>> callback) throws IOException { try { final String message; final BufferedReader read = new BufferedReader(new InputStreamReader(in)); final StringReader messageReader = new StringReader(); if (callback instanceof LineReader) { messageReader.forwardTo((LineReader)callback); } String line; messageReader.onStart(); while ((line = read.readLine())!=null) { messageReader.onLine(line); } // grab body >before< calling onEnd, as it cleans up its memory message = messageReader.toString(); messageReader.onEnd(); // All done. return message; } finally { in.close(); } } }