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:03 UTC

[johnzon] branch generated-bindings created (now 30ccfb9)

This is an automated email from the ASF dual-hosted git repository.

rmannibucau pushed a change to branch generated-bindings
in repository https://gitbox.apache.org/repos/asf/johnzon.git.


      at 30ccfb9  start a binding generator from our classmapping

This branch includes the following new commits:

     new 30ccfb9  start a binding generator from our classmapping

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


[johnzon] 01/01: start a binding generator from our classmapping

Posted by rm...@apache.org.
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);