JOHNZON-300 base to support java 14 records
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/JohnzonRecord.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/JohnzonRecord.java
new file mode 100644
index 0000000..d57a0b8
--- /dev/null
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/JohnzonRecord.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.johnzon.mapper;
+
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Forces method named as properties to be used as getters (String foo() will match the attribute foo).
+ * Also enables a constructor with all properties even if not marked as @ConstructorProperties or equivalent.
+ * It simulates java >= 14 record style.
+ */
+@Target({ TYPE })
+@Retention(RUNTIME)
+public @interface JohnzonRecord {
+ /**
+ * When not using -parameters compiler argument, enables to customize parameter names.
+ * It is only real in @JohnzonRecord classes.
+ */
+ @Target(PARAMETER)
+ @Retention(RUNTIME)
+ @interface Name {
+ String value();
+ }
+}
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java
index 1ae08ff..0ec10bc 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/MappingParserImpl.java
@@ -83,6 +83,7 @@
import static javax.json.JsonValue.ValueType.NUMBER;
import static javax.json.JsonValue.ValueType.STRING;
import static javax.json.JsonValue.ValueType.TRUE;
+import static org.apache.johnzon.mapper.Mappings.getPrimitiveDefault;
/**
* This class is not concurrently usable as it contains state.
@@ -1002,16 +1003,19 @@
final Object[] objects = new Object[length];
for (int i = 0; i < length; i++) {
-
- String paramName = mapping.factory.getParameterNames()[i];
+ final String paramName = mapping.factory.getParameterNames()[i];
+ final Type parameterType = mapping.factory.getParameterTypes()[i];
objects[i] = toValue(null,
object.get(paramName),
mapping.factory.getParameterConverter()[i],
mapping.factory.getParameterItemConverter()[i],
- mapping.factory.getParameterTypes()[i],
+ parameterType,
mapping.factory.getObjectConverter()[i],
isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, paramName) : null,
mapping.clazz); //X TODO ObjectConverter in @JohnzonConverter with Constructors!
+ if (objects[i] == null) {
+ objects[i] = getPrimitiveDefault(parameterType);
+ }
}
return objects;
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mappings.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mappings.java
index acf5d49..0f623a1 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mappings.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mappings.java
@@ -359,6 +359,25 @@
return false;
}
+ public static Object getPrimitiveDefault(final Type type) {
+ if (type == long.class) {
+ return 0L;
+ } else if (type == int.class) {
+ return 0;
+ } else if (type == short.class) {
+ return (short) 0;
+ } else if (type == byte.class) {
+ return (byte) 0;
+ } else if (type == double.class) {
+ return 0.;
+ } else if (type == float.class) {
+ return 0f;
+ } else if (type == boolean.class) {
+ return false;
+ }
+ return null;
+ }
+
public ClassMapping getClassMapping(final Type clazz) {
return classes.get(clazz);
}
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/BaseAccessMode.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/BaseAccessMode.java
index 0eba849..818ddec 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/BaseAccessMode.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/BaseAccessMode.java
@@ -20,7 +20,10 @@
import static java.util.Arrays.asList;
import static java.util.Collections.emptySet;
+import static java.util.Comparator.comparing;
+import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toSet;
+import static org.apache.johnzon.mapper.reflection.Records.isRecord;
import static org.apache.johnzon.mapper.reflection.Converters.matches;
import java.beans.ConstructorProperties;
@@ -34,11 +37,13 @@
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.Map;
+import java.util.stream.Stream;
import org.apache.johnzon.mapper.Adapter;
import org.apache.johnzon.mapper.Converter;
import org.apache.johnzon.mapper.JohnzonAny;
import org.apache.johnzon.mapper.JohnzonConverter;
+import org.apache.johnzon.mapper.JohnzonRecord;
import org.apache.johnzon.mapper.MapperConverter;
import org.apache.johnzon.mapper.ObjectConverter;
import org.apache.johnzon.mapper.internal.ConverterAdapter;
@@ -48,6 +53,7 @@
private static final Type[] NO_PARAMS = new Type[0];
private FieldFilteringStrategy fieldFilteringStrategy = new SingleEntryFieldFilteringStrategy();
+
private final boolean acceptHiddenConstructor;
private final boolean useConstructor;
@@ -105,25 +111,29 @@
@Override
public Factory findFactory(final Class<?> clazz) {
Constructor<?> constructor = null;
- for (final Constructor<?> c : clazz.getDeclaredConstructors()) {
- if (c.getParameterTypes().length == 0) {
- if (!Modifier.isPublic(c.getModifiers()) && acceptHiddenConstructor) {
- c.setAccessible(true);
- }
- constructor = c;
- if (!useConstructor) {
+ if (isRecord(clazz) || Meta.getAnnotation(clazz, JohnzonRecord.class) != null) {
+ constructor = findRecordConstructor(clazz);
+ } else {
+ for (final Constructor<?> c : clazz.getDeclaredConstructors()) {
+ if (c.getParameterTypes().length == 0) {
+ if (!Modifier.isPublic(c.getModifiers()) && acceptHiddenConstructor) {
+ c.setAccessible(true);
+ }
+ constructor = c;
+ if (!useConstructor) {
+ break;
+ }
+ } else if (c.getAnnotation(ConstructorProperties.class) != null) {
+ constructor = c;
break;
}
- } else if (c.getAnnotation(ConstructorProperties.class) != null) {
- constructor = c;
- break;
}
- }
- if (constructor == null) {
- try {
- constructor = clazz.getConstructor();
- } catch (final NoSuchMethodException e) {
- return null; // readOnly class
+ if (constructor == null) {
+ try {
+ constructor = clazz.getConstructor();
+ } catch (final NoSuchMethodException e) {
+ return null; // readOnly class
+ }
}
}
@@ -137,8 +147,16 @@
factoryParameterTypes = constructor.getGenericParameterTypes();
constructorParameters = new String[constructor.getGenericParameterTypes().length];
- final ConstructorProperties constructorProperties = constructor.getAnnotation(ConstructorProperties.class);
- System.arraycopy(constructorProperties.value(), 0, constructorParameters, 0, constructorParameters.length);
+
+ final Constructor<?> fc = constructor;
+ final String[] constructorProperties = ofNullable(constructor.getAnnotation(ConstructorProperties.class))
+ .map(ConstructorProperties::value)
+ .orElseGet(() -> Stream.of(fc.getParameters())
+ .map(p -> ofNullable(p.getAnnotation(JohnzonRecord.Name.class))
+ .map(JohnzonRecord.Name::value)
+ .orElseGet(p::getName))
+ .toArray(String[]::new));
+ System.arraycopy(constructorProperties, 0, constructorParameters, 0, constructorParameters.length);
constructorParameterConverters = new Adapter<?, ?>[constructor.getGenericParameterTypes().length];
constructorItemParameterConverters = new Adapter<?, ?>[constructorParameterConverters.length];
@@ -220,6 +238,18 @@
};
}
+ private Constructor<?> findRecordConstructor(Class<?> clazz) {
+ return Stream.of(clazz.getConstructors())
+ .max(comparing(Constructor::getParameterCount))
+ .map(c -> {
+ if (!c.isAccessible()) {
+ c.setAccessible(true);
+ }
+ return c;
+ })
+ .orElse(null);
+ }
+
@Override
public Method findAnyGetter(final Class<?> clazz) {
Method m = null;
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/FieldAndMethodAccessMode.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/FieldAndMethodAccessMode.java
index 87f416d..9b04452 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/FieldAndMethodAccessMode.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/FieldAndMethodAccessMode.java
@@ -43,11 +43,13 @@
final boolean useGettersAsWriter, final boolean alwaysPreferMethodVisibility) {
super(useConstructor, acceptHiddenConstructor);
this.fields = new FieldAccessMode(useConstructor, acceptHiddenConstructor);
- this.methods = new MethodAccessMode(useConstructor, acceptHiddenConstructor, useGettersAsWriter);
+ this.methods = new MethodAccessMode(
+ useConstructor, acceptHiddenConstructor, useGettersAsWriter);
this.alwaysPreferMethodVisibility = alwaysPreferMethodVisibility;
}
// backward compatibility, don't delete since it can be used from user code in jsonb delegate access mode property
+ @Deprecated
public FieldAndMethodAccessMode(final boolean useConstructor, final boolean acceptHiddenConstructor,
final boolean useGettersAsWriter) {
this(useConstructor, acceptHiddenConstructor, useGettersAsWriter, true);
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/MethodAccessMode.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/MethodAccessMode.java
index c43ef3b..67fab32 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/MethodAccessMode.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/access/MethodAccessMode.java
@@ -18,6 +18,9 @@
*/
package org.apache.johnzon.mapper.access;
+import static java.util.stream.Collectors.toMap;
+import static org.apache.johnzon.mapper.reflection.Records.isRecord;
+
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
@@ -27,10 +30,12 @@
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
+import java.util.stream.Stream;
import org.apache.johnzon.mapper.Adapter;
import org.apache.johnzon.mapper.JohnzonAny;
import org.apache.johnzon.mapper.JohnzonProperty;
+import org.apache.johnzon.mapper.JohnzonRecord;
import org.apache.johnzon.mapper.MapperException;
import org.apache.johnzon.mapper.ObjectConverter;
@@ -45,23 +50,30 @@
@Override
public Map<String, Reader> doFindReaders(final Class<?> clazz) {
final Map<String, Reader> readers = new HashMap<String, Reader>();
- final PropertyDescriptor[] propertyDescriptors = getPropertyDescriptors(clazz);
- for (final PropertyDescriptor descriptor : propertyDescriptors) {
- final Method readMethod = descriptor.getReadMethod();
- final String name = descriptor.getName();
- if (readMethod != null && readMethod.getDeclaringClass() != Object.class) {
- if (isIgnored(name) || Meta.getAnnotation(readMethod, JohnzonAny.class) != null) {
- continue;
- }
- readers.put(extractKey(name, readMethod, null), new MethodReader(readMethod, readMethod.getGenericReturnType()));
- } else if (readMethod == null && descriptor.getWriteMethod() != null && // isXXX, not supported by javabeans
- (descriptor.getPropertyType() == Boolean.class || descriptor.getPropertyType() == boolean.class)) {
- try {
- final Method method = clazz.getMethod(
- "is" + Character.toUpperCase(name.charAt(0)) + (name.length() > 1 ? name.substring(1) : ""));
- readers.put(extractKey(name, method, null), new MethodReader(method, method.getGenericReturnType()));
- } catch (final NoSuchMethodException e) {
- // no-op
+ if (isRecord(clazz) || Meta.getAnnotation(clazz, JohnzonRecord.class) != null) {
+ readers.putAll(Stream.of(clazz.getMethods())
+ .filter(it -> it.getDeclaringClass() != Object.class && it.getParameterCount() == 0)
+ .filter(it -> !"toString".equals(it.getName()) && !"hashCode".equals(it.getName()))
+ .collect(toMap(Method::getName, it -> new MethodReader(it, it.getGenericReturnType()))));
+ } else {
+ final PropertyDescriptor[] propertyDescriptors = getPropertyDescriptors(clazz);
+ for (final PropertyDescriptor descriptor : propertyDescriptors) {
+ final Method readMethod = descriptor.getReadMethod();
+ final String name = descriptor.getName();
+ if (readMethod != null && readMethod.getDeclaringClass() != Object.class) {
+ if (isIgnored(name) || Meta.getAnnotation(readMethod, JohnzonAny.class) != null) {
+ continue;
+ }
+ readers.put(extractKey(name, readMethod, null), new MethodReader(readMethod, readMethod.getGenericReturnType()));
+ } else if (readMethod == null && descriptor.getWriteMethod() != null && // isXXX, not supported by javabeans
+ (descriptor.getPropertyType() == Boolean.class || descriptor.getPropertyType() == boolean.class)) {
+ try {
+ final Method method = clazz.getMethod(
+ "is" + Character.toUpperCase(name.charAt(0)) + (name.length() > 1 ? name.substring(1) : ""));
+ readers.put(extractKey(name, method, null), new MethodReader(method, method.getGenericReturnType()));
+ } catch (final NoSuchMethodException e) {
+ // no-op
+ }
}
}
}
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/reflection/Records.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/reflection/Records.java
new file mode 100644
index 0000000..7d8d415
--- /dev/null
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/reflection/Records.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.johnzon.mapper.reflection;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+public final class Records {
+ private static final Method IS_RECORD;
+
+ static {
+ Method isRecord = null;
+ try {
+ isRecord = Class.class.getMethod("isRecord");
+ } catch (final NoSuchMethodException e) {
+ // no-op
+ }
+ IS_RECORD = isRecord;
+ }
+
+ private Records() {
+ // no-op
+ }
+
+ public static boolean isRecord(final Class<?> clazz) {
+ try {
+ return IS_RECORD != null && Boolean.class.cast(IS_RECORD.invoke(clazz));
+ } catch (final InvocationTargetException | IllegalAccessException e) {
+ return false;
+ }
+ }
+}
diff --git a/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/RecordTest.java b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/RecordTest.java
new file mode 100644
index 0000000..32a8738
--- /dev/null
+++ b/johnzon-mapper/src/test/java/org/apache/johnzon/mapper/RecordTest.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.johnzon.mapper;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.Objects;
+
+import org.junit.Test;
+
+public class RecordTest {
+ @Test
+ public void roundTrip() {
+ final Record ref = new Record(119, "Santa");
+ try (final Mapper mapper = new MapperBuilder().setAttributeOrder(String.CASE_INSENSITIVE_ORDER).build()) {
+ final String expectedJson = "{\"age\":119,\"name\":\"Santa\"}";
+ assertEquals(expectedJson, mapper.writeObjectAsString(ref));
+ assertEquals(ref, mapper.readObject(expectedJson, Record.class));
+ }
+ }
+
+ @JohnzonRecord
+ public static class Record {
+ private final int age;
+ private final String name;
+
+ public Record() { // simulate custom constructor
+ this.age = -1;
+ this.name = "failed";
+ }
+
+ public Record(final int age) { // simulate custom constructor
+ this.age = age;
+ this.name = "failed";
+ }
+
+ public Record(@JohnzonRecord.Name("age") final int age,
+ @JohnzonRecord.Name("name") final String name) {
+ this.age = age;
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return "Record{" +
+ "age=" + age +
+ ", name='" + name + '\'' +
+ '}';
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ final Record record = Record.class.cast(o);
+ return age == record.age && Objects.equals(name, record.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(age, name);
+ }
+ }
+}