google-nomulus/java/google/registry/util/ComparingInvocationHandler.java
mcilwain 07c1f58004 Fix two warnings thrown by Eclipse
One is an unnecessary import and the other is an incorrectly named
Javadoc parameter.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=153095269
2017-04-26 10:36:51 -04:00

200 lines
7.4 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 stringifyResult(
@SuppressWarnings("unused") Method method,
@Nullable Object object) {
return String.valueOf(object);
}
/**
* Checks whether the method results are as similar as we expect.
*
* <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 compareResults(
@SuppressWarnings("unused") Method method,
@Nullable Object actual,
@Nullable Object second) {
return Objects.equals(actual, second);
}
/**
* Checks whether the thrown exceptions are as similar as we expect.
*
* <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 compareThrown(
@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 throwable the exception thrown by a call to method
*/
protected String stringifyThrown(
@SuppressWarnings("unused") Method method,
Throwable throwable) {
return throwable.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 (!compareThrown(method, actualException, secondException)) {
log(
method,
String.format(
"Both implementations threw, but got different exceptions! '%s' vs '%s'",
stringifyThrown(method, actualException),
stringifyThrown(method, secondException)));
}
} else if (actualException != null) {
log(
method,
String.format(
"Only actual implementation threw exception: %s",
stringifyThrown(method, actualException)));
} else if (secondException != null) {
log(
method,
String.format(
"Only second implementation threw exception: %s",
stringifyThrown(method, secondException)));
} else {
// Neither threw exceptions - we compare the results
if (!compareResults(method, actualResult, secondResult)) {
log(
method,
String.format(
"Got different results! '%s' vs '%s'",
stringifyResult(method, actualResult),
stringifyResult(method, secondResult)));
}
}
// Now reproduce the actual implementation's behavior:
if (actualException != null) {
throw actualException;
}
return actualResult;
}
}