/*
* Copyright 2010-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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 com.amazonaws.mobileconnectors.apigateway;
import com.amazonaws.ClientConfiguration;
import com.amazonaws.DefaultRequest;
import com.amazonaws.Request;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.Signer;
import com.amazonaws.http.ExecutionContext;
import com.amazonaws.http.HttpClient;
import com.amazonaws.http.HttpMethodName;
import com.amazonaws.http.HttpRequest;
import com.amazonaws.http.HttpRequestFactory;
import com.amazonaws.http.HttpResponse;
import com.amazonaws.http.UrlHttpClient;
import com.amazonaws.mobileconnectors.apigateway.annotation.Operation;
import com.amazonaws.mobileconnectors.apigateway.annotation.Parameter;
import com.amazonaws.util.IOUtils;
import com.amazonaws.util.StringUtils;
import com.google.gson.Gson;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.Collection;
import java.util.Map;
/**
* Invocation handler responsible for serializing a request and deserializing a
* response.
*/
class ApiClientHandler implements InvocationHandler {
private static final Gson gson = new Gson();
private final String endpoint;
private final String apiName;
private final Signer signer;
// credentials provider. If null, request won't be signed
private final AWSCredentialsProvider provider;
// api key to invoke a method. If non null, its value will be sent in
// 'x-api-key' header.
private final String apiKey;
private final HttpClient client;
private final HttpRequestFactory requestFactory;
private final ClientConfiguration clientConfiguration;
ApiClientHandler(String endpoint, String apiName,
Signer signer, AWSCredentialsProvider provider, String apiKey, ClientConfiguration clientConfiguration) {
this.endpoint = endpoint;
this.apiName = apiName;
this.signer = signer;
this.provider = provider;
this.apiKey = apiKey;
this.clientConfiguration = clientConfiguration;
client = new UrlHttpClient(this.clientConfiguration);
requestFactory = new HttpRequestFactory();
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
try {
// the execute method call flow
if (isExecuteMethod(method)) {
final HttpRequest httpRequest = invokeExecuteMethod(args);
final HttpResponse response = client.execute(httpRequest);
return new ApiResponse(response);
} else {
final HttpRequest httpRequest = createHttpRequest(method, args);
final HttpResponse response = client.execute(httpRequest);
return handleResponse(response, method);
}
} catch (final ApiClientException ace) {
throw ace;
} catch (final Exception e) {
final String msg = e.getMessage() == null ? "" : e.getMessage();
throw new ApiClientException(msg, e);
}
}
/**
* Build a {@link HttpRequest} object for the given method.
*
* @param method method that annotated with {@link Operation}
* @param args arguments of the method
* @return a {@link HttpRequest} object
*/
HttpRequest createHttpRequest(Method method, Object[] args) {
final Request<?> request = buildRequest(method, args);
final ExecutionContext context = new ExecutionContext();
String userAgent = apiName;
if (request.getHeaders().containsKey("User-Agent")) {
// append it to execution context
userAgent += " " + request.getHeaders().get("User-Agent");
}
context.setContextUserAgent(userAgent);
return requestFactory.createHttpRequest(request, clientConfiguration, context);
}
/**
* Build a {@link Request} object for the given method.
*
* @param method method that annotated with {@link Operation}
* @param args arguments of the method
* @return a {@link Request} object
*/
Request<?> buildRequest(Method method, Object[] args) {
final Operation op = method.getAnnotation(Operation.class);
if (op == null) {
throw new IllegalArgumentException("Method isn't annotated with Operation");
}
final Request<?> request = new DefaultRequest<Object>(apiName);
request.setResourcePath(op.path());
request.setEndpoint(URI.create(endpoint));
String content = null;
final Annotation[][] annotations = method.getParameterAnnotations();
final int length = annotations.length;
for (int i = 0; i < length; i++) {
// content body
if (annotations[i].length == 0) {
if (content != null) {
throw new IllegalStateException("Can't have more than one Body");
}
content = args[i] == null ? null : gson.toJson(args[i]);
continue;
}
for (final Annotation annotation : annotations[i]) {
if (annotation instanceof Parameter) {
processParameter(request, (Parameter) annotation, args[i]);
break;
}
}
}
final boolean hasContent = content != null;
setHttpMethod(request, op.method(), hasContent);
if (hasContent) {
final byte[] contentBytes = content.getBytes(StringUtils.UTF8);
request.setContent(new ByteArrayInputStream(contentBytes));
request.addHeader("Content-Length", String.valueOf(contentBytes.length));
}
request.addHeader("Content-Type", "application/json");
request.addHeader("Accept", "application/json");
// add the api key
if (apiKey != null) {
request.addHeader("x-api-key", apiKey);
}
// sign the request
if (provider != null && signer != null) {
signer.sign(request, provider.getCredentials());
}
return request;
}
/**
* Process an argument annotated with {@link Parameter}.
*
* @param request request to be set
* @param p annotation
* @param arg argument
*/
void processParameter(Request<?> request, Parameter p, Object arg) {
final String name = p.name();
final String location = p.location();
if ("header".equals(location)) {
request.addHeader(name, String.valueOf(arg));
} else if ("path".equals(location)) {
String path = request.getResourcePath();
path = path.replaceAll("\\{" + name + "\\}", String.valueOf(arg));
request.setResourcePath(path);
} else if ("query".equals(location)) {
if (Map.class.isAssignableFrom(arg.getClass())) {
@SuppressWarnings("unchecked")
final
Map<String, Object> map = (Map<String, Object>) arg;
for (final Map.Entry<String, Object> entry : map.entrySet()) {
request.addParameter(entry.getKey(), String.valueOf(entry.getValue()));
}
} else if (Collection.class.isAssignableFrom(arg.getClass())) {
request.addParameter(name, joinList((Collection<?>) arg));
} else {
request.addParameter(name, String.valueOf(arg));
}
} else {
throw new IllegalArgumentException("unknown parameter location: " + location);
}
}
/**
* Sets HTTP method to the {@link Request} object. If the given method is
* none of GET, POST, PUT, DELETE, and HEAD, then it will be tunneled via
* X-HTTP-Method-Override. Note that not all servers support this header.
*
* @param request request to be set
* @param httpMethod given http method
* @param hasContent indicate whether the request has content body
*/
void setHttpMethod(Request<?> request, String httpMethod, boolean hasContent) {
try {
request.setHttpMethod(HttpMethodName.valueOf(httpMethod));
} catch (final IllegalArgumentException iae) {
// if an HTTP method is unsupported, then 'tunnel' it through
// another method by setting the intended method in the
// X-HTTP-Method-Override header.
request.addHeader("X-HTTP-Method-Override", httpMethod);
// depending on whether the request has content or not, choose an
// appropriate method.
request.setHttpMethod(hasContent ? HttpMethodName.POST : HttpMethodName.GET);
}
}
/**
* Converts response to method's declared returned object
*
* @param response http response
* @param method method
* @return object of method's declared returned type
* @throws Throwable
*/
Object handleResponse(HttpResponse response, Method method) throws Throwable {
final int code = response.getStatusCode();
final InputStream content = response.getContent();
// successful request if code is 2xx
if (code >= 200 && code < 300) {
final Type t = method.getReturnType();
if (t != void.class && content != null) {
final Reader reader = new InputStreamReader(response.getContent(),
StringUtils.UTF8);
final Object obj = gson.fromJson(reader, t);
reader.close();
return obj;
} else {
// discard response
if (content != null) {
content.close();
}
return null;
}
} else {
final String error = content == null ? "" : IOUtils.toString(content);
final ApiClientException ase = new ApiClientException(error);
ase.setStatusCode(response.getStatusCode());
ase.setServiceName(apiName);
final String requestId = response.getHeaders().get("x-amzn-RequestId");
if (requestId != null) {
ase.setRequestId(requestId);
}
throw ase;
}
}
boolean isExecuteMethod(Method method) {
final Operation op = method.getAnnotation(Operation.class);
return op == null && method.getName().equalsIgnoreCase("execute")
&& method.getReturnType().isAssignableFrom(ApiResponse.class)
&& method.getParameterTypes().length == 1
&& method.getParameterTypes()[0].isAssignableFrom(ApiRequest.class);
}
HttpRequest invokeExecuteMethod(Object[] args) {
final ExecutionContext context = new ExecutionContext();
final Request<?> request = ((ApiRequest) args[0]).getRequest();
if (request.getEndpoint() == null) {
request.setEndpoint(URI.create(endpoint));
}
String userAgent = apiName;
if (request.getHeaders().containsKey("User-Agent")) {
// append it to execution context
userAgent += " " + request.getHeaders().get("User-Agent");
}
context.setContextUserAgent(userAgent);
// add the api key
if (apiKey != null) {
request.addHeader("x-api-key", apiKey);
}
// sign the request
if (provider != null && signer != null) {
signer.sign(request, provider.getCredentials());
}
return requestFactory.createHttpRequest(request, clientConfiguration, context);
}
private String joinList(Collection<?> objects) {
if (objects == null || objects.isEmpty()) {
return "";
}
final StringBuilder sb = new StringBuilder();
boolean first = true;
for (final Object object : objects) {
if (first) {
first = false;
} else {
sb.append(",");
}
sb.append(object);
}
return sb.toString();
}
}