All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.nu.art.http.HttpModule Maven / Gradle / Ivy

There is a newer version: 1.2.59
Show newest version
/*
 * Copyright (c) 2016 to Adam van der Kruk (Zehavi) AKA TacB0sS - Nu-Art
 *
 * Restricted usage under specific license
 *
 */

package com.nu.art.http;

import com.nu.art.belog.BeLogged;
import com.nu.art.belog.Logger;
import com.nu.art.belog.consts.LogLevel;
import com.nu.art.core.generics.Processor;
import com.nu.art.core.interfaces.ILogger;
import com.nu.art.core.tools.ArrayTools;
import com.nu.art.core.tools.StreamTools;
import com.nu.art.core.utils.PoolQueue;
import com.nu.art.modular.core.Module;
import com.nu.art.modular.core.ModuleManager;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.cert.CertificateException;
import java.util.HashMap;
import java.util.List;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

@SuppressWarnings( {
	                   "unused",
	                   "WeakerAccess"
                   })
public final class HttpModule
	extends Module {

	public static class ExecutionPool {

		String key;
		int numberOfThreads;

		public ExecutionPool(String key, int numberOfThreads) {
			this.key = key;
			this.numberOfThreads = numberOfThreads;
		}
	}

	public ExecutionPool DefaultExecutionPool = new ExecutionPool("http-thread", 5);
	public static final HttpResponseListener EmptyResponseListener = new EmptyResponseListener();
	private LogLevel defaultLogLevel = LogLevel.Verbose;

	/**
	 * PoolQueue holding the requests to be executed by its thread pool
	 */
	private HashMap queues = new HashMap<>();

	private HttpModule() { }

	private HttpPoolQueue getOrCreateQueue(ExecutionPool executionPool) {
		if (executionPool == null)
			executionPool = DefaultExecutionPool;

		HttpPoolQueue queue = queues.get(executionPool.key);
		if (queue == null) {
			queues.put(executionPool.key, queue = new HttpPoolQueue());
			queue.createThreads(executionPool.key, executionPool.numberOfThreads);
		}
		return queue;
	}

	public void setDefaultLogLevel(LogLevel defaultLogLevel) {
		this.defaultLogLevel = defaultLogLevel;
	}

	public void setDefaultExecutionPoolThreadCount(int threadCount) {
		this.DefaultExecutionPool.numberOfThreads = threadCount;
	}

	@Override
	protected void init() {}

	public final void trustAllCertificates() {
		logWarning("Very bad idea... calling this is a debug feature ONLY!!!");
		try {
			// Create a trust manager that does not validate certificate chains
			final TrustManager[] trustAllCerts = new TrustManager[]{
				new X509TrustManager() {
					@Override
					public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType)
						throws CertificateException {
						// Workaround to silence the lint error... This is a debug feature only!
						int i = 0;
					}

					@Override
					public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType)
						throws CertificateException {
						// Workaround to silence the lint error... This is a debug feature only!
						int i = 0;
					}

					@Override
					public java.security.cert.X509Certificate[] getAcceptedIssuers() {
						return null;
					}
				}
			};

			HostnameVerifier hostnameVerifier = new HostnameVerifier() {

				@Override
				public boolean verify(String hostname, SSLSession session) {
					// Workaround to silence the lint error... This is a debug feature only!
					return hostname != null;
				}
			};

			setHostnameVerifier(hostnameVerifier);

			// Install the all-trusting trust manager
			final SSLContext sslContext = SSLContext.getInstance("SSL");
			sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
			// Create an ssl socket factory with our all-trusting manager
			final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();

			HttpsURLConnection.setDefaultSSLSocketFactory(sslSocketFactory);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public final void setHostnameVerifier(HostnameVerifier hostnameVerifier) {
		HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifier);
	}

	public static abstract class BaseTransaction
		extends Transaction {

		public BaseTransaction() {
			ModuleManager.ModuleManager.getModule(HttpModule.class).super();
		}
	}

	@SuppressWarnings("unused")
	private abstract class Transaction
		extends Logger {

		private class HoopTiming {

			HoopTiming redirectHoop;

			URL finalUrl;

			long connectionInterval;

			long uploadInterval;

			long waitForServerInterval;

			long downloadingInterval;

			long getTotalTime() {
				return getTotalHoopTime() + (redirectHoop == null ? 0 : redirectHoop.getTotalTime());
			}

			long getTotalHoopTime() {
				return connectionInterval + uploadInterval + waitForServerInterval + downloadingInterval;
			}
		}

		private HoopTiming hoop = new HoopTiming();

		protected IHttpRequest createRequest() {
			return new HttpRequestIn();
		}

		protected final  Manager getModule(Class moduleType) {
			return HttpModule.this.getModule(moduleType);
		}

		protected final  void dispatchModuleEvent(String message, Processor processor) {
			HttpModule.this.dispatchModuleEvent(message, processor);
		}

		protected final Throwable createException(HttpResponse httpResponse, String errorBody) {
			if (httpResponse.exception != null)
				return httpResponse.exception;

			return new HttpException(httpResponse, errorBody);
		}
	}

	private class HttpTransaction {

		private Logger logger = BeLogged.getInstance().getLogger(HttpModule.this);

		private HoopTiming hoop;
		private HttpRequest request;
		private HttpResponse response;
		private HttpResponseListener responseListener;

		private HttpTransaction(HttpRequest request, HttpResponseListener responseListener) {
			super();
			logger.setMinLogLevel(request.logLevel != null ? request.logLevel : defaultLogLevel);
			this.request = request;
			this.responseListener = responseListener;
		}

		@SuppressWarnings("unchecked")
		private boolean execute() {
			if (request.preExecutionProcessor != null)
				request.preExecutionProcessor.process(request);

			HoopTiming originalHoop = hoop;
			hoop = new HoopTiming(originalHoop);
			hoop.redirectHoop = originalHoop;

			HttpURLConnection connection = null;
			boolean redirect = false;
			response = new HttpResponse();
			try {
				connection = connect();
				request.printRequest(logger, hoop);
				postBody(connection, request.inputStream);

				waitForResponse(response, connection);

				if (processRedirect()) {
					return redirect = true;
				}

				if (response.processFailure(connection)) {
					responseListener.onError(response);
					return false;
				}

				processSuccess(connection);
			} catch (Throwable e) {
				if (connection == null)
					request.printRequest(logger, hoop);

				logger.logError("+-- Error: ", e);
				response.exception = e;
				responseListener.onError(response, null);
			} finally {
				response.printResponse(logger);
				printTiming(logger, hoop, "");
				if (!redirect)
					logger.logVerbose("+-------------------------------------------------------------------------+");

				request.close();
				response.close();

				if (connection != null)
					connection.disconnect();
			}
			return false;
		}

		private void processSuccess(HttpURLConnection connection)
			throws IOException {
			long start = System.currentTimeMillis();

			response.processSuccess(connection);
			responseListener.onSuccess(response);

			hoop.downloadingAndProcessingInterval = System.currentTimeMillis() - start;
		}

		final void waitForResponse(HttpResponse response, HttpURLConnection connection)
			throws IOException {
			long start = System.currentTimeMillis();

			response.responseCode = connection.getResponseCode();
			response.headers = new HashMap<>(connection.getHeaderFields());
			String[] keys = ArrayTools.asArray(response.headers.keySet(), String.class);

			for (String key : keys) {
				if (key == null)
					continue;

				List value = response.headers.remove(key);
				List olderValue = response.headers.put(key.toLowerCase(), value);
				if (olderValue != null)
					logger.logWarning("POTENTIAL BUG... SAME HEADER NAME DIFFERENT CASING FOR KEY: " + key);
			}

			hoop.waitForServerInterval = System.currentTimeMillis() - start;
		}

		final boolean processRedirect()
			throws IOException {
			if (!request.autoRedirect)
				return false;

			if (response.responseCode < 300 || response.responseCode >= 400)
				return false;

			List locations = response.getHeader("location");
			if (locations.size() > 1)
				throw new IOException("redirect has ambiguous locations... cannot determine which!!");

			if (locations.size() == 0)
				return false;

			String location = locations.get(0);
			if (location.length() == 0)
				return false;

			request.url = location;
			return true;
		}

		private void printTiming(ILogger logger, HoopTiming hoop, String indentation) {
			logger.logVerbose("+--" + indentation + " Timing, Url: " + hoop.finalUrl.toString());
			logger.logVerbose("+--" + indentation + " Timing, Connection: " + hoop.connectionInterval);
			logger.logVerbose("+--" + indentation + " Timing, Uploading: " + hoop.uploadInterval);
			logger.logVerbose("+--" + indentation + " Timing, Waiting for response : " + hoop.waitForServerInterval);
			logger.logVerbose("+--" + indentation + " Timing, Downloading & Processing: " + hoop.downloadingAndProcessingInterval);
			logger.logVerbose("+--" + indentation + " Timing, Total Hoop: " + hoop.getTotalHoopTime());
		}

		final OutputStream postBody(HttpURLConnection connection, InputStream postStream)
			throws IOException {

			if (postStream == null)
				return null;

			long start = System.currentTimeMillis();
			byte[] buffer = new byte[1024];
			int length;
			long cached = 0;
			int uploaded = 0;

			OutputStream outputStream = connection.getOutputStream();
			while ((length = postStream.read(buffer)) != -1) {
				outputStream.write(buffer, 0, length);
				cached += length;
				uploaded += length;
				responseListener.onUploadProgress(uploaded, postStream.available());
				if (cached < 1024 * 1024)
					continue;

				outputStream.flush();
				cached = 0;
			}

			outputStream.flush();
			postStream.close();
			hoop.uploadInterval = System.currentTimeMillis() - start;

			return outputStream;
		}

		private HttpURLConnection connect()
			throws IOException {
			long start = System.currentTimeMillis();

			String urlPath = request.composeURL();
			URL url;
			try {
				url = new URL(urlPath);
			} catch (MalformedURLException e) {
				throw new IOException("error parsing url: " + urlPath, e);
			}

			HttpURLConnection connection = request.connect(hoop.finalUrl = url);
			hoop.connectionInterval = System.currentTimeMillis() - start;
			return connection;
		}
	}

	private class HttpRequestIn
		extends HttpRequest {

		@Override
		public void execute(HttpResponseListener listener) {
			HttpPoolQueue queue = getOrCreateQueue(executionPool);
			queue.addItem(new HttpTransaction(this, listener));
		}

		/**
		 * To call this method you might be using a bad utility OR your architecture is flawed OR you don't know what you are doing OR you don't have a choice OR
		 * you
		 * are smarter then I have anticipated...
		 *
		 * Regardless I think this is a bad way to use a rest api client!
		 *
		 * @return The response input stream, be sure to close it when you are done!
		 */
		private Throwable error;
		private InputStream response;

		public InputStream executeSync()
			throws Throwable {
			executeAction(new HttpTransaction(this, new HttpResponseListener(InputStream.class, String.class) {
				@Override
				public void onSuccess(HttpResponse httpResponse, InputStream responseBody) {
					try {
						ByteArrayOutputStream baos = new ByteArrayOutputStream();
						StreamTools.copy(responseBody, baos);
						response = new ByteArrayInputStream(baos.toByteArray());
					} catch (IOException e) {
						error = e;
					}
				}

				@Override
				public void onError(HttpResponse httpResponse, String errorBody) {
					error = new IOException(errorBody, httpResponse.exception);
				}
			}));

			if (error != null)
				throw error;

			return response;
		}
	}

	protected void executeAction(HttpTransaction transaction) {
		while (transaction.execute())
			;
	}

	private class HttpPoolQueue
		extends PoolQueue {

		@Override
		protected void onExecutionError(HttpTransaction item, Throwable e) {
			logWarning("DO WE EVEN GET TO THIS ERROR??", e);
			HttpResponse httpResponse = new HttpResponse();
			httpResponse.exception = e;
			try {
				item.responseListener.onError(httpResponse);
			} catch (IOException e1) {
				logError("Not really sure what to do here....?", e1);
			}
		}

		@Override
		protected void executeAction(HttpTransaction transaction) {
			HttpModule.this.executeAction(transaction);
		}
	}

	static class HoopTiming {

		final int hoopIndex;

		HoopTiming redirectHoop;

		URL finalUrl;

		long connectionInterval;

		long uploadInterval;

		long waitForServerInterval;

		long downloadingAndProcessingInterval;

		public HoopTiming() {
			this(null);
		}

		public HoopTiming(HoopTiming originalHoop) {
			this.redirectHoop = originalHoop;
			if (originalHoop != null)
				hoopIndex = originalHoop.hoopIndex + 1;
			else
				hoopIndex = 0;
		}

		long getTotalTime() {
			return getTotalHoopTime() + (redirectHoop == null ? 0 : redirectHoop.getTotalTime());
		}

		long getTotalHoopTime() {
			return connectionInterval + uploadInterval + waitForServerInterval + downloadingAndProcessingInterval;
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy