diff --git a/.gitignore b/.gitignore index 24fbd6346..27e2aa33f 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,7 @@ build-logic/*/bin/ /format/*/.apt_generated_tests/ /format/*/.checkstyle /format/*/.settings/ -/extra/*/build/ +/extra/**/build/ /extra/*/out/ /extra/*/bin/ /extra/*/.factorypath diff --git a/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapper.java b/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapper.java index 5f8d2e5f1..6ca44b573 100644 --- a/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapper.java +++ b/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapper.java @@ -316,6 +316,44 @@ default Builder addProcessor(final Class definition, f */ Builder addProcessor(Class definition, Class valueType, Processor.Factory factory); + /** + * Register a {@link Processor} that will process fields after write. + * The difference between an AdvancedFactory and a Factory is that + * an AdvancedFactory has access to all the annotations on the + * field, which makes more advanced processors possible. + * + *

Processors registered without a specific data type should be + * able to operate on any value type.

+ * + * @param definition annotation providing data + * @param factory factory for callback function + * @param
annotation type + * @return this builder + * @since 4.0.0 + */ + default Builder addProcessor(final Class definition, final Processor.AdvancedFactory factory) { + return addProcessor(definition, Object.class, factory); + } + + /** + * Register a {@link Processor} that will process fields after write. + * The difference between an AdvancedFactory and a Factory is that + * an AdvancedFactory has access to all the annotations on the + * field, which makes more advanced processors possible. + * + *

All value types will be tested against types normalized to + * their boxed variants.

+ * + * @param definition annotation providing data + * @param valueType value types the processor will handle + * @param factory factory for callback function + * @param
annotation type + * @param data type + * @return this builder + * @since 4.0.0 + */ + Builder addProcessor(Class definition, Class valueType, Processor.AdvancedFactory factory); + /** * Register a {@link Constraint} that will be used to validate fields. * diff --git a/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapperFactoryImpl.java b/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapperFactoryImpl.java index 2426c87e4..f95849916 100644 --- a/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapperFactoryImpl.java +++ b/core/src/main/java/org/spongepowered/configurate/objectmapping/ObjectMapperFactoryImpl.java @@ -72,7 +72,7 @@ protected boolean removeEldestEntry(final Map.Entry> eldes private final List resolverFactories; private final List> fieldDiscoverers; private final Map, List>>> constraints; - private final Map, List>>> processors; + private final Map, List>>> processors; private final List postProcessors; ObjectMapperFactoryImpl(final Builder builder) { @@ -97,7 +97,7 @@ protected boolean removeEldestEntry(final Map.Entry> eldes this.constraints.values().forEach(Collections::reverse); this.processors = new HashMap<>(); - for (final Definition> def : builder.processors) { + for (final Definition> def : builder.processors) { this.processors.computeIfAbsent(def.annotation(), k -> new ArrayList<>()).add(def); } this.processors.values().forEach(Collections::reverse); @@ -206,11 +206,11 @@ private void makeData(final List> fields, final String na } } - final List>> processorDefs = this.processors.get(annotation.annotationType()); + final List>> processorDefs = this.processors.get(annotation.annotationType()); if (processorDefs != null) { - for (final Definition> processorDef : processorDefs) { + for (final Definition> processorDef : processorDefs) { if (isSuperType(processorDef.type(), normalizedType)) { - processors.add(((Processor.Factory) processorDef.factory()).make(annotation, type.getType())); + processors.add(((Processor.AdvancedFactory) processorDef.factory()).make(annotation, type.getType(), container)); } } } @@ -356,7 +356,7 @@ static class Builder implements ObjectMapper.Factory.Builder { private final List resolvers = new ArrayList<>(); private final List> discoverer = new ArrayList<>(); private final List>> constraints = new ArrayList<>(); - private final List>> processors = new ArrayList<>(); + private final List>> processors = new ArrayList<>(); private final List postProcessors = new ArrayList<>(); @Override @@ -384,6 +384,13 @@ public Builder addProcessor(final Class definition, return this; } + @Override + public Builder addProcessor(final Class definition, final Class valueType, + final Processor.AdvancedFactory factory) { + this.processors.add(Definition.of(definition, valueType, factory)); + return this; + } + @Override public Builder addConstraint(final Class definition, final Class valueType, final Constraint.Factory factory) { diff --git a/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Comment.java b/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Comment.java index 1b0a969dc..468620cd3 100644 --- a/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Comment.java +++ b/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Comment.java @@ -33,7 +33,7 @@ * * @since 4.0.0 */ -@Target({ElementType.FIELD, ElementType.PARAMETER}) +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface Comment { diff --git a/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Matches.java b/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Matches.java index 824b99632..beaf41ab2 100644 --- a/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Matches.java +++ b/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Matches.java @@ -35,7 +35,7 @@ */ @Documented @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) +@Target({ElementType.FIELD, ElementType.METHOD}) public @interface Matches { /** diff --git a/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Processor.java b/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Processor.java index be223b150..4ca2f6cf2 100644 --- a/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Processor.java +++ b/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Processor.java @@ -20,6 +20,7 @@ import org.spongepowered.configurate.ConfigurationNode; import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Type; import java.util.ResourceBundle; @@ -40,6 +41,31 @@ public interface Processor { */ void process(V value, ConfigurationNode destination); + /** + * Provider to, given an annotation instance and the type it's on, + * create a {@link Processor}. If you don't need access to the other + * annotations on the field, you can also choose the simpler {@link Factory}. + * + * @param annotation type + * @param handled value type + * @since 4.0.0 + */ + @FunctionalInterface + interface AdvancedFactory { + + /** + * Create a new processor given the annotation and data type. + * + * @param data annotation type on record field + * @param value declared field type + * @param container container holding the field, with its annotations + * @return new processor + * @since 4.0.0 + */ + Processor make(A data, Type value, AnnotatedElement container); + + } + /** * Provider to, given an annotation instance and the type it's on, * create a {@link Processor}. @@ -49,7 +75,7 @@ public interface Processor { * @since 4.0.0 */ @FunctionalInterface - interface Factory { + interface Factory extends AdvancedFactory { /** * Create a new processor given the annotation and data type. @@ -61,6 +87,10 @@ interface Factory { */ Processor make(A data, Type value); + @Override + default Processor make(A data, Type value, AnnotatedElement element) { + return make(data, value); + } } /** diff --git a/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Required.java b/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Required.java index c1354016d..95758e6b7 100644 --- a/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Required.java +++ b/core/src/main/java/org/spongepowered/configurate/objectmapping/meta/Required.java @@ -34,7 +34,7 @@ */ @Documented @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) +@Target({ElementType.FIELD, ElementType.METHOD}) @SubtypeOf(NonNull.class) public @interface Required { } diff --git a/extra/interface/ap/build.gradle b/extra/interface/ap/build.gradle new file mode 100644 index 000000000..9b4dc7e91 --- /dev/null +++ b/extra/interface/ap/build.gradle @@ -0,0 +1,29 @@ +plugins { + id "org.spongepowered.configurate.build.component" +} + +description = "Annotation processor for Configurate to generate an implementation for config interfaces" + +dependencies { + implementation projects.core + implementation projects.extra.extraInterface + implementation libs.javapoet + implementation libs.auto.service + annotationProcessor libs.auto.service + + testImplementation libs.compile.testing +} + +// there is no javadoc +tasks.withType(Javadoc).configureEach { enabled = false } + +tasks.withType(Test).configureEach { + doFirst { + // See: https://github.com/google/compile-testing/issues/222 + if (javaLauncher.get().metadata.languageVersion >= JavaLanguageVersion.of(9)) { + jvmArgs '--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED' + jvmArgs '--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED' + jvmArgs '--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED' + } + } +} diff --git a/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationDefaults.java b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationDefaults.java new file mode 100644 index 000000000..0ae7e3e33 --- /dev/null +++ b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationDefaults.java @@ -0,0 +1,135 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces.processor; + +import static org.spongepowered.configurate.interfaces.processor.Utils.annotation; + +import com.google.auto.common.MoreTypes; +import com.squareup.javapoet.AnnotationSpec; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.configurate.interfaces.meta.defaults.DefaultBoolean; +import org.spongepowered.configurate.interfaces.meta.defaults.DefaultDecimal; +import org.spongepowered.configurate.interfaces.meta.defaults.DefaultNumeric; +import org.spongepowered.configurate.interfaces.meta.defaults.DefaultString; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import javax.lang.model.AnnotatedConstruct; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; + +final class AnnotationDefaults implements AnnotationProcessor { + + static final AnnotationDefaults INSTANCE = new AnnotationDefaults(); + + private AnnotationDefaults() {} + + @Override + public Set> processes() { + return new HashSet<>(Arrays.asList(DefaultBoolean.class, DefaultDecimal.class, DefaultNumeric.class, DefaultString.class)); + } + + @Override + public void process( + final TypeElement targetInterface, + final ExecutableElement element, + final TypeMirror nodeType, + final FieldSpecBuilderTracker fieldSpec + ) throws IllegalStateException { + // there are two types of default values, one using annotations and one using the default value of a default method + + // first, handle default value of a default method getter + if (element.isDefault() && element.getParameters().isEmpty() && hasNoAnnotationDefaults(element)) { + fieldSpec.initializer("$T.super.$L()", targetInterface, element.getSimpleName()); + return; + } + + // if it's not using the default value of a default method, use the annotations + final @Nullable DefaultBoolean defaultBoolean = annotation(element, DefaultBoolean.class); + final @Nullable DefaultDecimal defaultDecimal = annotation(element, DefaultDecimal.class); + final @Nullable DefaultNumeric defaultNumeric = annotation(element, DefaultNumeric.class); + final @Nullable DefaultString defaultString = annotation(element, DefaultString.class); + final boolean hasDefault = defaultBoolean != null || defaultDecimal != null || defaultNumeric != null || defaultString != null; + + @Nullable Class annnotationType = null; + @Nullable Object value = null; + if (hasDefault) { + if (MoreTypes.isTypeOf(Boolean.TYPE, nodeType)) { + if (defaultBoolean == null) { + throw new IllegalStateException("A default value of the incorrect type was provided for " + element); + } + annnotationType = DefaultBoolean.class; + value = defaultBoolean.value(); + + } else if (Utils.isDecimal(nodeType)) { + if (defaultDecimal == null) { + throw new IllegalStateException("A default value of the incorrect type was provided for " + element); + } + annnotationType = DefaultDecimal.class; + value = defaultDecimal.value(); + + } else if (Utils.isNumeric(nodeType)) { + if (defaultNumeric == null) { + throw new IllegalStateException("A default value of the incorrect type was provided for " + element); + } + annnotationType = DefaultNumeric.class; + value = defaultNumeric.value(); + + } else if (MoreTypes.isTypeOf(String.class, nodeType)) { + if (defaultString == null) { + throw new IllegalStateException("A default value of the incorrect type was provided for " + element); + } + annnotationType = DefaultString.class; + value = defaultString.value(); + } + } + + if (annnotationType == null) { + return; + } + + final boolean isString = value instanceof String; + + // special cases are floats and longs, because the default for decimals + // is double and for numerics it's int. + if (MoreTypes.isTypeOf(Float.TYPE, nodeType)) { + value = value + "F"; + } else if (MoreTypes.isTypeOf(Long.TYPE, nodeType)) { + value = value + "L"; + } + + fieldSpec.addAnnotation( + AnnotationSpec.builder(annnotationType) + .addMember("value", isString ? "$S" : "$L", value) + ); + fieldSpec.initializer(isString ? "$S" : "$L", value); + } + + static boolean hasNoAnnotationDefaults(final AnnotatedConstruct construct) { + for (Class defaultAnnotation : INSTANCE.processes()) { + if (annotation(construct, defaultAnnotation) != null) { + return false; + } + } + return true; + } + +} diff --git a/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationHidden.java b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationHidden.java new file mode 100644 index 000000000..b55cf42b4 --- /dev/null +++ b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationHidden.java @@ -0,0 +1,63 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces.processor; + +import static org.spongepowered.configurate.interfaces.processor.Utils.hasAnnotation; + +import org.spongepowered.configurate.interfaces.meta.Hidden; + +import java.lang.annotation.Annotation; +import java.util.Collections; +import java.util.Set; + +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; + +final class AnnotationHidden implements AnnotationProcessor { + + static final AnnotationHidden INSTANCE = new AnnotationHidden(); + + private AnnotationHidden() {} + + @Override + public Set> processes() { + // the purpose of this class is to warn people, not to add the annotation. + // AnnotationOthers can do that just fine + return Collections.emptySet(); + } + + @Override + public void process( + final TypeElement targetInterface, + final ExecutableElement element, + final TypeMirror nodeType, + final FieldSpecBuilderTracker fieldSpec + ) throws IllegalStateException { + if (!element.isDefault()) { + return; + } + + // throw exception to prevent unexpected behaviour during runtime + if (hasAnnotation(element, Hidden.class) && AnnotationDefaults.hasNoAnnotationDefaults(element)) { + throw new IllegalStateException( + "Due to limitations there is no support for methods that use the default value and Hidden. Method: " + element + ); + } + } + +} diff --git a/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationOthers.java b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationOthers.java new file mode 100644 index 000000000..1452f96d6 --- /dev/null +++ b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationOthers.java @@ -0,0 +1,83 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces.processor; + +import static org.spongepowered.configurate.interfaces.processor.Utils.annotation; + +import com.google.auto.common.MoreElements; +import com.squareup.javapoet.AnnotationSpec; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; + +final class AnnotationOthers implements AnnotationProcessor { + + static final AnnotationOthers INSTANCE = new AnnotationOthers(); + + private AnnotationOthers() {} + + @Override + public Set> processes() { + return new HashSet<>(); + } + + @Override + public void process( + final TypeElement targetInterface, + final ExecutableElement element, + final TypeMirror nodeType, + final FieldSpecBuilderTracker fieldSpec) { + for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) { + //noinspection UnstableApiUsage + final TypeElement annotationType = MoreElements.asType(annotationMirror.getAnnotationType().asElement()); + + // only handle not yet processed annotations + if (fieldSpec.isProcessed(annotationType)) { + continue; + } + + final @Nullable Target target = annotation(annotationType, Target.class); + final boolean isCompatible = target == null || Arrays.stream(target.value()).anyMatch(elementType -> ElementType.FIELD == elementType); + // an annotation is only compatible if it supports fields, if it has no target it supports everything + if (!isCompatible) { + continue; + } + + final @Nullable Retention retention = annotation(annotationType, Retention.class); + final boolean hasRuntimeRetention = retention != null && RetentionPolicy.RUNTIME == retention.value(); + // not needed to add an annotation if it has no runtime retention + if (!hasRuntimeRetention) { + continue; + } + + fieldSpec.addAnnotation(AnnotationSpec.get(annotationMirror)); + } + } + +} diff --git a/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationProcessor.java b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationProcessor.java new file mode 100644 index 000000000..c4468c704 --- /dev/null +++ b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationProcessor.java @@ -0,0 +1,51 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces.processor; + +import java.lang.annotation.Annotation; +import java.util.Set; + +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; + +interface AnnotationProcessor { + + /** + * A set of annotations this processor will process. + * + * @return a set of annotations this processor will process. + */ + Set> processes(); + + /** + * Process a method. + * There is no guarantee that one of the {@link #processes()} annotations is present on this element. + * + * @param element the method that is being processed + * @param nodeType the type of the field that is being generated + * @param fieldSpec the builder of the field that is being generated + * @throws IllegalStateException when something goes wrong + */ + void process( + TypeElement targetInterface, + ExecutableElement element, + TypeMirror nodeType, + FieldSpecBuilderTracker fieldSpec + ) throws IllegalStateException; + +} diff --git a/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationProcessorHandler.java b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationProcessorHandler.java new file mode 100644 index 000000000..b16cd6a30 --- /dev/null +++ b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/AnnotationProcessorHandler.java @@ -0,0 +1,54 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces.processor; + +import com.squareup.javapoet.FieldSpec; + +import java.util.ArrayList; +import java.util.List; + +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; + +final class AnnotationProcessorHandler { + + private static final List HANDLERS = new ArrayList<>(); + + static { + HANDLERS.add(AnnotationDefaults.INSTANCE); + HANDLERS.add(AnnotationHidden.INSTANCE); + // always add others as last because it adds all annotations that have not been processed + HANDLERS.add(AnnotationOthers.INSTANCE); + } + + private AnnotationProcessorHandler() {} + + static void handle( + final TypeElement targetInterface, + final ExecutableElement element, + final TypeMirror nodeType, + final FieldSpec.Builder fieldSpec) { + final FieldSpecBuilderTracker fieldTracker = new FieldSpecBuilderTracker(fieldSpec); + + for (AnnotationProcessor handler : HANDLERS) { + handler.process(targetInterface, element, nodeType, fieldTracker); + fieldTracker.processed(handler.processes()); + } + } + +} diff --git a/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/ConfigImplementationGenerator.java b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/ConfigImplementationGenerator.java new file mode 100644 index 000000000..2283ac939 --- /dev/null +++ b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/ConfigImplementationGenerator.java @@ -0,0 +1,287 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces.processor; + +import static org.spongepowered.configurate.interfaces.processor.Utils.hasAnnotation; + +import com.google.auto.common.MoreTypes; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.configurate.interfaces.meta.Exclude; +import org.spongepowered.configurate.interfaces.meta.Field; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.PostProcess; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.Name; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; + +class ConfigImplementationGenerator { + + private final ConfigImplementationGeneratorProcessor processor; + private final TypeElement source; + + ConfigImplementationGenerator( + final ConfigImplementationGeneratorProcessor processor, + final TypeElement source + ) { + this.processor = processor; + this.source = source; + } + + /** + * Returns the generated class or null if something went wrong during + * generation. + */ + public TypeSpec.@Nullable Builder generate() { + final ClassName className = ClassName.get(this.source); + + final TypeSpec.Builder spec = TypeSpec + .classBuilder(className.simpleName() + "Impl") + .addSuperinterface(className) + .addModifiers(Modifier.FINAL) + .addAnnotation(ConfigSerializable.class) + .addJavadoc("Automatically generated implementation of the config"); + + final TypeSpecBuilderTracker tracker = new TypeSpecBuilderTracker(); + if (!gatherElementSpec(tracker, this.source)) { + return null; + } + tracker.writeTo(spec); + + final String qualifiedName = className.reflectionName(); + final String qualifiedImplName = qualifiedName.replace("$", "Impl$") + "Impl"; + this.processor.generatedClasses().put(qualifiedName, qualifiedImplName); + + return spec; + } + + /** + * Returns true if successful, otherwise false. + */ + private boolean gatherElementSpec( + final TypeSpecBuilderTracker spec, + final TypeElement type + ) { + return gatherElementSpec(spec, type, new HashSet<>()); + } + + /** + * Returns true if successful, otherwise false. + * + * @param excludedElements a set of all elements a superclass has annotated with {@link Exclude}. + */ + private boolean gatherElementSpec( + final TypeSpecBuilderTracker spec, + final TypeElement type, + final Set excludedElements + ) { + // first handle own elements + // If this interface is noted as ConfigSerializable, then its element order should override previous configs + final boolean hasConfigSerializable = hasAnnotation(type, ConfigSerializable.class); + + for (final Element enclosedElement : type.getEnclosedElements()) { + final ElementKind kind = enclosedElement.getKind(); + + if (kind == ElementKind.INTERFACE && hasAnnotation(enclosedElement, ConfigSerializable.class)) { + final TypeSpec.@Nullable Builder generated = + new ConfigImplementationGenerator(this.processor, (TypeElement) enclosedElement) + .generate(); + + // if something went wrong in the child class, the parent can't complete normally either + if (generated == null) { + return false; + } + + spec.add(enclosedElement.getSimpleName().toString(), generated.addModifiers(Modifier.STATIC)); + continue; + } + + if (kind != ElementKind.METHOD) { + continue; + } + + final ExecutableElement element = (ExecutableElement) enclosedElement; + + if (hasAnnotation(element, PostProcess.class)) { + // A postprocess annotated method is not a config node + continue; + } + + if (excludedElements.contains(element.getSimpleName())) { + continue; + } + final boolean excluded = hasAnnotation(element, Exclude.class); + if (excluded) { + if (element.isDefault()) { + // Do not add setters to the exclusion list as they will not be serialized anyway. + if (element.getParameters().isEmpty()) { + excludedElements.add(element.getSimpleName()); + } + continue; + } + this.processor.printError( + "Cannot make config due to method %s, which is an excluded method that has no implementation!", + element + ); + return false; + } + + // all methods are either setters or getters past this point + + final List parameters = element.getParameters(); + if (parameters.size() > 1) { + this.processor.printError("Setters cannot have more than one parameter! Method: " + element); + return false; + } + + final String simpleName = element.getSimpleName().toString(); + TypeMirror nodeType = element.getReturnType(); + + if (parameters.size() == 1) { + // setter + final VariableElement parameter = parameters.get(0); + final boolean success = handleSetter(element, simpleName, parameter, nodeType, spec); + if (!success) { + return false; + } + nodeType = parameter.asType(); + } else { + handleGetter(element, simpleName, nodeType, spec); + } + + final FieldSpec.Builder fieldSpec = FieldSpec.builder(TypeName.get(nodeType), simpleName, Modifier.PRIVATE); + + final boolean isField = hasAnnotation(element, Field.class); + if (isField) { + fieldSpec.addModifiers(Modifier.TRANSIENT); + } + + // set a default value for config subsections + final TypeElement nodeTypeElement = Utils.toBoxedTypeElement(nodeType, this.processor.typeUtils); + if (!isField && !element.isDefault() && hasAnnotation(nodeTypeElement, ConfigSerializable.class)) { + ClassName configClass = ClassName.get(nodeTypeElement); + if (nodeTypeElement.getKind().isInterface()) { + // first find the generated class for given type + String implName = this.processor.generatedClasses().getProperty(configClass.reflectionName()); + if (implName == null) { + this.processor.printError("Could not determine an implementation type for method " + element.getSimpleName()); + return false; + } + // make it canonical and replace superinterface type with source interface type if present + implName = implName.replace('$', '.').replace(type.getQualifiedName(), this.source.getQualifiedName()); + configClass = ClassName.bestGuess(implName); + } + fieldSpec.initializer("new $T()", configClass); + } + + //todo add tests for hidden in both ap and interfaces and defaults in interfaces + AnnotationProcessorHandler.handle(this.source, element, nodeType, fieldSpec); + + // If this is a getter and ConfigSerializable, then it should define where in the config + // this element should go. + spec.add(simpleName, fieldSpec, hasConfigSerializable && element.getParameters().isEmpty()); + } + + // then handle parent elements + for (final TypeMirror parent : type.getInterfaces()) { + gatherElementSpec(spec, (TypeElement) this.processor.typeUtils.asElement(parent), excludedElements); + } + return true; + } + + private boolean handleSetter( + final ExecutableElement element, + final String simpleName, + final VariableElement parameter, + final TypeMirror returnType, + final TypeSpecBuilderTracker spec + ) { + final MethodSpec.Builder method = MethodSpec.overriding(element); + + // we have two main branches of setters, default non-void setters and non-default any setters + if (element.isDefault()) { + if (MoreTypes.isTypeOf(Void.TYPE, returnType)) { + this.processor.printError("A default setter cannot have void as return type. Method: " + element); + return false; + } + + method.addStatement( + "this.$N = $T.super.$L($N)", + simpleName, + element.getEnclosingElement(), + simpleName, + parameter.getSimpleName() + ); + } else { + method.addStatement("this.$N = $N", simpleName, parameter.getSimpleName()); + } + + // if it's not void + if (!MoreTypes.isTypeOf(Void.TYPE, returnType)) { + // the return type can be a parent type of parameter, but it has to be assignable + if (!this.processor.typeUtils.isAssignable(parameter.asType(), returnType)) { + this.processor.printError( + "Cannot create a setter with return type %s for argument type %s. Method: %s", + returnType, + parameter.asType(), + element + ); + return false; + } + method.addStatement("return this.$N", simpleName); + } + + spec.add(simpleName + "#" + parameter.getSimpleName(), method); + return true; + } + + private void handleGetter( + final ExecutableElement element, + final String simpleName, + final TypeMirror nodeType, + final TypeSpecBuilderTracker spec + ) { + // voids aren't valid + if (MoreTypes.isTypeOf(Void.TYPE, nodeType)) { + this.processor.printError( + "Cannot create a getter with return type void for method %s, did you forget to @Exclude this method?", + element + ); + } + + spec.add( + simpleName, + MethodSpec.overriding(element) + .addStatement("return $N", element.getSimpleName()) + ); + } + +} diff --git a/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/ConfigImplementationGeneratorProcessor.java b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/ConfigImplementationGeneratorProcessor.java new file mode 100644 index 000000000..ba1dc9f8a --- /dev/null +++ b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/ConfigImplementationGeneratorProcessor.java @@ -0,0 +1,150 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces.processor; + +import static org.spongepowered.configurate.interfaces.processor.Utils.isNestedConfig; + +import com.google.auto.service.AutoService; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.JavaFile; +import com.squareup.javapoet.TypeSpec; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.configurate.interfaces.Constants; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; + +import java.io.IOException; +import java.io.Writer; +import java.util.Collections; +import java.util.Locale; +import java.util.Properties; +import java.util.Set; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.Filer; +import javax.annotation.processing.Messager; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.Processor; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Types; +import javax.tools.Diagnostic.Kind; +import javax.tools.FileObject; +import javax.tools.StandardLocation; + +/** + * Generates an implementation for a given interface based config, + * which then can be read by Configurate. + * + * @since 4.2.0 + */ +@AutoService(Processor.class) +public final class ConfigImplementationGeneratorProcessor extends AbstractProcessor { + + private final Properties mappings = new Properties(); + Types typeUtils; + private Filer filer; + private Messager messager; + + @Override + @SuppressWarnings("PMD.AvoidSynchronizedAtMethodLevel") + public synchronized void init(final ProcessingEnvironment processingEnv) { + super.init(processingEnv); + this.typeUtils = processingEnv.getTypeUtils(); + this.filer = processingEnv.getFiler(); + this.messager = processingEnv.getMessager(); + } + + @Override + public Set getSupportedAnnotationTypes() { + return Collections.singleton(ConfigSerializable.class.getCanonicalName()); + } + + @Override + public boolean process(final Set ignored, final RoundEnvironment env) { + if (env.processingOver()) { + if (!env.errorRaised()) { + writeMappings(); + } + return false; + } + + for (final Element element : env.getElementsAnnotatedWith(ConfigSerializable.class)) { + if (element.getKind() != ElementKind.INTERFACE) { + continue; + } + final TypeElement typeElement = (TypeElement) element; + + // nested classes are handled in their containing interfaces + if (isNestedConfig(typeElement)) { + continue; + } + + try { + processInterface(typeElement); + } catch (final IOException exception) { + printError(exception.getMessage()); + } + } + + return false; + } + + /** + * Generate a class for the given interface. + */ + private void processInterface(final TypeElement type) throws IOException { + final ClassName className = ClassName.get(type); + + final TypeSpec.@Nullable Builder generated = new ConfigImplementationGenerator(this, type).generate(); + if (generated == null) { + return; + } + + JavaFile.builder(className.packageName(), generated.build()) + .build() + .writeTo(this.filer); + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latest(); + } + + private void writeMappings() { + final FileObject resource; + try { + resource = this.filer.createResource(StandardLocation.CLASS_OUTPUT, "", Constants.MAPPING_FILE); + try (Writer writer = resource.openWriter()) { + this.mappings.store(writer, null); + } + } catch (final IOException exception) { + throw new RuntimeException("Failed to write interface mappings!", exception); + } + } + + Properties generatedClasses() { + return this.mappings; + } + + void printError(final String message, final Object... arguments) { + this.messager.printMessage(Kind.ERROR, String.format(Locale.ROOT, message, arguments)); + } + +} diff --git a/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/FieldSpecBuilderTracker.java b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/FieldSpecBuilderTracker.java new file mode 100644 index 000000000..22578ee33 --- /dev/null +++ b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/FieldSpecBuilderTracker.java @@ -0,0 +1,66 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces.processor; + +import com.squareup.javapoet.AnnotationSpec; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.FieldSpec; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import javax.lang.model.element.TypeElement; + +final class FieldSpecBuilderTracker { + + private final Set processed = new HashSet<>(); + private final FieldSpec.Builder builder; + + FieldSpecBuilderTracker(final FieldSpec.Builder builder) { + this.builder = builder; + } + + void addAnnotation(final AnnotationSpec annotation) { + this.processed.add(((ClassName) annotation.type)); + this.builder.addAnnotation(annotation); + } + + void addAnnotation(final AnnotationSpec.Builder annotationBuilder) { + addAnnotation(annotationBuilder.build()); + } + + void initializer(final String format, final Object... args) { + this.builder.initializer(format, args); + } + + boolean isProcessed(final Class annotation) { + return this.processed.contains(ClassName.get(annotation)); + } + + boolean isProcessed(final TypeElement annotationType) { + return this.processed.contains(ClassName.get(annotationType)); + } + + void processed(final Collection> annotations) { + for (Class annotation : annotations) { + this.processed.add(ClassName.get(annotation)); + } + } + +} diff --git a/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/TypeSpecBuilderTracker.java b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/TypeSpecBuilderTracker.java new file mode 100644 index 000000000..da3cd230a --- /dev/null +++ b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/TypeSpecBuilderTracker.java @@ -0,0 +1,101 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces.processor; + +import com.squareup.javapoet.AnnotationSpec; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeSpec; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * {@link TypeSpec.Builder} does not keep track of duplicates, resulting in failures to compile. + * This will only allow a single definition of a given method/field + */ +final class TypeSpecBuilderTracker { + + private final Map fieldSpecs = new LinkedHashMap<>(); + private final Map methodSpecs = new LinkedHashMap<>(); + private final Map typeSpecs = new LinkedHashMap<>(); + + void add(final String fieldIdentifier, final FieldSpec.Builder builder, final boolean override) { + final FieldSpec.Builder existing = override ? this.fieldSpecs.remove(fieldIdentifier) + : this.fieldSpecs.get(fieldIdentifier); + if (existing != null) { + final FieldSpec existingBuild = existing.build(); + final FieldSpec builderBuild = builder.build(); + // copy initializer of the builder to the existing one if the existing one doesn't have an initializer + if (existingBuild.initializer.isEmpty() && !builderBuild.initializer.isEmpty()) { + existing.initializer(builderBuild.initializer); + } + existing.addAnnotations(pickNewAnnotations(existingBuild.annotations, builderBuild.annotations)); + if (override) { + this.fieldSpecs.put(fieldIdentifier, existing); + } + return; + } + this.fieldSpecs.put(fieldIdentifier, builder); + } + + void add(final String methodIdentifier, final MethodSpec.Builder builder) { + final MethodSpec.Builder existing = this.methodSpecs.get(methodIdentifier); + if (existing != null) { + existing.addAnnotations(pickNewAnnotations(existing.build().annotations, builder.build().annotations)); + return; + } + this.methodSpecs.put(methodIdentifier, builder); + } + + void add(final String typeIdentifier, final TypeSpec.Builder builder) { + if (this.typeSpecs.putIfAbsent(typeIdentifier, builder.build()) != null) { + throw new IllegalStateException( + "Cannot have multiple nested types with the same name! Name: " + typeIdentifier); + } + } + + void writeTo(final TypeSpec.Builder builder) { + for (FieldSpec.Builder field : this.fieldSpecs.values()) { + builder.addField(field.build()); + } + for (MethodSpec.Builder method : this.methodSpecs.values()) { + builder.addMethod(method.build()); + } + this.typeSpecs.values().forEach(builder::addType); + } + + private List pickNewAnnotations( + final List existing, + final List newOne + ) { + final List result = new ArrayList<>(); + // only add annotations if they don't already exist + outer: for (AnnotationSpec spec : newOne) { + for (AnnotationSpec existingSpec : existing) { + if (existingSpec.type.equals(spec.type)) { + break outer; + } + } + result.add(spec); + } + return result; + } + +} diff --git a/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/Utils.java b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/Utils.java new file mode 100644 index 000000000..8f9143499 --- /dev/null +++ b/extra/interface/ap/src/main/java/org/spongepowered/configurate/interfaces/processor/Utils.java @@ -0,0 +1,78 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces.processor; + +import com.google.auto.common.MoreTypes; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; + +import java.lang.annotation.Annotation; + +import javax.lang.model.AnnotatedConstruct; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Types; + +final class Utils { + + private Utils() {} + + static boolean hasAnnotation(final AnnotatedConstruct element, final Class annotation) { + return annotation(element, annotation) != null; + } + + /** + * The same as {@link AnnotatedConstruct#getAnnotation(Class)} except that + * you don't have to suppress the ConstantValue warning everywhere. + */ + @SuppressWarnings("DataFlowIssue") + static @Nullable T annotation(final AnnotatedConstruct construct, final Class annotation) { + return construct.getAnnotation(annotation); + } + + static boolean isNestedConfig(final TypeElement type) { + if (!type.getNestingKind().isNested()) { + return false; + } + + Element current = type; + while (current.getKind() == ElementKind.INTERFACE && hasAnnotation(current, ConfigSerializable.class)) { + current = current.getEnclosingElement(); + } + return current.getKind() == ElementKind.PACKAGE; + } + + static boolean isDecimal(final TypeMirror typeMirror) { + return MoreTypes.isTypeOf(Float.TYPE, typeMirror) || MoreTypes.isTypeOf(Double.TYPE, typeMirror); + } + + static boolean isNumeric(final TypeMirror typeMirror) { + return MoreTypes.isTypeOf(Byte.TYPE, typeMirror) || MoreTypes.isTypeOf(Character.TYPE, typeMirror) + || MoreTypes.isTypeOf(Short.TYPE, typeMirror) || MoreTypes.isTypeOf(Integer.TYPE, typeMirror) + || MoreTypes.isTypeOf(Long.TYPE, typeMirror); + } + + public static TypeElement toBoxedTypeElement(final TypeMirror mirror, final Types typeUtils) { + if (mirror.getKind().isPrimitive()) { + return typeUtils.boxedClass(MoreTypes.asPrimitiveType(mirror)); + } + return MoreTypes.asTypeElement(mirror); + } + +} diff --git a/extra/interface/ap/src/main/resources/META-INF/gradle/incremental.annotation.processors b/extra/interface/ap/src/main/resources/META-INF/gradle/incremental.annotation.processors new file mode 100644 index 000000000..287495b9a --- /dev/null +++ b/extra/interface/ap/src/main/resources/META-INF/gradle/incremental.annotation.processors @@ -0,0 +1 @@ +org.spongepowered.configurate.interfaces.processor.ConfigImplementationGeneratorProcessor,aggregating diff --git a/extra/interface/ap/src/test/java/org/spongepowered/configurate/interfaces/processor/ConfigImplementationGenerationTest.java b/extra/interface/ap/src/test/java/org/spongepowered/configurate/interfaces/processor/ConfigImplementationGenerationTest.java new file mode 100644 index 000000000..e09b6e295 --- /dev/null +++ b/extra/interface/ap/src/test/java/org/spongepowered/configurate/interfaces/processor/ConfigImplementationGenerationTest.java @@ -0,0 +1,48 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces.processor; + +import static org.spongepowered.configurate.interfaces.processor.TestUtils.testCompilation; + +import org.junit.experimental.runners.Enclosed; +import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; + +@RunWith(Enclosed.class) +class ConfigImplementationGenerationTest { + + @Test + void testBasicCompilation() { + testCompilation("structure/BasicConfig"); + } + + @Test + void testExtendedCompilation() { + testCompilation("structure/ExtendedConfig"); + } + + @Test + void testMultiLayerCompilation() { + testCompilation("structure/MultiLayerConfig"); + } + + @Test + void testAnnotationOthersCompilation() { + testCompilation("test/OtherAnnotations"); + } + +} diff --git a/extra/interface/ap/src/test/java/org/spongepowered/configurate/interfaces/processor/DefaultValueTest.java b/extra/interface/ap/src/test/java/org/spongepowered/configurate/interfaces/processor/DefaultValueTest.java new file mode 100644 index 000000000..2e69455ed --- /dev/null +++ b/extra/interface/ap/src/test/java/org/spongepowered/configurate/interfaces/processor/DefaultValueTest.java @@ -0,0 +1,35 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces.processor; + +import static org.spongepowered.configurate.interfaces.processor.TestUtils.testCompilation; + +import org.junit.jupiter.api.Test; + +class DefaultValueTest { + + @Test + void testCorrectDefaults() { + testCompilation("defaults/CorrectDefaults"); + } + + @Test + void testMultipleDefaults() { + testCompilation("defaults/MultipleDefaults"); + } + +} diff --git a/extra/interface/ap/src/test/java/org/spongepowered/configurate/interfaces/processor/TestUtils.java b/extra/interface/ap/src/test/java/org/spongepowered/configurate/interfaces/processor/TestUtils.java new file mode 100644 index 000000000..8d103c44a --- /dev/null +++ b/extra/interface/ap/src/test/java/org/spongepowered/configurate/interfaces/processor/TestUtils.java @@ -0,0 +1,104 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces.processor; + +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; +import static org.junit.jupiter.api.Assertions.assertIterableEquals; + +import com.google.common.io.Resources; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import org.spongepowered.configurate.interfaces.Constants; + +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +import javax.tools.StandardLocation; + +final class TestUtils { + + private TestUtils() { + } + + /** + * Tests whether the compilation is successful, that the correct mappings + * have been made and that the generated impl matches the expected impl. + */ + static Compilation testCompilation(final String sourceResourceName) { + final Compilation compilation = + javac() + .withProcessors(new ConfigImplementationGeneratorProcessor()) + .compile(JavaFileObjects.forResource(sourceResourceName + ".java")); + + final String targetResourceName = sourceResourceName + "Impl"; + final String targetSourceName = targetResourceName.replace('/', '.'); + + assertThat(compilation).succeeded(); + assertThat(compilation) + .generatedSourceFile(targetSourceName) + .hasSourceEquivalentTo(JavaFileObjects.forResource(targetResourceName + ".java")); + + try { + + final String actualContent = compilation + .generatedFile(StandardLocation.CLASS_OUTPUT, Constants.MAPPING_FILE) + .orElseThrow(() -> new IllegalStateException("Expected the interface mappings file to be created")) + .getCharContent(false) + .toString(); + + final List expectedLines = readOrGenerateMappings(sourceResourceName, targetResourceName); + + assertIterableEquals(expectedLines, removeComments(actualContent)); + } catch (final IOException exception) { + throw new RuntimeException(exception); + } + + return compilation; + } + + private static List removeComments(final String content) { + return Arrays.stream(content.split(System.lineSeparator())) + .filter(line -> !line.startsWith("#")) + .collect(Collectors.toList()); + } + + private static List readOrGenerateMappings(final String sourceResourceName, final String targetResourceName) { + try { + final URL localMappings = Resources.getResource(sourceResourceName + ".properties"); + return Resources.asCharSource(localMappings, StandardCharsets.UTF_8).readLines(); + } catch (final IllegalArgumentException ignored) { + // we only support generating simple (not nested) configs, + // for complexer configs we need a mappings file + return Collections.singletonList(String.format( + Locale.ROOT, + "%s=%s", + sourceResourceName.replace('/', '.'), + targetResourceName.replace('/', '.') + )); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/extra/interface/ap/src/test/java/org/spongepowered/configurate/interfaces/processor/util/AnnotationOthersAnnotations.java b/extra/interface/ap/src/test/java/org/spongepowered/configurate/interfaces/processor/util/AnnotationOthersAnnotations.java new file mode 100644 index 000000000..97c0fb763 --- /dev/null +++ b/extra/interface/ap/src/test/java/org/spongepowered/configurate/interfaces/processor/util/AnnotationOthersAnnotations.java @@ -0,0 +1,40 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces.processor.util; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +public final class AnnotationOthersAnnotations { + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface AnnotationNoField {} + + @Retention(RetentionPolicy.RUNTIME) + public @interface AnnotationNoTarget {} + + @Target({ElementType.METHOD, ElementType.FIELD}) + @Retention(RetentionPolicy.CLASS) + public @interface AnnotationOtherRetention {} + + @Target({ElementType.METHOD, ElementType.FIELD}) + public @interface AnnotationNoRetention {} + +} diff --git a/extra/interface/ap/src/test/resources/defaults/CorrectDefaults.java b/extra/interface/ap/src/test/resources/defaults/CorrectDefaults.java new file mode 100644 index 000000000..e52b15958 --- /dev/null +++ b/extra/interface/ap/src/test/resources/defaults/CorrectDefaults.java @@ -0,0 +1,78 @@ +package defaults; + +import org.spongepowered.configurate.interfaces.meta.defaults.DefaultBoolean; +import org.spongepowered.configurate.interfaces.meta.defaults.DefaultDecimal; +import org.spongepowered.configurate.interfaces.meta.defaults.DefaultNumeric; +import org.spongepowered.configurate.interfaces.meta.defaults.DefaultString; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; + +@ConfigSerializable +public interface CorrectDefaults { + @DefaultBoolean + boolean apple(); + + @DefaultBoolean(true) + boolean blueberry(); + + @DefaultDecimal + float cherry(); + + @DefaultDecimal(123.5) + float dragonfruit(); + + @DefaultDecimal + double eggplant(); + + @DefaultDecimal(234.23) + double fig(); + + @DefaultNumeric + byte grape(); + + @DefaultNumeric(127) + byte huckleberry(); + + @DefaultNumeric + char italianPrunePlum(); + + @DefaultNumeric(126) + char jackfruit(); + + @DefaultNumeric('c') + char kiwi(); + + @DefaultNumeric + short lemon(); + + @DefaultNumeric(1341) + short mango(); + + @DefaultNumeric + int nectarine(); + + @DefaultNumeric(1231241) + int orange(); + + @DefaultNumeric + long pineapple(); + + @DefaultNumeric(24524524521L) + long quince(); + + @DefaultString + String raspberry(); + + @DefaultString("Hello world!") + String strawberry(); + + @DefaultString("Hi") + void tamarillo(String value); + + default String ugli() { + return "A fruit"; + } + + default int velvetApple() { + return 500; + } +} diff --git a/extra/interface/ap/src/test/resources/defaults/CorrectDefaultsImpl.java b/extra/interface/ap/src/test/resources/defaults/CorrectDefaultsImpl.java new file mode 100644 index 000000000..3e6b9486b --- /dev/null +++ b/extra/interface/ap/src/test/resources/defaults/CorrectDefaultsImpl.java @@ -0,0 +1,188 @@ +package defaults; + +import java.lang.Override; +import java.lang.String; +import org.spongepowered.configurate.interfaces.meta.defaults.DefaultBoolean; +import org.spongepowered.configurate.interfaces.meta.defaults.DefaultDecimal; +import org.spongepowered.configurate.interfaces.meta.defaults.DefaultNumeric; +import org.spongepowered.configurate.interfaces.meta.defaults.DefaultString; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; + +/** + * Automatically generated implementation of the config */ +@ConfigSerializable +final class CorrectDefaultsImpl implements CorrectDefaults { + @DefaultBoolean(false) + private boolean apple = false; + + @DefaultBoolean(true) + private boolean blueberry = true; + + @DefaultDecimal(0.0F) + private float cherry = 0.0F; + + @DefaultDecimal(123.5F) + private float dragonfruit = 123.5F; + + @DefaultDecimal(0.0D) + private double eggplant = 0.0D; + + @DefaultDecimal(234.23D) + private double fig = 234.23D; + + @DefaultNumeric(0) + private byte grape = 0; + + @DefaultNumeric(127) + private byte huckleberry = 127; + + @DefaultNumeric(0) + private char italianPrunePlum = 0; + + @DefaultNumeric(126) + private char jackfruit = 126; + + @DefaultNumeric(99) + private char kiwi = 99; + + @DefaultNumeric(0) + private short lemon = 0; + + @DefaultNumeric(1341) + private short mango = 1341; + + @DefaultNumeric(0) + private int nectarine = 0; + + @DefaultNumeric(1231241) + private int orange = 1231241; + + @DefaultNumeric(0L) + private long pineapple = 0L; + + @DefaultNumeric(24524524521L) + private long quince = 24524524521L; + + @DefaultString("") + private String raspberry = ""; + + @DefaultString("Hello world!") + private String strawberry = "Hello world!"; + + @DefaultString("Hi") + private String tamarillo = "Hi"; + + private String ugli = CorrectDefaults.super.ugli(); + + private int velvetApple = CorrectDefaults.super.velvetApple(); + + @Override + public boolean apple() { + return apple; + } + + @Override + public boolean blueberry() { + return blueberry; + } + + @Override + public float cherry() { + return cherry; + } + + @Override + public float dragonfruit() { + return dragonfruit; + } + + @Override + public double eggplant() { + return eggplant; + } + + @Override + public double fig() { + return fig; + } + + @Override + public byte grape() { + return grape; + } + + @Override + public byte huckleberry() { + return huckleberry; + } + + @Override + public char italianPrunePlum() { + return italianPrunePlum; + } + + @Override + public char jackfruit() { + return jackfruit; + } + + @Override + public char kiwi() { + return kiwi; + } + + @Override + public short lemon() { + return lemon; + } + + @Override + public short mango() { + return mango; + } + + @Override + public int nectarine() { + return nectarine; + } + + @Override + public int orange() { + return orange; + } + + @Override + public long pineapple() { + return pineapple; + } + + @Override + public long quince() { + return quince; + } + + @Override + public String raspberry() { + return raspberry; + } + + @Override + public String strawberry() { + return strawberry; + } + + @Override + public void tamarillo(String value) { + this.tamarillo = value; + } + + @Override + public String ugli() { + return ugli; + } + + @Override + public int velvetApple() { + return velvetApple; + } +} diff --git a/extra/interface/ap/src/test/resources/defaults/MultipleDefaults.java b/extra/interface/ap/src/test/resources/defaults/MultipleDefaults.java new file mode 100644 index 000000000..a6d7948e3 --- /dev/null +++ b/extra/interface/ap/src/test/resources/defaults/MultipleDefaults.java @@ -0,0 +1,22 @@ +package defaults; + +import org.spongepowered.configurate.interfaces.meta.defaults.DefaultBoolean; +import org.spongepowered.configurate.interfaces.meta.defaults.DefaultDecimal; +import org.spongepowered.configurate.interfaces.meta.defaults.DefaultNumeric; +import org.spongepowered.configurate.interfaces.meta.defaults.DefaultString; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; + +@ConfigSerializable +public interface MultipleDefaults { + @DefaultBoolean(true) + @DefaultDecimal(2) + @DefaultNumeric(3) + @DefaultString("Hi!") + int multipleSingle(); + + @DefaultString("Hey!") + String multipleOverride(); + + @DefaultString("Hello!") + void multipleOverride(String x); +} diff --git a/extra/interface/ap/src/test/resources/defaults/MultipleDefaultsImpl.java b/extra/interface/ap/src/test/resources/defaults/MultipleDefaultsImpl.java new file mode 100644 index 000000000..384241bdc --- /dev/null +++ b/extra/interface/ap/src/test/resources/defaults/MultipleDefaultsImpl.java @@ -0,0 +1,33 @@ +package defaults; + +import java.lang.Override; +import java.lang.String; +import org.spongepowered.configurate.interfaces.meta.defaults.DefaultNumeric; +import org.spongepowered.configurate.interfaces.meta.defaults.DefaultString; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; + +/** + * Automatically generated implementation of the config */ +@ConfigSerializable +final class MultipleDefaultsImpl implements MultipleDefaults { + @DefaultNumeric(3) + private int multipleSingle = 3; + + @DefaultString("Hey!") + private String multipleOverride = "Hey!"; // this is expected as "Hey!" is handled before "Hello!" + + @Override + public int multipleSingle() { + return multipleSingle; + } + + @Override + public String multipleOverride() { + return multipleOverride; + } + + @Override + public void multipleOverride(String x) { + this.multipleOverride = x; + } +} diff --git a/extra/interface/ap/src/test/resources/structure/BasicConfig.java b/extra/interface/ap/src/test/resources/structure/BasicConfig.java new file mode 100644 index 000000000..483ba767c --- /dev/null +++ b/extra/interface/ap/src/test/resources/structure/BasicConfig.java @@ -0,0 +1,16 @@ +package structure; + +import org.spongepowered.configurate.objectmapping.ConfigSerializable; + +@ConfigSerializable +public interface BasicConfig { + String hello(); + + void hi(String value); + + String hello(String value); + + default String hey(String value) { + return "Hello"; + } +} diff --git a/extra/interface/ap/src/test/resources/structure/BasicConfigImpl.java b/extra/interface/ap/src/test/resources/structure/BasicConfigImpl.java new file mode 100644 index 000000000..9f850914e --- /dev/null +++ b/extra/interface/ap/src/test/resources/structure/BasicConfigImpl.java @@ -0,0 +1,38 @@ +package structure; + +import java.lang.Override; +import java.lang.String; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; + +/** + * Automatically generated implementation of the config */ +@ConfigSerializable +final class BasicConfigImpl implements BasicConfig { + private String hello; + + private String hi; + + private String hey; + + @Override + public String hello() { + return hello; + } + + @Override + public void hi(String value) { + this.hi = value; + } + + @Override + public String hello(String value) { + this.hello = value; + return this.hello; + } + + @Override + public String hey(String value) { + this.hey = BasicConfig.super.hey(value); + return this.hey; + } +} diff --git a/extra/interface/ap/src/test/resources/structure/ExtendedConfig.java b/extra/interface/ap/src/test/resources/structure/ExtendedConfig.java new file mode 100644 index 000000000..78ba9a1c3 --- /dev/null +++ b/extra/interface/ap/src/test/resources/structure/ExtendedConfig.java @@ -0,0 +1,10 @@ +package structure; + +import org.spongepowered.configurate.objectmapping.ConfigSerializable; + +@ConfigSerializable +public interface ExtendedConfig extends BasicConfig { + String hi(); + + Number hey(int value); +} diff --git a/extra/interface/ap/src/test/resources/structure/ExtendedConfigImpl.java b/extra/interface/ap/src/test/resources/structure/ExtendedConfigImpl.java new file mode 100644 index 000000000..a2fc4e63b --- /dev/null +++ b/extra/interface/ap/src/test/resources/structure/ExtendedConfigImpl.java @@ -0,0 +1,44 @@ +package structure; + +import java.lang.Number; +import java.lang.Override; +import java.lang.String; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; + +/** + * Automatically generated implementation of the config */ +@ConfigSerializable +final class ExtendedConfigImpl implements ExtendedConfig { + private String hi; + + private int hey; + + private String hello; + + @Override + public String hi() { + return hi; + } + + @Override + public Number hey(int value) { + this.hey = value; + return this.hey; + } + + @Override + public String hello() { + return hello; + } + + @Override + public void hi(String value) { + this.hi = value; + } + + @Override + public String hello(String value) { + this.hello = value; + return this.hello; + } +} diff --git a/extra/interface/ap/src/test/resources/structure/MultiLayerConfig.java b/extra/interface/ap/src/test/resources/structure/MultiLayerConfig.java new file mode 100644 index 000000000..33de1c0eb --- /dev/null +++ b/extra/interface/ap/src/test/resources/structure/MultiLayerConfig.java @@ -0,0 +1,15 @@ +package structure; + +import org.spongepowered.configurate.objectmapping.ConfigSerializable; + +@ConfigSerializable +public interface MultiLayerConfig { + String test(); + SecondLayer second(); + + @ConfigSerializable + public interface SecondLayer { + String test(); + String test2(); + } +} diff --git a/extra/interface/ap/src/test/resources/structure/MultiLayerConfig.properties b/extra/interface/ap/src/test/resources/structure/MultiLayerConfig.properties new file mode 100644 index 000000000..a437a7fe5 --- /dev/null +++ b/extra/interface/ap/src/test/resources/structure/MultiLayerConfig.properties @@ -0,0 +1,2 @@ +structure.MultiLayerConfig=structure.MultiLayerConfigImpl +structure.MultiLayerConfig$SecondLayer=structure.MultiLayerConfigImpl$SecondLayerImpl diff --git a/extra/interface/ap/src/test/resources/structure/MultiLayerConfigImpl.java b/extra/interface/ap/src/test/resources/structure/MultiLayerConfigImpl.java new file mode 100644 index 000000000..15f6056eb --- /dev/null +++ b/extra/interface/ap/src/test/resources/structure/MultiLayerConfigImpl.java @@ -0,0 +1,43 @@ +package structure; + +import java.lang.Override; +import java.lang.String; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; + +/** + * Automatically generated implementation of the config */ +@ConfigSerializable +final class MultiLayerConfigImpl implements MultiLayerConfig { + private String test; + + private MultiLayerConfig.SecondLayer second = new SecondLayerImpl(); + + @Override + public String test() { + return test; + } + + @Override + public MultiLayerConfig.SecondLayer second() { + return second; + } + + /** + * Automatically generated implementation of the config */ + @ConfigSerializable + static final class SecondLayerImpl implements MultiLayerConfig.SecondLayer { + private String test; + + private String test2; + + @Override + public String test() { + return test; + } + + @Override + public String test2() { + return test2; + } + } +} diff --git a/extra/interface/ap/src/test/resources/test/OtherAnnotations.java b/extra/interface/ap/src/test/resources/test/OtherAnnotations.java new file mode 100644 index 000000000..dc7c7ef5b --- /dev/null +++ b/extra/interface/ap/src/test/resources/test/OtherAnnotations.java @@ -0,0 +1,30 @@ +package test; + +import org.spongepowered.configurate.interfaces.processor.util.AnnotationOthersAnnotations; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; +import org.spongepowered.configurate.objectmapping.meta.Matches; +import org.spongepowered.configurate.objectmapping.meta.Required; + +@ConfigSerializable +public interface OtherAnnotations { + @Comment(value = "Hello!", override = true) + @Matches(value = "abc", failureMessage = "ohno!") + @Required + String hello(); + + @Comment("Hi!") + String hi(); + + @AnnotationOthersAnnotations.AnnotationNoField + String noField(); + + @AnnotationOthersAnnotations.AnnotationNoTarget + String noTarget(); + + @AnnotationOthersAnnotations.AnnotationOtherRetention + String otherRetention(); + + @AnnotationOthersAnnotations.AnnotationNoRetention + String noRetention(); +} diff --git a/extra/interface/ap/src/test/resources/test/OtherAnnotationsImpl.java b/extra/interface/ap/src/test/resources/test/OtherAnnotationsImpl.java new file mode 100644 index 000000000..de0bdb968 --- /dev/null +++ b/extra/interface/ap/src/test/resources/test/OtherAnnotationsImpl.java @@ -0,0 +1,62 @@ +package test; + +import java.lang.Override; +import java.lang.String; + +import org.spongepowered.configurate.interfaces.processor.util.AnnotationOthersAnnotations; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; +import org.spongepowered.configurate.objectmapping.meta.Matches; +import org.spongepowered.configurate.objectmapping.meta.Required; + +/** + * Automatically generated implementation of the config */ +@ConfigSerializable +final class OtherAnnotationsImpl implements OtherAnnotations { + @Comment(value = "Hello!", override = true) + @Matches(value = "abc", failureMessage = "ohno!") + @Required + private String hello; + + @Comment("Hi!") + private String hi; + + private String noField; + + @AnnotationOthersAnnotations.AnnotationNoTarget + private String noTarget; + + private String otherRetention; + + private String noRetention; + + @Override + public String hello() { + return hello; + } + + @Override + public String hi() { + return hi; + } + + @Override + public String noField() { + return noField; + } + + @Override + public String noTarget() { + return noTarget; + } + + @Override + public String otherRetention() { + return otherRetention; + } + + @Override + public String noRetention() { + return noRetention; + } +} diff --git a/extra/interface/build.gradle b/extra/interface/build.gradle new file mode 100644 index 000000000..5c6236119 --- /dev/null +++ b/extra/interface/build.gradle @@ -0,0 +1,10 @@ +plugins { + id "org.spongepowered.configurate.build.component" +} + +description = "Utility classes for generated config interface implementations" + +dependencies { + api projects.core + testAnnotationProcessor projects.extra.extraInterface.extraInterfaceAp +} diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/Constants.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/Constants.java new file mode 100644 index 000000000..67371395c --- /dev/null +++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/Constants.java @@ -0,0 +1,35 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces; + +/** + * Constants that are used in multiple files. Meant to be used internally. + * + * @since 4.2.0 + */ +public final class Constants { + + private Constants() {} + + /** + * The file location of the interface mappings. + * + * @since 4.2.0 + */ + public static final String MAPPING_FILE = "org/spongepowered/configurate/interfaces/interface_mappings.properties"; + +} diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/InterfaceDefaultOptions.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/InterfaceDefaultOptions.java new file mode 100644 index 000000000..b99f3d820 --- /dev/null +++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/InterfaceDefaultOptions.java @@ -0,0 +1,84 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces; + +import org.spongepowered.configurate.ConfigurationOptions; +import org.spongepowered.configurate.objectmapping.ObjectMapper; +import org.spongepowered.configurate.serialize.TypeSerializerCollection; + +import java.util.function.Consumer; + +/** + * This class has the default {@link ConfigurationOptions} + * with {@link InterfaceTypeSerializer} added to the serializers. + * + * @since 4.2.0 + */ +public final class InterfaceDefaultOptions { + + private static final TypeSerializerCollection DEFAULTS = + TypeSerializerCollection.builder() + .registerAnnotated(InterfaceTypeSerializer::applicable, InterfaceTypeSerializer.INSTANCE) + .registerAnnotatedObjects(InterfaceMiddleware.buildObjectMapperWithMiddleware().build()) + .build(); + + private InterfaceDefaultOptions() { + } + + /** + * The default ConfigurationOptions with {@link InterfaceTypeSerializer} added to the serializers. + * + * @return the default ConfigurationOptions with {@link InterfaceTypeSerializer} added to the serializers. + * @since 4.2.0 + */ + public static ConfigurationOptions defaults() { + return addTo(ConfigurationOptions.defaults()); + } + + /** + * Sets the default configuration options to be used by the resultant loader + * by providing a function which takes interface's default serializer + * collection and applies any desired changes. + * + * @param options to transform the existing default options + * @return the default options with the applied changes + * @since 4.2.0 + */ + public static ConfigurationOptions addTo(final ConfigurationOptions options) { + // This creates a new TypeSerializerCollection with 'options' as parent. Child takes priority over parent. + return options.serializers(serializers -> serializers.registerAll(DEFAULTS)); + } + + /** + * {@link #addTo(ConfigurationOptions)} with an option to customize the {@link ObjectMapper.Factory}. + * + * @param options to transform the existing default options + * @param objectMapperConsumer to transform the ObjectMapper factory + * @return the default options with the applied changes + * @since 4.2.0 + */ + public static ConfigurationOptions addTo(final ConfigurationOptions options, + final Consumer objectMapperConsumer) { + return options.serializers(serializers -> { + final ObjectMapper.Factory.Builder builder = InterfaceMiddleware.buildObjectMapperWithMiddleware(); + objectMapperConsumer.accept(builder); + serializers.registerAnnotated(InterfaceTypeSerializer::applicable, InterfaceTypeSerializer.INSTANCE) + .registerAnnotatedObjects(builder.build()); + }); + } + +} diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/InterfaceMiddleware.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/InterfaceMiddleware.java new file mode 100644 index 000000000..6b744188e --- /dev/null +++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/InterfaceMiddleware.java @@ -0,0 +1,145 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.configurate.interfaces.meta.Hidden; +import org.spongepowered.configurate.interfaces.meta.defaults.DefaultBoolean; +import org.spongepowered.configurate.interfaces.meta.defaults.DefaultDecimal; +import org.spongepowered.configurate.interfaces.meta.defaults.DefaultNumeric; +import org.spongepowered.configurate.interfaces.meta.defaults.DefaultString; +import org.spongepowered.configurate.interfaces.meta.range.DecimalRange; +import org.spongepowered.configurate.interfaces.meta.range.NumericRange; +import org.spongepowered.configurate.interfaces.meta.range.StringRange; +import org.spongepowered.configurate.objectmapping.ObjectMapper; +import org.spongepowered.configurate.objectmapping.meta.Constraint; +import org.spongepowered.configurate.objectmapping.meta.Processor; +import org.spongepowered.configurate.serialize.SerializationException; + +import java.util.Locale; + +final class InterfaceMiddleware { + + private InterfaceMiddleware() { + } + + static ObjectMapper.Factory.Builder buildObjectMapperWithMiddleware() { + return ObjectMapper.factoryBuilder() + .addConstraint(DecimalRange.class, Number.class, decimalRange()) + .addConstraint(NumericRange.class, Number.class, numericRange()) + .addConstraint(StringRange.class, String.class, stringRange()) + .addProcessor(Hidden.class, hiddenProcessor()); + } + + private static Constraint.Factory decimalRange() { + return (data, type) -> number -> { + // null requirement is part of @Required + if (number == null) { + return; + } + + final double value = number.doubleValue(); + if (!(data.from() >= value && data.to() <= value)) { + throw new SerializationException(String.format( + Locale.ROOT, + "'%s' is not in the allowed range of from: %s, to: %s!", + value, data.from(), data.to() + )); + } + }; + } + + private static Constraint.Factory numericRange() { + return (data, type) -> number -> { + // null requirement is part of @Required + if (number == null) { + return; + } + + final long value = number.longValue(); + if (!(data.from() >= value && data.to() <= value)) { + throw new SerializationException(String.format( + Locale.ROOT, + "'%s' is not in the allowed range of from: %s, to: %s!", + value, data.from(), data.to() + )); + } + }; + } + + private static Constraint.Factory stringRange() { + return (data, type) -> string -> { + // null requirement is part of @Required + if (string == null) { + return; + } + + final int length = string.length(); + if (!(data.from() >= length && data.to() <= length)) { + throw new SerializationException(String.format( + Locale.ROOT, + "'%s' is not in the allowed string length range of from: %s, to: %s!", + length, data.from(), data.to() + )); + } + }; + } + + @SuppressWarnings("ConstantValue") + private static Processor.AdvancedFactory hiddenProcessor() { + return (ignored, fieldType, element) -> { + // prefetch everything we can + final @Nullable DefaultBoolean defaultBoolean = element.getAnnotation(DefaultBoolean.class); + final @Nullable DefaultDecimal defaultDecimal = element.getAnnotation(DefaultDecimal.class); + final @Nullable DefaultNumeric defaultNumeric = element.getAnnotation(DefaultNumeric.class); + final @Nullable DefaultString defaultString = element.getAnnotation(DefaultString.class); + final boolean isBoolean = TypeUtils.isBoolean(fieldType); + final boolean isDecimal = TypeUtils.isDecimal(fieldType); + final boolean isNumeric = TypeUtils.isNumeric(fieldType); + final boolean isString = String.class == fieldType; + + // unfortunately default methods cannot be supported in combination with Hidden in Configurate + + return (value, destination) -> { + // hidden fields are only absent from the config if the value is the default value, so we check that below + if (isBoolean && defaultBoolean != null) { + if (!value.equals(defaultBoolean.value())) { + return; + } + } else if (isDecimal && defaultDecimal != null) { + if (((Number) value).doubleValue() != defaultDecimal.value()) { + return; + } + } else if (isNumeric && defaultNumeric != null) { + if (((Number) value).longValue() != defaultNumeric.value()) { + return; + } + } else if (isString && defaultString != null) { + if (!defaultString.value().equals(value)) { + return; + } + } + + // as long as it uses the naming scheme-based resolver parent should be the object holding the field and + // key the field type + //noinspection DataFlowIssue + destination.parent().removeChild(destination.key()); + }; + }; + } + +} diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/InterfaceTypeSerializer.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/InterfaceTypeSerializer.java new file mode 100644 index 000000000..c76576dfc --- /dev/null +++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/InterfaceTypeSerializer.java @@ -0,0 +1,115 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces; + +import static io.leangen.geantyref.GenericTypeReflector.erase; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.serialize.SerializationException; +import org.spongepowered.configurate.serialize.TypeSerializer; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.Type; +import java.net.URL; +import java.util.Locale; +import java.util.Properties; +import java.util.StringJoiner; + +final class InterfaceTypeSerializer implements TypeSerializer { + + public static final InterfaceTypeSerializer INSTANCE = new InterfaceTypeSerializer(); + + private final Properties interfaceMappings = new Properties(); + + public static boolean applicable(final AnnotatedType type) { + return type.isAnnotationPresent(ConfigSerializable.class) && erase(type.getType()).isInterface(); + } + + private InterfaceTypeSerializer() { + final @Nullable URL mappingsUrl = getClass().getClassLoader().getResource(Constants.MAPPING_FILE); + if (mappingsUrl == null) { + return; + } + + try (InputStream stream = mappingsUrl.openStream()) { + this.interfaceMappings.load(stream); + } catch (final IOException exception) { + throw new RuntimeException("Could not load interface mappings!", exception); + } + } + + @Override + public Object deserialize(final Type type, final ConfigurationNode node) throws SerializationException { + final String canonicalName = erase(type).getTypeName(); + final @Nullable String typeImpl = this.interfaceMappings.getProperty(canonicalName); + if (typeImpl == null) { + throw new SerializationException(String.format( + Locale.ROOT, + "No mapping found for type %s. Available mappings: %s", + canonicalName, availableMappings() + )); + } + + final Class implClass; + try { + implClass = Class.forName(typeImpl, true, erase(type).getClassLoader()); + } catch (final ClassNotFoundException exception) { + throw new SerializationException(String.format( + Locale.ROOT, + "Could not find implementation class %s for type %s!", + typeImpl, canonicalName + )); + } + + final @Nullable TypeSerializer serializer = node.options().serializers().get(implClass); + if (serializer == null) { + throw new SerializationException("No serializer found for implementation class " + implClass); + } + return serializer.deserialize(implClass, node); + } + + @Override + @SuppressWarnings({"rawtypes", "unchecked"}) + public void serialize( + final Type type, + final @Nullable Object obj, + final ConfigurationNode node + ) throws SerializationException { + // don't determine serialize from type, because that might be incorrect for subsections + if (obj == null) { + node.set(null); + return; + } + + final @Nullable TypeSerializer serializer = node.options().serializers().get(obj.getClass()); + if (serializer == null) { + throw new SerializationException("No serializer found for implementation class " + obj.getClass()); + } + serializer.serialize(obj.getClass(), obj, node); + } + + private String availableMappings() { + final StringJoiner joiner = new StringJoiner(", "); + this.interfaceMappings.keySet().forEach((key) -> joiner.add((CharSequence) key)); + return joiner.toString(); + } + +} diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/TypeUtils.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/TypeUtils.java new file mode 100644 index 000000000..b87b203d4 --- /dev/null +++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/TypeUtils.java @@ -0,0 +1,42 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces; + +import static io.leangen.geantyref.GenericTypeReflector.box; + +import java.lang.reflect.Type; + +final class TypeUtils { + + private TypeUtils() {} + + static boolean isNumeric(final Type type) { + final Type boxed = box(type); + return Byte.class.equals(boxed) || Character.class.equals(boxed) || Short.class.equals(boxed) + || Integer.class.equals(boxed) || Long.class.equals(boxed); + } + + static boolean isDecimal(final Type type) { + final Type boxed = box(type); + return Float.class.equals(boxed) || Double.class.equals(boxed); + } + + static boolean isBoolean(final Type type) { + return Boolean.class.equals(box(type)); + } + +} diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/Exclude.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/Exclude.java new file mode 100644 index 000000000..5dc4c871d --- /dev/null +++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/Exclude.java @@ -0,0 +1,34 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces.meta; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Adding this will cause the implementation generator to completely ignore + * this method. This is practically only used for default methods, as normal + * interface methods need to have an implementation. + * + * @since 4.2.0 + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.METHOD) +public @interface Exclude { +} diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/Field.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/Field.java new file mode 100644 index 000000000..e474aaf04 --- /dev/null +++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/Field.java @@ -0,0 +1,33 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces.meta; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Creates a field which makes the annotated method act as a simple getter / + * setter without being handled as a config node. + * + * @since 4.2.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface Field { +} diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/Hidden.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/Hidden.java new file mode 100644 index 000000000..4ac1c83bc --- /dev/null +++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/Hidden.java @@ -0,0 +1,42 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces.meta; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation allows you to read the config node as normal, but it only + * writes the node when the value is not the default value. This is to ensure + * that when a user manually adds the entry, it remains there (as long as it's + * not the default value.) + * + *

Without a default value the annotated node will be read, but will never + * be written even if the user explicitly added it to their config.

+ * + * Note that Hidden doesn't work with default method getters due to a + * limitation, and Hidden will function like it doesn't have a default + * value. + * + * @since 4.2.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.FIELD}) +public @interface Hidden { +} diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/defaults/DefaultBoolean.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/defaults/DefaultBoolean.java new file mode 100644 index 000000000..f49b0a333 --- /dev/null +++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/defaults/DefaultBoolean.java @@ -0,0 +1,44 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces.meta.defaults; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation provides a default value for the annotated method. + * Because of annotation limits, there is an annotation for: + * {@link DefaultBoolean booleans}, {@link DefaultDecimal decimals}, + * {@link DefaultNumeric numerics} and {@link DefaultString Strings}. + * + * @since 4.2.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.FIELD}) +public @interface DefaultBoolean { + + /** + * The default value for the annotated method. + * + * @return the default value for the annotated method. + * @since 4.2.0 + */ + boolean value() default false; + +} diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/defaults/DefaultDecimal.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/defaults/DefaultDecimal.java new file mode 100644 index 000000000..9efda5816 --- /dev/null +++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/defaults/DefaultDecimal.java @@ -0,0 +1,44 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces.meta.defaults; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation provides a default value for the annotated method. + * Because of annotation limits, there is an annotation for: + * {@link DefaultBoolean booleans}, {@link DefaultDecimal decimals}, + * {@link DefaultNumeric numerics} and {@link DefaultString Strings}. + * + * @since 4.2.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.FIELD}) +public @interface DefaultDecimal { + + /** + * The default value for the annotated method. + * + * @return the default value for the annotated method. + * @since 4.2.0 + */ + double value() default 0.0; + +} diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/defaults/DefaultNumeric.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/defaults/DefaultNumeric.java new file mode 100644 index 000000000..c6f47f5cd --- /dev/null +++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/defaults/DefaultNumeric.java @@ -0,0 +1,44 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces.meta.defaults; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation provides a default value for the annotated method. + * Because of annotation limits, there is an annotation for: + * {@link DefaultBoolean booleans}, {@link DefaultDecimal decimals}, + * {@link DefaultNumeric numerics} and {@link DefaultString Strings}. + * + * @since 4.2.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.FIELD}) +public @interface DefaultNumeric { + + /** + * The default value for the annotated method. + * + * @return the default value for the annotated method. + * @since 4.2.0 + */ + long value() default 0; + +} diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/defaults/DefaultString.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/defaults/DefaultString.java new file mode 100644 index 000000000..f99cec6f7 --- /dev/null +++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/defaults/DefaultString.java @@ -0,0 +1,44 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces.meta.defaults; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation provides a default value for the annotated method. + * Because of annotation limits, there is an annotation for: + * {@link DefaultBoolean booleans}, {@link DefaultDecimal decimals}, + * {@link DefaultNumeric numerics} and {@link DefaultString Strings}. + * + * @since 4.2.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.FIELD}) +public @interface DefaultString { + + /** + * The default value for the annotated method. + * + * @return the default value for the annotated method. + * @since 4.2.0 + */ + String value() default ""; + +} diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/range/DecimalRange.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/range/DecimalRange.java new file mode 100644 index 000000000..fad986dc6 --- /dev/null +++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/range/DecimalRange.java @@ -0,0 +1,51 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces.meta.range; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation limits the values that a config node can have. + * Because of annotation limits, there is an annotation for: + * {@link DecimalRange decimals}, {@link NumericRange numerics} and + * {@link StringRange String length}. + * + * @since 4.2.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface DecimalRange { + /** + * The minimal value allowed (inclusive.) + * + * @return the minimal value allowed (inclusive.) + * @since 4.2.0 + */ + double from(); + + /** + * The maximal value allowed (inclusive.) + * + * @return the maximal value allowed (inclusive.) + * @since 4.2.0 + */ + double to(); + +} diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/range/NumericRange.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/range/NumericRange.java new file mode 100644 index 000000000..7595680ca --- /dev/null +++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/range/NumericRange.java @@ -0,0 +1,51 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces.meta.range; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation limits the values that a config node can have. + * Because of annotation limits, there is an annotation for: + * {@link DecimalRange decimals}, {@link NumericRange numerics} and + * {@link StringRange String length}. + * + * @since 4.2.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface NumericRange { + /** + * The minimal value allowed (inclusive.) + * + * @return the minimal value allowed (inclusive.) + * @since 4.2.0 + */ + long from(); + + /** + * The maximal value allowed (inclusive.) + * + * @return the maximal value allowed (inclusive.) + * @since 4.2.0 + */ + long to(); + +} diff --git a/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/range/StringRange.java b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/range/StringRange.java new file mode 100644 index 000000000..48ed5a74a --- /dev/null +++ b/extra/interface/src/main/java/org/spongepowered/configurate/interfaces/meta/range/StringRange.java @@ -0,0 +1,55 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces.meta.range; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation limits the values that a config node can have. + * Because of annotation limits, there is an annotation for: + * {@link DecimalRange decimals}, {@link NumericRange numerics} and + * {@link StringRange String length}. + * + *

