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 <rmannibu...@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);