Support for records (#499)

Support for records added

Signed-off-by: David Kral <david.k.kral@oracle.com>
diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index 90e0e46..6f9dc93 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -53,6 +53,6 @@
           distribution: 'adopt'
           java-version: ${{ matrix.java_version }}
       - name: Yasson tests
-        run: mvn -U -C -Pstaging test
+        run: mvn -U -C -Pstaging verify
       - name: JSONB-API TCK
         run: cd yasson-tck && mvn -U -B test
diff --git a/pom.xml b/pom.xml
index 732d89f..2d0057d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -240,6 +240,50 @@
                 </plugins>
             </build>
         </profile>
+        <profile>
+            <id>jdk16</id>
+            <activation>
+                <jdk>[16,)</jdk>
+            </activation>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-compiler-plugin</artifactId>
+                        <version>3.8.1</version>
+                        <executions>
+                            <execution>
+                                <id>default-testCompile</id>
+                                <configuration>
+                                    <release>16</release>
+                                    <compileSourceRoots>
+                                        <compileSourceRoot>${project.basedir}/src/test/java</compileSourceRoot>
+                                        <compileSourceRoot>${project.basedir}/src/test/java16</compileSourceRoot>
+                                    </compileSourceRoots>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-failsafe-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <goals>
+                                    <goal>integration-test</goal>
+                                    <goal>verify</goal>
+                                </goals>
+                            </execution>
+                        </executions>
+                        <configuration>
+                            <includes>
+                                <include>**/RecordTest.java</include>
+                            </includes>
+                        </configuration>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
     </profiles>
 
     <build>
@@ -273,6 +317,11 @@
                 </plugin>
                 <plugin>
                     <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-failsafe-plugin</artifactId>
+                    <version>3.0.0-M3</version>
+                </plugin>
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
                     <artifactId>maven-compiler-plugin</artifactId>
                     <version>3.8.1</version>
                     <executions>
@@ -290,7 +339,7 @@
                             </configuration>
                         </execution>
                         <execution>
-                            <id>multi-release-compile</id>
+                            <id>multi-release-compile-9</id>
                             <goals>
                                 <goal>compile</goal>
                             </goals>
@@ -303,6 +352,19 @@
                             </configuration>
                         </execution>
                         <execution>
+                            <id>multi-release-compile-16</id>
+                            <goals>
+                                <goal>compile</goal>
+                            </goals>
+                            <configuration>
+                                <release>16</release>
+                                <compileSourceRoots>
+                                    <compileSourceRoot>${project.basedir}/src/main/java16</compileSourceRoot>
+                                </compileSourceRoots>
+                                <multiReleaseOutput>true</multiReleaseOutput>
+                            </configuration>
+                        </execution>
+                        <execution>
                             <id>base-compile</id>
                             <goals>
                                 <goal>compile</goal>
@@ -430,6 +492,7 @@
                     </executions>
                 </plugin>
                 <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
                     <artifactId>maven-surefire-plugin</artifactId>
                     <version>3.0.0-M3</version>
                     <executions>
@@ -440,10 +503,12 @@
                                 <goal>test</goal>
                             </goals>
                             <configuration>
+<!--                                <testFailureIgnore>true</testFailureIgnore>-->
                                 <trimStackTrace>false</trimStackTrace>
                                 <excludes>
-                                    <exclude>**/JavaxNamingExcludedTest.class</exclude>
-                                    <exclude>**/AnnotationIntrospectorWithoutOptionalModulesTest.class</exclude>
+                                    <exclude>**/JavaxNamingExcludedTest.java</exclude>
+                                    <exclude>**/AnnotationIntrospectorWithoutOptionalModulesTest.java</exclude>
+                                    <exclude>**/*Record*</exclude>
                                 </excludes>
                                 <argLine>
                                     <!--Remove when CDI is updated to support modules
diff --git a/src/main/java/org/eclipse/yasson/internal/AnnotationIntrospector.java b/src/main/java/org/eclipse/yasson/internal/AnnotationIntrospector.java
index 97c0010..20359ce 100644
--- a/src/main/java/org/eclipse/yasson/internal/AnnotationIntrospector.java
+++ b/src/main/java/org/eclipse/yasson/internal/AnnotationIntrospector.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2021 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -148,10 +148,12 @@
      * @return JsonbCreator metadata object
      */
     public JsonbCreator getCreator(Class<?> clazz) {
-        JsonbCreator jsonbCreator = null;
+        JsonbCreator jsonbCreator;
         Constructor<?>[] declaredConstructors =
                 AccessController.doPrivileged((PrivilegedAction<Constructor<?>[]>) clazz::getDeclaredConstructors);
 
+        jsonbCreator = ClassMultiReleaseExtension.findJsonbCreator(clazz, declaredConstructors, this);
+
         for (Constructor<?> constructor : declaredConstructors) {
             final jakarta.json.bind.annotation.JsonbCreator annot = findAnnotation(constructor.getDeclaredAnnotations(),
                                                                                  jakarta.json.bind.annotation.JsonbCreator.class);
@@ -180,7 +182,7 @@
         return jsonbCreator;
     }
 
-    private JsonbCreator createJsonbCreator(Executable executable, JsonbCreator existing, Class<?> clazz) {
+    JsonbCreator createJsonbCreator(Executable executable, JsonbCreator existing, Class<?> clazz) {
         if (existing != null) {
             throw new JsonbException(Messages.getMessage(MessageKeys.MULTIPLE_JSONB_CREATORS, clazz));
         }
diff --git a/src/main/java/org/eclipse/yasson/internal/ClassMultiReleaseExtension.java b/src/main/java/org/eclipse/yasson/internal/ClassMultiReleaseExtension.java
new file mode 100644
index 0000000..9b94193
--- /dev/null
+++ b/src/main/java/org/eclipse/yasson/internal/ClassMultiReleaseExtension.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0,
+ * or the Eclipse Distribution License v. 1.0 which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
+ */
+
+package org.eclipse.yasson.internal;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+
+import org.eclipse.yasson.internal.model.JsonbCreator;
+
+/**
+ * Search for instance creator from other sources.
+ * Mainly intended to add extensibility for different java versions and new features.
+ */
+class ClassMultiReleaseExtension {
+
+    private ClassMultiReleaseExtension() {
+        throw new IllegalStateException("This class cannot be instantiated");
+    }
+
+    static boolean shouldTransformToPropertyName(Method method) {
+        return true;
+    }
+
+    static boolean isGetAccessorMethod(Method method) {
+        return false;
+    }
+
+    static JsonbCreator findJsonbCreator(Class<?> clazz, Constructor<?>[] declaredConstructors, AnnotationIntrospector introspector) {
+        return null;
+    }
+
+}
diff --git a/src/main/java/org/eclipse/yasson/internal/ClassParser.java b/src/main/java/org/eclipse/yasson/internal/ClassParser.java
index 63ee6e7..1d61b1d 100644
--- a/src/main/java/org/eclipse/yasson/internal/ClassParser.java
+++ b/src/main/java/org/eclipse/yasson/internal/ClassParser.java
@@ -201,7 +201,9 @@
             if (!isPropertyMethod(method) || method.isBridge() || isSpecialCaseMethod(clazz, method)) {
                 continue;
             }
-            final String propertyName = toPropertyMethod(name);
+            final String propertyName = ClassMultiReleaseExtension.shouldTransformToPropertyName(method)
+                    ? toPropertyMethod(name)
+                    : name;
 
             registerMethod(propertyName, method, classElement, classProperties);
         }
@@ -230,6 +232,9 @@
     }
 
     private static boolean isGetter(Method m) {
+        if (ClassMultiReleaseExtension.isGetAccessorMethod(m)) {
+            return true;
+        }
         return (m.getName().startsWith(GET_PREFIX) || m.getName().startsWith(IS_PREFIX)) && m.getParameterCount() == 0;
     }
 
@@ -238,7 +243,7 @@
     }
 
     private static String toPropertyMethod(String name) {
-        return lowerFirstLetter(name.substring(name.startsWith(IS_PREFIX) ? 2 : 3, name.length()));
+        return lowerFirstLetter(name.substring(name.startsWith(IS_PREFIX) ? 2 : 3));
     }
 
     private static String lowerFirstLetter(String name) {
diff --git a/src/main/java16/org/eclipse/yasson/internal/ClassMultiReleaseExtension.java b/src/main/java16/org/eclipse/yasson/internal/ClassMultiReleaseExtension.java
new file mode 100644
index 0000000..55adab3
--- /dev/null
+++ b/src/main/java16/org/eclipse/yasson/internal/ClassMultiReleaseExtension.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0,
+ * or the Eclipse Distribution License v. 1.0 which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
+ */
+
+package org.eclipse.yasson.internal;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.Set;
+
+import org.eclipse.yasson.internal.model.JsonbCreator;
+
+/**
+ * Search for instance creator from other sources.
+ * Mainly intended to add extensibility for different java versions and new features.
+ */
+class ClassMultiReleaseExtension {
+
+    private static final Set<String> NOT_VALID_ACCESSOR_METHODS = Set.of("equals", "hashCode", "toString");
+
+    private ClassMultiReleaseExtension() {
+        throw new IllegalStateException("This class cannot be instantiated");
+    }
+
+    static boolean shouldTransformToPropertyName(Method method) {
+        return !method.getDeclaringClass().isRecord();
+    }
+
+    static boolean isGetAccessorMethod(Method method) {
+        if (method.getDeclaringClass().isRecord()) {
+            return !NOT_VALID_ACCESSOR_METHODS.contains(method.getName());
+        }
+        return false;
+    }
+
+    static JsonbCreator findJsonbCreator(Class<?> clazz, Constructor<?>[] declaredConstructors, AnnotationIntrospector introspector) {
+        if (clazz.isRecord()) {
+            return introspector.createJsonbCreator(declaredConstructors[0], null, clazz);
+        }
+        return null;
+    }
+
+}
diff --git a/src/test/java16/org/eclipse/yasson/records/Car.java b/src/test/java16/org/eclipse/yasson/records/Car.java
new file mode 100644
index 0000000..d001315
--- /dev/null
+++ b/src/test/java16/org/eclipse/yasson/records/Car.java
@@ -0,0 +1,18 @@
+/*
+ * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0,
+ * or the Eclipse Distribution License v. 1.0 which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
+ */
+
+package org.eclipse.yasson.records;
+
+import jakarta.json.bind.annotation.JsonbProperty;
+
+public record Car(@JsonbProperty("typeChanged") String type, @JsonbProperty("colorChanged") String color) {
+}
diff --git a/src/test/java16/org/eclipse/yasson/records/CarWithoutAnnotations.java b/src/test/java16/org/eclipse/yasson/records/CarWithoutAnnotations.java
new file mode 100644
index 0000000..fc1511e
--- /dev/null
+++ b/src/test/java16/org/eclipse/yasson/records/CarWithoutAnnotations.java
@@ -0,0 +1,16 @@
+/*
+ * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0,
+ * or the Eclipse Distribution License v. 1.0 which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
+ */
+
+package org.eclipse.yasson.records;
+
+public record CarWithoutAnnotations(String type, String color) {
+}
diff --git a/src/test/java16/org/eclipse/yasson/records/RecordTest.java b/src/test/java16/org/eclipse/yasson/records/RecordTest.java
new file mode 100644
index 0000000..0170eac
--- /dev/null
+++ b/src/test/java16/org/eclipse/yasson/records/RecordTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0,
+ * or the Eclipse Distribution License v. 1.0 which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
+ */
+
+package org.eclipse.yasson.records;
+
+import org.eclipse.yasson.Jsonbs;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class RecordTest {
+
+    @Test
+    public void testRecordProcessing() {
+        Car car = new Car("skoda", "green");
+        String expected = "{\"colorChanged\":\"green\",\"typeChanged\":\"skoda\"}";
+
+        String json = Jsonbs.defaultJsonb.toJson(car);
+        assertEquals(expected, json);
+        Car deserialized = Jsonbs.defaultJsonb.fromJson(expected, Car.class);
+        assertEquals(car, deserialized);
+    }
+
+    @Test
+    public void testRecordProcessingWithoutJsonbProperties() {
+        CarWithoutAnnotations car = new CarWithoutAnnotations("skoda", "green");
+        String expected = "{\"color\":\"green\",\"type\":\"skoda\"}";
+
+        String json = Jsonbs.defaultJsonb.toJson(car);
+        assertEquals(expected, json);
+        CarWithoutAnnotations deserialized = Jsonbs.defaultJsonb.fromJson(expected, CarWithoutAnnotations.class);
+        assertEquals(car, deserialized);
+    }
+
+}