When the String is null, the range validation is skipped. Use + * {@link org.spongepowered.configurate.objectmapping.meta.Required Required} + * if null shouldn't be allowed.

+ * + * @since 4.2.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface StringRange { + /** + * The minimal String length allowed (inclusive.) + * + * @return the minimal value allowed (inclusive.) + * @since 4.2.0 + */ + int from(); + + /** + * The maximal String length allowed (inclusive.) + * + * @return the maximal value allowed (inclusive.) + * @since 4.2.0 + */ + int to(); + +} diff --git a/extra/interface/src/test/java/org/spongepowered/configurate/interfaces/ConfigEmpty.java b/extra/interface/src/test/java/org/spongepowered/configurate/interfaces/ConfigEmpty.java new file mode 100644 index 000000000..5cfc33b86 --- /dev/null +++ b/extra/interface/src/test/java/org/spongepowered/configurate/interfaces/ConfigEmpty.java @@ -0,0 +1,27 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces; + +import org.spongepowered.configurate.objectmapping.ConfigSerializable; + +@ConfigSerializable +interface ConfigEmpty { + + @ConfigSerializable + interface ConfigEmptyInner {} + +} diff --git a/extra/interface/src/test/java/org/spongepowered/configurate/interfaces/InterfaceTypeSerializerTest.java b/extra/interface/src/test/java/org/spongepowered/configurate/interfaces/InterfaceTypeSerializerTest.java new file mode 100644 index 000000000..23e826a1b --- /dev/null +++ b/extra/interface/src/test/java/org/spongepowered/configurate/interfaces/InterfaceTypeSerializerTest.java @@ -0,0 +1,54 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.spongepowered.configurate.interfaces.TypeUtils.configImplementationFor; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.spongepowered.configurate.BasicConfigurationNode; +import org.spongepowered.configurate.ConfigurateException; + +class InterfaceTypeSerializerTest { + + @Test + void testDeserialization() throws ConfigurateException { + final BasicConfigurationNode node = BasicConfigurationNode.root(InterfaceDefaultOptions.defaults()); + // doesn't deserialize if value is NullValue + node.node("hello").set("world"); + + final @Nullable ConfigEmpty config = Assertions.assertDoesNotThrow(() -> node.get(ConfigEmpty.class)); + assertNotNull(config); + assertInstanceOf(configImplementationFor(ConfigEmpty.class), config); + } + + @Test + void testInnerDeserialization() throws ConfigurateException { + final BasicConfigurationNode node = BasicConfigurationNode.root(InterfaceDefaultOptions.defaults()); + // doesn't deserialize if value is NullValue + node.node("hello").set("world"); + + final ConfigEmpty.@Nullable ConfigEmptyInner config = + Assertions.assertDoesNotThrow(() -> node.get(ConfigEmpty.ConfigEmptyInner.class)); + assertNotNull(config); + assertInstanceOf(configImplementationFor(ConfigEmpty.ConfigEmptyInner.class), config); + } + +} diff --git a/extra/interface/src/test/java/org/spongepowered/configurate/interfaces/TypeUtils.java b/extra/interface/src/test/java/org/spongepowered/configurate/interfaces/TypeUtils.java new file mode 100644 index 000000000..de682c970 --- /dev/null +++ b/extra/interface/src/test/java/org/spongepowered/configurate/interfaces/TypeUtils.java @@ -0,0 +1,53 @@ +/* + * Configurate + * Copyright (C) zml and Configurate contributors + * + * 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 org.spongepowered.configurate.interfaces; + +import java.util.Arrays; + +final class TypeUtils { + + private TypeUtils() {} + + static Class configImplementationFor(final Class interfaceClass) { + try { + final String implClassName = implClassNameFor(interfaceClass); + + //noinspection unchecked + return (Class) Class.forName(implClassName); + } catch (final ClassNotFoundException notFound) { + throw new IllegalStateException("No implementation for " + interfaceClass.getCanonicalName(), notFound); + } + } + + private static String implClassNameFor(final Class interfaceClass) { + final String packageName = interfaceClass.getPackage().getName(); + // include the package name dot as well + final String classHierarchy = interfaceClass.getCanonicalName().substring(packageName.length() + 1); + + // every subclass and the class itself has 'Impl' behind it + final String implClassName = + Arrays.stream(classHierarchy.split("\\.")) + .reduce("", (reduced, current) -> { + if (!reduced.isEmpty()) { + reduced += "$"; + } + return reduced + current + "Impl"; + }); + return packageName + "." + implClassName; + } + +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0b93eb852..9e1a3f388 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,16 +1,19 @@ [versions] -assertj="3.24.2" -autoValue="1.10.4" -checkerQual="3.40.0" -checkstyle="10.12.5" +assertj = "3.24.2" +autoValue = "1.10.4" +checkerQual = "3.40.0" +checkstyle = "10.12.5" geantyref = "1.3.14" -errorprone="2.23.0" +errorprone = "2.23.0" indra = "3.1.3" -junit="5.10.1" -ktlint="0.49.1" -ktfmt="0.46" +junit = "5.10.1" +ktlint = "0.49.1" +ktfmt = "0.46" pmd = "6.55.0" spotless = "6.23.2" +javapoet = "1.10.0" +auto-service = "1.1.1" +compile-testing = "0.21.0" [libraries] # Shared @@ -28,11 +31,16 @@ stylecheck = "ca.stellardrift:stylecheck:0.2.1" # Kotlin kotlin-coroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2" -kotlin-reflect = {module = "org.jetbrains.kotlin:kotlin-reflect"} # version from Kotlin BOM +kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect" } # version from Kotlin BOM # Core checkerQual = { module = "org.checkerframework:checker-qual", version.ref = "checkerQual" } -geantyref = {module = "io.leangen.geantyref:geantyref", version.ref = "geantyref" } +geantyref = { module = "io.leangen.geantyref:geantyref", version.ref = "geantyref" } + +# Interface +auto-service = { module = "com.google.auto.service:auto-service", version.ref = "auto-service" } +javapoet = { module = "com.squareup:javapoet", version.ref = "javapoet" } +compile-testing = { module = "com.google.testing.compile:compile-testing", version.ref = "compile-testing" } # DFU dfu-v2 = "com.mojang:datafixerupper:2.0.24" diff --git a/settings.gradle b/settings.gradle index b2dcf8727..bd00951f7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -56,10 +56,12 @@ rootProject.name = "$prefix-parent" } // extras -["kotlin", "guice", "dfu2", "dfu3", "dfu4"].each { +["kotlin", "guice", "dfu2", "dfu3", "dfu4", "interface"].each { include ":extra:$it" findProject(":extra:$it")?.name = "extra-$it" } +include ":extra:extra-interface:ap" +findProject(":extra:extra-interface:ap")?.name = "extra-interface-ap" includeBuild 'vendor', { name = "configurate-vendor"