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);