diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/util/DeprecationUtil.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/util/DeprecationUtil.java new file mode 100644 index 000000000..1376383c7 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/util/DeprecationUtil.java @@ -0,0 +1,97 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.internal.util; + +import com.google.common.base.Joiner; +import com.google.common.base.Throwables; + +import java.lang.reflect.Method; +import java.util.stream.Stream; + +public class DeprecationUtil { + + private DeprecationUtil() { + } + + /** + * Verify that one of the two functions is overridden. Caller method must be the new method, + * annotated with {@link NonAbstractForCompatibility}. + * + * @param implementingClass the result of calling {@link Object#getClass()} + */ + public static void checkDelegatingOverride(Class implementingClass) { + // pull the information about the caller + StackTraceElement caller = Throwables.lazyStackTrace(new Throwable()).get(1); + // find the matching caller method + Method callingMethod = getCallingMethod(caller); + NonAbstractForCompatibility annotation = + callingMethod.getAnnotation(NonAbstractForCompatibility.class); + // get the deprecated method + Method deprecatedMethod; + try { + deprecatedMethod = implementingClass.getMethod( + annotation.delegateName(), annotation.delegateParams() + ); + } catch (NoSuchMethodException e) { + throw new AssertionError( + "Missing method referenced by " + NonAbstractForCompatibility.class, e + ); + } + // Check if the deprecated method was overridden. If the declaring class is the caller's + // class, then it wasn't. That means that the caller method (i.e. the new method) should be + // overridden by the implementing class. + // There's no need to check if the new method has been overridden, since the only other + // way this could be reached is if someone calls `super.xyz`, which they have no reason to. + if (deprecatedMethod.getDeclaringClass().getName().equals(caller.getClassName())) { + throw new IllegalStateException("Class " + implementingClass.getName() + + " must override " + methodToString(callingMethod)); + } + } + + private static Method getCallingMethod(StackTraceElement callerInfo) { + Method[] declaredMethods; + try { + declaredMethods = Class.forName(callerInfo.getClassName()).getDeclaredMethods(); + } catch (ClassNotFoundException e) { + throw new AssertionError("Caller class missing?", e); + } + for (Method declaredMethod : declaredMethods) { + if (declaredMethod.isAnnotationPresent(NonAbstractForCompatibility.class) && + declaredMethod.getName().equals(callerInfo.getMethodName())) { + return declaredMethod; + } + } + throw new IllegalStateException("Failed to find caller method " + + callerInfo.getMethodName() + " annotated with " + NonAbstractForCompatibility.class); + } + + private static String methodToString(Method method) { + StringBuilder builder = new StringBuilder(method.getDeclaringClass().getCanonicalName()) + .append('.') + .append(method.getName()) + .append('('); + Joiner.on(", ").appendTo(builder, Stream.of(method.getParameterTypes()) + .map(Class::getSimpleName) + .iterator()); + builder.append(')'); + return builder.toString(); + } + +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/util/NonAbstractForCompatibility.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/util/NonAbstractForCompatibility.java new file mode 100644 index 000000000..f67131d73 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/util/NonAbstractForCompatibility.java @@ -0,0 +1,57 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.internal.util; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The annotated method is only non-{@code abstract} for compatibility with old subclasses, + * and will be made {@code abstract} in the next major version of WorldEdit. + * + *

+ * Any new subclasses must override the annotated method, failing to do so will result in + * an exception at runtime. + *

+ */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface NonAbstractForCompatibility { + + // Note that this annotation only functions properly if no other method in the same class + // shares the name of the annotated function AND is also annotated with this annotation. + // Otherwise, we cannot uniquely determine the calling method via reflection hacks. + // This could be changed, but it's not currently necessary. + + /** + * The name of the method delegated to by the annotated method. + */ + String delegateName(); + + /** + * The parameter types of the method delegated to by the annotated method. + */ + Class[] delegateParams(); + +}