Java tutorial
package com.kixeye.janus.client.http.rest; /* * #%L * Janus * %% * Copyright (C) 2014 KIXEYE, Inc * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import javax.net.ssl.SSLContext; import org.apache.http.client.HttpResponseException; import org.apache.http.client.config.RequestConfig; import org.codehaus.jackson.map.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.kixeye.janus.Janus; import com.kixeye.janus.ServerStats; import com.kixeye.janus.client.exception.NoServerAvailableException; import com.kixeye.janus.client.exception.RetriesExceededException; import com.kixeye.relax.AsyncRestClient; import com.kixeye.relax.HttpResponse; import com.kixeye.relax.RestClientSerDe; import com.kixeye.relax.RestClients; /** * A REST client that uses the {@link AsyncRestClient} * * @author ebahtijaragic */ public class DefaultRestHttpClient { private static final Logger logger = LoggerFactory.getLogger(DefaultRestHttpClient.class); private static final String USER_AGENT_NAME = "Janus" + DefaultRestHttpClient.class.getSimpleName(); private static final long MAX_WAIT_SECONDS = 30; private final AsyncRestClient client; private final Janus janus; private final int numRetries; private final String contentType; /** * Creates a new HTTP client with a JSON serializer. * * @param janus * @param numRetries */ public DefaultRestHttpClient(Janus janus, int numRetries) { assert janus != null : "'janus' cannot be null."; assert numRetries >= 0 : "'numRetries' must be >= 0"; this.janus = janus; this.numRetries = numRetries; this.contentType = null; this.client = (AsyncRestClient) RestClients.create(JACKSON_JSON_SER_DE).withUserAgentName(USER_AGENT_NAME) .build(); } /** * Creates a new HTTP client with the given serializer. * * @param janus * @param numRetries * @param serDe * @param contentType */ public DefaultRestHttpClient(Janus janus, int numRetries, RestClientSerDe serDe, String contentType) { assert janus != null : "'janus' cannot be null."; assert numRetries >= 0 : "'numRetries' must be >= 0"; assert serDe != null : "'serDe' cannot be null."; this.janus = janus; this.numRetries = numRetries; this.contentType = contentType; this.client = (AsyncRestClient) RestClients.create(serDe).withUserAgentName(USER_AGENT_NAME).build(); } /** * Creates a new HTTP client with the given serializer and request config. * * @param janus * @param numRetries * @param serDe * @param contentType * @param requestConfig */ public DefaultRestHttpClient(Janus janus, int numRetries, RestClientSerDe serDe, String contentType, RequestConfig requestConfig) { assert janus != null : "'janus' cannot be null."; assert numRetries >= 0 : "'numRetries' must be >= 0"; assert serDe != null : "'serDe' cannot be null."; assert requestConfig != null : "'requestConfig' cannot be null."; this.janus = janus; this.numRetries = numRetries; this.contentType = contentType; this.client = (AsyncRestClient) RestClients.create(serDe).withRequestConfig(requestConfig) .withUserAgentName(USER_AGENT_NAME).build(); } /** * Creates a new HTTP clien t with the given serializer, request config, and SSL context. * @param janus * @param numRetries * @param serDe * @param contentType * @param requestConfig * @param sslContext */ public DefaultRestHttpClient(Janus janus, int numRetries, RestClientSerDe serDe, String contentType, RequestConfig requestConfig, SSLContext sslContext) { assert janus != null : "'janus' cannot be null."; assert numRetries >= 0 : "'numRetries' must be >= 0"; assert serDe != null : "'serDe' cannot be null."; assert requestConfig != null : "'requestConfig' cannot be null."; assert sslContext != null : "'sslContext' cannot be null."; this.janus = janus; this.numRetries = numRetries; this.contentType = contentType; this.client = (AsyncRestClient) RestClients.create(serDe).withRequestConfig(requestConfig) .withSSLContext(sslContext).withUserAgentName(USER_AGENT_NAME).build(); } /** * Performs a asynchronous http GET request, returning an object of the given responseType converted from the response payload's body * * @param path path to the resource to request * @param responseType the type of object to convert the response body to. it is assumed that the * implementation knows how to make this conversion. * @return an instance of responseType corresponding to the http response body * @throws NoServerAvailableException if no server instance could be found for the given request * @throws RetriesExceededException if the maximum number of retries was exceeded */ public <T> HttpResponse<T> get(String path, final Class<T> responseType) throws NoServerAvailableException, RetriesExceededException { FunctionWrapper<T> wrapped = new FunctionWrapper<T>() { @Override public HttpResponse<T> execute(String url) throws Exception { return client.get(url, contentType, responseType) .waitForComplete(MAX_WAIT_SECONDS, TimeUnit.SECONDS).get(); } }; return executeWithLoadBalancer(path, wrapped); } /** * Performs a asynchronous http GET request, substituting the given urlVariables into the given path, * and returning an object of the given responseType converted from the response payload's body * * @param path path to the resource to request * @param responseType the type of object to convert the response body to. it is assumed that the * implementation knows how to make this conversion. * @param pathVariables variables that will substituted into the given path in the order they appear. For example, * variables 1, 4 will be substituted into path /stores/{storeId}/items/{itemId} as * /stores/1/items/4. * @return an instance of responseType corresponding to the http response body * @throws NoServerAvailableException if no server instance could be found for the given request * @throws RetriesExceededException if the maximum number of retries was exceeded */ public <T> HttpResponse<T> get(String path, final Class<T> responseType, final Object... pathVariables) throws NoServerAvailableException, RetriesExceededException { FunctionWrapper<T> wrapped = new FunctionWrapper<T>() { @Override public HttpResponse<T> execute(String url) throws Exception { return client.get(url, contentType, responseType, pathVariables) .waitForComplete(MAX_WAIT_SECONDS, TimeUnit.SECONDS).get(); } }; return executeWithLoadBalancer(path, wrapped); } /** * Performs a asynchronous http POST request, substituting the given urlVariables into the given path, * and returning an object of the given responseType converted from the response payload's body * * @param path path to the resource to create * @param requestBody an object that will be converted and sent as the request body * @param responseType the type of object to convert the response body to. it is assumed that the * implementation knows how to make this conversion. * @return an instance of responseType corresponding to the http response body * @throws NoServerAvailableException if no server instance could be found for the given request * @throws RetriesExceededException if the maximum number of retries was exceeded */ public <T> HttpResponse<T> post(String path, final Object requestBody, final Class<T> responseType) throws NoServerAvailableException, RetriesExceededException { FunctionWrapper<T> wrapped = new FunctionWrapper<T>() { @Override public HttpResponse<T> execute(String url) throws Exception { return client.post(url, contentType, contentType, requestBody, responseType) .waitForComplete(MAX_WAIT_SECONDS, TimeUnit.SECONDS).get(); } }; return executeWithLoadBalancer(path, wrapped); } /** * Performs a asynchronous http POST request, substituting the given urlVariables into the given path, * and returning an object of the given responseType converted from the response payload's body * * @param path path to the resource to create * @param requestBody an object that will be converted and sent as the request body * @param responseType the type of object to convert the response body to. it is assumed that the * implementation knows how to make this conversion. * @param pathVariables variables that will substituted into the given path in the order they appear. For example, * variables 1, 4 will be substituted into path /stores/{storeId}/items/{itemId} as * /stores/1/items/4. * @return an instance of responseType corresponding to the http response body * @throws NoServerAvailableException if no server instance could be found for the given request * @throws RetriesExceededException if the maximum number of retries was exceeded */ public <T> HttpResponse<T> post(String path, final Object requestBody, final Class<T> responseType, final Object... pathVariables) throws NoServerAvailableException, RetriesExceededException { FunctionWrapper<T> wrapped = new FunctionWrapper<T>() { @Override public HttpResponse<T> execute(String url) throws Exception { return client.post(url, contentType, contentType, requestBody, responseType, pathVariables) .waitForComplete(MAX_WAIT_SECONDS, TimeUnit.SECONDS).get(); } }; return executeWithLoadBalancer(path, wrapped); } /** * Performs a asynchronous http PUT request, storing the given request body at the given path. * * @param path path to the resource to create * @param requestBody an object that will be converted and sent as the request body * @throws NoServerAvailableException if no server instance could be found for the given request * @throws RetriesExceededException if the maximum number of retries was exceeded */ public void put(String path, final Object requestBody) throws NoServerAvailableException, RetriesExceededException { FunctionWrapper<Void> wrapped = new FunctionWrapper<Void>() { @Override public HttpResponse<Void> execute(String url) throws Exception { return client.put(url, contentType, contentType, requestBody) .waitForComplete(MAX_WAIT_SECONDS, TimeUnit.SECONDS).get(); } }; executeWithLoadBalancer(path, wrapped); } /** * Performs a asynchronous http PUT request, storing the given request body at the given path. * * @param path path to the resource to create * @param requestBody an object that will be converted and sent as the request body * @param pathVariables variables that will substituted into the given path in the order they appear. For example, * variables 1, 4 will be substituted into path /stores/{storeId}/items/{itemId} as * /stores/1/items/4. * @throws NoServerAvailableException if no server instance could be found for the given request * @throws RetriesExceededException if the maximum number of retries was exceeded */ public void put(String path, final Object requestBody, final Object... pathVariables) throws NoServerAvailableException, RetriesExceededException { FunctionWrapper<Void> wrapped = new FunctionWrapper<Void>() { @Override public HttpResponse<Void> execute(String url) throws Exception { return client.put(url, contentType, contentType, requestBody, pathVariables) .waitForComplete(MAX_WAIT_SECONDS, TimeUnit.SECONDS).get(); } }; executeWithLoadBalancer(path, wrapped); } /** * Performs a asynchronous http DELETE request, deleting the resource at the given path. * * @param path path to the resource to create * @throws NoServerAvailableException if no server instance could be found for the given request * @throws RetriesExceededException if the maximum number of retries was exceeded */ public void delete(String path) throws NoServerAvailableException, RetriesExceededException { FunctionWrapper<Void> wrapped = new FunctionWrapper<Void>() { @Override public HttpResponse<Void> execute(String url) throws Exception { return client.delete(url).waitForComplete(MAX_WAIT_SECONDS, TimeUnit.SECONDS).get(); } }; executeWithLoadBalancer(path, wrapped); } /** * Performs a asynchronous http DELETE request, deleting the resource at the given path. * * @param path path to the resource to create * @param pathVariables variables that will substituted into the given path in the order they appear. For example, * variables 1, 4 will be substituted into path /stores/{storeId}/items/{itemId} as * /stores/1/items/4. * @throws NoServerAvailableException if no server instance could be found for the given request * @throws RetriesExceededException if the maximum number of retries was exceeded */ public void delete(String path, final Object... pathVariables) throws NoServerAvailableException, RetriesExceededException { FunctionWrapper<Void> wrapped = new FunctionWrapper<Void>() { @Override public HttpResponse<Void> execute(String url) throws Exception { return client.delete(url, pathVariables).waitForComplete(MAX_WAIT_SECONDS, TimeUnit.SECONDS).get(); } }; executeWithLoadBalancer(path, wrapped); } /** * Executes a function with load balancer. * * @param path * @param function * @return * @throws NoServerAvailableException * @throws RetriesExceededException */ private <T> HttpResponse<T> executeWithLoadBalancer(String path, FunctionWrapper<T> function) throws NoServerAvailableException, RetriesExceededException { long retries = numRetries; do { // get a load balanced server ServerStats server = janus.getServer(); if (server == null) { throw new NoServerAvailableException(janus.getServiceName()); } // prefix URL with selected server String newUrl = server.getServerInstance().getUrl() + path; // call into REST Template wrapper HttpResponse<T> result = null; long latency = -1; try { server.incrementSentMessages(); server.incrementOpenRequests(); long startTime = System.currentTimeMillis(); result = function.execute(newUrl); latency = System.currentTimeMillis() - startTime; // exit if successful if (result == null) { throw new TimeoutException("Timed out while waiting for a response."); } else { if (result.getStatusCode() >= 500) { throw new HttpResponseException(result.getStatusCode(), "Unexpected response"); } return result; } } catch (Exception e) { // unexpected exception, treat as a server problem but also log it. logger.warn("RestClient threw unexpected exception, retrying another server", e); server.incrementErrors(); } finally { server.decrementOpenRequests(); if (latency > 0) { server.recordLatency(latency); } } retries -= 1; } while (retries >= 0); throw new RetriesExceededException(janus.getServiceName(), numRetries); } /** * A JSON SerDe that uses Jackson. */ public static final RestClientSerDe JACKSON_JSON_SER_DE = new RestClientSerDe() { private final byte[] EMPTY = new byte[0]; private ObjectMapper objectMapper = new ObjectMapper(); /** * @see com.kixeye.relax.RestClientSerDe#serialize(java.lang.String, java.lang.Object) */ public byte[] serialize(String mimeType, Object obj) throws IOException { return obj == null ? EMPTY : objectMapper.writeValueAsBytes(obj); } /** * @see com.kixeye.relax.RestClientSerDe#deserialize(java.lang.String, byte[], int, int, java.lang.Class) */ public <T> T deserialize(String mimeType, byte[] data, int offset, int length, Class<T> clazz) throws IOException { return objectMapper.readValue(data, offset, length, clazz); } }; /** * A String SerDe. */ public static final RestClientSerDe UTF8_STRING_SER_DE = new RestClientSerDe() { private final byte[] EMPTY = new byte[0]; /** * @see com.kixeye.relax.RestClientSerDe#serialize(java.lang.String, java.lang.Object) */ public byte[] serialize(String mimeType, Object obj) throws IOException { return obj == null ? EMPTY : obj.toString().getBytes(StandardCharsets.UTF_8); } /** * @see com.kixeye.relax.RestClientSerDe#deserialize(java.lang.String, byte[], int, int, java.lang.Class) */ @SuppressWarnings("unchecked") public <T> T deserialize(String mimeType, byte[] data, int offset, int length, Class<T> clazz) throws IOException { return (T) new String(data, offset, length, StandardCharsets.UTF_8); } }; private interface FunctionWrapper<T> { HttpResponse<T> execute(String url) throws Exception; } }