You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@johnzon.apache.org by rm...@apache.org on 2022/02/02 20:03:04 UTC
[johnzon] 01/01: start a binding generator from our classmapping
This is an automated email from the ASF dual-hosted git repository.
rmannibucau pushed a commit to branch generated-bindings
in repository https://gitbox.apache.org/repos/asf/johnzon.git
commit 30ccfb92428c647114b20a2a352a127988d733e6
Author: Romain Manni-Bucau <rm...@gmail.com>
AuthorDate: Wed Feb 2 21:02:58 2022 +0100
start a binding generator from our classmapping
---
.../org/apache/johnzon/jsonb/JohnzonJsonb.java | 4 +
.../jsonb/generator/GeneratedJohnzonJsonb.java | 36 +++
.../jsonb/generator/JsonbMapperGenerator.java | 284 +++++++++++++++++++++
.../jsonb/generator/GeneratedJsonbTest.java | 89 +++++++
.../java/org/apache/johnzon/mapper/Mapper.java | 16 ++
5 files changed, 429 insertions(+)
diff --git a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JohnzonJsonb.java b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JohnzonJsonb.java
index 69c4816..2cb3b63 100644
--- a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JohnzonJsonb.java
+++ b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/JohnzonJsonb.java
@@ -69,6 +69,10 @@ public class JohnzonJsonb implements Jsonb, AutoCloseable, JsonbExtension {
this.onClose = onClose;
}
+ public Mapper getDelegate() {
+ return delegate;
+ }
+
@Override
public <T> T fromJson(final String str, final Class<T> type) throws JsonbException {
try {
diff --git a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/generator/GeneratedJohnzonJsonb.java b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/generator/GeneratedJohnzonJsonb.java
new file mode 100644
index 0000000..df3b42d
--- /dev/null
+++ b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/generator/GeneratedJohnzonJsonb.java
@@ -0,0 +1,36 @@
+/*
+ * 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.jsonb.generator;
+
+import org.apache.johnzon.jsonb.JohnzonJsonb;
+
+import java.io.Reader;
+import java.io.Writer;
+
+public abstract class GeneratedJohnzonJsonb {
+ protected final JohnzonJsonb root;
+
+ protected GeneratedJohnzonJsonb(final JohnzonJsonb root) {
+ this.root = root;
+ }
+
+ public abstract <T> T fromJson(Reader reader);
+
+ public abstract void toJson(Object object, Writer writer);
+}
diff --git a/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/generator/JsonbMapperGenerator.java b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/generator/JsonbMapperGenerator.java
new file mode 100644
index 0000000..ad0917e
--- /dev/null
+++ b/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/generator/JsonbMapperGenerator.java
@@ -0,0 +1,284 @@
+/*
+ * 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.jsonb.generator;
+
+import org.apache.johnzon.jsonb.JohnzonBuilder;
+import org.apache.johnzon.jsonb.JohnzonJsonb;
+import org.apache.johnzon.mapper.Mappings;
+import org.apache.johnzon.mapper.access.AccessMode;
+import org.apache.johnzon.mapper.access.FieldAndMethodAccessMode;
+import org.apache.johnzon.mapper.access.MethodAccessMode;
+
+import javax.json.bind.JsonbConfig;
+import java.io.IOException;
+import java.io.Writer;
+import java.lang.reflect.Field;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.function.Supplier;
+import java.util.logging.Logger;
+import java.util.stream.Stream;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.requireNonNull;
+import static java.util.logging.Level.SEVERE;
+import static java.util.stream.Collectors.joining;
+
+public class JsonbMapperGenerator implements Runnable {
+ private final Configuration configuration;
+
+ public JsonbMapperGenerator(final Configuration configuration) {
+ this.configuration = configuration;
+ }
+
+ @Override
+ public void run() {
+ requireNonNull(configuration.output, "no output set");
+ requireNonNull(configuration.classes, "no classes set");
+ try (final JohnzonJsonb jsonb = JohnzonJsonb.class.cast(new JohnzonBuilder()
+ .withConfig(configuration.config == null ? new JsonbConfig() : configuration.config)
+ .build())) {
+ final Mappings mappings = jsonb.getDelegate().getMappings();
+ configuration.classes.forEach(clazz -> {
+ final Mappings.ClassMapping mapping = mappings.findOrCreateClassMapping(clazz);
+
+ final String suffix = "$$JohnzonJsonb"; // todo: make it configurable?
+ final Path target = configuration.output.resolve(clazz.getName().replace('.', '/') + suffix + ".class");
+ info(() -> "Generating JSON-B for '" + clazz.getName() + "' to '" + target + "'");
+
+ final StringBuilder out = new StringBuilder();
+ if (configuration.header != null) {
+ out.append(configuration.header);
+ }
+ if (clazz.getPackage() != null) {
+ out.append("package ").append(clazz.getPackage().getName()).append(";\n\n");
+ }
+
+ out.append("import org.apache.johnzon.jsonb.generator.GeneratedJohnzonJsonb;\n");
+ out.append("import org.apache.johnzon.jsonb.JohnzonJsonb;\n");
+ out.append("import javax.json.JsonGenerator;\n");
+ out.append("import javax.json.JsonReader;\n");
+ out.append("import javax.json.JsonValue;\n");
+ out.append("\n");
+ out.append("public class ").append(clazz.getSimpleName()).append(suffix).append(" implements GeneratedJohnzonJsonb {\n");
+ out.append(" public ").append(clazz.getSimpleName()).append(suffix).append("(final JohnzonJsonb root) {\n");
+ out.append(" super(root);\n");
+ out.append(" }\n");
+ out.append("\n");
+ out.append(" @Override\n");
+ out.append(" public <T> T fromJson(final Reader reader) {\n");
+ if (mapping.setters.isEmpty()) { // will always be empty
+ out.append(" return JsonValue.EMPTY_JSON_OBJECT;\n");
+ } else {
+ // todo: use mappings.getters and expose with getters jsonb.getMapper().getJsonReaderFactory()
+ out.append(" try (final JsonReader reader = root.getMapper().getReaderFactory().createReader(reader)) {\n");
+ out.append(" final JsonValue value = reader.readValue();\n");
+ out.append(" switch (value.getValueType()) {\n");
+ out.append(" case OBJECT: {\n");
+ out.append(" final ").append(clazz.getSimpleName()).append(suffix).append(" instance = new ")
+ .append(clazz.getSimpleName()).append(suffix).append("();\n");
+ out.append(mapping.setters.entrySet().stream()
+ .map(setter -> toSetter(setter.getValue(), setter.getKey()))
+ .collect(joining("\n", "", "\n")));
+ out.append(" return null;\n");
+ out.append(" }\n");
+ out.append(" case NULL:\n");
+ out.append(" case ARRAY:\n");
+ out.append(" case STRING:\n");
+ out.append(" case NUMBER:\n");
+ out.append(" case TRUE:\n");
+ out.append(" case FALSE:\n");
+ out.append(" default:\n");
+ // todo: check if there is an adapter or alike
+ out.append(" throw new IllegalStateException(\"invalid value type: '\" + value.getValueType() + \"'\");\n");
+ out.append(" }\n");
+ out.append(" }\n");
+ }
+ out.append(" }\n");
+ out.append("\n");
+ out.append(" @Override\n");
+ out.append(" public void toJson(final Object object, final Writer writer) {\n");
+ // todo: use mappings.setters and expose with getters jsonb.getMapper().getJsongeneratorFactory()
+ out.append(" // TBD\n");
+ out.append(" }\n");
+ out.append("}\n\n");
+
+ try {
+ Files.createDirectories(target.getParent());
+ } catch (final IOException e) {
+ throw new IllegalStateException(e);
+ }
+
+ String content = out.toString();
+ boolean preferJakarta;
+ if (configuration.preferJakarta != null) {
+ preferJakarta = configuration.preferJakarta;
+ } else {
+ try {
+ Thread.currentThread().getContextClassLoader().loadClass("jakarta.json.spi.JsonProvider");
+ preferJakarta = true;
+ } catch (final NoClassDefFoundError | ClassNotFoundException e) {
+ preferJakarta = false;
+ }
+ }
+ if (preferJakarta) {
+ content = content.replace(" javax.json.", " jakarta.json.");
+ }
+ try (final Writer writer = Files.newBufferedWriter(target, UTF_8)) {
+ writer.append(content);
+ } catch (final IOException e) {
+ throw new IllegalStateException(e);
+ }
+ });
+ } catch (final Exception e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private String toGetter(final Mappings.Getter value) {
+ try {
+ final Field reader = value.getClass().getDeclaredField("reader");
+ if (!reader.isAccessible()) {
+ reader.setAccessible(true);
+ }
+ final Object wrapped = reader.get(value);
+ final Field finalReader = Stream.of(wrapped.getClass().getDeclaredFields())
+ .filter(it -> it.getName().contains("finalReader") && AccessMode.Reader.class == it.getType())
+ .peek(it -> {
+ if (!it.isAccessible()) {
+ it.setAccessible(true);
+ }
+ })
+ .findFirst()
+ .orElseThrow(() -> new IllegalStateException("No finalReader field in " + wrapped));
+ return toGetter(AccessMode.Reader.class.cast(finalReader.get(wrapped)));
+ } catch (final IllegalAccessException | NoSuchFieldException nsfe) {
+ throw new IllegalArgumentException("Unsupported getter: " + value, nsfe);
+ }
+ }
+
+ private String toGetter(final MethodAccessMode.MethodReader reader) {
+ return "instance." + reader.getMethod().getName() + "();";
+ }
+
+ private String toGetter(final AccessMode.Reader reader) {
+ if (FieldAndMethodAccessMode.CompositeReader.class.isInstance(reader)) {
+ final MethodAccessMode.MethodReader mr = MethodAccessMode.MethodReader.class.cast(
+ FieldAndMethodAccessMode.CompositeReader.class.cast(reader).getType2());
+ return toGetter(mr);
+ } else if (MethodAccessMode.MethodReader.class.isInstance(reader)) {
+ final MethodAccessMode.MethodReader mr = MethodAccessMode.MethodReader.class.cast(reader);
+ return toGetter(mr);
+ }
+ throw new IllegalArgumentException("Unsupported reader: " + reader);
+ }
+
+
+ private String toSetter(final MethodAccessMode.MethodWriter reader, final String name) {
+ return "" +
+ " {\n" +
+ " final JsonValue value = instance.get(\""+name+"\");\n" +
+ " if (value != null) {\n" +
+ " final Object coerced = coerce(value);\n" +
+ " instance." + reader.getMethod().getName() + "(coerced);\n" +
+ " }\n" +
+ " }" +
+ "";
+ }
+
+ private String toSetter(final AccessMode.Writer writer, final String setter) {
+ if (FieldAndMethodAccessMode.CompositeWriter.class.isInstance(writer)) {
+ final MethodAccessMode.MethodWriter mr = MethodAccessMode.MethodWriter.class.cast(
+ FieldAndMethodAccessMode.CompositeWriter.class.cast(writer).getType1());
+ return toSetter(mr, setter);
+ } else if (MethodAccessMode.MethodWriter.class.isInstance(writer)) {
+ final MethodAccessMode.MethodWriter mr = MethodAccessMode.MethodWriter.class.cast(writer);
+ return toSetter(mr, setter);
+ }
+ throw new IllegalArgumentException("Unsupported writer: " + writer);
+ }
+
+ private String toSetter(final Mappings.Setter value, final String name) {
+ try {
+ final Field writer = value.getClass().getDeclaredField("writer");
+ if (!writer.isAccessible()) {
+ writer.setAccessible(true);
+ }
+ final Object wrapped = writer.get(value);
+ final Field finalWriter = Stream.of(wrapped.getClass().getDeclaredFields())
+ .filter(it -> it.getName().contains("initialWriter") && AccessMode.Writer.class == it.getType())
+ .peek(it -> {
+ if (!it.isAccessible()) {
+ it.setAccessible(true);
+ }
+ })
+ .findFirst()
+ .orElseThrow(() -> new IllegalStateException("No initialWriter field in " + wrapped));
+ return toSetter(AccessMode.Writer.class.cast(finalWriter.get(wrapped)), name);
+ } catch (final IllegalAccessException | NoSuchFieldException nsfe) {
+ throw new IllegalArgumentException("Unsupported getter: " + value, nsfe);
+ }
+ }
+
+ protected void info(final Supplier<String> message) {
+ logger().info(message);
+ }
+
+ protected void error(final Supplier<String> message, final Throwable throwable) {
+ logger().log(SEVERE, throwable, message);
+ }
+
+ private Logger logger() {
+ return Logger.getLogger(getClass().getName());
+ }
+
+ public static class Configuration {
+ private Boolean preferJakarta;
+ private String header;
+ private Collection<Class<?>> classes;
+ private Path output;
+ private JsonbConfig config;
+
+ public Configuration setUseJakarta(final Boolean preferJakarta) {
+ this.preferJakarta = preferJakarta;
+ return this;
+ }
+
+ public Configuration setHeader(final String header) {
+ this.header = header;
+ return this;
+ }
+
+ public Configuration setConfig(final JsonbConfig config) {
+ this.config = config;
+ return this;
+ }
+
+ public Configuration setClasses(final Collection<Class<?>> classes) {
+ this.classes = classes;
+ return this;
+ }
+
+ public Configuration setOutput(final Path output) {
+ this.output = output;
+ return this;
+ }
+ }
+}
diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/generator/GeneratedJsonbTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/generator/GeneratedJsonbTest.java
new file mode 100644
index 0000000..05a23b7
--- /dev/null
+++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/generator/GeneratedJsonbTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.jsonb.generator;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Collections.singleton;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class GeneratedJsonbTest {
+ @Rule
+ public final TemporaryFolder temp = new TemporaryFolder();
+
+ @Test
+ public void empty() throws IOException {
+ final Path output = temp.getRoot().toPath();
+ new JsonbMapperGenerator(new JsonbMapperGenerator.Configuration()
+ .setClasses(singleton(Empty.class))
+ .setOutput(output))
+ .run();
+ final Path result = output.resolve("org/apache/johnzon/jsonb/generator/GeneratedJsonbTest$Empty$$JohnzonJsonb.class");
+ assertTrue(Files.exists(result));
+ assertEquals("" +
+ "" +
+ "", new String(Files.readAllBytes(result), UTF_8));
+ }
+
+ @Test
+ public void simplePOJO() throws IOException {
+ final Path output = temp.getRoot().toPath();
+ new JsonbMapperGenerator(new JsonbMapperGenerator.Configuration()
+ .setClasses(singleton(Simple.class))
+ .setOutput(output))
+ .run();
+ final Path result = output.resolve("org/apache/johnzon/jsonb/generator/GeneratedJsonbTest$Simple$$JohnzonJsonb.class");
+ assertTrue(Files.exists(result));
+ assertEquals("" +
+ "" +
+ "", new String(Files.readAllBytes(result), UTF_8));
+ }
+
+ public static class Empty {
+ }
+
+ public static class Simple {
+ private String name;
+ private int age;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(final String name) {
+ this.name = name;
+ }
+
+ public int getAge() {
+ return age;
+ }
+
+ public void setAge(final int age) {
+ this.age = age;
+ }
+ }
+}
diff --git a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mapper.java b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mapper.java
index 022eaee..678ea54 100644
--- a/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mapper.java
+++ b/johnzon-mapper/src/main/java/org/apache/johnzon/mapper/Mapper.java
@@ -79,6 +79,22 @@ public class Mapper implements Closeable {
this.charset = config.getEncoding();
}
+ public Mappings getMappings() {
+ return mappings;
+ }
+
+ public JsonReaderFactory getReaderFactory() {
+ return readerFactory;
+ }
+
+ public JsonGeneratorFactory getGeneratorFactory() {
+ return generatorFactory;
+ }
+
+ public Charset getCharset() {
+ return charset;
+ }
+
public <T> void writeArray(final Object object, final OutputStream stream) {
if (object instanceof short[]) {
writeObject(ArrayUtil.asList((short[]) object), stream);