diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 5847721..ae4909f 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - java-version: [ 11 ] + java-version: [ 11, 17, 21, 23 ] steps: - name: Check out Git repository uses: actions/checkout@v3 diff --git a/domino-jackson-processor/pom.xml b/domino-jackson-processor/pom.xml index 1f3b779..45b55b2 100644 --- a/domino-jackson-processor/pom.xml +++ b/domino-jackson-processor/pom.xml @@ -6,7 +6,7 @@ org.dominokit domino-jackson-parent - 1.0.4 + 1.0.5 domino-jackson-processor @@ -108,7 +108,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.2.2 + 3.6.0 package diff --git a/domino-jackson-processor/src/it/java/org/dominokit/jackson/records/Circle.java b/domino-jackson-processor/src/it/java/org/dominokit/jackson/records/Circle.java new file mode 100644 index 0000000..729e477 --- /dev/null +++ b/domino-jackson-processor/src/it/java/org/dominokit/jackson/records/Circle.java @@ -0,0 +1,33 @@ +/* + * Copyright © 2019 Dominokit + * + * 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.dominokit.jackson.records; + +import org.dominokit.jackson.annotation.JSONMapper; + +@JSONMapper +public record Circle(double radius) { + public static final double PI = 3.14159; + + public Circle { + if (radius < 0) { + throw new IllegalArgumentException("Radius cannot be negative"); + } + } + + public double area() { + return PI * radius * radius; + } +} diff --git a/domino-jackson-processor/src/it/java/org/dominokit/jackson/records/ComplexNumber.java b/domino-jackson-processor/src/it/java/org/dominokit/jackson/records/ComplexNumber.java new file mode 100644 index 0000000..3f8587f --- /dev/null +++ b/domino-jackson-processor/src/it/java/org/dominokit/jackson/records/ComplexNumber.java @@ -0,0 +1,29 @@ +/* + * Copyright © 2019 Dominokit + * + * 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.dominokit.jackson.records; + +import org.dominokit.jackson.annotation.JSONMapper; + +@JSONMapper +public record ComplexNumber(double real, double imaginary) { + public ComplexNumber(double real) { + this(real, 0); // Calls the canonical constructor with 0 for the imaginary part + } + + public double magnitude() { + return Math.sqrt(real * real + imaginary * imaginary); + } +} diff --git a/domino-jackson-processor/src/it/java/org/dominokit/jackson/records/Point.java b/domino-jackson-processor/src/it/java/org/dominokit/jackson/records/Point.java new file mode 100644 index 0000000..475f371 --- /dev/null +++ b/domino-jackson-processor/src/it/java/org/dominokit/jackson/records/Point.java @@ -0,0 +1,22 @@ +/* + * Copyright © 2019 Dominokit + * + * 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.dominokit.jackson.records; + +import org.dominokit.jackson.annotation.JSONMapper; + +@JSONMapper +public record Point(int x, int y) { +} diff --git a/domino-jackson-processor/src/it/java/org/dominokit/jackson/records/RecordIT.java b/domino-jackson-processor/src/it/java/org/dominokit/jackson/records/RecordIT.java new file mode 100644 index 0000000..cca62a2 --- /dev/null +++ b/domino-jackson-processor/src/it/java/org/dominokit/jackson/records/RecordIT.java @@ -0,0 +1,63 @@ +/* + * Copyright © 2019 Dominokit + * + * 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.dominokit.jackson.records; + +import org.junit.Assert; +import org.junit.Test; + +public class RecordIT { + + private static final Point_MapperImpl POINT_MAPPER = new Point_MapperImpl(); + private static final Circle_MapperImpl CIRCLE_MAPPER = new Circle_MapperImpl(); + private static final ComplexNumber_MapperImpl COMPLEX_NUMBER_MAPPER = new ComplexNumber_MapperImpl(); + private static final Rectangle_MapperImpl RECTANGLE_MAPPER = new Rectangle_MapperImpl(); + + @Test + public void simpleRecordTest(){ + String pointJson = POINT_MAPPER.write(new Point(10, 20)); + Point point = POINT_MAPPER.read(pointJson); + + Assert.assertEquals("{\"x\":10,\"y\":20}", pointJson); + Assert.assertEquals(10, point.x()); + Assert.assertEquals(20, point.y()); + Assert.assertEquals(new Point(10, 20), point); + + String circleJson = CIRCLE_MAPPER.write(new Circle(10.0)); + Circle circle = CIRCLE_MAPPER.read(circleJson); + + Assert.assertEquals("{\"radius\":10.0}", circleJson); + Assert.assertEquals(10.0, circle.radius(), 0.001); + Assert.assertEquals(new Circle(10), circle); + + String complexNumberJson = COMPLEX_NUMBER_MAPPER.write(new ComplexNumber(10.0)); + ComplexNumber complexNumber = COMPLEX_NUMBER_MAPPER.read(complexNumberJson); + + Assert.assertEquals("{\"real\":10.0,\"imaginary\":0.0}", complexNumberJson); + Assert.assertEquals(10.0, complexNumber.real(), 0.001); + Assert.assertEquals(0, complexNumber.imaginary(), 0.001); + Assert.assertEquals(new ComplexNumber(10.0), complexNumber); + + String rectangleJson = RECTANGLE_MAPPER.write(new Rectangle(new Point(10,20), new Point(20, 30))); + Rectangle rectangle = RECTANGLE_MAPPER.read(rectangleJson); + + Assert.assertEquals("{\"bottomLeft\":{\"x\":10,\"y\":20},\"topRight\":{\"x\":20,\"y\":30}}", rectangleJson); + Assert.assertEquals(new Point(10, 20), rectangle.a()); + Assert.assertEquals(new Point(20, 30), rectangle.b()); + Assert.assertEquals(new Rectangle(new Point(10,20), new Point(20, 30)), rectangle); + + } + +} diff --git a/domino-jackson-processor/src/it/java/org/dominokit/jackson/records/Rectangle.java b/domino-jackson-processor/src/it/java/org/dominokit/jackson/records/Rectangle.java new file mode 100644 index 0000000..c2f6780 --- /dev/null +++ b/domino-jackson-processor/src/it/java/org/dominokit/jackson/records/Rectangle.java @@ -0,0 +1,24 @@ +/* + * Copyright © 2019 Dominokit + * + * 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.dominokit.jackson.records; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.dominokit.jackson.annotation.JSONMapper; + +@JSONMapper +public record Rectangle(@JsonProperty("bottomLeft") Point a, @JsonProperty("topRight") Point b) { + +} diff --git a/domino-jackson-processor/src/main/java/org/dominokit/jackson/processor/Type.java b/domino-jackson-processor/src/main/java/org/dominokit/jackson/processor/Type.java index 508873d..f311350 100644 --- a/domino-jackson-processor/src/main/java/org/dominokit/jackson/processor/Type.java +++ b/domino-jackson-processor/src/main/java/org/dominokit/jackson/processor/Type.java @@ -970,4 +970,14 @@ private static TypeElement toTypeElement(TypeMirror type) { public static String getTypeQualifiedName(TypeMirror typeMirror) { return ((TypeElement) typeUtils.asElement(typeMirror)).getQualifiedName().toString(); } + + public static boolean isRecord(TypeMirror type) { + boolean result = false; + TypeElement recordElement = elementUtils.getTypeElement("java.lang.Record"); + if (recordElement != null) { // If running on Java 16+ + TypeMirror recordType = recordElement.asType(); + result = typeUtils.isSubtype(type, recordType); + } + return result; + } } diff --git a/domino-jackson-processor/src/main/java/org/dominokit/jackson/processor/deserialization/AptDeserializerBuilder.java b/domino-jackson-processor/src/main/java/org/dominokit/jackson/processor/deserialization/AptDeserializerBuilder.java index 4acc913..30147b4 100644 --- a/domino-jackson-processor/src/main/java/org/dominokit/jackson/processor/deserialization/AptDeserializerBuilder.java +++ b/domino-jackson-processor/src/main/java/org/dominokit/jackson/processor/deserialization/AptDeserializerBuilder.java @@ -27,22 +27,51 @@ import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import com.google.auto.common.MoreElements; import com.google.auto.common.MoreTypes; -import com.squareup.javapoet.*; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import com.squareup.javapoet.WildcardTypeName; import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import javax.annotation.processing.Filer; -import javax.lang.model.element.*; +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.TypeElement; +import javax.lang.model.element.VariableElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.MirroredTypeException; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.ElementFilter; import org.dominokit.jackson.JacksonContextProvider; import org.dominokit.jackson.JsonDeserializationContext; import org.dominokit.jackson.JsonDeserializer; import org.dominokit.jackson.JsonDeserializerParameters; -import org.dominokit.jackson.deser.bean.*; +import org.dominokit.jackson.deser.bean.AbstractBeanJsonDeserializer; +import org.dominokit.jackson.deser.bean.AbstractIdentityDeserializationInfo; +import org.dominokit.jackson.deser.bean.BeanPropertyDeserializer; +import org.dominokit.jackson.deser.bean.HasDeserializerAndParameters; +import org.dominokit.jackson.deser.bean.IdentityDeserializationInfo; +import org.dominokit.jackson.deser.bean.Instance; +import org.dominokit.jackson.deser.bean.InstanceBuilder; +import org.dominokit.jackson.deser.bean.MapLike; +import org.dominokit.jackson.deser.bean.PropertyIdentityDeserializationInfo; +import org.dominokit.jackson.deser.bean.SubtypeDeserializer; import org.dominokit.jackson.deser.bean.SubtypeDeserializer.BeanSubtypeDeserializer; +import org.dominokit.jackson.deser.bean.TypeDeserializationInfo; import org.dominokit.jackson.exception.JsonDeserializationException; import org.dominokit.jackson.processor.AbstractJsonMapperGenerator; import org.dominokit.jackson.processor.AbstractMapperProcessor; @@ -167,7 +196,7 @@ protected Set moreMethods() { // Object instance can be created by InstanceBuilder // only for non-abstract classes if (beanType.getKind() == TypeKind.DECLARED - && ((DeclaredType) beanType).asElement().getKind() == ElementKind.CLASS + && (((DeclaredType) beanType).asElement().getKind() == ElementKind.CLASS || isRecord()) && !((DeclaredType) beanType).asElement().getModifiers().contains(Modifier.ABSTRACT)) { methods.add(buildInitInstanceBuilderMethod()); } @@ -261,6 +290,10 @@ private boolean isUseJsonCreator() { .anyMatch(o -> o.getAnnotation(JsonCreator.class) != null); } + private boolean isRecord() { + return Type.isRecord(beanType); + } + private ExecutableElement getCreator() { return ((DeclaredType) beanType) .asElement().getEnclosedElements().stream() @@ -270,6 +303,40 @@ private ExecutableElement getCreator() { .orElse(null); } + public Optional getRecordCanonicalConstructor() { + TypeElement typeElement = (TypeElement) typeUtils.asElement(beanType); + // Get all fields declared in the class itself (assuming these are the record components) + List recordFields = + ElementFilter.fieldsIn(typeElement.getEnclosedElements()).stream() + .filter( + field -> + field.getModifiers().contains(Modifier.FINAL) + && !field.getModifiers().contains(Modifier.STATIC)) + .collect(Collectors.toList()); + + // Find a constructor whose parameters match the record fields + for (ExecutableElement constructor : + ElementFilter.constructorsIn(typeElement.getEnclosedElements())) { + List parameters = constructor.getParameters(); + + if (parameters.size() == recordFields.size()) { + boolean matches = true; + + // Check if parameter types match field types in order + for (int i = 0; i < parameters.size(); i++) { + if (!typeUtils.isSameType(parameters.get(i).asType(), recordFields.get(i).asType())) { + matches = false; + break; + } + } + if (matches) { + return Optional.of(constructor); // Canonical constructor found + } + } + } + return Optional.empty(); // Canonical constructor not found + } + private boolean isUseBuilder() { String builderName = getBuilderName(); return builderName != null && !builderName.isEmpty(); @@ -359,8 +426,16 @@ private MethodSpec buildInitInstanceBuilderMethod() { ParameterizedTypeName.get( ClassName.get(InstanceBuilder.class), ClassName.get(beanType))); - if (isUseJsonCreator()) { - ExecutableElement creator = getCreator(); + boolean jsonCreator = isUseJsonCreator(); + boolean record = isRecord(); + + if (jsonCreator || record) { + ExecutableElement creator; + if (jsonCreator) { + creator = getCreator(); + } else { + creator = getRecordCanonicalConstructor().get(); + } List parameterTypes = creator.getParameters(); parameterBuilders = parameterTypes.stream() @@ -424,7 +499,7 @@ private TypeSpec instanceBuilderReturnType(boolean useBuilder) { .addModifiers(Modifier.PRIVATE) .returns(ClassName.get(beanType)); - if (isUseJsonCreator()) { + if (isUseJsonCreator() || isRecord()) { for (ParameterDeserializerBuilder parameterBuilder : parameterBuilders) { createMethodBuilder.addParameter( Type.wrapperType(parameterBuilder.getParameterType()), @@ -493,7 +568,7 @@ private MethodSpec newInstanceMethod( + buildMethodName + "(), bufferedProperties)", ParameterizedTypeName.get(ClassName.get(Instance.class), ClassName.get(beanType))); - } else if (isUseJsonCreator()) { + } else if (isUseJsonCreator() || isRecord()) { buildAssignProperties(beanType, createMethod, builder); } else { builder.addStatement( @@ -549,7 +624,7 @@ private MethodSpec getDeserializerMethod() { } private Optional buildInitDeserializersMethod(TypeMirror beanType) { - if (isUseBuilder() || isUseJsonCreator() || isAbstract(beanType)) { + if (isUseBuilder() || isUseJsonCreator() || isAbstract(beanType) || isRecord()) { return Optional.empty(); } TypeName resultType = diff --git a/domino-jackson-processor/src/main/java/org/dominokit/jackson/processor/deserialization/DeserializerBuilder.java b/domino-jackson-processor/src/main/java/org/dominokit/jackson/processor/deserialization/DeserializerBuilder.java index 3f2ace6..eb35752 100644 --- a/domino-jackson-processor/src/main/java/org/dominokit/jackson/processor/deserialization/DeserializerBuilder.java +++ b/domino-jackson-processor/src/main/java/org/dominokit/jackson/processor/deserialization/DeserializerBuilder.java @@ -120,14 +120,23 @@ private MethodSpec buildParametersMethod() { } private AccessorInfo setterInfo(Element field) { - Optional accessor = - getAccessors(beanType).stream() - .filter(accessorInfo -> accessorInfo.getName().startsWith("set")) - .filter( - accessorInfo -> - Introspector.decapitalize(accessorInfo.getName().substring(3)) - .equals(field.getSimpleName().toString())) - .findFirst(); + Optional accessor; + if (Type.isRecord(beanType)) { + accessor = + getAccessors(beanType).stream() + .filter( + accessorInfo -> accessorInfo.getName().equals(field.getSimpleName().toString())) + .findFirst(); + } else { + accessor = + getAccessors(beanType).stream() + .filter(accessorInfo -> accessorInfo.getName().startsWith("set")) + .filter( + accessorInfo -> + Introspector.decapitalize(accessorInfo.getName().substring(3)) + .equals(field.getSimpleName().toString())) + .findFirst(); + } return accessor.orElseGet(() -> new AccessorInfo(field.getSimpleName().toString())); } diff --git a/domino-jackson-processor/src/main/java/org/dominokit/jackson/processor/serialization/SerializerBuilder.java b/domino-jackson-processor/src/main/java/org/dominokit/jackson/processor/serialization/SerializerBuilder.java index d824161..c7e3645 100644 --- a/domino-jackson-processor/src/main/java/org/dominokit/jackson/processor/serialization/SerializerBuilder.java +++ b/domino-jackson-processor/src/main/java/org/dominokit/jackson/processor/serialization/SerializerBuilder.java @@ -161,18 +161,28 @@ private boolean hasTypeJsonInclude() { } AbstractJsonMapperGenerator.AccessorInfo getterInfo() { - String prefix = field.asType().getKind() == TypeKind.BOOLEAN ? "is" : "get"; - Optional accessor = - getAccessors(beanType).stream() - .filter( - accessorInfo -> - accessorInfo.getName().startsWith("is") - || accessorInfo.getName().startsWith("get")) - .filter( - accessorInfo -> - Introspector.decapitalize(accessorInfo.getName().substring(prefix.length())) - .equals(field.getSimpleName().toString())) - .findFirst(); + Optional accessor; + if (Type.isRecord(beanType)) { + accessor = + getAccessors(beanType).stream() + .filter( + accessorInfo -> accessorInfo.getName().equals(field.getSimpleName().toString())) + .findFirst(); + } else { + String prefix = field.asType().getKind() == TypeKind.BOOLEAN ? "is" : "get"; + accessor = + getAccessors(beanType).stream() + .filter( + accessorInfo -> + accessorInfo.getName().startsWith("is") + || accessorInfo.getName().startsWith("get")) + .filter( + accessorInfo -> + Introspector.decapitalize(accessorInfo.getName().substring(prefix.length())) + .equals(field.getSimpleName().toString())) + .findFirst(); + } + return accessor.orElseGet( () -> new AbstractJsonMapperGenerator.AccessorInfo(field.getSimpleName().toString())); } diff --git a/domino-jackson/pom.xml b/domino-jackson/pom.xml index ac5d8ab..d65868c 100644 --- a/domino-jackson/pom.xml +++ b/domino-jackson/pom.xml @@ -6,7 +6,7 @@ org.dominokit domino-jackson-parent - 1.0.4 + 1.0.5 domino-jackson diff --git a/pom.xml b/pom.xml index 307c64a..9533ef0 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.dominokit domino-jackson-parent - 1.0.4 + 1.0.5 pom domino-jackson-parent @@ -66,15 +66,15 @@ HEAD-SNAPSHOT - 1.0.4 + 1.0.5 11 11 UTF-8 UTF-8 - 3.11.0 + 3.13.0 3.0.1 - 2.10.4 + 3.11.1 1.6 1.6.8 3.0.0-M1 @@ -85,7 +85,7 @@ 1.1.0 2.16.0 - 1.2.1 + 1.2.3 1.0.2 @@ -94,7 +94,7 @@ org.gwtproject gwt - 2.10.0 + 2.12.0 pom import @@ -383,5 +383,82 @@ + + java17-tests + + [17,) + + + + + org.codehaus.mojo + build-helper-maven-plugin + 1.9.1 + + + add-integration-test-source-as-test-sources + generate-test-sources + + add-test-source + + + + src/it/java + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.5.2 + + + integration-test + + integration-test + verify + + + + + **/*IT.java + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 17 + + + + + + + + java23-processors + + [23,) + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 23 + + -proc:full + + + + + +