Skip to content

Instantly share code, notes, and snippets.

@oasisfeng
Last active April 6, 2023 04:54
Show Gist options
  • Star 23 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save oasisfeng/75d3774ca5441372f049818de4d52605 to your computer and use it in GitHub Desktop.
Save oasisfeng/75d3774ca5441372f049818de4d52605 to your computer and use it in GitHub Desktop.
Reflection helper for hacking non-public APIs.
package com.oasisfeng.hack;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import java.io.IOException;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Comparator;
/**
* Java reflection helper optimized for hacking non-public APIs.
* The core design philosophy behind is compile-time consistency enforcement.
*
* It's suggested to declare all hacks in a centralized point, typically as static fields in a class.
* Then call it during application initialization, thus they are verified all together in an early stage.
* If any assertion failed, a fall-back strategy is suggested.
*
* <p>https://gist.github.com/oasisfeng/75d3774ca5441372f049818de4d52605
*
* @see Demo
*
* @author Oasis
*/
@SuppressWarnings({"Convert2Lambda", "WeakerAccess", "unused"})
public class Hack {
public static Class<?> ANY_TYPE = $.class; private static class $ {}
// TODO: Also lazy-resolve class by name
private static final boolean LAZY_RESOLVE = true; // Disable if you want all hacks verified in initialization
public static class AssertionException extends Throwable {
private Class<?> mClass;
private Field mHackedField;
private Method mHackedMethod;
private String mHackedFieldName;
private @Nullable String mHackedMethodName;
private Class<?>[] mParamTypes;
AssertionException(final String e) { super(e); }
AssertionException(final Exception e) { super(e); }
@Override public String toString() {
return getCause() != null ? getClass().getName() + ": " + getCause() : super.toString();
}
public String getDebugInfo() {
final StringBuilder info = new StringBuilder(getCause() != null ? getCause().toString() : super.toString());
final Throwable cause = getCause();
if (cause instanceof NoSuchMethodException) {
info.append(" Potential candidates:");
final int initial_length = info.length();
final String name = getHackedMethodName();
if (name != null) {
for (final Method method : getHackedClass().getDeclaredMethods())
if (method.getName().equals(name)) // Exact name match
info.append(' ').append(method);
if (info.length() == initial_length)
for (final Method method : getHackedClass().getDeclaredMethods())
if (method.getName().startsWith(name)) // Name prefix match
info.append(' ').append(method);
if (info.length() == initial_length)
for (final Method method : getHackedClass().getDeclaredMethods())
if (! method.getName().startsWith("-")) // Dump all but generated methods
info.append(' ').append(method);
} else for (final Constructor<?> constructor : getHackedClass().getDeclaredConstructors())
info.append(' ').append(constructor);
} else if (cause instanceof NoSuchFieldException) {
info.append(" Potential candidates:");
final int initial_length = info.length();
final String name = getHackedFieldName();
final Field[] fields = getHackedClass().getDeclaredFields();
for (final Field field : fields)
if (field.getName().equals(name)) // Exact name match
info.append(' ').append(field);
if (info.length() == initial_length) for (final Field field : fields)
if (field.getName().startsWith(name)) // Name prefix match
info.append(' ').append(field);
if (info.length() == initial_length) for (final Field field : fields)
if (! field.getName().startsWith("$")) // Dump all but generated fields
info.append(' ').append(field);
}
return info.toString();
}
public Class<?> getHackedClass() {
return mClass;
}
AssertionException setHackedClass(final Class<?> hacked_class) {
mClass = hacked_class; return this;
}
public Method getHackedMethod() {
return mHackedMethod;
}
AssertionException setHackedMethod(final Method method) {
mHackedMethod = method;
return this;
}
public String getHackedMethodName() {
return mHackedMethodName;
}
AssertionException setHackedMethodName(final String method) {
mHackedMethodName = method;
return this;
}
public Class<?>[] getParamTypes() { return mParamTypes; }
AssertionException setParamTypes(final Class<?>[] param_types) {
mParamTypes = param_types;
return this;
}
public Field getHackedField() {
return mHackedField;
}
AssertionException setHackedField(final Field field) {
mHackedField = field;
return this;
}
public String getHackedFieldName() {
return mHackedFieldName;
}
AssertionException setHackedFieldName(final String field) {
mHackedFieldName = field;
return this;
}
private static final long serialVersionUID = 1L;
}
/** Placeholder for unchecked exception */
public class Unchecked extends RuntimeException {}
/** Use {@link Hack#setAssertionFailureHandler(AssertionFailureHandler) } to set the global handler */
public interface AssertionFailureHandler {
void onAssertionFailure(AssertionException failure);
}
public static class FieldToHack<C> {
protected @Nullable <T> Field findField(final @Nullable Class<T> type) {
if (mClass == ANY_TYPE) return null; // AnyType as a internal indicator for class not found.
Field field = null;
try {
field = mClass.getDeclaredField(mName);
if (Modifier.isStatic(mModifiers) != Modifier.isStatic(field.getModifiers())) {
fail(new AssertionException(field + (Modifier.isStatic(mModifiers) ? " is not static" : " is static")).setHackedFieldName(mName));
field = null;
} else if (mModifiers > 0 && (field.getModifiers() & mModifiers) != mModifiers) {
fail(new AssertionException(field + " does not match modifiers: " + mModifiers).setHackedFieldName(mName));
field = null;
} else if (! field.isAccessible()) field.setAccessible(true);
} catch (final NoSuchFieldException e) {
final AssertionException hae = new AssertionException(e);
hae.setHackedClass(mClass);
hae.setHackedFieldName(mName);
fail(hae);
}
if (type != null && field != null && ! type.isAssignableFrom(field.getType()))
fail(new AssertionException(new ClassCastException(field + " is not of type " + type)).setHackedField(field));
return field;
}
/** @param modifiers the modifiers this field must have */
protected FieldToHack(final Class<C> clazz, final String name, final int modifiers) {
mClass = clazz;
mName = name;
mModifiers = modifiers;
}
protected final Class<C> mClass;
protected final String mName;
protected final int mModifiers;
}
public static class MemberFieldToHack<C> extends FieldToHack<C> {
/** Assert the field type. */
public @Nullable <T> HackedField<C, T> ofType(final Class<T> type) {
return ofType(type, false, null);
}
public @Nullable <T> HackedField<C, T> ofType(final String type_name) {
try { //noinspection unchecked
return ofType((Class<T>) Class.forName(type_name, false, mClass.getClassLoader()));
} catch (final ClassNotFoundException e) {
fail(new AssertionException(e));
return null;
}
}
public @NonNull HackedField<C, Byte> fallbackTo(final byte value) { //noinspection ConstantConditions
return ofType(byte.class, true, value);
}
public @NonNull HackedField<C, Character> fallbackTo(final char value) { //noinspection ConstantConditions
return ofType(char.class, true, value);
}
public @NonNull HackedField<C, Short> fallbackTo(final short value) { //noinspection ConstantConditions
return ofType(short.class, true, value);
}
public @NonNull HackedField<C, Integer> fallbackTo(final int value) { //noinspection ConstantConditions
return ofType(int.class, true, value);
}
public @NonNull HackedField<C, Long> fallbackTo(final long value) { //noinspection ConstantConditions
return ofType(long.class, true, value);
}
public @NonNull HackedField<C, Boolean> fallbackTo(final boolean value) { //noinspection ConstantConditions
return ofType(boolean.class, true, value);
}
public @NonNull HackedField<C, Float> fallbackTo(final float value) { //noinspection ConstantConditions
return ofType(float.class, true, value);
}
public @NonNull HackedField<C, Double> fallbackTo(final double value) { //noinspection ConstantConditions
return ofType(double.class, true, value);
}
/** Fallback to the given value if this field is unavailable at runtime */
public @NonNull <T> HackedField<C, T> fallbackTo(final @Nullable T value) {
@SuppressWarnings("unchecked") final Class<T> type = value == null ? null : (Class<T>) value.getClass();
//noinspection ConstantConditions
return ofType(type, true, value);
}
private <T> HackedField<C, T> ofType(final Class<T> type, final boolean fallback, final @Nullable T fallback_value) {
if (LAZY_RESOLVE && fallback) return new LazyHackedField<>(this, type, fallback_value);
final Field field = findField(type);
return field != null ? new HackedFieldImpl<C, T>(field) : fallback ? new FallbackField<C, T>(type, fallback_value) : null;
}
/** @param modifiers the modifiers this field must have */
private MemberFieldToHack(final Class<C> clazz, final String name, final int modifiers) {
super(clazz, name, modifiers);
}
}
public static class StaticFieldToHack<C> extends FieldToHack<C> {
/** Assert the field type. */
public @Nullable <T> HackedTargetField<T> ofType(final Class<T> type) {
return ofType(type, false, null);
}
public @Nullable <T> HackedTargetField<T> ofType(final String type_name) {
try { //noinspection unchecked
return ofType((Class<T>) Class.forName(type_name, false, mClass.getClassLoader()));
} catch (final ClassNotFoundException e) {
fail(new AssertionException(e));
return null;
}
}
/** Fallback to the given value if this field is unavailable at runtime */
public @NonNull <T> HackedTargetField<T> fallbackTo(final @Nullable T value) {
@SuppressWarnings("unchecked") final Class<T> type = value == null ? null : (Class<T>) value.getClass();
//noinspection ConstantConditions
return ofType(type, true, value);
}
private <T> HackedTargetField<T> ofType(final Class<T> type, final boolean fallback, final @Nullable T fallback_value) {
if (LAZY_RESOLVE && fallback) return new LazyHackedField<>(this, type, fallback_value);
final Field field = findField(type);
return field != null ? new HackedFieldImpl<C, T>(field).onTarget(null) : fallback ? new FallbackField<C, T>(type, fallback_value) : null;
}
/** @param modifiers the modifiers this field must have */
private StaticFieldToHack(final Class<C> clazz, final String name, final int modifiers) {
super(clazz, name, modifiers);
}
}
public interface HackedField<C, T> {
T get(C instance);
void set(C instance, @Nullable T value);
HackedTargetField<T> on(C target);
Class<T> getType();
boolean isAbsent();
}
public interface HackedTargetField<T> {
T get();
void set(T value);
Class<T> getType();
boolean isAbsent();
}
private static class HackedFieldImpl<C, T> implements HackedField<C, T> {
@Override public HackedTargetFieldImpl<T> on(final C target) {
return onTarget(target);
}
private HackedTargetFieldImpl<T> onTarget(final @Nullable C target) { return new HackedTargetFieldImpl<>(mField, target); }
/** Get current value of this field */
@Override public T get(final C instance) {
try {
@SuppressWarnings("unchecked") final T value = (T) mField.get(instance);
return value;
} catch (final IllegalAccessException e) { return null; } // Should never happen
}
/**
* Set value of this field
*
* <p>No type enforced here since most type mismatch can be easily tested and exposed early.</p>
*/
@Override public void set(final C instance, final @Nullable T value) {
try {
mField.set(instance, value);
} catch (final IllegalAccessException ignored) {} // Should never happen
}
@Override @SuppressWarnings("unchecked") public @Nullable Class<T> getType() {
return (Class<T>) mField.getType();
}
@Override public boolean isAbsent() { return false; }
HackedFieldImpl(final @NonNull Field field) { mField = field; }
public @Nullable Field getField() { return mField; }
private final @NonNull Field mField;
}
private static class FallbackField<C, T> implements HackedField<C, T>, HackedTargetField<T> {
@Override public T get(final C instance) { return mValue; }
@Override public void set(final C instance, final @Nullable T value) {}
@Override public T get() { return mValue; }
@Override public void set(final T value) {}
@Override public HackedTargetField<T> on(final C target) { return this; }
@Override public Class<T> getType() { return mType; }
@Override public boolean isAbsent() { return true; }
FallbackField(final Class<T> type, final @Nullable T value) { mType = type; mValue = value; }
private final Class<T> mType;
private final T mValue;
}
private static class LazyHackedField<C, T> implements HackedField<C, T>, HackedTargetField<T> {
@Override public T get(final C instance) { return delegate.get().get(instance); }
@Override public void set(final C instance, final @Nullable T value) { delegate.get().set(instance, value); }
@Override public HackedTargetField<T> on(final C target) { return delegate.get().on(target); }
@Override public T get() { return delegate.get().get(null); }
@Override public void set(final T value) { delegate.get().set(null, value); }
@Override public Class<T> getType() { return delegate.get().getType(); }
@Override public boolean isAbsent() { return delegate.get().isAbsent(); }
LazyHackedField(final FieldToHack<C> field, final Class<T> type, final @Nullable T fallback_value) {
mField = field;
mType = type;
mFallbackValue = fallback_value;
}
private final FieldToHack<C> mField;
private final Class<T> mType;
private final T mFallbackValue;
private final Supplier<HackedField<C, T>> delegate = Suppliers.memoize(new Supplier<HackedField<C, T>>() { @Override public HackedField<C, T> get() {
final Field field = LazyHackedField.this.mField.findField(mType);
return field != null ? new HackedFieldImpl<C, T>(field) : new FallbackField<C, T>(mType, mFallbackValue);
}});
}
public static class HackedTargetFieldImpl<T> implements HackedTargetField<T> {
@Override public T get() {
try {
@SuppressWarnings("unchecked") final T value = (T) mField.get(mInstance);
return value;
} catch (final IllegalAccessException e) { return null; } // Should never happen
}
@Override public void set(final T value) {
try {
mField.set(mInstance, value);
} catch (final IllegalAccessException ignored) {} // Should never happen
}
@Override @SuppressWarnings("unchecked") public @Nullable Class<T> getType() { return (Class<T>) mField.getType(); }
@Override public boolean isAbsent() { return false; }
HackedTargetFieldImpl(final Field field, final @Nullable Object instance) {
mField = field;
mInstance = instance;
}
private final Field mField;
private final Object mInstance; // Instance type is already checked
}
public interface HackedInvokable<R, C, T1 extends Throwable, T2 extends Throwable, T3 extends Throwable> {
@CheckResult <TT1 extends Throwable> HackedInvokable<R, C, TT1, T2, T3> throwing(Class<TT1> type);
@CheckResult <TT1 extends Throwable, TT2 extends Throwable> HackedInvokable<R, C, TT1, TT2, T3> throwing(Class<TT1> type1, Class<TT2> type2);
@CheckResult <TT1 extends Throwable, TT2 extends Throwable, TT3 extends Throwable> HackedInvokable<R, C, TT1, TT2, TT3> throwing(Class<TT1> type1, Class<TT2> type2, Class<TT3> type3);
@Nullable HackedMethod0<R, C, T1, T2, T3> withoutParams();
@Nullable <A1> HackedMethod1<R, C, T1, T2, T3, A1> withParam(Class<A1> type);
@Nullable <A1, A2> HackedMethod2<R, C, T1, T2, T3, A1, A2> withParams(Class<A1> type1, Class<A2> type2);
@Nullable <A1, A2, A3> HackedMethod3<R, C, T1, T2, T3, A1, A2, A3> withParams(Class<A1> type1, Class<A2> type2, Class<A3> type3);
@Nullable <A1, A2, A3, A4> HackedMethod4<R, C, T1, T2, T3, A1, A2, A3, A4> withParams(Class<A1> type1, Class<A2> type2, Class<A3> type3, Class<A4> type4);
@Nullable <A1, A2, A3, A4, A5> HackedMethod5<R, C, T1, T2, T3, A1, A2, A3, A4, A5> withParams(Class<A1> type1, final Class<A2> type2, final Class<A3> type3, final Class<A4> type4, final Class<A5> type5);
@Nullable HackedMethodN<R, C, T1, T2, T3> withParams(Class<?>... types);
}
public interface NonNullHackedInvokable<R, C, T1 extends Throwable, T2 extends Throwable, T3 extends Throwable> extends HackedInvokable<R, C, T1,T2,T3> {
@CheckResult <TT1 extends Throwable> NonNullHackedInvokable<R, C, TT1, T2, T3> throwing(Class<TT1> type);
@CheckResult <TT1 extends Throwable, TT2 extends Throwable> NonNullHackedInvokable<R, C, TT1, TT2, T3> throwing(Class<TT1> type1, Class<TT2> type2);
@CheckResult <TT1 extends Throwable, TT2 extends Throwable, TT3 extends Throwable> NonNullHackedInvokable<R, C, TT1, TT2, TT3> throwing(Class<TT1> type1, Class<TT2> type2, Class<TT3> type3);
@NonNull HackedMethod0<R, C, T1, T2, T3> withoutParams();
@NonNull <A1> HackedMethod1<R, C, T1, T2, T3, A1> withParam(Class<A1> type);
@NonNull <A1, A2> HackedMethod2<R, C, T1, T2, T3, A1, A2> withParams(Class<A1> type1, Class<A2> type2);
@NonNull <A1, A2, A3> HackedMethod3<R, C, T1, T2, T3, A1, A2, A3> withParams(Class<A1> type1, Class<A2> type2, Class<A3> type3);
@NonNull <A1, A2, A3, A4> HackedMethod4<R, C, T1, T2, T3, A1, A2, A3, A4> withParams(Class<A1> type1, Class<A2> type2, Class<A3> type3, Class<A4> type4);
@NonNull <A1, A2, A3, A4, A5> HackedMethod5<R, C, T1, T2, T3, A1, A2, A3, A4, A5> withParams(Class<A1> type1, final Class<A2> type2, final Class<A3> type3, final Class<A4> type4, final Class<A5> type5);
@NonNull HackedMethodN<R, C, T1, T2, T3> withParams(Class<?>... types);
}
public interface HackedMethod<R, C, T1 extends Throwable, T2 extends Throwable, T3 extends Throwable> extends HackedInvokable<R, C, T1,T2,T3> {
/** Optional */
@CheckResult <RR> HackedMethod<RR, C, T1, T2, T3> returning(Class<RR> type);
/** Fallback to the given value if this field is unavailable at runtime. (Optional) */
@CheckResult NonNullHackedMethod<R, C, T1, T2, T3> fallbackReturning(@Nullable R return_value);
@CheckResult <TT1 extends Throwable> HackedMethod<R, C, TT1, T2, T3> throwing(Class<TT1> type);
@CheckResult <TT1 extends Throwable, TT2 extends Throwable> HackedMethod<R, C, TT1, TT2, T3> throwing(Class<TT1> type1, Class<TT2> type2);
@CheckResult <TT1 extends Throwable, TT2 extends Throwable, TT3 extends Throwable> HackedMethod<R, C, TT1, TT2, TT3> throwing(Class<TT1> type1, Class<TT2> type2, Class<TT3> type3);
@CheckResult HackedMethod<R, C, Exception, T2, T3> throwing(Class<?>... types);
}
@SuppressWarnings("NullableProblems") // Force to NonNull
public interface NonNullHackedMethod<R, C, T1 extends Throwable, T2 extends Throwable, T3 extends Throwable> extends HackedMethod<R, C, T1,T2,T3>, NonNullHackedInvokable<R, C, T1,T2,T3> {
/** Optional */
@CheckResult <RR> HackedMethod<RR, C, T1, T2, T3> returning(Class<RR> type);
/** Whatever exception or none */
@CheckResult <TT1 extends Throwable> NonNullHackedMethod<R, C, Exception, T2, T3> throwing();
@CheckResult <TT1 extends Throwable> NonNullHackedMethod<R, C, TT1, T2, T3> throwing(Class<TT1> type);
@CheckResult <TT1 extends Throwable, TT2 extends Throwable> NonNullHackedMethod<R, C, TT1, TT2, T3> throwing(Class<TT1> type1, Class<TT2> type2);
@CheckResult <TT1 extends Throwable, TT2 extends Throwable, TT3 extends Throwable> NonNullHackedMethod<R, C, TT1, TT2, TT3> throwing(Class<TT1> type1, Class<TT2> type2, Class<TT3> type3);
}
public static class CheckedHackedMethod<R, C, T1 extends Throwable, T2 extends Throwable, T3 extends Throwable> {
CheckedHackedMethod(final Invokable invokable) { mInvokable = invokable; }
protected HackInvocation<R, C, T1, T2, T3> invoke(final Object... args) { return new HackInvocation<>(mInvokable, args); }
/** Whether this hack is absent, thus will be fallen-back when invoked */
public boolean isAbsent() { return mInvokable instanceof FallbackInvokable; }
private final Invokable mInvokable;
}
public static class HackedMethod0<R, C, T1 extends Throwable, T2 extends Throwable, T3 extends Throwable> extends CheckedHackedMethod<R, C, T1,T2,T3> {
HackedMethod0(final Invokable invokable) { super(invokable); }
public @CheckResult HackInvocation<R, C, T1, T2, T3> invoke() { return super.invoke(); }
}
public static class HackedMethod1<R, C, T1 extends Throwable, T2 extends Throwable, T3 extends Throwable, A1> extends CheckedHackedMethod<R, C, T1,T2,T3> {
HackedMethod1(final Invokable invokable) { super(invokable); }
public @CheckResult HackInvocation<R, C, T1, T2, T3> invoke(final A1 arg) { return super.invoke(arg); }
}
public static class HackedMethod2<R, C, T1 extends Throwable, T2 extends Throwable, T3 extends Throwable, A1, A2> extends CheckedHackedMethod<R, C, T1,T2,T3> {
HackedMethod2(final Invokable invokable) { super(invokable); }
public @CheckResult HackInvocation<R, C, T1, T2, T3> invoke(final A1 arg1, final A2 arg2) { return super.invoke(arg1, arg2); }
}
public static class HackedMethod3<R, C, T1 extends Throwable, T2 extends Throwable, T3 extends Throwable, A1, A2, A3> extends CheckedHackedMethod<R, C, T1,T2,T3> {
HackedMethod3(final Invokable invokable) { super(invokable); }
public @CheckResult HackInvocation<R, C, T1, T2, T3> invoke(final A1 arg1, final A2 arg2, final A3 arg3) { return super.invoke(arg1, arg2, arg3); }
}
public static class HackedMethod4<R, C, T1 extends Throwable, T2 extends Throwable, T3 extends Throwable, A1, A2, A3, A4> extends CheckedHackedMethod<R, C, T1,T2,T3> {
HackedMethod4(final Invokable invokable) { super(invokable); }
public @CheckResult HackInvocation<R, C, T1, T2, T3> invoke(final A1 arg1, final A2 arg2, final A3 arg3, final A4 arg4) { return super.invoke(arg1, arg2, arg3, arg4); }
}
public static class HackedMethod5<R, C, T1 extends Throwable, T2 extends Throwable, T3 extends Throwable, A1, A2, A3, A4, A5> extends CheckedHackedMethod<R, C, T1,T2,T3> {
HackedMethod5(final Invokable invokable) { super(invokable); }
public @CheckResult HackInvocation<R, C, T1, T2, T3> invoke(final A1 arg1, final A2 arg2, final A3 arg3, final A4 arg4, final A5 arg5) { return super.invoke(arg1, arg2, arg3, arg4, arg5); }
}
public static class HackedMethodN<R, C, T1 extends Throwable, T2 extends Throwable, T3 extends Throwable> extends CheckedHackedMethod<R, C, T1,T2,T3> {
HackedMethodN(final Invokable invokable) { super(invokable); }
public @CheckResult HackInvocation<R, C, T1, T2, T3> invoke(final Object... args) { return super.invoke(args); }
}
public static class HackInvocation<R, C, T1 extends Throwable, T2 extends Throwable, T3 extends Throwable> {
HackInvocation(final Invokable invokable, final Object... args) {
this.invokable = invokable;
this.args = args;
}
public R on(final @NonNull C target) throws T1, T2, T3 { return onTarget(target); }
public R statically() throws T1, T2, T3 { return onTarget(null); }
private R onTarget(final C target) throws T1 { //noinspection TryWithIdenticalCatches
try {
@SuppressWarnings("unchecked") final R result = (R) invokable.invoke(target, args);
return result;
} catch (final IllegalAccessException e) { throw new RuntimeException(e); // Should never happen
} catch (final InstantiationException e) { throw new RuntimeException(e);
} catch (final InvocationTargetException e) {
final Throwable ex = e.getTargetException();
//noinspection unchecked
throw (T1) ex;
}
}
private final Invokable invokable;
private final Object[] args;
}
interface Invokable<C> {
Object invoke(C target, Object[] args) throws InvocationTargetException, IllegalAccessException, InstantiationException;
}
private static class HackedMethodImpl<R, C, T1 extends Throwable, T2 extends Throwable, T3 extends Throwable> implements NonNullHackedMethod<R, C, T1, T2, T3> {
HackedMethodImpl(final Class<?> clazz, @Nullable final String name, final int modifiers) {
//noinspection unchecked, to be compatible with HackedClass.staticMethod()
mClass = (Class<C>) clazz;
mName = name;
mModifiers = modifiers;
}
@Override public <RR> HackedMethod<RR, C, T1, T2, T3> returning(final Class<RR> type) {
mReturnType = type;
@SuppressWarnings("unchecked") final HackedMethod<RR, C, T1, T2, T3> casted = (HackedMethod<RR, C, T1, T2, T3>) this;
return casted;
}
@Override public NonNullHackedMethod<R, C, T1, T2, T3> fallbackReturning(final @Nullable R value) {
mFallbackReturnValue = value; mHasFallback = true; return this;
}
@Override public NonNullHackedMethod<R, C, Exception, T2, T3> throwing() {
mThrowTypes = new Class[] {};
@SuppressWarnings("unchecked") final NonNullHackedMethod<R, C, Exception, T2, T3> casted = (NonNullHackedMethod<R, C, Exception, T2, T3>) this;
return casted;
}
@Override public <TT extends Throwable> NonNullHackedMethod<R, C, TT, T2, T3> throwing(final Class<TT> type) {
mThrowTypes = new Class[] { type };
@SuppressWarnings("unchecked") final NonNullHackedMethod<R, C, TT, T2, T3> casted = (NonNullHackedMethod<R, C, TT, T2, T3>) this;
return casted;
}
@Override public <TT1 extends Throwable, TT2 extends Throwable> NonNullHackedMethod<R, C, TT1, TT2, T3>
throwing(final Class<TT1> type1, final Class<TT2> type2) {
mThrowTypes = new Class<?>[] { type1, type2 };
Arrays.sort(mThrowTypes, CLASS_COMPARATOR);
@SuppressWarnings("unchecked") final NonNullHackedMethod<R, C, TT1, TT2, T3> cast = (NonNullHackedMethod<R, C, TT1, TT2, T3>) this;
return cast;
}
@Override public <TT1 extends Throwable, TT2 extends Throwable, TT3 extends Throwable> NonNullHackedMethod<R, C, TT1, TT2, TT3>
throwing(final Class<TT1> type1, final Class<TT2> type2, final Class<TT3> type3) {
mThrowTypes = new Class<?>[] { type1, type2, type3 };
Arrays.sort(mThrowTypes, CLASS_COMPARATOR);
@SuppressWarnings("unchecked") final NonNullHackedMethod<R, C, TT1, TT2, TT3> cast = (NonNullHackedMethod<R, C, TT1, TT2, TT3>) this;
return cast;
}
@Override public HackedMethod<R, C, Exception, T2, T3> throwing(final Class<?>... types) {
mThrowTypes = types;
Arrays.sort(mThrowTypes, CLASS_COMPARATOR);
@SuppressWarnings("unchecked") final HackedMethod<R, C, Exception, T2, T3> cast = (HackedMethod<R, C, Exception, T2, T3>) this;
return cast;
}
@NonNull @SuppressWarnings("ConstantConditions")
@Override public HackedMethod0<R, C, T1, T2, T3> withoutParams() {
final Invokable<C> invokable = buildInvokable();
return invokable == null ? null : new HackedMethod0<R, C, T1, T2, T3>(invokable);
}
@NonNull @SuppressWarnings("ConstantConditions")
@Override public <A1> HackedMethod1<R, C, T1, T2, T3, A1> withParam(final Class<A1> type) {
final Invokable invokable = buildInvokable(type);
return invokable == null ? null : new HackedMethod1<R, C, T1, T2, T3, A1>(invokable);
}
@NonNull @SuppressWarnings("ConstantConditions")
@Override public <A1, A2> HackedMethod2<R, C, T1, T2, T3, A1, A2> withParams(final Class<A1> type1, final Class<A2> type2) {
final Invokable invokable = buildInvokable(type1, type2);
return invokable == null ? null : new HackedMethod2<R, C, T1, T2, T3, A1, A2>(invokable);
}
@NonNull @SuppressWarnings("ConstantConditions")
@Override public <A1, A2, A3> HackedMethod3<R, C, T1, T2, T3, A1, A2, A3> withParams(final Class<A1> type1, final Class<A2> type2, final Class<A3> type3) {
final Invokable invokable = buildInvokable(type1, type2, type3);
return invokable == null ? null : new HackedMethod3<R, C, T1, T2, T3, A1, A2, A3>(invokable);
}
@NonNull @SuppressWarnings("ConstantConditions")
@Override public <A1, A2, A3, A4> HackedMethod4<R, C, T1, T2, T3, A1, A2, A3, A4> withParams(final Class<A1> type1, final Class<A2> type2, final Class<A3> type3, final Class<A4> type4) {
final Invokable invokable = buildInvokable(type1, type2, type3, type4);
return invokable == null ? null : new HackedMethod4<R, C, T1, T2, T3, A1, A2, A3, A4>(invokable);
}
@NonNull @SuppressWarnings("ConstantConditions")
@Override public <A1, A2, A3, A4, A5> HackedMethod5<R, C, T1, T2, T3, A1, A2, A3, A4, A5> withParams(final Class<A1> type1, final Class<A2> type2, final Class<A3> type3, final Class<A4> type4, final Class<A5> type5) {
final Invokable invokable = buildInvokable(type1, type2, type3, type4, type5);
return invokable == null ? null : new HackedMethod5<R, C, T1, T2, T3, A1, A2, A3, A4, A5>(invokable);
}
@NonNull @SuppressWarnings("ConstantConditions")
@Override public HackedMethodN<R, C, T1, T2, T3> withParams(final Class<?>... types) {
final Invokable invokable = buildInvokable(types);
return invokable == null ? null : new HackedMethodN<R, C, T1, T2, T3>(invokable);
}
private @Nullable Invokable<C> buildInvokable(final Class<?>... param_types) {
return LAZY_RESOLVE && mHasFallback ? new LazyInvokable<>(this, param_types) : findInvokable(param_types);
}
private @Nullable Invokable<C> findInvokable(final Class<?>... param_types) {
if (mClass == ANY_TYPE) // AnyType as a internal indicator for class not found.
return mHasFallback ? new FallbackInvokable<C>(mFallbackReturnValue) : null;
final int modifiers; Invokable<C> invokable; final AccessibleObject accessible; final Class<?>[] ex_types;
try {
if (mName != null) {
final Method candidate = mClass.getDeclaredMethod(mName, param_types); Method method = candidate;
ex_types = candidate.getExceptionTypes();
modifiers = method.getModifiers();
if (Modifier.isStatic(mModifiers) != Modifier.isStatic(candidate.getModifiers())) {
fail(new AssertionException(candidate + (Modifier.isStatic(mModifiers) ? " is not static" : "is static")).setHackedMethod(method));
method = null;
}
if (mReturnType != null && mReturnType != ANY_TYPE && ! candidate.getReturnType().equals(mReturnType)) {
fail(new AssertionException("Return type mismatch: " + candidate));
method = null;
}
if (method != null) {
invokable = new InvokableMethod<>(method);
accessible = method;
} else { invokable = null; accessible = null; }
} else {
final Constructor<C> ctor = mClass.getDeclaredConstructor(param_types);
modifiers = ctor.getModifiers(); invokable = new InvokableConstructor<>(ctor); accessible = ctor;
ex_types = ctor.getExceptionTypes();
}
} catch (final NoSuchMethodException e) {
final AssertionException failure = new AssertionException(e).setHackedClass(mClass).setParamTypes(param_types);
if (mName != null) failure.setHackedMethodName(mName);
fail(failure);
return mHasFallback ? new FallbackInvokable<C>(mFallbackReturnValue) : null;
}
if (mModifiers > 0 && (modifiers & mModifiers) != mModifiers) {
final AssertionException failure = new AssertionException(invokable + " does not match modifiers: " + mModifiers);
if (mName != null) failure.setHackedMethodName(mName);
fail(failure);
}
if (mThrowTypes == null && ex_types.length > 0 || mThrowTypes != null && mThrowTypes.length > 0 && ex_types.length == 0) {
fail(new AssertionException("Checked exception(s) not match: " + invokable));
if (ex_types.length > 0) invokable = null; // No need to fall-back if expected checked exceptions are absent.
} else if (mThrowTypes != null && mThrowTypes.length > 0) { // Empty array stands for "whatever exception or none"
if (mThrowTypes.length > 1) Arrays.sort(ex_types, CLASS_COMPARATOR);
if (! Arrays.equals(ex_types, mThrowTypes)) { // TODO: Check derived relation of exceptions
fail(new AssertionException("Checked exception(s) not match: " + invokable));
invokable = null;
}
}
if (invokable == null) {
if (! mHasFallback) return null;
return new FallbackInvokable<>(mFallbackReturnValue);
}
if (! accessible.isAccessible()) accessible.setAccessible(true);
return invokable;
}
private final Class<C> mClass;
private final @Nullable String mName; // Null for constructor
private final int mModifiers;
private Class<?> mReturnType;
private Class<?>[] mThrowTypes;
private R mFallbackReturnValue;
private boolean mHasFallback;
private static final Comparator<Class> CLASS_COMPARATOR = new Comparator<Class>() {
@Override public int compare(final Class lhs, final Class rhs) {
return lhs.toString().compareTo(rhs.toString());
}
@Override public boolean equals(final Object object) {
return this == object;
}
};
}
private static class InvokableMethod<C> implements Invokable<C> {
InvokableMethod(final Method method) { this.method = method; }
public Object invoke(final C target, final Object[] args) throws IllegalAccessException,
IllegalArgumentException, InvocationTargetException {
return method.invoke(target, args);
}
@Override public String toString() { return method.toString(); }
private final Method method;
}
private static class InvokableConstructor<C> implements Invokable<C> {
InvokableConstructor(final Constructor<C> method) { this.constructor = method; }
public Object invoke(final C target, final Object[] args) throws InstantiationException,
IllegalAccessException, IllegalArgumentException, InvocationTargetException {
return constructor.newInstance(args);
}
@Override public String toString() { return constructor.toString(); }
private final Constructor<C> constructor;
}
private static class FallbackInvokable<C> implements Invokable<C> {
FallbackInvokable(final @Nullable Object value) { mValue = value; }
@Override public Object invoke(final C target, final Object[] args) {
return mValue;
}
private final @Nullable Object mValue;
}
private static class LazyInvokable<C> implements Invokable<C> {
LazyInvokable(final HackedMethodImpl<?, C, ?, ?, ?> method, final Class<?>[] param_types) {
mMethod = method;
mParamTypes = param_types;
}
@Override public Object invoke(final C target, final Object[] args) throws InvocationTargetException, IllegalAccessException, InstantiationException {
//noinspection ConstantConditions, since fallback is provided
return mMethod.findInvokable(mParamTypes).invoke(target, args);
}
private final HackedMethodImpl<?, C, ?, ?, ?> mMethod;
private final Class<?>[] mParamTypes;
}
public static class HackedClass<C> {
public @CheckResult <T> MemberFieldToHack<C> field(final @NonNull String name) {
return new MemberFieldToHack<>(mClass, name, 0);
}
public @CheckResult <T> StaticFieldToHack<C> staticField(final @NonNull String name) {
return new StaticFieldToHack<>(mClass, name, Modifier.STATIC);
}
public @CheckResult HackedMethod<Void, C, Unchecked, Unchecked, Unchecked> method(final String name) {
return new HackedMethodImpl<>(mClass, name, 0);
}
public @CheckResult HackedMethod<Void, Void, Unchecked, Unchecked, Unchecked> staticMethod(final String name) {
return new HackedMethodImpl<>(mClass, name, Modifier.STATIC);
}
public @CheckResult NonNullHackedInvokable<C, Void, Unchecked, Unchecked, Unchecked> constructor() {
final HackedMethodImpl<C, Void, Unchecked, Unchecked, Unchecked> constructor = new HackedMethodImpl<>(mClass, null, 0);
return constructor.fallbackReturning(null); // Always fallback to null.
}
HackedClass(final Class<C> clazz) { mClass = clazz; }
private final Class<C> mClass;
}
public static <T> HackedClass<T> into(final @NonNull Class<T> clazz) {
return new HackedClass<>(clazz);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
public static <T> HackedClass<T> into(final String class_name) {
try {
return new HackedClass(Class.forName(class_name));
} catch (final ClassNotFoundException e) {
fail(new AssertionException(e));
return new HackedClass(ANY_TYPE); // Use AnyType as a lazy trick to make fallback working and avoid null.
}
}
@SuppressWarnings("unchecked") public static <C> Hack.HackedClass<C> onlyIf(final boolean condition, final Hacking<Hack.HackedClass<C>> hacking) {
if (condition) return hacking.hack();
return (Hack.HackedClass<C>) FALLBACK;
}
public interface Hacking<T> { T hack(); }
private static final Hack.HackedClass<?> FALLBACK = new HackedClass<>(ANY_TYPE);
public static ConditionalHack onlyIf(final boolean condition) {
return condition ? new ConditionalHack() {
@Override public <T> HackedClass<T> into(@NonNull final Class<T> clazz) {
return Hack.into(clazz);
}
@Override public <T> HackedClass<T> into(final String class_name) {
return Hack.into(class_name);
}
} : new ConditionalHack() {
@SuppressWarnings("unchecked") @Override public <T> HackedClass<T> into(@NonNull final Class<T> clazz) {
return (HackedClass<T>) FALLBACK;
}
@SuppressWarnings("unchecked") @Override public <T> HackedClass<T> into(final String class_name) {
return (HackedClass<T>) FALLBACK;
}
};
}
public interface ConditionalHack {
/** WARNING: Never use this method if the target class may not exist when the condition is not met, use {@link #onlyIf(boolean, Hacking)} instead. */
<T> HackedClass<T> into(final @NonNull Class<T> clazz);
<T> HackedClass<T> into(final String class_name);
}
private static void fail(final AssertionException e) {
if (sFailureHandler != null) sFailureHandler.onAssertionFailure(e);
}
/** Specify a handler to deal with assertion failure, and decide whether the failure should be thrown. */
public static AssertionFailureHandler setAssertionFailureHandler(final AssertionFailureHandler handler) {
final AssertionFailureHandler old = sFailureHandler;
sFailureHandler = handler;
return old;
}
private Hack() {}
private static AssertionFailureHandler sFailureHandler;
/** This is a simple demo for the common usage of {@link Hack} */
@SuppressWarnings("unused")
private static class Demo {
@SuppressWarnings({"FieldCanBeLocal", "UnnecessarilyQualifiedStaticUsage"})
static class Hacks {
static {
Hack.setAssertionFailureHandler(new AssertionFailureHandler() { @Override public void onAssertionFailure(@NonNull final AssertionException failure) {
Log.w("Demo", "Partially incompatible: " + failure.getDebugInfo());
// Report the incompatibility silently.
//...
}});
Demo_ctor = Hack.into(Demo.class).constructor().withParam(int.class);
// Method without fallback (will be null if absent)
Demo_methodThrows = Hack.into(Demo.class).method("methodThrows").returning(Void.class).fallbackReturning(null)
.throwing(InterruptedException.class, IOException.class).withoutParams();
// Method with fallback (will never be null)
Demo_staticMethod = Hack.into(Demo.class).staticMethod("methodWith2Params").returning(boolean.class)
.fallbackReturning(false).withParams(int.class, String.class);
Demo_mField = Hack.into(Demo.class).field("mField").fallbackTo(false);
Demo_sField = Hack.into(Demo.class).staticField("sField").ofType(String.class);
}
static HackedMethod1<Demo, Void, Unchecked, Unchecked, Unchecked, Integer> Demo_ctor;
static Hack.HackedMethod0<Void, Demo, InterruptedException, IOException, Unchecked> Demo_methodThrows;
static Hack.HackedMethod2<Boolean, Void, Unchecked, Unchecked, Unchecked, Integer, String> Demo_staticMethod;
static @Nullable HackedField<Demo, Boolean> Demo_mField; // Optional hack may be null if assertion failed
static @Nullable HackedTargetField<String> Demo_sField;
}
static void demo() {
final Demo demo = Hacks.Demo_ctor.invoke(0).statically();
try {
Hacks.Demo_methodThrows.invoke().on(demo);
} catch (final InterruptedException | IOException e) { // The checked exceptions declared by throwing() in hack definition.
e.printStackTrace();
}
Hacks.Demo_staticMethod.invoke(1, "xx").statically();
}
Demo(final int flags) {}
@SuppressWarnings("RedundantThrows") private void methodThrows() throws InterruptedException, IOException {}
static boolean staticMethod(final int a, final String c) { return false; }
boolean mField;
static String sField;
}
}
/*
* Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.oasisfeng.hack;
/**
* Represents a supplier of results.
*
* <p>There is no requirement that a new or distinct result be returned each
* time the supplier is invoked.
*
* <p>This is a <a href="package-summary.html">functional interface</a>
* whose functional method is {@link #get()}.
*
* @param <T> the type of results supplied by this supplier
*/
public interface Supplier<T> {
/**
* Gets a result.
*
* @return a result
*/
T get();
}
/*
* Copyright (C) 2007 The Guava Authors
*
* 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.oasisfeng.hack;
import android.os.SystemClock;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
/**
* Useful suppliers.
*
* <p>All methods return serializable suppliers as long as they're given serializable parameters.
*
* @author Laurence Gonsalves
* @author Harry Heymann
*/
public class Suppliers {
public static <T> Supplier<T> memoize(final Supplier<T> delegate) {
if (delegate instanceof NonSerializableMemoizingSupplier || delegate instanceof MemoizingSupplier) return delegate;
return delegate instanceof Serializable ? new MemoizingSupplier<>(delegate) : new NonSerializableMemoizingSupplier<>(delegate);
}
static class MemoizingSupplier<T> implements Supplier<T>, Serializable {
final Supplier<T> delegate;
transient volatile boolean initialized;
// "value" does not need to be volatile; visibility piggy-backs
// on volatile read of "initialized".
transient T value;
MemoizingSupplier(final Supplier<T> delegate) {
if (delegate == null) throw new NullPointerException();
this.delegate = delegate;
}
@Override
public T get() {
// A 2-field variant of Double Checked Locking.
if (!initialized) {
synchronized (this) {
if (!initialized) {
final T t = delegate.get();
value = t;
initialized = true;
return t;
}
}
}
return value;
}
@Override
public String toString() {
return "Suppliers.memoize(" + delegate + ")";
}
private static final long serialVersionUID = 0;
}
static class NonSerializableMemoizingSupplier<T> implements Supplier<T> {
volatile Supplier<T> delegate;
volatile boolean initialized;
// "value" does not need to be volatile; visibility piggy-backs
// on volatile read of "initialized".
T value;
NonSerializableMemoizingSupplier(final Supplier<T> delegate) {
if (delegate == null) throw new NullPointerException();
this.delegate = delegate;
}
@Override public T get() {
// A 2-field variant of Double Checked Locking.
if (!initialized) {
synchronized (this) {
if (!initialized) {
final T t = delegate.get();
value = t;
initialized = true;
// Release the delegate to GC.
delegate = null;
return t;
}
}
}
return value;
}
@Override public String toString() {
return "Suppliers.memoize(" + delegate + ")";
}
}
/**
* Returns a supplier that caches the instance supplied by the delegate and removes the cached
* value after the specified time has passed. Subsequent calls to {@code get()} return the cached
* value if the expiration time has not passed. After the expiration time, a new value is
* retrieved, cached, and returned. See:
* <a href="http://en.wikipedia.org/wiki/Memoization">memoization</a>
*
* <p>The returned supplier is thread-safe. The supplier's serialized form does not contain the
* cached value, which will be recalculated when {@code
* get()} is called on the reserialized instance.
*
* @param duration the length of time after a value is created that it should stop being returned
* by subsequent {@code get()} calls
* @param unit the unit that {@code duration} is expressed in
* @throws IllegalArgumentException if {@code duration} is not positive
* @since 2.0
*/
public static <T> Supplier<T> memoizeWithExpiration(final Supplier<T> delegate, final long duration, final TimeUnit unit) {
return new ExpiringMemoizingSupplier<T>(delegate, duration, unit);
}
@VisibleForTesting
static class ExpiringMemoizingSupplier<T> implements Supplier<T>, Serializable {
final Supplier<T> delegate;
final long durationMillis;
transient volatile T value;
// The special value 0 means "not yet initialized".
transient volatile long expirationMillis;
ExpiringMemoizingSupplier(final Supplier<T> delegate, final long duration, final TimeUnit unit) {
if (delegate == null) throw new NullPointerException();
this.delegate = delegate;
this.durationMillis = unit.toMillis(duration);
if (duration <= 0) throw new IllegalArgumentException();
}
@Override
public T get() {
// Another variant of Double Checked Locking.
//
// We use two volatile reads. We could reduce this to one by
// putting our fields into a holder class, but (at least on x86)
// the extra memory consumption and indirection are more
// expensive than the extra volatile reads.
long millis = expirationMillis;
final long now = SystemClock.uptimeMillis();
if (millis == 0 || now - millis >= 0) {
synchronized (this) {
if (millis == expirationMillis) { // recheck for lost race
final T t = delegate.get();
value = t;
millis = now + durationMillis;
// In the very unlikely event that millis is 0, set it to 1;
// no one will notice 1 ns of tardiness.
expirationMillis = (millis == 0) ? 1 : millis;
return t;
}
}
}
return value;
}
@Override public String toString() {
return "Suppliers.memoizeWithExpiration(" + delegate + ", " + durationMillis + "ms)";
}
private static final long serialVersionUID = 0;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment