diff --git a/.gitignore b/.gitignore index 2b4251a..e60c34c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /api/out/ /api/run/ /run/ +/reflection/build/ /string/build/ /string/out/ /string/run/ diff --git a/api/src/main/java/net/kyori/examination/Examinable.java b/api/src/main/java/net/kyori/examination/Examinable.java index 63dee1b..2a4b11e 100644 --- a/api/src/main/java/net/kyori/examination/Examinable.java +++ b/api/src/main/java/net/kyori/examination/Examinable.java @@ -31,7 +31,7 @@ * * @since 1.0.0 */ -public interface Examinable { +public interface Examinable extends ExaminablePropertySource { /** * Gets the examinable name. * @@ -48,6 +48,7 @@ public interface Examinable { * @return a stream of examinable properties * @since 1.0.0 */ + @Override default @NonNull Stream examinableProperties() { return Stream.empty(); } diff --git a/api/src/main/java/net/kyori/examination/ExaminablePropertySource.java b/api/src/main/java/net/kyori/examination/ExaminablePropertySource.java new file mode 100644 index 0000000..be9cc38 --- /dev/null +++ b/api/src/main/java/net/kyori/examination/ExaminablePropertySource.java @@ -0,0 +1,42 @@ +/* + * This file is part of examination, licensed under the MIT License. + * + * Copyright (c) 2018-2020 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.examination; + +import java.util.stream.Stream; +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * A source of {@link ExaminableProperty properties}. + * + * @since 1.1.0 + */ +public interface ExaminablePropertySource { + /** + * Gets a stream of examinable properties. + * + * @return a stream of examinable properties + * @since 1.1.0 + */ + @NonNull Stream examinableProperties(); +} diff --git a/reflection/build.gradle b/reflection/build.gradle new file mode 100644 index 0000000..d21e17c --- /dev/null +++ b/reflection/build.gradle @@ -0,0 +1,10 @@ +dependencies { + api project(':examination-api') + testImplementation(project(':examination-string')) +} + +jar { + manifest.attributes( + 'Automatic-Module-Name': 'net.kyori.examination.reflection' + ) +} diff --git a/reflection/src/main/java/net/kyori/examination/reflection/Examine.java b/reflection/src/main/java/net/kyori/examination/reflection/Examine.java new file mode 100644 index 0000000..24c9ca6 --- /dev/null +++ b/reflection/src/main/java/net/kyori/examination/reflection/Examine.java @@ -0,0 +1,48 @@ +/* + * This file is part of examination, licensed under the MIT License. + * + * Copyright (c) 2018-2020 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.examination.reflection; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a field which should be examined. + * + * @since 1.1.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Examine { + /** + * Gets the name. + * + *

The name of the field will be used if no value is provided.

+ * + * @return the name + * @since 1.1.0 + */ + String name() default ""; +} diff --git a/reflection/src/main/java/net/kyori/examination/reflection/ReflectiveExaminableProperties.java b/reflection/src/main/java/net/kyori/examination/reflection/ReflectiveExaminableProperties.java new file mode 100644 index 0000000..a9b9eaa --- /dev/null +++ b/reflection/src/main/java/net/kyori/examination/reflection/ReflectiveExaminableProperties.java @@ -0,0 +1,46 @@ +/* + * This file is part of examination, licensed under the MIT License. + * + * Copyright (c) 2018-2020 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.examination.reflection; + +import net.kyori.examination.ExaminableProperty; +import net.kyori.examination.ExaminablePropertySource; +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * An examinable property source which provides {@link ExaminableProperty properties} by reflectively examining an object. + * + * @since 1.1.0 + */ +public interface ReflectiveExaminableProperties extends ExaminablePropertySource { + /** + * Creates an examinable property source from the fields in {@code object} annotated with {@link Examine}. + * + * @param object the object to be examined + * @return an examinable property source + * @since 1.1.0 + */ + static @NonNull ReflectiveExaminableProperties forFields(final @NonNull Object object) { + return ReflectiveExaminablePropertiesImpl.forFields(object); + } +} diff --git a/reflection/src/main/java/net/kyori/examination/reflection/ReflectiveExaminablePropertiesImpl.java b/reflection/src/main/java/net/kyori/examination/reflection/ReflectiveExaminablePropertiesImpl.java new file mode 100644 index 0000000..5931f90 --- /dev/null +++ b/reflection/src/main/java/net/kyori/examination/reflection/ReflectiveExaminablePropertiesImpl.java @@ -0,0 +1,90 @@ +/* + * This file is part of examination, licensed under the MIT License. + * + * Copyright (c) 2018-2020 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.examination.reflection; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Supplier; +import java.util.stream.Stream; +import net.kyori.examination.ExaminableProperty; +import org.checkerframework.checker.nullness.qual.NonNull; + +final class ReflectiveExaminablePropertiesImpl implements ReflectiveExaminableProperties { + private final List> properties; + + private ReflectiveExaminablePropertiesImpl(final List> properties) { + this.properties = properties; + } + + public static ReflectiveExaminablePropertiesImpl forFields(final Object object) { + final List> properties = new ArrayList<>(); + for(final Field field : object.getClass().getDeclaredFields()) { + final Examine examine = field.getAnnotation(Examine.class); + if(examine != null) { + field.setAccessible(true); // TODO(kashike): does this break on new Java versions? + + final String name = name(field, examine); + final MethodHandle handle; + try { + handle = MethodHandles.lookup().unreflectGetter(field); + } catch(final IllegalAccessException e) { + // TODO(kashike): how to handle errors? + e.printStackTrace(); + continue; + } + properties.add(() -> { + final Object value; + try { + value = handle.invoke(object); + } catch(final Throwable e) { + // TODO(kashike): how to handle errors? + e.printStackTrace(); + return null; + } + return ExaminableProperty.of(name, value); + }); + } + } + return new ReflectiveExaminablePropertiesImpl(properties); + } + + @Override + public @NonNull Stream examinableProperties() { + return this.properties.stream() + .map(Supplier::get) + .filter(Objects::nonNull); + } + + private static String name(final Field field, final Examine examine) { + String name = examine.name(); + if(name.isEmpty()) { + name = field.getName(); + } + return name; + } +} diff --git a/reflection/src/test/java/net/kyori/examination/reflection/ReflectiveExaminablePropertiesSourceTest.java b/reflection/src/test/java/net/kyori/examination/reflection/ReflectiveExaminablePropertiesSourceTest.java new file mode 100644 index 0000000..b5f3989 --- /dev/null +++ b/reflection/src/test/java/net/kyori/examination/reflection/ReflectiveExaminablePropertiesSourceTest.java @@ -0,0 +1,57 @@ +/* + * This file is part of examination, licensed under the MIT License. + * + * Copyright (c) 2018-2020 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.examination.reflection; + +import java.util.stream.Stream; +import net.kyori.examination.Examinable; +import net.kyori.examination.ExaminableProperty; +import net.kyori.examination.string.StringExaminer; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.junit.jupiter.api.Test; + +class ReflectiveExaminablePropertiesSourceTest { + @Test + void test() { + final TestExaminable te = new TestExaminable("kashike", 0); + te.examinableProperties().forEach(ep -> { + System.out.println(ep.name() + ": " + ep.examine(StringExaminer.simpleEscaping())); + }); + } + + static class TestExaminable implements Examinable { + private final ReflectiveExaminableProperties reps = ReflectiveExaminableProperties.forFields(this); + private final @Examine String name; + private final @Examine int age; + + TestExaminable(final String name, final int age) { + this.name = name; + this.age = age; + } + + @Override + public @NonNull Stream examinableProperties() { + return this.reps.examinableProperties(); + } + } +} diff --git a/settings.gradle b/settings.gradle index 282661c..cd4cc53 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,5 +3,8 @@ rootProject.name = 'examination' include 'api' findProject(':api')?.name = 'examination-api' +include 'reflection' +findProject(':reflection')?.name = 'examination-reflection' + include 'string' findProject(':string')?.name = 'examination-string'