@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = false)
public class BitcoindException extends Exception {

	private static final long serialVersionUID = 1L;

	private int code;

	
	public BitcoindException(int code, String message) {
		super(message); 
		this.code = code;
	}
} @Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = false)
public abstract class CommunicationException extends Exception {

	private static final long serialVersionUID = 1L;

	private int code;
	
	
	public CommunicationException(Errors error) {
		this(error, Constants.STRING_EMPTY);
	}
	
	public CommunicationException(Errors error, String additionalMsg) {
		super(error.getDescription() + additionalMsg);
		code = error.getCode();
	}
	
	public CommunicationException(Errors error, Exception cause) {
		super(error.getDescription(), cause);
		code = error.getCode();
	}
} @NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class Defaults {

	public static final int DECIMAL_SCALE = 8;
	public static final RoundingMode ROUNDING_MODE = RoundingMode.HALF_UP;

	public static final String[] NODE_VERSIONS = {"0.10.0", "0.10.1", "0.10.2", "0.10.3"};
	public static final String JSON_RPC_VERSION = "1.0";
} @Getter
@ToString
@AllArgsConstructor
public enum Errors {

	ARGS_COUNT_UNEVEN(1001001, "Expected the argument count to be 'even', but was 'uneven' instead."),
	ARGS_COUNT_UNEQUAL(1001002, "Expected the argument count to be 'equal', but was 'unequal' instead."),
	ARGS_VALUE_NEGATIVE(1001003, "Expected the argument value to be positive (>=0), but was negative (<0) "
			+ "instead."),
	ARGS_NULL(1001004, "Expected a non-null argument, but got 'null' instead."),
	ARGS_CONTAIN_NULL(1001005, "Expected only non-null arguments, but got >0 'null' instead."),
	ARGS_HTTP_METHOD_UNSUPPORTED(1001006, "Expected the argument to be a valid HTTP method, but was "
			+ "invalid/unsupported instead."),
	ARGS_HTTP_AUTHSCHEME_UNSUPPORTED(1001007, "Expected the argument to be a valid HTTP auth scheme, but "
			+ "was invalid/unsupported instead."),
	ARGS_BTCD_BLOCKSTATUS_UNSUPPORTED(1001008, "Expected the argument to be a valid 'bitcoind' block status, "
			+ "but was invalid/unsupported instead."),
	ARGS_BTCD_CHAINSTATUS_UNSUPPORTED(1001009, "Expected the argument to be a valid 'bitcoind' chain status, "
			+ "but was invalid/unsupported instead."),
	ARGS_BTCD_CHAINTYPE_UNSUPPORTED(1001010, "Expected the argument to be a valid 'bitcoind' chain type, but "
			+ "was invalid/unsupported instead."),
	ARGS_BTCD_CHECKLEVEL_UNSUPPORTED(1001011, "Expected the argument to be a valid 'bitcoind' check level, "
			+ "but was invalid/unsupported instead."),
	ARGS_BTCD_CONNECTIONTYPE_UNSUPPORTED(1001012, "Expected the argument to be a valid 'bitcoind' connection "
			+ "type, but was invalid/unsupported instead."),
	ARGS_BTCD_NETWORKTYPE_UNSUPPORTED(1001013, "Expected the argument to be a valid 'bitcoind' network type, "
			+ "but was invalid/unsupported instead."),
	ARGS_BTCD_PAYMENTCATEGORY_UNSUPPORTED(1001014, "Expected the argument to be a valid 'bitcoind' payment "
			+ "category, but was invalid/unsupported instead."),
	ARGS_BTCD_PEERCONTROL_UNSUPPORTED(1001015, "Expected the argument to be a valid 'bitcoind' peer control, "
			+ "but was invalid/unsupported instead."),
	ARGS_BTCD_SCRIPTTYPE_UNSUPPORTED(1001016, "Expected the argument to be a valid 'bitcoind' script type, "
			+ "but was invalid/unsupported instead."),
	ARGS_BTCD_SIGHASHTYPE_UNSUPPORTED(1001017, "Expected the argument to be a valid 'bitcoind' signature hash "
			+ "type, but was invalid/unsupported instead."),
	ARGS_BTCD_NOTIFICATION_UNSUPPORTED(1001018, "Expected the argument to be a valid 'bitcoind' notification "
			+ "type, but was invalid/unsupported instead."),
	ARGS_BTCD_PROVIDER_NULL(1001019, "Expected a preconfigured 'bitcoind' JSON-RPC API provider, but got "
			+ "'null' instead."),
	REQUEST_HTTP_FAULT(1002001, "Request execution failed due an error in the HTTP protocol."),
	RESPONSE_HTTP_CLIENT_FAULT(1003001, "The server responded with a non-OK (4xx) HTTP status code. "
			+ "Status line: "),
	RESPONSE_HTTP_SERVER_FAULT(1003002, "The server responded with a non-OK (5xx) HTTP status code. "
			+ "Status line: "),
	RESPONSE_JSONRPC_NULL(1003003, "Expected a non-null JSON-RPC response object, but got 'null' instead."),
	RESPONSE_JSONRPC_NULL_ID(1003004, "Expected a non-null JSON-RPC response id, but got 'null' instead."),
	RESPONSE_JSONRPC_UNEQUAL_IDS(1003005, "Expected the JSON-RPC request and response ids to be 'equal', "
			+ "but were 'unequal' instead."),
	IO_STREAM_UNCLOSED(1004001, "Unable to close the specified stream."),
	IO_SOCKET_UNINITIALIZED(1004002, "Unable to open the specified socket."),
	IO_SERVERSOCKET_UNINITIALIZED(1004003, "Unable to open the specified server socket."),
	IO_UNKNOWN(1004004, "The operation failed due to an unknown IO exception."),
	PARSE_URI_FAILED(1005001, "Unable to parse the specified URI."),
	PARSE_JSON_UNKNOWN(1005002, "An unknown exception occurred while parsing/generating JSON content."),
	PARSE_JSON_MALFORMED(1005003, "Unable to parse the specified JSON content (malformed syntax detected)."),
	MAP_JSON_UNKNOWN(1006001, "An unknown exception ocurred while mapping the JSON content.");

