/**
* Copyright 2014 Opower, 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 com.opower.rest.client.generator.core;
import com.google.common.base.Predicate;
import com.opower.rest.client.generator.extractors.ClientErrorHandler;
import com.opower.rest.client.generator.extractors.DefaultClientErrorHandler;
import com.opower.rest.client.generator.extractors.DefaultEntityExtractorFactory;
import com.opower.rest.client.generator.util.IsHttpMethod;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.ws.rs.Path;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.opower.rest.client.generator.util.HttpResponseCodes.SC_BAD_REQUEST;
/**
* Base class to make the return types of the inherited builders work correctly.
* @param <T> the type of the client to be created
* @param <B> the type of the concrete builder
*/
public abstract class Client<T, B extends Client<T, B>> {
public static final Predicate<Integer> DEFAULT_ERROR_STATUS_CRITERIA = new Predicate<Integer>() {
@Override
public boolean apply(Integer status) {
checkNotNull(status);
return status >= SC_BAD_REQUEST && status <= NETWORK_CONNECT_TIMEOUT;
}
};
protected static final int NETWORK_CONNECT_TIMEOUT = 599;
private final ConcurrentMap<Method, Predicate<Integer>> errorStatusCriteria = new ConcurrentHashMap<>();
protected ClientExecutor executor;
protected ClientProviders clientProviders = new ClientProviders();
protected List<ClientErrorInterceptor> clientErrorInterceptors;
protected final ResourceInterface<T> resourceInterface;
protected final UriProvider uriProvider;
protected final ClassLoader loader;
protected Client(ResourceInterface<T> resourceInterface, UriProvider uriProvider) {
this.resourceInterface = checkNotNull(resourceInterface);
this.uriProvider = checkNotNull(uriProvider);
this.loader = checkNotNull(resourceInterface.getInterface().getClassLoader());
for (Method method : resourceInterface.getInterface().getMethods()) {
this.errorStatusCriteria.put(method, DEFAULT_ERROR_STATUS_CRITERIA);
}
}
/**
* Configures a custom {@link com.google.common.base.Predicate<Integer>} that defines which Http response codes should
* be treated as errors and cause the client proxy to throw an Exception. The specified {@link com.google.common.base.Predicate<Integer>}
* will be used for all methods on the resource interface.
* @param errorStatusCriteria The Predicate to use.
* @return the builder
*/
@SuppressWarnings("unchecked")
public B errorStatusCriteria(Predicate<Integer> errorStatusCriteria) {
checkNotNull(errorStatusCriteria);
for (Method method : this.resourceInterface.getInterface().getDeclaredMethods()) {
errorStatusCriteriaForMethod(method, errorStatusCriteria);
}
return (B) this;
}
/**
*
* Configures a custom {@link com.google.common.base.Predicate<Integer>} that defines which Http response codes should
* be treated as errors and cause the client proxy to throw an Exception. The specified {@link com.google.common.base.Predicate<Integer>}
* will be used ONLY for the specified method.
*
* @param method the method on the resource interface
* @param errorStatusCriteria the Predicate to use.
* @return the builder
*/
@SuppressWarnings("unchecked")
public B errorStatusCriteriaForMethod(Method method, Predicate<Integer> errorStatusCriteria) {
checkArgument(method != null && method.getDeclaringClass().equals(this.resourceInterface.getInterface()));
this.errorStatusCriteria.put(method, checkNotNull(errorStatusCriteria));
return (B) this;
}
@SuppressWarnings("unchecked")
public B clientErrorInterceptors(List<ClientErrorInterceptor> clientErrorInterceptors) {
this.clientErrorInterceptors = checkNotNull(clientErrorInterceptors);
return (B) this;
}
@SuppressWarnings("unchecked")
public B executor(ClientExecutor exec) {
this.executor = exec;
return (B) this;
}
@SuppressWarnings("unchecked")
public B registerProviderInstance(Object provider) {
this.clientProviders.registerProviderInstance(provider);
return (B) this;
}
public T build() {
if (this.executor == null)
throw new IllegalArgumentException("You must provide a ClientExecutor");
if (this.clientProviders == null)
throw new IllegalArgumentException("you must specify a MessageBodyWriter and a MessageBodyReader for serialization");
final ProxyConfig config = new ProxyConfig(this.loader, this.executor, this.clientProviders, new DefaultEntityExtractorFactory(),
this.errorStatusCriteria, getClientErrorHandler());
return createProxy(this.resourceInterface.getInterface(), this.uriProvider, config);
}
protected ClientErrorHandler getClientErrorHandler() {
return new DefaultClientErrorHandler(this.clientErrorInterceptors);
}
@SuppressWarnings("unchecked")
static <S> S createProxy(final Class<S> iface, UriProvider uriProvider, final ProxyConfig config) {
HashMap<Method, MethodInvoker> methodMap = new HashMap<Method, MethodInvoker>();
for (Method method : iface.getMethods()) {
MethodInvoker invoker;
Set<String> httpMethods = IsHttpMethod.getHttpMethods(method);
if ((httpMethods == null || httpMethods.size() == 0) && method.isAnnotationPresent(Path.class) && method.getReturnType().isInterface()) {
invoker = new SubResourceInvoker(uriProvider, method, config);
} else {
invoker = createClientInvoker(iface, method, uriProvider, config);
}
methodMap.put(method, invoker);
}
Class<?>[] intfs = { iface };
ClientProxy clientProxy = new ClientProxy(methodMap, config);
// this is done so that equals and hashCode work ok. Adding the rest to a
// Collection will cause equals and hashCode to be invoked. The Spring
// infrastructure had some problems without this.
clientProxy.setClazz(iface);
return (S) Proxy.newProxyInstance(config.getLoader(), intfs, clientProxy);
}
private static ClientInvoker createClientInvoker(Class<?> clazz, Method method, UriProvider uriProvider, ProxyConfig config) {
Set<String> httpMethods = IsHttpMethod.getHttpMethods(method);
if (httpMethods == null || httpMethods.size() != 1) {
throw new RuntimeException("You must use at least one, but no more than one http method annotation on: " + method.toString());
}
ClientInvoker invoker = new ClientInvoker(uriProvider, clazz, method, config);
invoker.setHttpMethod(httpMethods.iterator().next());
return invoker;
}
/**
* Basic JAX-RS client proxy builder.
* @param <T> the type of the ResourceInterface to build a client for
*/
public static final class Builder<T> extends Client<T, Builder<T>> {
public Builder(ResourceInterface<T> resourceInterface, UriProvider uriProvider) {
super(resourceInterface, uriProvider);
}
}
}