com.netflix.hystrix.contrib.javanica.utils.FallbackMethod Maven / Gradle / Ivy
Show all versions of hystrix-javanica Show documentation
/**
* Copyright 2016 Netflix, 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.netflix.hystrix.contrib.javanica.utils;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.base.Supplier;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.command.ExecutionType;
import com.netflix.hystrix.contrib.javanica.exception.FallbackDefinitionException;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static com.netflix.hystrix.contrib.javanica.utils.TypeHelper.flattenTypeVariables;
import static com.netflix.hystrix.contrib.javanica.utils.TypeHelper.isGenericReturnType;
import static com.netflix.hystrix.contrib.javanica.utils.TypeHelper.isParametrizedType;
import static com.netflix.hystrix.contrib.javanica.utils.TypeHelper.isReturnTypeParametrized;
import static com.netflix.hystrix.contrib.javanica.utils.TypeHelper.isTypeVariable;
import static com.netflix.hystrix.contrib.javanica.utils.TypeHelper.isWildcardType;
public class FallbackMethod {
private final Method method;
private final boolean extended;
private final boolean defaultFallback;
private ExecutionType executionType;
public static final FallbackMethod ABSENT = new FallbackMethod(null, false, false);
public FallbackMethod(Method method) {
this(method, false, false);
}
public FallbackMethod(Method method, boolean extended, boolean defaultFallback) {
this.method = method;
this.extended = extended;
this.defaultFallback = defaultFallback;
if (method != null) {
this.executionType = ExecutionType.getExecutionType(method.getReturnType());
}
}
public boolean isCommand() {
return method.isAnnotationPresent(HystrixCommand.class);
}
public boolean isPresent() {
return method != null;
}
public Method getMethod() {
return method;
}
public ExecutionType getExecutionType() {
return executionType;
}
public boolean isExtended() {
return extended;
}
public boolean isDefault() {
return defaultFallback;
}
public void validateReturnType(Method commandMethod) throws FallbackDefinitionException {
if (isPresent()) {
Class> commandReturnType = commandMethod.getReturnType();
if (ExecutionType.OBSERVABLE == ExecutionType.getExecutionType(commandReturnType)) {
if (ExecutionType.OBSERVABLE != getExecutionType()) {
Type commandParametrizedType = commandMethod.getGenericReturnType();
if (isReturnTypeParametrized(commandMethod)) {
commandParametrizedType = getFirstParametrizedType(commandMethod);
}
validateParametrizedType(commandParametrizedType, method.getGenericReturnType(), commandMethod, method);
} else {
validateReturnType(commandMethod, method);
}
} else if (ExecutionType.ASYNCHRONOUS == ExecutionType.getExecutionType(commandReturnType)) {
if (isCommand() && ExecutionType.ASYNCHRONOUS == getExecutionType()) {
validateReturnType(commandMethod, method);
}
if (ExecutionType.ASYNCHRONOUS != getExecutionType()) {
Type commandParametrizedType = commandMethod.getGenericReturnType();
if (isReturnTypeParametrized(commandMethod)) {
commandParametrizedType = getFirstParametrizedType(commandMethod);
}
validateParametrizedType(commandParametrizedType, method.getGenericReturnType(), commandMethod, method);
}
if (!isCommand() && ExecutionType.ASYNCHRONOUS == getExecutionType()) {
throw new FallbackDefinitionException(createErrorMsg(commandMethod, method, "fallback cannot return Future if the fallback isn't command when the command is async."));
}
} else {
if (ExecutionType.ASYNCHRONOUS == getExecutionType()) {
throw new FallbackDefinitionException(createErrorMsg(commandMethod, method, "fallback cannot return Future if command isn't asynchronous."));
}
if (ExecutionType.OBSERVABLE == getExecutionType()) {
throw new FallbackDefinitionException(createErrorMsg(commandMethod, method, "fallback cannot return Observable if command isn't observable."));
}
validateReturnType(commandMethod, method);
}
}
}
private Type getFirstParametrizedType(Method m) {
Type gtype = m.getGenericReturnType();
if (gtype instanceof ParameterizedType) {
ParameterizedType pType = (ParameterizedType) gtype;
return pType.getActualTypeArguments()[0];
}
return null;
}
private void validateReturnType(Method commandMethod, Method fallbackMethod) {
if (isGenericReturnType(commandMethod)) {
List commandParametrizedTypes = flattenTypeVariables(commandMethod.getGenericReturnType());
List fallbackParametrizedTypes = flattenTypeVariables(fallbackMethod.getGenericReturnType());
Result result = equalsParametrizedTypes(commandParametrizedTypes, fallbackParametrizedTypes);
if (!result.success) {
List msg = new ArrayList();
for (Error error : result.errors) {
Optional parentKindOpt = getParentKind(error.commandType, commandParametrizedTypes);
String extraHint = "";
if (parentKindOpt.isPresent()) {
Type parentKind = parentKindOpt.get();
if (isParametrizedType(parentKind)) {
extraHint = "--> " + ((ParameterizedType) parentKind).getRawType().toString() + "\n";
}
}
msg.add(String.format(error.reason + "\n" + extraHint + "Command type literal pos: %s; Fallback type literal pos: %s",
positionAsString(error.commandType, commandParametrizedTypes),
positionAsString(error.fallbackType, fallbackParametrizedTypes)));
}
throw new FallbackDefinitionException(createErrorMsg(commandMethod, method, StringUtils.join(msg, "\n")));
}
}
validatePlainReturnType(commandMethod, fallbackMethod);
}
private void validatePlainReturnType(Method commandMethod, Method fallbackMethod) {
validatePlainReturnType(commandMethod.getReturnType(), fallbackMethod.getReturnType(), commandMethod, fallbackMethod);
}
private void validatePlainReturnType(Class> commandReturnType, Class> fallbackReturnType, Method commandMethod, Method fallbackMethod) {
if (!commandReturnType.isAssignableFrom(fallbackReturnType)) {
throw new FallbackDefinitionException(createErrorMsg(commandMethod, fallbackMethod, "Fallback method '"
+ fallbackMethod + "' must return: " + commandReturnType + " or its subclass"));
}
}
private void validateParametrizedType(Type commandReturnType, Type fallbackReturnType, Method commandMethod, Method fallbackMethod) {
if (!commandReturnType.equals(fallbackReturnType)) {
throw new FallbackDefinitionException(createErrorMsg(commandMethod, fallbackMethod, "Fallback method '"
+ fallbackMethod + "' must return: " + commandReturnType + " or its subclass"));
}
}
private String createErrorMsg(Method commandMethod, Method fallbackMethod, String hint) {
return "Incompatible return types. \nCommand method: " + commandMethod + ";\nFallback method: " + fallbackMethod + ";\n"
+ (StringUtils.isNotBlank(hint) ? "Hint: " + hint : "");
}
private static final Result SUCCESS = Result.success();
private Result equalsParametrizedTypes(List commandParametrizedTypes, List fallbackParametrizedTypes) {
if (commandParametrizedTypes.size() != fallbackParametrizedTypes.size()) {
return Result.failure(Collections.singletonList(
new Error("Different size of types variables.\n" +
"Command type literals size = " + commandParametrizedTypes.size() + ": " + commandParametrizedTypes + "\n" +
"Fallback type literals size = " + fallbackParametrizedTypes.size() + ": " + fallbackParametrizedTypes + "\n"
)));
}
for (int i = 0; i < commandParametrizedTypes.size(); i++) {
Type commandParametrizedType = commandParametrizedTypes.get(i);
Type fallbackParametrizedType = fallbackParametrizedTypes.get(i);
Result result = equals(commandParametrizedType, fallbackParametrizedType);
if (!result.success) return result;
}
return SUCCESS;
}
// Regular Type#equals method cannot be used to compare parametrized types and type variables
// because it compares generic declarations, see java.lang.reflect.GenericDeclaration.
// If generic declaration is an instance of java.lang.reflect.Method then command and fallback return types have with different generic declarations which aren't the same.
// In this case we need to compare only few type properties, such as bounds for type literal and row types for parametrized types.
private static Result equals(Type commandType, Type fallbackType) {
if (isParametrizedType(commandType) && isParametrizedType(fallbackType)) {
final ParameterizedType pt1 = (ParameterizedType) commandType;
final ParameterizedType pt2 = (ParameterizedType) fallbackType;
Result result = regularEquals(pt1.getRawType(), pt2.getRawType());
return result.andThen(new Supplier() {
@Override
public Result get() {
return FallbackMethod.equals(pt1.getActualTypeArguments(), pt2.getActualTypeArguments());
}
});
} else if (isTypeVariable(commandType) && isTypeVariable(fallbackType)) {
final TypeVariable tv1 = (TypeVariable) commandType;
final TypeVariable tv2 = (TypeVariable) fallbackType;
if (tv1.getGenericDeclaration() instanceof Method && tv2.getGenericDeclaration() instanceof Method) {
Result result = equals(tv1.getBounds(), tv2.getBounds());
return result.append(new Supplier>() {
@Override
public List get() {
return Collections.singletonList(boundsError(tv1, tv1.getBounds(), "", tv2, tv2.getBounds()));
}
});
}
return regularEquals(tv1, tv2);
} else if (isWildcardType(commandType) && isWildcardType(fallbackType)) {
final WildcardType wt1 = (WildcardType) commandType;
final WildcardType wt2 = (WildcardType) fallbackType;
Result result = equals(wt1.getLowerBounds(), wt2.getLowerBounds());
result = result.append(new Supplier>() {
@Override
public List get() {
return Collections.singletonList(boundsError(wt1, wt1.getLowerBounds(), "lower", wt2, wt2.getLowerBounds()));
}
});
if (result.isFailure()) return result;
result = equals(wt1.getUpperBounds(), wt2.getUpperBounds());
return result.append(new Supplier>() {
@Override
public List get() {
return Collections.singletonList(boundsError(wt1, wt1.getUpperBounds(), "upper", wt2, wt2.getUpperBounds()));
}
});
} else {
return regularEquals(commandType, fallbackType);
}
}
private static Result regularEquals(final Type commandType, final Type fallbackType) {
return Result.of(Objects.equal(commandType, fallbackType), new Supplier>() {
@Override
public List get() {
return Collections.singletonList(new Error(
commandType,
String.format("Different types. Command type: '%s'; fallback type: '%s'", commandType, fallbackType),
fallbackType));
}
});
}
private static Optional getParentKind(Type type, List types) {
int pos = position(type, types);
if (pos <= 0) return Optional.absent();
return Optional.of(types.get(pos - 1));
}
private static String positionAsString(Type type, List types) {
int pos = position(type, types);
if (pos < 0) {
return "unknown";
}
return String.valueOf(pos);
}
private static int position(Type type, List types) {
if (type == null) return -1;
if (types == null || types.isEmpty()) return -1;
return types.indexOf(type);
}
private static Error boundsError(Type t1, Type[] b1, String boundType, Type t2, Type[] b2) {
return new Error(t1,
String.format("Different %s bounds. Command bounds: '%s'; Fallback bounds: '%s'",
boundType,
StringUtils.join(b1, ", "),
StringUtils.join(b2, ", ")),
t2);
}
private static Result equals(Type[] t1, Type[] t2) {
if (t1 == null && t2 == null) return SUCCESS;
if (t1 == null) return Result.failure();
if (t2 == null) return Result.failure();
if (t1.length != t2.length)
return Result.failure(new Error(String.format("Different size of type literals. Command size = %d, fallback size = %d",
t1.length, t2.length)));
Result result = SUCCESS;
for (int i = 0; i < t1.length; i++) {
result = result.combine(equals(t1[i], t2[i]));
if (result.isFailure()) return result;
}
return result;
}
private static class Result {
boolean success;
List errors = Collections.emptyList();
boolean isSuccess() {
return success;
}
boolean isFailure() {
return !success;
}
static Result of(boolean res, Supplier> errors) {
if (res) return success();
return failure(errors.get());
}
static Result success() {
return new Result(true);
}
static Result failure() {
return new Result(false);
}
static Result failure(Error... errors) {
return new Result(false, Arrays.asList(errors));
}
static Result failure(List errors) {
return new Result(false, errors);
}
Result combine(Result r) {
return new Result(this.success && r.success, merge(this.errors, r.errors));
}
Result andThen(Supplier resultSupplier) {
if (!success) return this;
return resultSupplier.get();
}
Result append(List errors) {
if (success) return this;
return failure(merge(this.errors, errors));
}
Result append(Supplier> errors) {
if (success) return this;
return append(errors.get());
}
static List merge(@Nonnull List e1, @Nonnull List e2) {
List res = new ArrayList(e1.size() + e2.size());
res.addAll(e1);
res.addAll(e2);
return Collections.unmodifiableList(res);
}
Result(boolean success, List errors) {
Validate.notNull(errors, "errors cannot be null");
this.success = success;
this.errors = errors;
}
Result(boolean success) {
this.success = success;
this.errors = Collections.emptyList();
}
}
private static class Error {
@Nullable
Type commandType;
String reason;
@Nullable
Type fallbackType;
Error(String reason) {
this.reason = reason;
}
Error(Type commandType, String reason, Type fallbackType) {
this.commandType = commandType;
this.reason = reason;
this.fallbackType = fallbackType;
}
}
}