	private final int code;
	private final String message;


	public String getDescription() {
		return String.format("Error #%s: %s", code, message);
	}
} @NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class HttpConstants {

	public static final String REQ_METHOD_GET = "GET";
	public static final String REQ_METHOD_POST = "POST";
	
	public static final String HEADER_AUTH = "Authorization";
	public static final String HEADER_CONTENT_TYPE = "Content-Type";
	public static final String HEADER_SERVER = "Server";
	
	public static final String AUTH_SCHEME_NONE = "";
	public static final String AUTH_SCHEME_BASIC = "Basic";
} public interface SimpleHttpClient {
	
	String execute(String reqMethod, String reqPayload) throws HttpLayerException;
	
	void close();
} public class SimpleHttpClientImpl implements SimpleHttpClient {
	
	private static final Logger LOG = LoggerFactory.getLogger(SimpleHttpClientImpl.class);
	
	private CloseableHttpClient provider;
	private Properties nodeConfig;


	public SimpleHttpClientImpl(CloseableHttpClient provider, Properties nodeConfig) {
		LOG.info("** SimpleHttpClientImpl(): initiating the HTTP communication layer");
		this.provider = provider;
		this.nodeConfig = nodeConfig;
	}

	@Override
	public String execute(String reqMethod, String reqPayload) throws HttpLayerException {
		CloseableHttpResponse response = null;
		try {
			response = provider.execute(getNewRequest(reqMethod, reqPayload), new BasicHttpContext());
			response = checkResponse(response);
			HttpEntity respPayloadEntity = response.getEntity();
			String respPayload = Constants.STRING_EMPTY;
			if (respPayloadEntity != null) {
				respPayload = EntityUtils.toString(respPayloadEntity);
				EntityUtils.consume(respPayloadEntity);
			}
			LOG.debug("-- execute(..): '{}' response payload received for HTTP '{}' request with " 
					+ "status line '{}'", ((respPayloadEntity == null) ? "null" : "non-null"), 
					reqMethod, response.getStatusLine());
			return respPayload;
		} catch (ClientProtocolException e) {
			throw new HttpLayerException(Errors.REQUEST_HTTP_FAULT, e);
		} catch (IOException e) {
			throw new HttpLayerException(Errors.IO_UNKNOWN, e);
		} catch (URISyntaxException e) {
			throw new HttpLayerException(Errors.PARSE_URI_FAILED, e);
		} finally {
			if (response != null) {
				try {
					LOG.debug("-- execute(..): attempting to recycle old HTTP response (reply to a "
							+ "'{}' request) with status line '{}'", reqMethod, response
							.getStatusLine());
					response.close();
				} catch (IOException e) {
					LOG.warn("<< execute(..): failed to recycle old HTTP response, message was: "
							+ "'{}'", e.getMessage());
				}
			}
		}
	}
	
	@Override
	public void close() {
		try {
			LOG.info(">> close(..): attempting to shut down the underlying HTTP provider");
			provider.close();
		} catch (IOException e) {
			LOG.warn("<< close(..): failed to shut down the underlying HTTP provider, message was: "
					+ "'{}'", e.getMessage());
		}
	}

	private HttpRequestBase getNewRequest(String reqMethod, String reqPayload) 
			throws URISyntaxException, UnsupportedEncodingException {
		HttpRequestBase request;
		if (reqMethod.equals(HttpConstants.REQ_METHOD_POST)) {
			HttpPost postRequest = new HttpPost();
			postRequest.setEntity(new StringEntity(reqPayload, ContentType.create(
					DataFormats.JSON.getMediaType(), Constants.UTF_8)));
			request = postRequest;
		} else {
			throw new IllegalArgumentException(Errors.ARGS_HTTP_METHOD_UNSUPPORTED.getDescription());
		}
		request.setURI(new URI(String.format("%s://%s:%s/", 
					nodeConfig.getProperty(NodeProperties.RPC_PROTOCOL.getKey()), 
					nodeConfig.getProperty(NodeProperties.RPC_HOST.getKey()), 
					nodeConfig.getProperty(NodeProperties.RPC_PORT.getKey()))));
		String authScheme = nodeConfig.getProperty(NodeProperties.HTTP_AUTH_SCHEME.getKey());
		request.addHeader(resolveAuthHeader(authScheme));
		LOG.debug("<< getNewRequest(..): returning a new HTTP '{}' request with target endpoint "
				+ "'{}' and headers '{}'", reqMethod, request.getURI(), request.getAllHeaders());
		return request;
	}

	private Header resolveAuthHeader(String authScheme) {
		if (authScheme.equals(HttpConstants.AUTH_SCHEME_NONE)) {
			return null;
		}
		if (authScheme.equals(HttpConstants.AUTH_SCHEME_BASIC)) {
			return new BasicHeader(HttpConstants.HEADER_AUTH, HttpConstants.AUTH_SCHEME_BASIC 
					+ " " + getCredentials(HttpConstants.AUTH_SCHEME_BASIC));
		}
		return null;
	}

	private String getCredentials(String authScheme) {
		if (authScheme.equals(HttpConstants.AUTH_SCHEME_NONE)) {
			return Constants.STRING_EMPTY;
		} else if (authScheme.equals(HttpConstants.AUTH_SCHEME_BASIC)) {
			return Base64.encodeBase64String((nodeConfig.getProperty(NodeProperties.RPC_USER.getKey())
					+ ":" + nodeConfig.getProperty(NodeProperties.RPC_PASSWORD.getKey())).getBytes());
		}
		throw new IllegalArgumentException(Errors.ARGS_HTTP_AUTHSCHEME_UNSUPPORTED.getDescription());
	}
	
	private CloseableHttpResponse checkResponse(CloseableHttpResponse response) 
			throws HttpLayerException {
		LOG.debug(">> checkResponse(..): checking HTTP response for non-OK status codes & "
				+ "unexpected header values");
		StatusLine statusLine = response.getStatusLine();
		if ((statusLine.getStatusCode() >= 400) && (statusLine.getStatusCode() <= 499)) {
			throw new HttpLayerException(Errors.RESPONSE_HTTP_CLIENT_FAULT, statusLine.toString());
		}
		if ((statusLine.getStatusCode() == 500)) {
			return response;
		}
		if ((statusLine.getStatusCode() >= 501) && (statusLine.getStatusCode() <= 599)) {
			throw new HttpLayerException(Errors.RESPONSE_HTTP_SERVER_FAULT, statusLine.toString());
		}	
		return response;
	}
} public class JsonMapper {
	
	private static final Logger LOG = LoggerFactory.getLogger(JsonMapper.class);
	
	private ObjectMapper rawMapper;
	private ObjectWriter rawWriter;


	public JsonMapper() {
		LOG.info("** JsonMapper(): initiating & configuring external JSON processor (Jackson"
				+ " ObjectMapper)");
		rawMapper = new ObjectMapper();
		rawWriter = rawMapper.writer();
		configureMappingProvider();
	}
	
	private void configureMappingProvider() {
		rawMapper.configure(SerializationFeature.INDENT_OUTPUT, true);
		rawMapper.configure(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS, true);
		rawMapper.configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, true);
	}

	public <T> String mapToJson(T entity) throws JsonRpcLayerException {
		try {
			String entityJson = rawWriter.writeValueAsString(entity);
			return entityJson;
		} catch (JsonProcessingException e) {
			throw new JsonRpcLayerException(Errors.PARSE_JSON_UNKNOWN, e);
		}
	}

	public <T> List<String> mapToJson(List<T> entities) throws JsonRpcLayerException {
		try {
			List<String> entitiesJson = new ArrayList<String>();
			for (T entity : entities) {
				entitiesJson.add(rawWriter.writeValueAsString(entity));
			}
			return entitiesJson;
		} catch (JsonProcessingException e) {
			throw new JsonRpcLayerException(Errors.PARSE_JSON_UNKNOWN, e);
		}
	}

	public <T> T mapToEntity(String entityJson, Class<T> entityClass) throws JsonRpcLayerException {
		try {
			T entity = rawMapper.readValue(entityJson, entityClass);
			return entity;
		} catch (JsonParseException e) {
			throw new JsonRpcLayerException(Errors.PARSE_JSON_MALFORMED, e);
		} catch (JsonMappingException e) {
			throw new JsonRpcLayerException(Errors.MAP_JSON_UNKNOWN, e);
		} catch (IOException e) {
			throw new JsonRpcLayerException(Errors.IO_UNKNOWN, e);
		}
	}

	public <T> List<T> mapToList(String entitiesJson, Class<T> entityClass)	
			throws JsonRpcLayerException {
		try {
			CollectionType listType = rawMapper.getTypeFactory().constructCollectionType(
					ArrayList.class, entityClass);
			List<T> entities = rawMapper.readValue(entitiesJson, listType);
			return entities;
		} catch (JsonParseException e) {
			throw new JsonRpcLayerException(Errors.PARSE_JSON_MALFORMED, e);
		} catch (JsonMappingException e) {
			throw new JsonRpcLayerException(Errors.MAP_JSON_UNKNOWN, e);
		} catch (IOException e) {
			throw new JsonRpcLayerException(Errors.IO_UNKNOWN, e);
		}
	}

	public <T, S> S mapToNestedLists(int depth, String entitiesJson, Class<T> entityClass) 
			throws JsonRpcLayerException {
		try {
			CollectionType outmostListType = rawMapper.getTypeFactory().constructCollectionType(
					ArrayList.class, entityClass);
			for (int i = 0; i < depth; i++) {
				outmostListType = rawMapper.getTypeFactory().constructCollectionType(
						ArrayList.class, outmostListType);
			}
			S entities = rawMapper.readValue(entitiesJson, outmostListType);
			return entities;
		} catch (JsonParseException e) {
			throw new JsonRpcLayerException(Errors.PARSE_JSON_MALFORMED, e);
		} catch (JsonMappingException e) {
			throw new JsonRpcLayerException(Errors.MAP_JSON_UNKNOWN, e);
		} catch (IOException e) {
			throw new JsonRpcLayerException(Errors.IO_UNKNOWN, e);
		}
	}
	
	public <T, S> Map<T, S> mapToMap(String entitiesJson, Class<T> keyClass, Class<S> valueClass) 
			throws JsonRpcLayerException {
		try {
			MapType mapType = rawMapper.getTypeFactory().constructMapType(HashMap.class, keyClass,
					valueClass);
			Map<T, S> entities = rawMapper.readValue(entitiesJson, mapType);
			return entities;
		} catch (JsonParseException e) {
			throw new JsonRpcLayerException(Errors.PARSE_JSON_MALFORMED, e);
		} catch (JsonMappingException e) {
			throw new JsonRpcLayerException(Errors.MAP_JSON_UNKNOWN, e);
		} catch (IOException e) {
			throw new JsonRpcLayerException(Errors.IO_UNKNOWN, e);
		}
	}
} public class JsonPrimitiveParser {

	public Integer parseInteger(String integerJson) {
		try {
			return Integer.valueOf(integerJson);		
		} catch (NumberFormatException e) {
			return null;
		}
	}

	public Long parseLong(String longJson) {
		try {
			return Long.valueOf(longJson);		
		} catch (NumberFormatException e) {
			return null;
		}
	}

	public BigInteger parseBigInteger(String bigIntegerJson) {
		try {
			return new BigInteger(bigIntegerJson);
		} catch (NumberFormatException e) {
			return null;
		}
	}

	public BigDecimal parseBigDecimal(String bigDecimalJson) {
		try {
			return new BigDecimal(bigDecimalJson).setScale(Defaults.DECIMAL_SCALE, 
					Defaults.ROUNDING_MODE);
		} catch (NumberFormatException e) {
			return null;
		}
	}

	public Boolean parseBoolean(String booleanJson) {
		if (!booleanJson.equals(Constants.STRING_NULL)) {
			return Boolean.valueOf(booleanJson);
		} else {
			return null;
		}
	}

	public String parseString(String stringJson) {
		if (!stringJson.equals(Constants.STRING_NULL)) {
			return unescapeJson(stringJson.replaceAll("^\"|\"$", ""));
		} else {
			return null;
		}
	}

	public String unescapeJson(String json) {
		return StringEscapeUtils.unescapeJson(json);
	}
} @Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = false)
public class JsonRpcLayerException extends CommunicationException {

