/**
* 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.hystrix;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixCommandProperties;
import com.netflix.hystrix.HystrixThreadPoolKey;
import com.netflix.hystrix.HystrixThreadPoolProperties;
import com.opower.rest.client.ConfigurationCallback;
import com.opower.rest.client.generator.core.Client;
import com.opower.rest.client.generator.core.ResourceInterface;
import com.opower.rest.client.generator.core.UriProvider;
import com.opower.rest.client.generator.extractors.ClientErrorHandler;
import com.opower.rest.client.generator.hystrix.HystrixClientErrorHandler.BadRequestCriteria;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.Callable;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* 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 HystrixClient<T, B extends HystrixClient<T, B>> extends Client<T, B> {
protected final HystrixCommandGroupKey groupKey;
// You don't get a fallback by default. You have to provide one
protected Map<Method, Callable<? extends Object>> fallbackMap = ImmutableMap.of();
// The default of a thread pool per HystrixCommandKey is sufficient. If you need something different this will allow that.
protected Map<Method, HystrixThreadPoolKey> threadPoolKeysMap = ImmutableMap.of();
// we will make default versions of all these and allow people to tweak them with callbacks
protected final Map<Method, HystrixThreadPoolProperties.Setter> threadPoolPropertiesMap;
protected final Map<Method, HystrixCommandProperties.Setter> commandPropertiesMap;
protected Map<Method, HystrixCommandKey> commandKeyMap;
private Map<Method, BadRequestCriteria> badRequestCriteriaMap = ImmutableMap.of();
/**
* Creates a HystrixClientBuilder with the default HystrixCommand.Setter based on the ResourceClass name.
*
* @param resourceInterface The ResourceClass to create a client for
* @param uriProvider The uriProvider to use.
* @param groupKey The HystrixCommandGroupKey to use
*/
protected HystrixClient(ResourceInterface<T> resourceInterface,
UriProvider uriProvider,
HystrixCommandGroupKey groupKey) {
super(resourceInterface, uriProvider);
this.groupKey = checkNotNull(groupKey);
ImmutableMap.Builder<Method, HystrixCommandKey> commandKeyMapBuilder = ImmutableMap.builder();
ImmutableMap.Builder<Method, HystrixCommandProperties.Setter> commandPropertiesMapBuilder = ImmutableMap.builder();
ImmutableMap.Builder<Method, HystrixThreadPoolProperties.Setter> threadPoolPropertiesBuilder = ImmutableMap.builder();
for (Method method : resourceInterface.getInterface().getMethods()) {
commandKeyMapBuilder.put(method, keyForMethod(method));
// fallbacks disabled till you specify one
commandPropertiesMapBuilder.put(method, HystrixCommandProperties.Setter().withFallbackEnabled(false));
threadPoolPropertiesBuilder.put(method, HystrixThreadPoolProperties.Setter());
}
this.commandKeyMap = commandKeyMapBuilder.build();
this.commandPropertiesMap = commandPropertiesMapBuilder.build();
this.threadPoolPropertiesMap = threadPoolPropertiesBuilder.build();
}
/**
* Generate the HystrixCommandKey for the given method. The command key will be of this format:
* <p/>
* < canonicalName of method's declaring class >.< method name>
*
* @param method the method to generate a HystrixCommandKey for
* @return the HystrixCommandKey
*/
public static HystrixCommandKey keyForMethod(Method method) {
return HystrixCommandKey.Factory.asKey(String
.format("%s.%s",
method.getDeclaringClass().getCanonicalName(),
method.getName()));
}
@SuppressWarnings("unchecked")
private <P> B applyCallback(Map<Method, P> map, Method method, ConfigurationCallback<P> callback) {
if (map.containsKey(checkMethod(method))) {
checkNotNull(callback).configure(map.get(method));
} else {
throw new IllegalArgumentException(String.format("Method %s is not a method on the ResourceInterface", method));
}
return (B) this;
}
/**
* Specify custom HystrixCommandProperties for a specific method on the ResourceInterface.
*
* @param method the method to apply the HystrixCommandProperties to
* @param callback the ConfigurationCallback that applies your custom settings
* @return the HystrixClientBuilder
*/
public B methodProperties(Method method, ConfigurationCallback<HystrixCommandProperties.Setter> callback) {
return applyCallback(this.commandPropertiesMap, checkMethod(method), callback);
}
/**
* Apply custom HystrixCommandProperties for all methods on the ResourceInterface.
*
* @param callback the ConfigurationCallback that applies your custom settings to all methods
* @return the HystrixClientBuilder
*/
@SuppressWarnings("unchecked")
public B commandProperties(ConfigurationCallback<HystrixCommandProperties.Setter> callback) {
for (HystrixCommandProperties.Setter setter : this.commandPropertiesMap.values()) {
checkNotNull(callback).configure(setter);
}
return (B) this;
}
/**
* Specify custom HystrixThreadPoolProperties for a specific method on the ResourceInterface.
*
* @param method the method to apply the HystrixThreadPoolProperties to
* @param callback the ConfigurationCallback that applies your custom settings
* @return the HystrixClientBuilder
*/
public B methodThreadPoolProperties(Method method,
ConfigurationCallback<HystrixThreadPoolProperties.Setter> callback) {
return applyCallback(this.threadPoolPropertiesMap, checkMethod(method), callback);
}
/**
* Apply custom HystrixThreadPoolProperties for all methods on the ResourceInterface.
*
* @param callback the ConfigurationCallback that applies your custom settings to all methods
* @return the HystrixClientBuilder
*/
@SuppressWarnings("unchecked")
public B threadPoolProperties(ConfigurationCallback<HystrixThreadPoolProperties.Setter> callback) {
for (HystrixThreadPoolProperties.Setter setter : this.threadPoolPropertiesMap.values()) {
checkNotNull(callback).configure(setter);
}
return (B) this;
}
/**
* Specify a custom HystrixCommandKey for a particular method on the ResourceInterface.
*
* @param method the method to use this HystrixCommandKey for
* @param commandKey the HystrixCommandKey to use
* @return the HystrixClientBuilder
*/
@SuppressWarnings("unchecked")
public B methodCommandKey(Method method, HystrixCommandKey commandKey) {
this.commandKeyMap = ImmutableMap.<Method, HystrixCommandKey>builder()
.putAll(this.commandKeyMap)
.put(checkMethod(method),
commandKey)
.build();
return (B) this;
}
/**
* Specify a specific fallback for a particular method on the ResourceClass.
*
* @param method the method that this fallback is to be used for
* @param fallback the fallback to use
* @return the HystrixClientBuilder
*/
@SuppressWarnings("unchecked")
public B methodFallback(Method method, Callable<?> fallback) {
this.fallbackMap = ImmutableMap.<Method, Callable<?>>builder()
.putAll(this.fallbackMap).put(checkMethod(method), fallback).build();
this.commandPropertiesMap.get(method).withFallbackEnabled(true);
return (B) this;
}
/**
* Specify specific criteria for bad requests for a particular method on the ResourceClass.
*
* @param method the method that this fallback is to be used for
* @param badRequestCriteria the criteria to use
* @return the HystrixClientBuilder
*/
@SuppressWarnings("unchecked")
public B methodBadRequestCriteria(Method method, BadRequestCriteria badRequestCriteria) {
ImmutableMap.Builder<Method, BadRequestCriteria> builder = ImmutableMap.builder();
builder.putAll(this.badRequestCriteriaMap).put(checkMethod(method), checkNotNull(badRequestCriteria));
this.badRequestCriteriaMap = builder.build();
return (B) this;
}
/**
* Specify default criteria for bad requests on the ResourceClass.
*
* @param badRequestCriteria the criteria to use
* @return the HystrixClientBuilder
*/
@SuppressWarnings("unchecked")
public B badRequestCriteria(BadRequestCriteria badRequestCriteria) {
checkNotNull(badRequestCriteria);
ImmutableMap.Builder<Method, BadRequestCriteria> builder = ImmutableMap.builder();
for (Method method : this.resourceInterface.getInterface().getMethods()) {
builder.put(method, badRequestCriteria);
}
this.badRequestCriteriaMap = builder.build();
return (B) this;
}
@Override
protected ClientErrorHandler getClientErrorHandler() {
return new HystrixClientErrorHandler(this.badRequestCriteriaMap, super.getClientErrorHandler());
}
@Override
public T build() {
return HystrixCommandInvocationHandler.proxy(this.resourceInterface.getInterface(),
super.build(),
ImmutableMap.copyOf(assembleHystrixCommandSetters()),
ImmutableMap.copyOf(this.fallbackMap));
}
private Map<Method, HystrixCommand.Setter> assembleHystrixCommandSetters() {
return Maps.transformEntries(this.commandKeyMap,
new Maps.EntryTransformer<Method, HystrixCommandKey, HystrixCommand.Setter>() {
@Override
public HystrixCommand.Setter transformEntry(Method method, HystrixCommandKey value) {
HystrixClient<T, B> builder = HystrixClient.this;
HystrixCommand.Setter setter = HystrixCommand.Setter
.withGroupKey(builder.groupKey)
.andCommandKey(value)
.andCommandPropertiesDefaults(builder.commandPropertiesMap.get(method))
.andThreadPoolPropertiesDefaults(builder.threadPoolPropertiesMap.get(method));
if (builder.threadPoolKeysMap.containsKey(method)) {
setter.andThreadPoolKey(builder.threadPoolKeysMap.get(method));
}
return setter;
}
});
}
/**
* Ensures that the provided method is from the resource interface.
* @param method the method in question
* @return the method for convenience
*/
protected Method checkMethod(Method method) {
checkArgument(method != null && method.getDeclaringClass().equals(this.resourceInterface.getInterface()),
String.format("Only methods from the resource interface %s are valid",
this.resourceInterface.getInterface().getCanonicalName()));
return method;
}
/**
* ClientBuilder that adds basic Hystrix capabilities to each client instance. The resulting client will use the provided
* HystrixCommandGroupKey and a HystrixCommandKey per method on the ResourceInterface by default.
*
* @author chris.phillips
* @param <T> The type of the Client we are building
*/
public static final class Builder<T> extends HystrixClient<T, Builder<T>> {
/**
* Creates a HystrixClientBuilder with the default HystrixCommand.Setter based on the ResourceClass name.
*
* @param resourceInterface The ResourceClass to create a client for
* @param uriProvider The uriProvider to use.
* @param groupKey The HystrixCommandGroupKey to use
*/
@SuppressWarnings("unchecked")
public Builder(ResourceInterface resourceInterface, UriProvider uriProvider, HystrixCommandGroupKey groupKey) {
super(resourceInterface, uriProvider, groupKey);
}
}
}