google-nomulus/java/google/registry/util/ComparingInvocationHandler.java
Ben McIlwain 9a11f125ff Remove "@ForOverride" from .toString and .equals
Inheriting classes reimplementing these methods might want to call the super.
version of them in some cases (only change the default behavior for some
methods).

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=149772767
2017-03-13 11:22:56 -04:00

200 lines
7.3 KiB
Java

// Copyright 2017 The Nomulus Authors. 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.
// 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 google.registry.util;
import com.google.common.reflect.Reflection;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Objects;
import javax.annotation.Nullable;
/**
* Abstract InvocationHandler comparing two implementations of some interface.
*
* <p>Given an interface, and two instances of that interface (the "original" instance we know
* works, and a "second" instance we wish to test), creates an InvocationHandler that acts like an
* exact proxy to the "original" instance.
*
* <p>In addition, it will log any differences in return values or thrown exception between the
* "original" and "second" instances.
*
* <p>This can be used to create an exact proxy to the original instance that can be placed in any
* code, while live testing the second instance.
*/
public abstract class ComparingInvocationHandler<T> implements InvocationHandler {
private final T actualImplementation;
private final T secondImplementation;
private final Class<T> interfaceClass;
/**
* Creates a new InvocationHandler for the given interface.
*
* @param interfaceClass the interface we want to create.
* @param actualImplementation the resulting proxy will be an exact proxy to this object
* @param secondImplementation Only used to log difference compared to actualImplementation.
* Otherwise has no effect on the resulting proxy's behavior.
*/
public ComparingInvocationHandler(
Class<T> interfaceClass, T actualImplementation, T secondImplementation) {
this.actualImplementation = actualImplementation;
this.secondImplementation = secondImplementation;
this.interfaceClass = interfaceClass;
}
/**
* Returns the proxy to the actualImplementation.
*
* <p>The return value is a drop-in replacement to the actualImplementation, but will log any
* difference with the secondImplementation during normal execution.
*/
public final T makeProxy() {
return Reflection.newProxy(interfaceClass, this);
}
/**
* Called when there was a difference between the implementations.
*
* @param method the method where the difference was found
* @param message human readable description of the difference found
*/
protected abstract void log(Method method, String message);
/**
* Implements toString for specific types.
*
* <p>By default objects are logged using their .toString. If .toString isn't implemented for
* some relevant classes (or if we want to use a different version), override this method with
* the desired implementation.
*
* @param method the method whose return value is given
* @param object the object returned by a call to method
*/
protected String toString(
@SuppressWarnings("unused") Method method,
@Nullable Object object) {
return String.valueOf(object);
}
/**
* Implements equals for specific types.
*
* <p>By default objects are compared using their .equals. If .equals isn't implemented for some
* relevant classes (or if we want change what is considered "not equal"), override this method
* with the desired implementation.
*
* @param method the method whose return value is given
* @param actual the object returned by a call to method for the "actual" implementation
* @param second the object returned by a call to method for the "second" implementation
*/
protected boolean equals(
@SuppressWarnings("unused") Method method,
@Nullable Object actual,
@Nullable Object second) {
return Objects.equals(actual, second);
}
/**
* Implements equals for thrown exceptions.
*
* <p>By default this returns 'true' for any input: all we check by default is that both
* implementations threw something. Override if you need to actually compare both throwables.
*
* @param method the method whose return value is given
* @param actual the exception thrown by a call to method for the "actual" implementation
* @param second the exception thrown by a call to method for the "second" implementation
*/
protected boolean exceptionEquals(
@SuppressWarnings("unused") Method method,
Throwable actual,
Throwable second) {
return true;
}
/**
* Implements toString for thrown exceptions.
*
* <p>By default exceptions are logged using their .toString. If more data is needed (part of
* stack trace for example), override this method with the desired implementation.
*
* @param method the method whose return value is given
* @param exception the exception thrown by a call to method
*/
protected String exceptionToString(
@SuppressWarnings("unused") Method method,
Throwable exception) {
return exception.toString();
}
@Override
public final Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object actualResult = null;
Throwable actualException = null;
try {
actualResult = method.invoke(actualImplementation, args);
} catch (InvocationTargetException e) {
actualException = e.getCause();
}
Object secondResult = null;
Throwable secondException = null;
try {
secondResult = method.invoke(secondImplementation, args);
} catch (InvocationTargetException e) {
secondException = e.getCause();
}
// First compare the two implementations' result, and log any differences:
if (actualException != null && secondException != null) {
if (!exceptionEquals(method, actualException, secondException)) {
log(
method,
String.format(
"Both implementations threw, but got different exceptions! '%s' vs '%s'",
exceptionToString(method, actualException),
exceptionToString(method, secondException)));
}
} else if (actualException != null) {
log(
method,
String.format(
"Only actual implementation threw exception: %s",
exceptionToString(method, actualException)));
} else if (secondException != null) {
log(
method,
String.format(
"Only second implementation threw exception: %s",
exceptionToString(method, secondException)));
} else {
// Neither threw exceptions - we compare the results
if (!equals(method, actualResult, secondResult)) {
log(
method,
String.format(
"Got different results! '%s' vs '%s'",
toString(method, actualResult),
toString(method, secondResult)));
}
}
// Now reproduce the actual implementation's behavior:
if (actualException != null) {
throw actualException;
}
return actualResult;
}
}