	private static final long serialVersionUID = 1L;

	
	public JsonRpcLayerException(Errors error) {
		super(error);
	}
	
	public JsonRpcLayerException(Errors error, String additionalMsg) {
		super(error, additionalMsg);
	}

	public JsonRpcLayerException(Errors error, Exception cause) {
		super(error, cause);
	}
} @Data
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public class JsonRpcError {
	
	private Integer code;
	private String message;
} @Data
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = false)
@JsonInclude(Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public class JsonRpcRequest<T> extends JsonRpcMessage {

	private String method;
	private List<T> params;
} @Data
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = false)
@JsonInclude(Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonDeserialize(using = JsonRpcResponseDeserializer.class)
public class JsonRpcResponse extends JsonRpcMessage {
	
	private String result;
	private JsonRpcError error;
}
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.UUID;
import org.apache.http.impl.client.CloseableHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.neemre.btcdcli4j.core.BitcoindException;
import com.neemre.btcdcli4j.core.CommunicationException;
import com.neemre.btcdcli4j.core.common.Defaults;
import com.neemre.btcdcli4j.core.common.Errors;
import com.neemre.btcdcli4j.core.http.HttpConstants;
import com.neemre.btcdcli4j.core.http.client.SimpleHttpClient;
import com.neemre.btcdcli4j.core.http.client.SimpleHttpClientImpl;
import com.neemre.btcdcli4j.core.jsonrpc.JsonMapper;
import com.neemre.btcdcli4j.core.jsonrpc.JsonPrimitiveParser;
import com.neemre.btcdcli4j.core.jsonrpc.JsonRpcLayerException;
import com.neemre.btcdcli4j.core.jsonrpc.domain.JsonRpcError;
import com.neemre.btcdcli4j.core.jsonrpc.domain.JsonRpcRequest;
import com.neemre.btcdcli4j.core.jsonrpc.domain.JsonRpcResponse;
package com.neemre.btcdcli4j.core.jsonrpc.client;




public class JsonRpcClientImpl implements JsonRpcClient {
	
	private static final Logger LOG = LoggerFactory.getLogger(JsonRpcClientImpl.class);
	
	private SimpleHttpClient httpClient;
	private JsonPrimitiveParser parser;
	private JsonMapper mapper;