Skip to content

Commit 474e45e

Browse files
committed
Added NonInheritImplements.
This allows classes to declare that they implement a specific interface, but does not strictly require that subclasses also implement this interface as well (through inheritance). There is a custom compile check as well, but in general, this requires using the Helper class as well to correctly cast to the interface and check instanceof. The use case imagined here is for serialization and/or cloning, where you want some classes in the heirarchy to be serializable or cloneable, but not necessarily subclasses of those classes.
1 parent 8b9e709 commit 474e45e

8 files changed

Lines changed: 204 additions & 1 deletion

File tree

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,7 @@
461461
<target>1.8</target>
462462
<compilerArgs>
463463
<arg>-XDignore.symbol.file</arg>
464+
<arg>-parameters</arg>
464465
</compilerArgs>
465466
<fork>true</fork>
466467
<optimize>true</optimize>

src/main/java/com/laytonsmith/PureUtilities/ClassLoading/Annotations/CacheAnnotations.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public static void main(String[] args) throws Exception {
5151
AnnotationChecks.checkForceImplementation();
5252
AnnotationChecks.checkForTypeInTypeofClasses();
5353
AnnotationChecks.verifyExhaustiveVisitors();
54+
AnnotationChecks.verifyNonInheritImplements();
5455

5556
Implementation.setServerType(Implementation.Type.SHELL);
5657
List<String> uhohs = new ArrayList<>();

src/main/java/com/laytonsmith/PureUtilities/ClassLoading/ClassMirror/AnnotationMirror.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.laytonsmith.PureUtilities.ClassLoading.ClassMirror;
22

3+
import com.laytonsmith.PureUtilities.ClassLoading.ClassDiscovery;
34
import com.laytonsmith.PureUtilities.Common.ClassUtils;
45
import com.laytonsmith.PureUtilities.Common.StringUtils;
56
import java.io.Serializable;
@@ -56,6 +57,10 @@ public AnnotationMirror(Annotation annotation) {
5657
* actually loading the annotation class into memory. See {@link #getValueWithDefault} if you are ok with loading
5758
* the annotation class into memory. Null is returned if this value doesn't exist.
5859
*
60+
* If the underlying value's type is of type Class, the class name of it is returned as a String, which you can
61+
* then choose to load yourself (with either {@link ClassDiscovery#forName} or {@link Class#forName}). This is done
62+
* to prevent loading classes referenced in annotations by default.
63+
*
5964
* @param forName
6065
* @return
6166
*/
@@ -188,6 +193,10 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl
188193
}
189194

190195
private static boolean matches(Object[] args, Class... types) {
196+
if(args == null) {
197+
return types.length == 0;
198+
}
199+
191200
if(args.length != types.length) {
192201
return false;
193202
}

src/main/java/com/laytonsmith/PureUtilities/ClassLoading/ClassMirror/ClassMirrorVisitor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ public AnnotationMirrorVisitor(AnnotationVisitor next, AnnotationMirror mirror)
217217
@Override
218218
public void visit(String name, Object value) {
219219
if(value instanceof Type) {
220-
value = ((Type) value).getDescriptor();
220+
value = ((Type) value).getClassName();
221221
}
222222
mirror.addAnnotationValue(name, value);
223223
super.visit(name, value);

src/main/java/com/laytonsmith/PureUtilities/Common/Annotations/AnnotationChecks.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,22 @@
55
import com.laytonsmith.PureUtilities.Common.ReflectionUtils;
66
import com.laytonsmith.PureUtilities.Common.StringUtils;
77
import com.laytonsmith.PureUtilities.ExhaustiveVisitor;
8+
import com.laytonsmith.annotations.NonInheritImplements;
89
import com.laytonsmith.annotations.typeof;
910
import com.laytonsmith.core.constructs.CClassType;
1011
import java.lang.reflect.Constructor;
1112
import java.lang.reflect.Member;
1213
import java.lang.reflect.Method;
1314
import java.lang.reflect.Modifier;
15+
import java.lang.reflect.Parameter;
1416
import java.util.ArrayList;
1517
import java.util.Arrays;
1618
import java.util.Collections;
1719
import java.util.HashSet;
1820
import java.util.List;
1921
import java.util.Set;
22+
import java.util.logging.Level;
23+
import java.util.logging.Logger;
2024

2125
/**
2226
* This class is run by maven at compile time, and checks to ensure that the various annotations referenced here are
@@ -138,4 +142,39 @@ public static void verifyExhaustiveVisitors() throws ClassNotFoundException {
138142
}
139143
}
140144

145+
public static void verifyNonInheritImplements() throws ClassNotFoundException {
146+
Set<ClassMirror<?>> toVerify;
147+
toVerify = ClassDiscovery.getDefaultInstance()
148+
.getClassesWithAnnotation(NonInheritImplements.class);
149+
Set<String> uhohs = new HashSet<>();
150+
for(ClassMirror<?> c : toVerify) {
151+
Class<?> iface = Class.forName(c.getAnnotation(NonInheritImplements.class).getValue("value").toString());
152+
if(!iface.isInterface()) {
153+
uhohs.add("The class given to @NonInheritImplements, tagged on " + c.getClassName() + " is not an interface, and must be.");
154+
continue;
155+
}
156+
// It's an interface, so go through all the methods it has, and make sure that the class c contains all the
157+
// methods.
158+
for(Method im : iface.getDeclaredMethods()) {
159+
try {
160+
c.getMethod(im.getName(), im.getParameterTypes());
161+
} catch(NoSuchMethodException ex) {
162+
String msg = "The class " + c.getClassName() + " implements " + iface.getSimpleName() + " but does not"
163+
+ " implement the method public " + im.getReturnType().getSimpleName() + " " + im.getName() + "(";
164+
List<String> params = new ArrayList<>();
165+
msg += StringUtils.Join(im.getParameters(), ", ", ", ", ", ", "", (Object item) -> {
166+
Parameter ci = (Parameter)item;
167+
return ci.getType().getSimpleName() + " " + ci.getName();
168+
});
169+
msg += ") {}";
170+
uhohs.add(msg);
171+
}
172+
}
173+
}
174+
if(!uhohs.isEmpty()) {
175+
String error = StringUtils.Join(uhohs, "\n");
176+
throw new Error(error);
177+
}
178+
}
179+
141180
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package com.laytonsmith.annotations;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
import java.lang.reflect.InvocationHandler;
8+
import java.lang.reflect.Method;
9+
import java.lang.reflect.Proxy;
10+
11+
/**
12+
* A class that must implement the methods of the given interface, but whose subclasses are not necessarily required to
13+
* implement them. The value must be an interface, or a compile error will be given. Note that classes that are tagged
14+
* with this are not directly castable to the specified interface, but will have all the methods required of them at
15+
* compile time. Further, it is also allowed for a subclass to be tagged with this, yet a superclass (which may or may
16+
* not be tagged with it) to actually provide the implementation. It is also an error to tag a class which is already an
17+
* instanceof this interface.
18+
*
19+
* A good use case for this mechanism is the case of serialization. Which it is useful to provide a default
20+
* serialization method, not all subclasses may want to be serializable themselves. However, if the superclass defines
21+
* itself as serializable, all subclasses would then be required to implement these methods, even if they do not
22+
* want to be serializable.
23+
*
24+
* For convenience, this interface also provides a Cast method and an Instanceof method, which replace the Java cast
25+
* mechanism and instanceof mechanism to be aware of the fact that the cast appears to not work.
26+
* @author cailin
27+
*/
28+
@Retention(RetentionPolicy.RUNTIME)
29+
@Target(ElementType.TYPE)
30+
public @interface NonInheritImplements {
31+
Class<?> value();
32+
33+
34+
/**
35+
* Provides helper methods for operations on a {@link NonInheritImplements} ecosystem.
36+
*/
37+
public static class Helper {
38+
39+
// No constructing allowed!
40+
private Helper(){}
41+
42+
/**
43+
* Casts a given object to the given type, assuming it actually is truly castable to the specified type. (Either
44+
* because it implements the interface, or NonInheritImplements it.) Proxies are used as necessary to actually
45+
* provide a first class interface.
46+
* @param <T> The return interface type
47+
* @param castType The return interface type
48+
* @param o The object to cast
49+
* @return The original object, cast to the appropriate type
50+
* @throws If the underlying object does not implement the correct type
51+
*/
52+
public static <T> T Cast(Class<T> castType, Object o) throws ClassCastException {
53+
NonInheritImplements nii = o.getClass().getAnnotation(NonInheritImplements.class);
54+
if(castType.isAssignableFrom(o.getClass())) {
55+
return castType.cast(o);
56+
} else if(nii != null) {
57+
Class<?> iface = nii.value();
58+
@SuppressWarnings("unchecked")
59+
T ifaceProxy = (T) Proxy.newProxyInstance(o.getClass().getClassLoader(), new Class[]{iface}, (Object proxy, Method method, Object[] args) -> {
60+
return o.getClass().getMethod(method.getName(), method.getParameterTypes()).invoke(o, args);
61+
});
62+
return ifaceProxy;
63+
} else {
64+
throw new ClassCastException("The object cannot be cast to " + castType.getName());
65+
}
66+
}
67+
68+
/**
69+
* Works similar to the java {@code o instanceof value} mechanism, but is aware of the NonInheritImplements
70+
* mechanism. It also checks to see if the object is castable using the normal cast mechanism too, but in
71+
* general should not replace the instanceof keyword for normal use.
72+
* @param o
73+
* @param value
74+
* @return
75+
*/
76+
public static boolean Instanceof(Object o, Class<?> value) {
77+
return value.isAssignableFrom(o.getClass())
78+
|| o.getClass().getAnnotation(NonInheritImplements.class) != null;
79+
}
80+
}
81+
}

src/main/java/com/laytonsmith/core/MainSandbox.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
package com.laytonsmith.core;
22

3+
import com.laytonsmith.annotations.NonInheritImplements;
4+
35
/**
46
* This class is for testing concepts
57
*/
68
public class MainSandbox {
79

10+
public static interface B1 {
11+
String method(int i, String b);
12+
}
13+
@NonInheritImplements(B1.class)
14+
public static class A1 {
15+
public String method(int i, String b) {
16+
return "";
17+
}
18+
}
819
public static void main(String[] argv) throws Exception {
920

1021
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.laytonsmith.PureUtilities;
2+
3+
import com.laytonsmith.annotations.NonInheritImplements;
4+
import org.junit.After;
5+
import org.junit.AfterClass;
6+
import static org.junit.Assert.assertFalse;
7+
import static org.junit.Assert.assertTrue;
8+
import static org.hamcrest.MatcherAssert.assertThat;
9+
import static org.hamcrest.core.Is.is;
10+
import org.junit.Before;
11+
import org.junit.BeforeClass;
12+
import org.junit.Test;
13+
14+
/**
15+
*
16+
* @author cailin
17+
*/
18+
public class NonInheritImplementsTest {
19+
20+
public static class A1 implements B1 {
21+
@Override
22+
public void method(){}
23+
}
24+
public static interface B1 {
25+
void method();
26+
}
27+
28+
@Test
29+
public void testNormalWorks() throws Exception {
30+
assertThat(NonInheritImplements.Helper.Instanceof(new A1(), B1.class), is(true));
31+
A1 a1 = new A1();
32+
B1 b1 = NonInheritImplements.Helper.Cast(B1.class, a1);
33+
assertTrue(a1 == b1);
34+
}
35+
36+
@Test(expected=ClassCastException.class)
37+
public void testNormalFailsCorrectly1() throws Exception {
38+
assertThat(NonInheritImplements.Helper.Instanceof(new A1(), NonInheritImplements.class), is(false));
39+
NonInheritImplements.Helper.Cast(NonInheritImplements.class, this);
40+
}
41+
42+
@NonInheritImplements(B2.class)
43+
public static class A2 {
44+
public String method2(int i) {
45+
return Integer.toString(i);
46+
}
47+
}
48+
49+
public static interface B2 {
50+
String method2(int i);
51+
}
52+
53+
@Test
54+
public void testProxyWorks() throws Exception {
55+
assertThat(NonInheritImplements.Helper.Instanceof(new A2(), B2.class), is(true));
56+
A2 a2 = new A2();
57+
B2 b2 = NonInheritImplements.Helper.Cast(B2.class, a2);
58+
assertThat(b2.method2(12), is("12"));
59+
}
60+
61+
}

0 commit comments

Comments
 (0)