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

retrofit.RestAdapter Maven / Gradle / Ivy

There is a newer version: 2.0.0-beta2
Show newest version
/*
 * Copyright (C) 2012 Square, 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.
 */
package retrofit;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import retrofit.Profiler.RequestInformation;
import retrofit.client.Client;
import retrofit.client.Header;
import retrofit.client.Request;
import retrofit.client.Response;
import retrofit.converter.ConversionException;
import retrofit.converter.Converter;
import retrofit.mime.MimeUtil;
import retrofit.mime.TypedByteArray;
import retrofit.mime.TypedInput;
import retrofit.mime.TypedOutput;

/**
 * Adapts a Java interface to a REST API.
 * 

* API endpoints are defined as methods on an interface with annotations providing metadata about * the form in which the HTTP call should be made. *

* The relative path for a given method is obtained from an annotation on the method describing * the request type. The built-in methods are {@link retrofit.http.GET GET}, * {@link retrofit.http.PUT PUT}, {@link retrofit.http.POST POST}, {@link retrofit.http.HEAD HEAD}, * and {@link retrofit.http.DELETE DELETE}. You can define your own HTTP method by creating an * annotation that takes a {code String} value and itself is annotated with * {@link retrofit.http.RestMethod @RestMethod}. *

* Method parameters can be used to replace parts of the URL by annotating them with * {@link retrofit.http.Path @Path}. Replacement sections are denoted by an identifier surrounded * by curly braces (e.g., "{foo}"). To add items to the query string of a URL use * {@link retrofit.http.Query @Query}. *

* HTTP requests happen in one of two ways: *

    *
  • On the provided HTTP {@link Executor} with callbacks marshaled to the callback * {@link Executor}. The last method parameter should be of type {@link Callback}. The HTTP * response will be converted to the callback's parameter type using the specified * {@link retrofit.converter.Converter Converter}. If the callback parameter type uses a wildcard, * the lower bound will be used as the conversion type. *
  • On the current thread returning the response or throwing a {@link RetrofitError}. The HTTP * response will be converted to the method's return type using the specified * {@link retrofit.converter.Converter Converter}. *
*

* The body of a request is denoted by the {@link retrofit.http.Body @Body} annotation. The object * will be converted to request representation by a call to * {@link retrofit.converter.Converter#toBody(Object) toBody} on the supplied * {@link retrofit.converter.Converter Converter} for this instance. The body can also be a * {@link TypedOutput} where it will be used directly. *

* Alternative request body formats are supported by method annotations and corresponding parameter * annotations: *

    *
  • {@link retrofit.http.FormUrlEncoded @FormUrlEncoded} - Form-encoded data with key-value * pairs specified by the {@link retrofit.http.Field @Field} parameter annotation. *
  • {@link retrofit.http.Multipart @Multipart} - RFC 2387-compliant multi-part data with parts * specified by the {@link retrofit.http.Part @Part} parameter annotation. *
*

* Additional static headers can be added for an endpoint using the * {@link retrofit.http.Headers @Headers} method annotation. For per-request control over a header * annotate a parameter with {@link Header @Header}. *

* For example: *

 * public interface MyApi {
 *   @POST("/category/{cat}") // Asynchronous execution.
 *   void categoryList(@Path("cat") String a, @Query("page") int b, Callback<List<Item>> cb);
 *   @POST("/category/{cat}") // Synchronous execution.
 *   List<Item> categoryList(@Path("cat") String a, @Query("page") int b);
 * }
 * 
*

* Calling {@link #create(Class)} with {@code MyApi.class} will validate and create a new * implementation of the API. * * @author Bob Lee ([email protected]) * @author Jake Wharton ([email protected]) */ public class RestAdapter { private static final int LOG_CHUNK_SIZE = 4000; static final String THREAD_PREFIX = "Retrofit-"; static final String IDLE_THREAD_NAME = THREAD_PREFIX + "Idle"; /** Simple logging abstraction for debug messages. */ public interface Log { /** Log a debug message to the appropriate console. */ void log(String message); } private final Server server; private final Client.Provider clientProvider; private final Executor httpExecutor; private final Executor callbackExecutor; private final RequestHeaders requestHeaders; private final Converter converter; private final Profiler profiler; private final Log log; private volatile boolean debug; private RestAdapter(Server server, Client.Provider clientProvider, Executor httpExecutor, Executor callbackExecutor, RequestHeaders requestHeaders, Converter converter, Profiler profiler, Log log, boolean debug) { this.server = server; this.clientProvider = clientProvider; this.httpExecutor = httpExecutor; this.callbackExecutor = callbackExecutor; this.requestHeaders = requestHeaders; this.converter = converter; this.profiler = profiler; this.log = log; this.debug = debug; } /** Toggle debug logging on or off. */ public void setDebug(boolean debug) { this.debug = debug; } /** Create an implementation of the API defined by the specified {@code service} interface. */ @SuppressWarnings("unchecked") public T create(Class service) { if (!service.isInterface()) { throw new IllegalArgumentException("Only interface endpoint definitions are supported."); } return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class[] { service }, new RestHandler()); } private class RestHandler implements InvocationHandler { private final Map methodDetailsCache = new LinkedHashMap(); @SuppressWarnings("unchecked") // @Override public Object invoke(Object proxy, Method method, final Object[] args) throws InvocationTargetException, IllegalAccessException { // If the method is a method from Object then defer to normal invocation. if (method.getDeclaringClass() == Object.class) { return method.invoke(this, args); } // Load or create the details cache for the current method. final RestMethodInfo methodDetails; synchronized (methodDetailsCache) { RestMethodInfo tempMethodDetails = methodDetailsCache.get(method); if (tempMethodDetails == null) { tempMethodDetails = new RestMethodInfo(method); methodDetailsCache.put(method, tempMethodDetails); } methodDetails = tempMethodDetails; } if (methodDetails.isSynchronous) { return invokeRequest(methodDetails, args); } if (httpExecutor == null || callbackExecutor == null) { throw new IllegalStateException("Asynchronous invocation requires calling setExecutors."); } Callback callback = (Callback) args[args.length - 1]; httpExecutor.execute(new CallbackRunnable(callback, callbackExecutor) { @Override public ResponseWrapper obtainResponse() { return (ResponseWrapper) invokeRequest(methodDetails, args); } }); return null; // Asynchronous methods should have return type of void. } /** * Execute an HTTP request. * * @return HTTP response object of specified {@code type} or {@code null}. * @throws RetrofitError if any error occurs during the HTTP request. */ private Object invokeRequest(RestMethodInfo methodDetails, Object[] args) { methodDetails.init(); // Ensure all relevant method information has been loaded. String serverUrl = server.getUrl(); String url = serverUrl; // Keep some url in case RequestBuilder throws an exception. try { Request request = new RequestBuilder(converter) // .apiUrl(serverUrl) // .args(args) // .headers(requestHeaders.get()) // .methodInfo(methodDetails) // .build(); url = request.getUrl(); if (!methodDetails.isSynchronous) { // If we are executing asynchronously then update the current thread with a useful name. Thread.currentThread().setName(THREAD_PREFIX + url.substring(serverUrl.length())); } if (debug) { request = logAndReplaceRequest(request); } Object profilerObject = null; if (profiler != null) { profilerObject = profiler.beforeCall(); } long start = System.nanoTime(); Response response = clientProvider.get().execute(request); long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); int statusCode = response.getStatus(); if (profiler != null) { RequestInformation requestInfo = getRequestInfo(serverUrl, methodDetails, request); //noinspection unchecked profiler.afterCall(requestInfo, elapsedTime, statusCode, profilerObject); } if (debug) { response = logAndReplaceResponse(url, response, elapsedTime); } Type type = methodDetails.responseObjectType; if (statusCode >= 200 && statusCode < 300) { // 2XX == successful request // Caller requested the raw Response object directly. if (type.equals(Response.class)) { // Read the entire stream and replace with one backed by a byte[] response = Utils.readBodyToBytesIfNecessary(response); if (methodDetails.isSynchronous) { return response; } return new ResponseWrapper(response, response); } TypedInput body = response.getBody(); if (body == null) { return new ResponseWrapper(response, null); } try { Object convert = converter.fromBody(body, type); if (methodDetails.isSynchronous) { return convert; } return new ResponseWrapper(response, convert); } catch (ConversionException e) { // The response body was partially read by the converter. Replace it with null. response = Utils.replaceResponseBody(response, null); throw RetrofitError.conversionError(url, response, converter, type, e); } } response = Utils.readBodyToBytesIfNecessary(response); throw RetrofitError.httpError(url, response, converter, type); } catch (RetrofitError e) { throw e; // Pass through our own errors. } catch (IOException e) { throw RetrofitError.networkError(url, e); } catch (Throwable t) { throw RetrofitError.unexpectedError(url, t); } finally { if (!methodDetails.isSynchronous) { Thread.currentThread().setName(IDLE_THREAD_NAME); } } } } /** Log request headers and body. Consumes request body and returns identical replacement. */ private Request logAndReplaceRequest(Request request) throws IOException { log.log(String.format("---> HTTP %s %s", request.getMethod(), request.getUrl())); for (Header header : request.getHeaders()) { log.log(header.getName() + ": " + header.getValue()); } TypedOutput body = request.getBody(); int bodySize = 0; if (body != null) { if (!request.getHeaders().isEmpty()) { log.log(""); } ByteArrayOutputStream baos = new ByteArrayOutputStream(); body.writeTo(baos); byte[] bodyBytes = baos.toByteArray(); bodySize = bodyBytes.length; String bodyMime = body.mimeType(); String bodyString = new String(bodyBytes, MimeUtil.parseCharset(bodyMime)); for (int i = 0, len = bodyString.length(); i < len; i += LOG_CHUNK_SIZE) { int end = Math.min(len, i + LOG_CHUNK_SIZE); log.log(bodyString.substring(i, end)); } body = new TypedByteArray(bodyMime, bodyBytes); } log.log(String.format("---> END HTTP (%s-byte body)", bodySize)); // Since we consumed the original request, return a new, identical one from its bytes. return new Request(request.getMethod(), request.getUrl(), request.getHeaders(), body); } /** Log response headers and body. Consumes response body and returns identical replacement. */ private Response logAndReplaceResponse(String url, Response response, long elapsedTime) throws IOException { log.log(String.format("<--- HTTP %s %s (%sms)", response.getStatus(), url, elapsedTime)); for (Header header : response.getHeaders()) { log.log(header.getName() + ": " + header.getValue()); } TypedInput body = response.getBody(); int bodySize = 0; if (body != null) { if (!response.getHeaders().isEmpty()) { log.log(""); } if (!(body instanceof TypedByteArray)) { // Read the entire response body to we can log it and replace the original response response = Utils.readBodyToBytesIfNecessary(response); body = response.getBody(); } byte[] bodyBytes = ((TypedByteArray) body).getBytes(); bodySize = bodyBytes.length; String bodyMime = body.mimeType(); String bodyCharset = MimeUtil.parseCharset(bodyMime); String bodyString = new String(bodyBytes, bodyCharset); for (int i = 0, len = bodyString.length(); i < len; i += LOG_CHUNK_SIZE) { int end = Math.min(len, i + LOG_CHUNK_SIZE); log.log(bodyString.substring(i, end)); } } log.log(String.format("<--- END HTTP (%s-byte body)", bodySize)); return response; } private static Profiler.RequestInformation getRequestInfo(String serverUrl, RestMethodInfo methodDetails, Request request) { long contentLength = 0; String contentType = null; TypedOutput body = request.getBody(); if (body != null) { contentLength = body.length(); contentType = body.mimeType(); } return new Profiler.RequestInformation(methodDetails.requestMethod, serverUrl, methodDetails.requestUrl, contentLength, contentType); } /** * Build a new {@link RestAdapter}. *

* Calling the following methods is required before calling {@link #build()}: *

    *
  • {@link #setServer(Server)}
  • *
  • {@link #setClient(Client.Provider)}
  • *
  • {@link #setConverter(Converter)}
  • *
*

* If you are using asynchronous execution (i.e., with {@link Callback Callbacks}) the following * is also required: *

    *
  • {@link #setExecutors(java.util.concurrent.Executor, java.util.concurrent.Executor)}
  • *
*/ public static class Builder { private Server server; private Client.Provider clientProvider; private Executor httpExecutor; private Executor callbackExecutor; private RequestHeaders requestHeaders; private Converter converter; private Profiler profiler; private Log log; private boolean debug; /** API server base URL. */ public Builder setServer(String endpoint) { if (endpoint == null) throw new NullPointerException("endpoint"); return setServer(new Server(endpoint)); } /** API server. */ public Builder setServer(Server server) { if (server == null) throw new NullPointerException("server"); this.server = server; return this; } /** The HTTP client used for requests. */ public Builder setClient(final Client client) { if (client == null) throw new NullPointerException("client"); return setClient(new Client.Provider() { @Override public Client get() { return client; } }); } /** The HTTP client used for requests. */ public Builder setClient(Client.Provider clientProvider) { if (clientProvider == null) throw new NullPointerException("clientProvider"); this.clientProvider = clientProvider; return this; } /** * Executors used for asynchronous HTTP client downloads and callbacks. * * @param httpExecutor Executor on which HTTP client calls will be made. * @param callbackExecutor Executor on which any {@link Callback} methods will be invoked. If * this argument is {@code null} then callback methods will be run on the same thread as the * HTTP client. */ public Builder setExecutors(Executor httpExecutor, Executor callbackExecutor) { if (httpExecutor == null) throw new NullPointerException("httpExecutor"); if (callbackExecutor == null) callbackExecutor = new Utils.SynchronousExecutor(); this.httpExecutor = httpExecutor; this.callbackExecutor = callbackExecutor; return this; } /** */ public Builder setRequestHeaders(RequestHeaders requestHeaders) { if (requestHeaders == null) throw new NullPointerException("requestHeaders"); this.requestHeaders = requestHeaders; return this; } /** The converter used for serialization and deserialization of objects. */ public Builder setConverter(Converter converter) { if (converter == null) throw new NullPointerException("converter"); this.converter = converter; return this; } /** Set the profiler used to measure requests. */ public Builder setProfiler(Profiler profiler) { if (profiler == null) throw new NullPointerException("profiler"); this.profiler = profiler; return this; } /** Configure debug logging mechanism. */ public Builder setLog(Log log) { if (log == null) throw new NullPointerException("log"); this.log = log; return this; } /** Enable debug logging. */ public Builder setDebug(boolean debug) { this.debug = debug; return this; } /** Create the {@link RestAdapter} instances. */ public RestAdapter build() { if (server == null) { throw new IllegalArgumentException("Server may not be null."); } ensureSaneDefaults(); return new RestAdapter(server, clientProvider, httpExecutor, callbackExecutor, requestHeaders, converter, profiler, log, debug); } private void ensureSaneDefaults() { if (converter == null) { converter = Platform.get().defaultConverter(); } if (clientProvider == null) { clientProvider = Platform.get().defaultClient(); } if (httpExecutor == null) { httpExecutor = Platform.get().defaultHttpExecutor(); } if (callbackExecutor == null) { callbackExecutor = Platform.get().defaultCallbackExecutor(); } if (log == null) { log = Platform.get().defaultLog(); } if (requestHeaders == null) { requestHeaders = RequestHeaders.NONE; } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy