This is an automated email from the ASF dual-hosted git repository.
rmannibucau pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/johnzon.git
The following commit(s) were added to refs/heads/master by this push:
new c3a49a5 JOHNZON-300 base to support java 14 records
c3a49a5 is described below
commit c3a49a5aca8a612b4dd95de6f68b25d8fe1f4c15
Author: Romain Manni-Bucau <[email protected]>
AuthorDate: Thu Dec 19 18:53:56 2019 +0100
JOHNZON-300 base to support java 14 records
---
.../org/apache/johnzon/mapper/JohnzonRecord.java | 45 ++++++++++++
.../apache/johnzon/mapper/MappingParserImpl.java | 10 ++-
.../java/org/apache/johnzon/mapper/Mappings.java | 19 +++++
.../johnzon/mapper/access/BaseAccessMode.java | 66 ++++++++++++-----
.../mapper/access/FieldAndMethodAccessMode.java | 4 +-
.../johnzon/mapper/access/MethodAccessMode.java | 46 +++++++-----
.../apache/johnzon/mapper/reflection/Records.java | 48 +++++++++++++
.../java/org/apache/johnzon/mapper/RecordTest.java | 84 ++++++++++++++++++++++
8 files changed, 283 insertions(+), 39 deletions(-)
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.NULL;
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 @@ public class MappingParserImpl implements MappingParser
{
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 @@ public class Mappings {
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 @@ package org.apache.johnzon.mapper.access;
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.Collection;
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 @@ public abstract class BaseAccessMode implements AccessMode {
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 @@ public abstract class BaseAccessMode implements
AccessMode {
@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 @@ public abstract class BaseAccessMode implements AccessMode
{
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 @@ public abstract class BaseAccessMode implements AccessMode
{
};
}
+ 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 @@ public class FieldAndMethodAccessMode extends
BaseAccessMode {
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.lang.reflect.Type;
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 @@ public class MethodAccessMode extends BaseAccessMode {
@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);
+ }
+ }
+}