/*
* Copyright 2010-2015 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.hal.client;
import com.amazonaws.hal.Link;
import com.amazonaws.hal.ResourceInfo;
import com.amazonaws.hal.UriValue;
import com.amazonaws.hal.UriVariable;
import com.damnhandy.uri.template.MalformedUriTemplateException;
import com.damnhandy.uri.template.UriTemplate;
import com.damnhandy.uri.template.VariableExpansionException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.lang.annotation.Annotation;
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.List;
import java.util.Map;
import static com.amazonaws.hal.client.ConversionUtil.convert;
import static com.amazonaws.hal.client.ConversionUtil.getCollectionType;
import static com.amazonaws.hal.client.ConversionUtil.getPropertyName;
class HalResourceInvocationHandler
implements InvocationHandler {
//-------------------------------------------------------------
// Variables - Private
//-------------------------------------------------------------
private HalResource halResource;
private String resourcePath;
private HalClient halClient;
private static Log log = LogFactory.getLog(HalResourceInvocationHandler.class);
private static final Object[] EMPTY_ARGS = new Object[0];
//-------------------------------------------------------------
// Constructors
//-------------------------------------------------------------
HalResourceInvocationHandler(HalResource halResource, String resourcePath, HalClient halClient) {
this.halResource = halResource;
this.resourcePath = resourcePath;
this.halClient = halClient;
}
//-------------------------------------------------------------
// Implementation - InvocationHandler
//-------------------------------------------------------------
/**
*/
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if (halResource == null || !halResource.isDefined()) {
halResource = halClient.getHalResource(resourcePath);
}
try {
Method resourceInfoMethod = ResourceInfo.class.getMethod(method.getName(), method.getParameterTypes());
return resourceInfoMethod.invoke(halResource, args);
} catch (NoSuchMethodException ignore) {
// If the method is not defined in ResourceInfo, we handle it below
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
Link link;
if ((link = method.getAnnotation(Link.class)) != null) {
switch (link.method()) {
case GET:
if (List.class.isAssignableFrom(method.getReturnType())) {
//noinspection unchecked
return new HalLinkList(halResource, link.relation(),
(Class) getCollectionType(method.getGenericReturnType(), 0, ResourceInfo.class),
halClient);
} else if (Map.class.isAssignableFrom(method.getReturnType())) {
//noinspection unchecked
return new HalLinkMap(halResource, link.relation(), link.keyField(),
(Class) getCollectionType(method.getGenericReturnType(), 1, ResourceInfo.class),
halClient);
} else {
return halClient.getResource(halResource, method.getReturnType(),
getRelationHref(link, args == null ? EMPTY_ARGS : args, method.getParameterAnnotations()), false);
}
case POST:
if (args == null) {
throw new IllegalArgumentException("POST operations require a representation argument.");
}
return halClient.postResource(method.getReturnType(),
getRelationHref(link, args, method.getParameterAnnotations()), args[0]);
case PUT:
if (args == null) {
throw new IllegalArgumentException("PUT operations require a representation argument.");
}
return halClient.putResource(method.getReturnType(),
getRelationHref(link, args, method.getParameterAnnotations()), args[0]);
case DELETE:
return halClient.deleteResource(method.getReturnType(),
getRelationHref(link, args == null ? EMPTY_ARGS : args, method.getParameterAnnotations()));
case PATCH:
if (args == null) {
throw new IllegalArgumentException("PATCH operations require a representation argument.");
}
return halClient.patchResource(method.getReturnType(),
getRelationHref(link, args, method.getParameterAnnotations()), args[0]);
default:
throw new UnsupportedOperationException("Unexpected HTTP method: " + link.method());
}
} else if (method.getName().startsWith("get")) {
String propertyName = getPropertyName(method.getName());
Object property = halResource.getProperty(propertyName);
Type returnType = method.getGenericReturnType();
// When a value is accessed, it's intended type can either be a
// class or some other type (like a ParameterizedType).
//
// If the target type is a class and the value is of that type,
// we return it. If the value is not of that type, we convert
// it and store the converted value (trusting it was converted
// properly) back to the backing store.
//
// If the target type is not a class, it may be ParameterizedType
// like List<T> or Map<K, V>. We check if the value is already
// a converting type and if so, we return it. If the value is
// not, we convert it and if it's now a converting type, we store
// the new value in the backing store.
if (returnType instanceof Class) {
if (!((Class) returnType).isInstance(property)) {
property = convert(returnType, property);
//noinspection unchecked
halResource.addProperty(propertyName, property);
}
} else {
if (!(property instanceof ConvertingMap) && !(property instanceof ConvertingList)) {
property = convert(returnType, property);
if (property instanceof ConvertingMap || property instanceof ConvertingList) {
//noinspection unchecked
halResource.addProperty(propertyName, property);
}
}
}
return property;
} else if (method.getName().equals("toString") && args == null) {
return resourcePath;
} else if (method.getName().equals("equals") && args != null && args.length == 1) {
HalResourceInvocationHandler other;
try {
other = (HalResourceInvocationHandler) Proxy.getInvocationHandler(args[0]);
} catch (IllegalArgumentException e) {
// argument is not a proxy
return false;
} catch (ClassCastException e) {
// argument is the wrong type of proxy
return false;
}
return resourcePath.equals(other.resourcePath);
} else if (method.getName().equals("hashCode") && args == null) {
return resourcePath.hashCode();
}
throw new UnsupportedOperationException("Don't know how to handle '" + method.getName() + "'");
}
//-------------------------------------------------------------
// Methods - Package
//-------------------------------------------------------------
/**
* The resource this InvocationHandler manages has been updated or deemed stale.
*
* @param halResource The new HalResource or null if the resource was stale and the new value is not yet known.
*/
void resourceUpdated(HalResource halResource) {
this.halResource = halResource;
}
//-------------------------------------------------------------
// Methods - Private
//-------------------------------------------------------------
private String getRelationHref(Link link, Object[] args, Annotation[][] parameterAnnotations) {
HalLink halLink = halResource.getLink(link.relation());
if (halLink == null) {
throw new UnsupportedOperationException(link.relation());
}
if (halLink.getDeprecation() != null) {
log.warn("Link '" + link + "' has been deprecated: " + halLink.getDeprecation());
}
String href;
if (halLink.isTemplated()) {
try {
UriTemplate uriTemplate = UriTemplate.fromTemplate(halLink.getHref());
for (int i = 0; i < args.length; i++) {
for (Annotation annotation : parameterAnnotations[i]) {
if (annotation.annotationType() == UriVariable.class) {
UriVariable uriVariable = (UriVariable) annotation;
assignTemplateValue(uriTemplate, uriVariable.name(), args[i]);
}
}
}
for (int i = 0; i < link.uriValues().length; i++) {
UriValue uriValue = link.uriValues()[i];
assignTemplateValue(uriTemplate, uriValue.name(), uriValue.value());
}
href = uriTemplate.expand();
} catch (MalformedUriTemplateException | VariableExpansionException e) {
throw new RuntimeException(e);
}
} else {
href = halLink.getHref();
}
return href;
}
private void assignTemplateValue(UriTemplate uriTemplate, String variableName, Object value) {
if (uriTemplate.hasVariable(variableName)) {
log.warn(String.format("Duplicate assignment to variable %s. Current = '%s', skipping new value '%s'.",
variableName, uriTemplate.get(variableName), value));
return;
}
uriTemplate.set(variableName, value);
}
}