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 f6c4c3c  [JOHNZON-330] add jsonschema generator
f6c4c3c is described below

commit f6c4c3c3f49646fcd809c60e652dd4746d934d0d
Author: Romain Manni-Bucau <[email protected]>
AuthorDate: Thu Dec 17 11:30:18 2020 +0100

    [JOHNZON-330] add jsonschema generator
---
 johnzon-jsonschema/pom.xml                         |   7 +
 .../johnzon/jsonschema/generator/Schema.java       | 448 +++++++++++++
 .../jsonschema/generator/SchemaProcessor.java      | 723 +++++++++++++++++++++
 johnzon-maven-plugin/pom.xml                       |  20 +-
 .../johnzon/maven/plugin/ExampleToModelMojo.java   |  24 +-
 .../johnzon/maven/plugin/PojoToJsonSchemaMojo.java | 125 ++++
 .../maven/plugin/PojoToJsonSchemaMojoTest.java     |  95 +++
 7 files changed, 1430 insertions(+), 12 deletions(-)

diff --git a/johnzon-jsonschema/pom.xml b/johnzon-jsonschema/pom.xml
index 46a5797..94e8f4f 100644
--- a/johnzon-jsonschema/pom.xml
+++ b/johnzon-jsonschema/pom.xml
@@ -36,6 +36,13 @@
       <scope>provided</scope>
       <optional>true</optional>
     </dependency>
+    <dependency>
+      <groupId>org.apache.geronimo.specs</groupId>
+      <artifactId>geronimo-jsonb_1.0_spec</artifactId>
+      <version>${geronimo-jsonb.version}</version>
+      <scope>provided</scope>
+      <optional>true</optional>
+    </dependency>
 
     <dependency>
       <groupId>org.apache.johnzon</groupId>
diff --git 
a/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/generator/Schema.java
 
b/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/generator/Schema.java
new file mode 100644
index 0000000..b05b879
--- /dev/null
+++ 
b/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/generator/Schema.java
@@ -0,0 +1,448 @@
+/*
+ * 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.jsonschema.generator;
+
+import javax.json.bind.adapter.JsonbAdapter;
+import javax.json.bind.annotation.JsonbProperty;
+import javax.json.bind.annotation.JsonbPropertyOrder;
+import javax.json.bind.annotation.JsonbTypeAdapter;
+import java.util.List;
+import java.util.Map;
+
+@JsonbPropertyOrder({
+        "$id",
+        "$ref",
+        "type",
+        "title",
+        "description",
+        "required",
+        "deprecated",
+        "$schema",
+        "additionalProperties",
+        "allOf",
+        "anyOf",
+        "default",
+        "definitions",
+        "enum",
+        "example",
+        "exclusiveMaximum",
+        "exclusiveMinimum",
+        "format",
+        "items",
+        "maximum",
+        "maxItems",
+        "maxLength",
+        "maxProperties",
+        "minimum",
+        "minItems",
+        "minLength",
+        "minProperties",
+        "multipleOf",
+        "not",
+        "nullable",
+        "oneOf",
+        "pattern",
+        "properties",
+        "readOnly",
+        "uniqueItems",
+        "writeOnly"
+})
+public class Schema {
+    private Map<String, Schema> definitions;
+
+    @JsonbTypeAdapter(SchemaTypeAdapter.class)
+    private SchemaType type;
+
+    private Map<String, Schema> properties;
+
+    private Object additionalProperties;
+
+    private List<Schema> allOf;
+
+    private List<Schema> anyOf;
+
+    @JsonbProperty("default")
+    private Object defaultValue;
+
+    private Boolean deprecated;
+
+    private String description;
+
+    @JsonbProperty("enum")
+    private List<Object> enumeration;
+
+    private Object example;
+
+    private Boolean exclusiveMaximum;
+
+    private Boolean exclusiveMinimum;
+
+    private String format;
+
+    private Schema items;
+
+    private Integer maxItems;
+
+    private Integer maxLength;
+
+    private Integer maxProperties;
+
+    private Integer minItems;
+
+    private Integer minLength;
+
+    private Integer minProperties;
+
+    private Double maximum;
+
+    private Double minimum;
+
+    private Double multipleOf;
+
+    private Schema not;
+
+    private Boolean nullable;
+
+    private List<Schema> oneOf;
+
+    private String pattern;
+
+    private Boolean readOnly;
+
+    @JsonbProperty("$ref")
+    private String ref;
+
+    @JsonbProperty("$id")
+    private String id;
+
+    @JsonbProperty("$schema")
+    private String schema;
+
+    private List<String> required;
+
+    private String title;
+
+    private Boolean uniqueItems;
+
+    private Boolean writeOnly;
+
+    public Map<String, Schema> getDefinitions() {
+        return definitions;
+    }
+
+    public void setDefinitions(final Map<String, Schema> definitions) {
+        this.definitions = definitions;
+    }
+
+    public SchemaType getType() {
+        return type;
+    }
+
+    public void setType(final SchemaType type) {
+        this.type = type;
+    }
+
+    public Map<String, Schema> getProperties() {
+        return properties;
+    }
+
+    public void setProperties(final Map<String, Schema> properties) {
+        this.properties = properties;
+    }
+
+    public Object getAdditionalProperties() {
+        return additionalProperties;
+    }
+
+    public void setAdditionalProperties(final Object additionalProperties) {
+        this.additionalProperties = additionalProperties;
+    }
+
+    public List<Schema> getAllOf() {
+        return allOf;
+    }
+
+    public void setAllOf(final List<Schema> allOf) {
+        this.allOf = allOf;
+    }
+
+    public List<Schema> getAnyOf() {
+        return anyOf;
+    }
+
+    public void setAnyOf(final List<Schema> anyOf) {
+        this.anyOf = anyOf;
+    }
+
+    public Object getDefaultValue() {
+        return defaultValue;
+    }
+
+    public void setDefaultValue(final Object defaultValue) {
+        this.defaultValue = defaultValue;
+    }
+
+    public Boolean getDeprecated() {
+        return deprecated;
+    }
+
+    public void setDeprecated(final Boolean deprecated) {
+        this.deprecated = deprecated;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(final String description) {
+        this.description = description;
+    }
+
+    public List<Object> getEnumeration() {
+        return enumeration;
+    }
+
+    public void setEnumeration(final List<Object> enumeration) {
+        this.enumeration = enumeration;
+    }
+
+    public Object getExample() {
+        return example;
+    }
+
+    public void setExample(final Object example) {
+        this.example = example;
+    }
+
+    public Boolean getExclusiveMaximum() {
+        return exclusiveMaximum;
+    }
+
+    public void setExclusiveMaximum(final Boolean exclusiveMaximum) {
+        this.exclusiveMaximum = exclusiveMaximum;
+    }
+
+    public Boolean getExclusiveMinimum() {
+        return exclusiveMinimum;
+    }
+
+    public void setExclusiveMinimum(final Boolean exclusiveMinimum) {
+        this.exclusiveMinimum = exclusiveMinimum;
+    }
+
+    public String getFormat() {
+        return format;
+    }
+
+    public void setFormat(final String format) {
+        this.format = format;
+    }
+
+    public Schema getItems() {
+        return items;
+    }
+
+    public void setItems(final Schema items) {
+        this.items = items;
+    }
+
+    public Integer getMaxItems() {
+        return maxItems;
+    }
+
+    public void setMaxItems(final Integer maxItems) {
+        this.maxItems = maxItems;
+    }
+
+    public Integer getMaxLength() {
+        return maxLength;
+    }
+
+    public void setMaxLength(final Integer maxLength) {
+        this.maxLength = maxLength;
+    }
+
+    public Integer getMaxProperties() {
+        return maxProperties;
+    }
+
+    public void setMaxProperties(final Integer maxProperties) {
+        this.maxProperties = maxProperties;
+    }
+
+    public Integer getMinItems() {
+        return minItems;
+    }
+
+    public void setMinItems(final Integer minItems) {
+        this.minItems = minItems;
+    }
+
+    public Integer getMinLength() {
+        return minLength;
+    }
+
+    public void setMinLength(final Integer minLength) {
+        this.minLength = minLength;
+    }
+
+    public Integer getMinProperties() {
+        return minProperties;
+    }
+
+    public void setMinProperties(final Integer minProperties) {
+        this.minProperties = minProperties;
+    }
+
+    public Double getMaximum() {
+        return maximum;
+    }
+
+    public void setMaximum(final Double maximum) {
+        this.maximum = maximum;
+    }
+
+    public Double getMinimum() {
+        return minimum;
+    }
+
+    public void setMinimum(final Double minimum) {
+        this.minimum = minimum;
+    }
+
+    public Double getMultipleOf() {
+        return multipleOf;
+    }
+
+    public void setMultipleOf(final Double multipleOf) {
+        this.multipleOf = multipleOf;
+    }
+
+    public Schema getNot() {
+        return not;
+    }
+
+    public void setNot(final Schema not) {
+        this.not = not;
+    }
+
+    public Boolean getNullable() {
+        return nullable;
+    }
+
+    public void setNullable(final Boolean nullable) {
+        this.nullable = nullable;
+    }
+
+    public List<Schema> getOneOf() {
+        return oneOf;
+    }
+
+    public void setOneOf(final List<Schema> oneOf) {
+        this.oneOf = oneOf;
+    }
+
+    public String getPattern() {
+        return pattern;
+    }
+
+    public void setPattern(final String pattern) {
+        this.pattern = pattern;
+    }
+
+    public Boolean getReadOnly() {
+        return readOnly;
+    }
+
+    public void setReadOnly(final Boolean readOnly) {
+        this.readOnly = readOnly;
+    }
+
+    public String getRef() {
+        return ref;
+    }
+
+    public void setRef(final String ref) {
+        this.ref = ref;
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(final String id) {
+        this.id = id;
+    }
+
+    public String getSchema() {
+        return schema;
+    }
+
+    public void setSchema(final String schema) {
+        this.schema = schema;
+    }
+
+    public List<String> getRequired() {
+        return required;
+    }
+
+    public void setRequired(final List<String> required) {
+        this.required = required;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(final String title) {
+        this.title = title;
+    }
+
+    public Boolean getUniqueItems() {
+        return uniqueItems;
+    }
+
+    public void setUniqueItems(final Boolean uniqueItems) {
+        this.uniqueItems = uniqueItems;
+    }
+
+    public Boolean getWriteOnly() {
+        return writeOnly;
+    }
+
+    public void setWriteOnly(final Boolean writeOnly) {
+        this.writeOnly = writeOnly;
+    }
+
+    public enum SchemaType {
+        integer, number, string, object, array, bool
+    }
+
+    public static class SchemaTypeAdapter implements JsonbAdapter<SchemaType, 
String> {
+        @Override
+        public String adaptToJson(final SchemaType obj) {
+            return obj == null ? null : obj == SchemaType.bool ? "boolean" : 
obj.name();
+        }
+
+        @Override
+        public SchemaType adaptFromJson(final String obj) {
+            return obj == null ? null : "boolean".equals(obj) ? 
SchemaType.bool : SchemaType.valueOf(obj);
+        }
+    }
+}
diff --git 
a/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/generator/SchemaProcessor.java
 
b/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/generator/SchemaProcessor.java
new file mode 100644
index 0000000..5062567
--- /dev/null
+++ 
b/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/generator/SchemaProcessor.java
@@ -0,0 +1,723 @@
+/*
+ * 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.jsonschema.generator;
+
+import javax.json.JsonArray;
+import javax.json.JsonNumber;
+import javax.json.JsonObject;
+import javax.json.JsonString;
+import javax.json.JsonStructure;
+import javax.json.JsonValue;
+import javax.json.bind.annotation.JsonbProperty;
+import javax.json.bind.annotation.JsonbTransient;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.lang.reflect.WildcardType;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.function.BiPredicate;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.singleton;
+import static java.util.Collections.singletonList;
+import static java.util.Optional.empty;
+import static java.util.Optional.of;
+import static java.util.Optional.ofNullable;
+import static java.util.stream.Collectors.toMap;
+
+// simplified from geronimo-openapi
+// todo: introduce a @Schema annotation
+public class SchemaProcessor {
+    private final Class<?> persistenceCapable;
+    private final boolean setClassAsTitle;
+    private final boolean useReflectionForDefaults;
+
+    public SchemaProcessor() {
+        this(false, false);
+    }
+
+    public SchemaProcessor(final boolean setClassAsTitle, final boolean 
useReflectionForDefaults) {
+        this.setClassAsTitle = setClassAsTitle;
+        this.useReflectionForDefaults = useReflectionForDefaults;
+
+        Class<?> pc = null;
+        try {
+            pc = Thread.currentThread().getContextClassLoader()
+                    
.loadClass("org.apache.openjpa.enhance.PersistenceCapable");
+        } catch (final NoClassDefFoundError | ClassNotFoundException e) {
+            // no-op
+        }
+        persistenceCapable = pc;
+    }
+
+    public Schema mapSchemaFromClass(final Type model) {
+        return mapSchemaFromClass(model, new InMemoryCache());
+    }
+
+    public Schema mapSchemaFromClass(final Type model, final Cache cache) {
+        final ReflectionValueExtractor reflectionValueExtractor = 
useReflectionForDefaults ? new ReflectionValueExtractor() : null;
+        return doMapSchemaFromClass(model, cache, reflectionValueExtractor, 
useReflectionForDefaults ? reflectionValueExtractor.createInstance(model) : 
null);
+    }
+
+    private Schema doMapSchemaFromClass(final Type model, final Cache cache,
+                                        final ReflectionValueExtractor 
reflectionValueExtractor,
+                                        final Instance instance) {
+        final Schema schema = new Schema();
+        fillSchema(model, schema, cache, reflectionValueExtractor, instance);
+        return schema;
+    }
+
+    public void fillSchema(final Type rawModel, final Schema schema, final 
Cache cache,
+                           final ReflectionValueExtractor 
reflectionValueExtractor,
+                           final Instance instance) {
+        final Type model = unwrapType(rawModel);
+        if (Class.class.isInstance(model)) {
+            if (boolean.class == model) {
+                schema.setType(Schema.SchemaType.bool);
+            } else if (Boolean.class == model) {
+                schema.setType(Schema.SchemaType.bool);
+                schema.setNullable(true);
+            } else if (String.class == model || JsonString.class == model) {
+                schema.setType(Schema.SchemaType.string);
+            } else if (double.class == model || float.class == model) {
+                schema.setType(Schema.SchemaType.number);
+            } else if (Double.class == model || Float.class == model || 
JsonNumber.class == model) {
+                schema.setType(Schema.SchemaType.number);
+                schema.setNullable(true);
+            } else if (int.class == model || short.class == model || 
byte.class == model || long.class == model) {
+                schema.setType(Schema.SchemaType.integer);
+            } else if (Integer.class == model || Short.class == model || 
Byte.class == model || Long.class == model) {
+                schema.setType(Schema.SchemaType.integer);
+                schema.setNullable(true);
+            } else if (JsonObject.class == model || JsonValue.class == model 
|| JsonStructure.class == model) {
+                schema.setType(Schema.SchemaType.object);
+                schema.setNullable(true);
+                schema.setProperties(new TreeMap<>());
+            } else if (JsonArray.class == model) {
+                schema.setType(Schema.SchemaType.array);
+                schema.setNullable(true);
+                final Schema items = new Schema();
+                items.setType(Schema.SchemaType.object);
+                items.setProperties(new TreeMap<>());
+            } else if (isStringable(model)) {
+                schema.setType(Schema.SchemaType.string);
+                schema.setNullable(true);
+            } else {
+                final Class<?> from = Class.class.cast(model);
+                if (from.isEnum()) {
+                    schema.setId(from.getName().replace('.', '_').replace('$', 
'_'));
+                    schema.setType(Schema.SchemaType.string);
+                    schema.setEnumeration(asList(from.getEnumConstants()));
+                    schema.setNullable(true);
+                } else if (from.isArray()) {
+                    schema.setType(Schema.SchemaType.array);
+                    final Schema items = new Schema();
+                    fillSchema(from.getComponentType(), items, cache, 
reflectionValueExtractor, instance);
+                    schema.setItems(items);
+                } else if (Collection.class.isAssignableFrom(from)) {
+                    schema.setType(Schema.SchemaType.array);
+                    final Schema items = new Schema();
+                    fillSchema(Object.class, items, cache, 
reflectionValueExtractor, instance);
+                    schema.setItems(items);
+                } else {
+                    schema.setType(Schema.SchemaType.object);
+                    getOrCreateReusableObjectComponent(from, schema, cache, 
reflectionValueExtractor, instance);
+                }
+            }
+        } else {
+            if (ParameterizedType.class.isInstance(model)) {
+                final ParameterizedType pt = 
ParameterizedType.class.cast(model);
+                if (Class.class.isInstance(pt.getRawType()) && 
Map.class.isAssignableFrom(Class.class.cast(pt.getRawType()))) {
+                    schema.setType(Schema.SchemaType.object);
+                    getOrCreateReusableObjectComponent(Object.class, schema, 
cache, reflectionValueExtractor, instance);
+                } else if (pt.getActualTypeArguments().length == 1 && 
Class.class.isInstance(pt.getActualTypeArguments()[0])) {
+                    schema.setType(Schema.SchemaType.array);
+                    final Schema items = new Schema();
+                    final Class<?> type = 
Class.class.cast(pt.getActualTypeArguments()[0]);
+                    final Instance item;
+                    if (instance != null && 
Collection.class.isInstance(instance.value) && 
!Collection.class.cast(instance.value).isEmpty()) {
+                        item = new 
Instance(Collection.class.cast(instance.value).iterator().next(), 
instance.isCreated());
+                    } else {
+                        item = null;
+                    }
+                    fillSchema(type, items, cache, reflectionValueExtractor, 
item);
+                    schema.setItems(items);
+                } else {
+                    schema.setType(Schema.SchemaType.array);
+                }
+            } else if (TypeVariable.class.isInstance(model)) {
+                schema.setType(Schema.SchemaType.object);
+            } else { // todo?
+                schema.setType(Schema.SchemaType.array);
+                schema.setItems(new Schema());
+            }
+        }
+    }
+
+    private void getOrCreateReusableObjectComponent(final Class<?> from, final 
Schema schema,
+                                                    final Cache cache,
+                                                    final 
ReflectionValueExtractor reflectionValueExtractor,
+                                                    final Instance instance) {
+        schema.setType(Schema.SchemaType.object);
+        final String ref = cache.findRef(from);
+        if (ref != null) {
+            schema.setRef(ref);
+            cache.initDefinitions(from);
+            return;
+        } else if (Object.class == from) {
+            schema.setProperties(new TreeMap<>());
+            return;
+        }
+        if (setClassAsTitle) {
+            schema.setTitle(from.getName());
+        }
+
+        final BiPredicate<Type, String> ignored = createIgnorePredicate(from);
+
+        cache.onClass(from);
+        schema.setProperties(new TreeMap<>());
+        Class<?> current = from;
+        while (current != null && current != Object.class) {
+            final Map<String, String> fields = 
Stream.of(current.getDeclaredFields())
+                    .filter(it -> isVisible(it, it.getModifiers()))
+                    .peek(f -> {
+                        if (Modifier.isFinal(f.getModifiers())) {
+                            handleRequired(schema, () -> findFieldName(f));
+                        }
+                    })
+                    .peek(f -> {
+                        final String fieldName = findFieldName(f);
+                        if (!ignored.test(f.getGenericType(), fieldName)) {
+                            final Instance fieldInstance;
+                            if (reflectionValueExtractor != null) {
+                                fieldInstance = 
reflectionValueExtractor.createDemoInstance(
+                                        instance == null ? null : 
instance.value, f);
+                            } else {
+                                fieldInstance = null;
+                            }
+                            final Schema value = 
doMapSchemaFromClass(resolveType(f.getGenericType(), from), cache, 
reflectionValueExtractor, fieldInstance);
+                            fillMeta(f, value);
+                            if (fieldInstance != null && 
!fieldInstance.isCreated()) {
+                                switch (value.getType()) {
+                                    case array:
+                                    case object:
+                                        break;
+                                    default:
+                                        
value.setDefaultValue(fieldInstance.value);
+                                }
+                            }
+                            schema.getProperties().put(fieldName, value);
+                        } else {
+                            onIgnored(schema, f, cache);
+                        }
+                    }).collect(toMap(Field::getName, this::findFieldName));
+            Stream.of(current.getDeclaredMethods())
+                    .filter(it -> isVisible(it, it.getModifiers()))
+                    .filter(it -> (it.getName().startsWith("get") || 
it.getName().startsWith("is")) && it.getName().length() > 2)
+                    .forEach(m -> {
+                        final String methodName = findMethodName(m);
+                        final String key = fields.getOrDefault(methodName, 
methodName); // ensure we respect jsonbproperty on fields
+                        if 
(!ignored.test(resolveType(m.getGenericReturnType(), from), key) && 
!schema.getProperties().containsKey(key)) {
+                            schema.getProperties().put(key, 
doMapSchemaFromClass(m.getGenericReturnType(), cache, null, null));
+                        }
+                    });
+            current = current.getSuperclass();
+        }
+        cache.onSchemaCreated(from, schema);
+    }
+
+    protected void fillMeta(final Field f, final Schema schema) {
+        findDocAnnotation(f).ifPresent(doc -> {
+            find("title", doc).ifPresent(schema::setTitle);
+            if (schema.getTitle() == null) {
+                find("value", doc).ifPresent(schema::setTitle);
+            }
+            find("description", doc).ifPresent(schema::setDescription);
+        });
+        ofNullable(f.getAnnotation(Deprecated.class)).map(it -> 
true).ifPresent(schema::setDeprecated);
+    }
+
+    protected Optional<Annotation> findDocAnnotation(final Field f) {
+        return Stream.of(f.getAnnotations())
+                .filter(it -> 
it.annotationType().getSimpleName().startsWith("Doc") || 
it.annotationType().getSimpleName().startsWith("Desc"))
+                .min(Comparator.comparing(a -> a.annotationType().getName()));
+    }
+
+    private Optional<String> find(final String method, final Annotation doc) {
+        try {
+            final String value = 
String.valueOf(doc.annotationType().getMethod(method).invoke(doc));
+            return value.isEmpty() ? empty() : of(value);
+        } catch (final Exception ex) {
+            return empty();
+        }
+    }
+
+    protected void onIgnored(final Schema schema, final Field f, final Cache 
cache) {
+        // no-op
+    }
+
+    protected BiPredicate<Type, String> createIgnorePredicate(final Class<?> 
from) {
+        return persistenceCapable != null && 
persistenceCapable.isAssignableFrom(from) ?
+                (t, v) -> v.startsWith("pc") : (t, v) -> false;
+    }
+
+    private boolean isVisible(final AnnotatedElement elt, final int modifiers) 
{
+        return !Modifier.isStatic(modifiers) && 
!elt.isAnnotationPresent(JsonbTransient.class);
+    }
+
+    private Type unwrapType(final Type rawModel) {
+        if (ParameterizedType.class.isInstance(rawModel)) {
+            final ParameterizedType parameterizedType = 
ParameterizedType.class.cast(rawModel);
+            if 
(Stream.of(parameterizedType.getActualTypeArguments()).allMatch(WildcardType.class::isInstance))
 {
+                return parameterizedType.getRawType();
+            }
+            if (Class.class.isInstance(parameterizedType.getRawType()) &&
+                    
CompletionStage.class.isAssignableFrom(Class.class.cast(parameterizedType.getRawType())))
 {
+                return parameterizedType.getActualTypeArguments()[0];
+            }
+        }
+        return rawModel;
+    }
+
+    private boolean isStringable(final Type model) {
+        return Date.class == model ||
+                model.getTypeName().startsWith("java.time.") ||
+                Class.class == model ||
+                Type.class == model ||
+                BigInteger.class == model ||
+                BigDecimal.class == model;
+    }
+
+    private void handleRequired(final Schema schema, final Supplier<String> 
nameSupplier) {
+        if (schema.getRequired() == null) {
+            schema.setRequired(new ArrayList<>());
+        }
+        final String name = nameSupplier.get();
+        if (!schema.getRequired().contains(name)) {
+            schema.getRequired().add(name);
+        }
+    }
+
+    private String findFieldName(final Field f) {
+        if (f.isAnnotationPresent(JsonbProperty.class)) {
+            return f.getAnnotation(JsonbProperty.class).value();
+        }
+        // getter
+        final String fName = f.getName();
+        final String subName = Character.toUpperCase(fName.charAt(0))
+                + (fName.length() > 1 ? fName.substring(1) : "");
+        try {
+            final Method getter = f.getDeclaringClass().getMethod("get" + 
subName);
+            if (getter.isAnnotationPresent(JsonbProperty.class)) {
+                return getter.getAnnotation(JsonbProperty.class).value();
+            }
+        } catch (final NoSuchMethodException e) {
+            if (boolean.class == f.getType()) {
+                try {
+                    final Method isser = f.getDeclaringClass().getMethod("is" 
+ subName);
+                    if (isser.isAnnotationPresent(JsonbProperty.class)) {
+                        return 
isser.getAnnotation(JsonbProperty.class).value();
+                    }
+                } catch (final NoSuchMethodException e2) {
+                    // no-op
+                }
+            }
+        }
+        return fName;
+    }
+
+    private String findMethodName(final Method m) {
+        if (m.isAnnotationPresent(JsonbProperty.class)) {
+            return m.getAnnotation(JsonbProperty.class).value();
+        }
+        final String name = m.getName();
+        if (name.startsWith("get")) {
+            return decapitalize(name.substring("get".length()));
+        }
+        if (name.startsWith("is")) {
+            try {
+                m.getDeclaringClass().getDeclaredField(name);
+                return name;
+            } catch (final NoSuchFieldException nsme) {
+                return decapitalize(name.substring("is".length()));
+            }
+        }
+        return decapitalize(name);
+    }
+
+    private String decapitalize(final String name) {
+        return Character.toLowerCase(name.charAt(0)) + name.substring(1);
+    }
+
+    // not a full and complete impl but what we use
+    private Type resolveType(final Type type, final Class<?> declaringClass) {
+        final Type realType = extractRealType(declaringClass, type);
+        if (ParameterizedType.class.isInstance(type) && (realType != type ||
+                
Stream.of(ParameterizedType.class.cast(type).getActualTypeArguments()).anyMatch(TypeVariable.class::isInstance)))
 {
+            return resolveParameterizedType(type, declaringClass);
+        }
+        if (TypeVariable.class.isInstance(type) && 
declaringClass.getSuperclass() != null) {
+            final TypeVariable tv = TypeVariable.class.cast(type);
+            final TypeVariable<? extends Class<?>>[] typeParameters = 
declaringClass.getSuperclass().getTypeParameters();
+            if (typeParameters != null && 
ParameterizedType.class.isInstance(declaringClass.getGenericSuperclass())) {
+                final ParameterizedType pt = 
ParameterizedType.class.cast(declaringClass.getGenericSuperclass());
+                if (typeParameters.length == 
pt.getActualTypeArguments().length) {
+                    for (int i = 0; i < typeParameters.length; i++) {
+                        if (tv == typeParameters[i]) {
+                            return pt.getActualTypeArguments()[i];
+                        }
+                    }
+                }
+            }
+        }
+        return type;
+    }
+
+    private Type resolveParameterizedType(final Type type, final Class<?> 
declaringClass) {
+        final ParameterizedType pt = ParameterizedType.class.cast(type);
+        final Type resolvedParam = resolveType(pt.getActualTypeArguments()[0], 
declaringClass);
+        if (pt.getActualTypeArguments()[0] != resolvedParam) {
+            return new ParameterizedTypeImpl(pt.getRawType(), new 
Type[]{resolvedParam});
+        }
+        return type;
+    }
+
+    private Map<Type, Type> toResolvedTypes(final Type clazz, final int maxIt) 
{
+        if (maxIt > 15) { // avoid loops
+            return emptyMap();
+        }
+        if (Class.class.isInstance(clazz)) {
+            return 
toResolvedTypes(Class.class.cast(clazz).getGenericSuperclass(), maxIt + 1);
+        }
+        if (ParameterizedType.class.isInstance(clazz)) {
+            final ParameterizedType parameterizedType = 
ParameterizedType.class.cast(clazz);
+            if (!Class.class.isInstance(parameterizedType.getRawType())) {
+                return emptyMap(); // not yet supported
+            }
+            final Class<?> raw = 
Class.class.cast(parameterizedType.getRawType());
+            final Type[] arguments = 
parameterizedType.getActualTypeArguments();
+            if (arguments.length > 0) {
+                final TypeVariable<? extends Class<?>>[] parameters = 
raw.getTypeParameters();
+                final Map<Type, Type> map = new HashMap<>(parameters.length);
+                for (int i = 0; i < parameters.length && i < arguments.length; 
i++) {
+                    map.put(parameters[i], arguments[i]);
+                }
+                return Stream.concat(map.entrySet().stream(), 
toResolvedTypes(raw, maxIt + 1).entrySet().stream())
+                        .collect(toMap(Map.Entry::getKey, Map.Entry::getValue, 
(a, b) -> a));
+            }
+        }
+        return emptyMap();
+    }
+
+    private Type extractRealType(final Class<?> root, final Type type) {
+        if (ParameterizedType.class.isInstance(type)) {
+            final ParameterizedType pt = ParameterizedType.class.cast(type);
+            return Stream.of(Optional.class, CompletionStage.class, 
CompletableFuture.class)
+                    .anyMatch(gt -> pt.getRawType() == gt) ?
+                    
(ParameterizedType.class.isInstance(pt.getActualTypeArguments()[0]) ?
+                            
resolveParameterizedType(pt.getActualTypeArguments()[0], root) : 
pt.getActualTypeArguments()[0]) :
+                    pt;
+        }
+        if (TypeVariable.class.isInstance(type)) {
+            final Map<Type, Type> resolution = toResolvedTypes(root, 0);
+            Type value = type;
+            int max = 15;
+            do {
+                value = resolution.get(value);
+                max--;
+            } while (max > 0 && value != null && 
resolution.containsKey(value));
+            return ofNullable(value).orElse(type);
+        }
+        return type;
+    }
+
+    private static class ParameterizedTypeImpl implements ParameterizedType {
+        private final Type rawType;
+        private final Type[] actualTypeArguments;
+
+        private ParameterizedTypeImpl(final Type rawType, final Type[] 
actualTypeArguments) {
+            this.rawType = rawType;
+            this.actualTypeArguments = actualTypeArguments;
+        }
+
+        @Override
+        public Type getRawType() {
+            return rawType;
+        }
+
+        @Override
+        public Type[] getActualTypeArguments() {
+            return actualTypeArguments;
+        }
+
+        @Override
+        public Type getOwnerType() {
+            return null;
+        }
+
+        @Override
+        public String toString() {
+            final StringBuilder buffer = new StringBuilder();
+            buffer.append(((Class<?>) rawType).getSimpleName());
+            final Type[] actualTypes = getActualTypeArguments();
+            if (actualTypes.length > 0) {
+                buffer.append("<");
+                int length = actualTypes.length;
+                for (int i = 0; i < length; i++) {
+                    buffer.append(actualTypes[i].toString());
+                    if (i != actualTypes.length - 1) {
+                        buffer.append(",");
+                    }
+                }
+
+                buffer.append(">");
+            }
+            return buffer.toString();
+        }
+    }
+
+    public interface Cache {
+        String findRef(Class<?> type);
+
+        void onClass(Class<?> type);
+
+        void onSchemaCreated(Class<?> type, Schema schema);
+
+        void initDefinitions(Class<?> from);
+    }
+
+    public static class InMemoryCache implements Cache {
+        private final Map<Class<?>, String> refs = new HashMap<>();
+
+        private final Map<Class<?>, Schema> schemas = new HashMap<>();
+        private final Map<String, Schema> definitions = new TreeMap<>();
+
+        public Map<Class<?>, Schema> getSchemas() {
+            return schemas;
+        }
+
+        public Map<String, Schema> getDefinitions() {
+            return definitions;
+        }
+
+        @Override
+        public String findRef(final Class<?> type) {
+            if (type != Object.class) {
+                return refs.get(type);
+            }
+            return null;
+        }
+
+        @Override
+        public void onClass(final Class<?> type) {
+            refs.putIfAbsent(type, sanitize(type));
+        }
+
+        @Override
+        public void onSchemaCreated(final Class<?> type, final Schema schema) {
+            if (schemas.putIfAbsent(type, schema) == null) {
+                if (schema.getId() == null) {
+                    final String ref = findRef(type);
+                    if (ref != null) {
+                        schema.setId(ref.substring(getRefPrefix().length()));
+                    }
+                }
+            } else if (schema.getRef() == null) {
+                final String ref = findRef(type);
+                if (ref != null) {
+                    schema.setRef(ref);
+                }
+            }
+        }
+
+        @Override
+        public void initDefinitions(final Class<?> from) { // we add it only 
if reuse since some editor don't accept that
+            if (from == Object.class) {
+                return;
+            }
+            ofNullable(schemas.get(from)).ifPresent(s -> 
definitions.put(findRef(from).substring(getRefPrefix().length()), s));
+        }
+
+        private String sanitize(final Class<?> type) {
+            return getRefPrefix() + type.getName().replace('$', 
'_').replace('.', '_');
+        }
+
+        protected String getRefPrefix() {
+            return "#/definitions/";
+        }
+    }
+
+    public static class ReflectionValueExtractor {
+        private Instance createDemoInstance(final Object rootInstance, final 
Field field) {
+            if (rootInstance != null && field != null) {
+                try {
+                    if (!field.isAccessible()) {
+                        field.setAccessible(true);
+                    }
+                    final Object value = field.get(rootInstance);
+                    if (value != null) {
+                        return new Instance(value, false);
+                    }
+                } catch (final IllegalAccessException e) {
+                    throw new IllegalStateException(e);
+                }
+            }
+
+            final Type javaType = field.getGenericType();
+            if (Class.class.isInstance(javaType)) {
+                return new Instance(tryCreatingObjectInstance(javaType), true);
+            } else if (ParameterizedType.class.isInstance(javaType)) {
+                final ParameterizedType pt = 
ParameterizedType.class.cast(javaType);
+                final Type rawType = pt.getRawType();
+                if (Class.class.isInstance(rawType) && 
Collection.class.isAssignableFrom(Class.class.cast(rawType))
+                        && pt.getActualTypeArguments().length == 1
+                        && 
Class.class.isInstance(pt.getActualTypeArguments()[0])) {
+                    final Object instance = 
tryCreatingObjectInstance(pt.getActualTypeArguments()[0]);
+                    final Class<?> collectionType = Class.class.cast(rawType);
+                    if (Set.class == collectionType) {
+                        return new Instance(singleton(instance), true);
+                    }
+                    if (SortedSet.class == collectionType) {
+                        return new Instance(new 
TreeSet<>(singletonList(instance)), true);
+                    }
+                    if (List.class == collectionType || Collection.class == 
collectionType) {
+                        return new Instance(singletonList(instance), true);
+                    }
+                    // todo?
+                    return null;
+                }
+            }
+            return null;
+        }
+
+        private Object tryCreatingObjectInstance(final Type javaType) {
+            final Class<?> type = Class.class.cast(javaType);
+            if (type.isPrimitive()) {
+                if (int.class == type) {
+                    return 0;
+                }
+                if (long.class == type) {
+                    return 0L;
+                }
+                if (double.class == type) {
+                    return 0.;
+                }
+                if (float.class == type) {
+                    return 0f;
+                }
+                if (short.class == type) {
+                    return (short) 0;
+                }
+                if (byte.class == type) {
+                    return (byte) 0;
+                }
+                if (boolean.class == type) {
+                    return false;
+                }
+                throw new IllegalArgumentException("Not a primitive: " + type);
+            }
+            if (Integer.class == type) {
+                return 0;
+            }
+            if (Long.class == type) {
+                return 0L;
+            }
+            if (Double.class == type) {
+                return 0.;
+            }
+            if (Float.class == type) {
+                return 0f;
+            }
+            if (Short.class == type) {
+                return (short) 0;
+            }
+            if (Byte.class == type) {
+                return (byte) 0;
+            }
+            if (Boolean.class == type) {
+                return false;
+            }
+            if (type.getName().startsWith("java.") || 
type.getName().startsWith("javax.")) {
+                return null;
+            }
+            try {
+                return type.getConstructor().newInstance();
+            } catch (final NoSuchMethodException | InstantiationException | 
IllegalAccessException
+                    | InvocationTargetException e) {
+                // no-op, ignore defaults there
+            }
+            return null;
+        }
+
+        private Instance createInstance(final Type model) {
+            if (Class.class.isInstance(model)) {
+                try {
+                    return new 
Instance(Class.class.cast(model).getConstructor().newInstance(), true);
+                } catch (final NoSuchMethodException | InstantiationException 
| IllegalAccessException
+                        | InvocationTargetException e) {
+                    // no-op, ignore defaults there
+                }
+            }
+            return null;
+        }
+    }
+
+    public static class Instance {
+        private final Object value;
+        private final boolean created;
+
+        public Instance(final Object value, final boolean created) {
+            this.value = value;
+            this.created = created;
+        }
+
+        public Object getValue() {
+            return value;
+        }
+
+        public boolean isCreated() {
+            return created;
+        }
+    }
+}
+
diff --git a/johnzon-maven-plugin/pom.xml b/johnzon-maven-plugin/pom.xml
index 4f13ee4..6373765 100644
--- a/johnzon-maven-plugin/pom.xml
+++ b/johnzon-maven-plugin/pom.xml
@@ -43,15 +43,25 @@
       <scope>compile</scope>
     </dependency>
     <dependency>
-      <groupId>org.apache.johnzon</groupId>
+      <groupId>org.apache.geronimo.specs</groupId>
+      <artifactId>geronimo-jsonb_1.0_spec</artifactId>
+      <version>${geronimo-jsonb.version}</version>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
       <artifactId>johnzon-mapper</artifactId>
       <version>${project.version}</version>
     </dependency>
-
     <dependency>
-      <groupId>org.apache.commons</groupId>
-      <artifactId>commons-lang3</artifactId>
-      <version>3.4</version>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>johnzon-jsonb</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>johnzon-jsonschema</artifactId>
+      <version>${project.version}</version>
     </dependency>
 
     <dependency>
diff --git 
a/johnzon-maven-plugin/src/main/java/org/apache/johnzon/maven/plugin/ExampleToModelMojo.java
 
b/johnzon-maven-plugin/src/main/java/org/apache/johnzon/maven/plugin/ExampleToModelMojo.java
index 8577b4b..bbdb590 100644
--- 
a/johnzon-maven-plugin/src/main/java/org/apache/johnzon/maven/plugin/ExampleToModelMojo.java
+++ 
b/johnzon-maven-plugin/src/main/java/org/apache/johnzon/maven/plugin/ExampleToModelMojo.java
@@ -18,7 +18,6 @@
  */
 package org.apache.johnzon.maven.plugin;
 
-import org.apache.commons.lang3.StringUtils;
 import org.apache.maven.plugin.AbstractMojo;
 import org.apache.maven.plugin.MojoExecutionException;
 import org.apache.maven.plugin.MojoFailureException;
@@ -68,7 +67,7 @@ public class ExampleToModelMojo extends AbstractMojo {
     @Parameter
     protected String header;
 
-    @Parameter
+    @Parameter(defaultValue = "${project}", readonly = true)
     protected MavenProject project;
 
     @Parameter(property = "johnzon.attach", defaultValue = "true")
@@ -199,7 +198,7 @@ public class ExampleToModelMojo extends AbstractMojo {
     private void handleArray(final Writer writer, final String prefix,
                              final Map<String, JsonObject> nestedTypes,
                              final JsonValue value,
-                             final String jsonField,final String fieldName,
+                             final String jsonField, final String fieldName,
                              final int arrayLevel,
                              final Collection<String> imports) throws 
IOException {
         final JsonArray array = JsonArray.class.cast(value);
@@ -239,7 +238,7 @@ public class ExampleToModelMojo extends AbstractMojo {
                                     final Collection<String> imports) throws 
IOException {
         final String actualType = buildArrayType(arrayLevel, type);
         final String actualField = buildValidFieldName(jsonField);
-        final String methodName = StringUtils.capitalize(actualField);
+        final String methodName = capitalize(actualField);
 
         if (!jsonField.equals(field)) { // TODO: add it to imports in eager 
visitor
             imports.add("org.apache.johnzon.mapper.JohnzonProperty");
@@ -255,6 +254,16 @@ public class ExampleToModelMojo extends AbstractMojo {
         writer.append(prefix).append("}\n");
     }
 
+    private String capitalize(final String str) {
+        if (str != null && !str.isEmpty()) {
+            final char firstChar = str.charAt(0);
+            return Character.isTitleCase(firstChar) ?
+                    str :
+                    (Character.toTitleCase(firstChar) + str.substring(1));
+        }
+        return str;
+    }
+
     private String buildArrayType(final int arrayLevel, final String type) {
         if (arrayLevel == 0) { // quick exit
             return type;
@@ -282,7 +291,7 @@ public class ExampleToModelMojo extends AbstractMojo {
     }
 
     private void generateFile(final JsonReaderFactory readerFactory, final 
File source) throws MojoExecutionException {
-        final String javaName = 
StringUtils.capitalize(toJavaName(source.getName()));
+        final String javaName = capitalize(toJavaName(source.getName()));
         final String jsonToClass = packageBase + '.' + javaName;
         final File outputFile = new File(target, jsonToClass.replace('.', '/') 
+ ".java");
 
@@ -313,7 +322,8 @@ public class ExampleToModelMojo extends AbstractMojo {
     }
 
     private String toJavaFieldName(final String key) {
-        return StringUtils.uncapitalize(toJavaName(key));
+        final String javaName = toJavaName(key);
+        return Character.toLowerCase(javaName.charAt(0)) + 
javaName.substring(1);
     }
 
     private String toJavaName(final String file) {
@@ -329,7 +339,7 @@ public class ExampleToModelMojo extends AbstractMojo {
                 builder.append(anIn);
             }
         }
-        return StringUtils.capitalize(builder.toString());
+        return capitalize(builder.toString());
     }
 
     private interface Visitor {
diff --git 
a/johnzon-maven-plugin/src/main/java/org/apache/johnzon/maven/plugin/PojoToJsonSchemaMojo.java
 
b/johnzon-maven-plugin/src/main/java/org/apache/johnzon/maven/plugin/PojoToJsonSchemaMojo.java
new file mode 100644
index 0000000..38f3b99
--- /dev/null
+++ 
b/johnzon-maven-plugin/src/main/java/org/apache/johnzon/maven/plugin/PojoToJsonSchemaMojo.java
@@ -0,0 +1,125 @@
+/*
+ * 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.maven.plugin;
+
+import org.apache.johnzon.jsonschema.generator.Schema;
+import org.apache.johnzon.jsonschema.generator.SchemaProcessor;
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugins.annotations.Component;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.project.MavenProject;
+import org.apache.maven.project.MavenProjectHelper;
+
+import javax.json.bind.Jsonb;
+import javax.json.bind.JsonbBuilder;
+import javax.json.bind.JsonbConfig;
+import javax.json.bind.config.PropertyOrderStrategy;
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+import static 
org.apache.maven.plugins.annotations.LifecyclePhase.PROCESS_CLASSES;
+import static 
org.apache.maven.plugins.annotations.ResolutionScope.RUNTIME_PLUS_SYSTEM;
+
+@Mojo(name = "jsonschema", defaultPhase = PROCESS_CLASSES, 
requiresDependencyResolution = RUNTIME_PLUS_SYSTEM)
+public class PojoToJsonSchemaMojo extends AbstractMojo {
+    @Parameter(property = "johnzon.jsonschema.schemaClass")
+    protected String schemaClass;
+
+    @Parameter(property = "johnzon.jsonschema.target", defaultValue = 
"${project.build.outputDirectory}/jsonschema/schema.json")
+    protected File target;
+
+    @Parameter(property = "johnzon.jsonschema.classesDir", defaultValue = 
"${project.build.outputDirectory}")
+    protected File classesDir;
+
+    @Parameter(defaultValue = "${project}", readonly = true)
+    protected MavenProject project;
+
+    @Component
+    protected MavenProjectHelper projectHelper;
+
+    @Parameter(property = "johnzon.attach", defaultValue = "true")
+    protected boolean attach;
+
+    @Parameter(property = "johnzon.jsonschema.classifier", defaultValue = 
"jsonschema")
+    protected String classifier;
+
+    @Parameter(property = "johnzon.jsonschema.title")
+    protected String title;
+
+    @Parameter(property = "johnzon.jsonschema.description")
+    protected String description;
+
+    @Override
+    public void execute() throws MojoExecutionException {
+        final Thread thread = Thread.currentThread();
+        final ClassLoader old = thread.getContextClassLoader();
+        try (final URLClassLoader loader = newLoader(old);
+             final Jsonb jsonb = JsonbBuilder.create(new JsonbConfig()
+                     .withFormatting(true)
+                     
.withPropertyOrderStrategy(PropertyOrderStrategy.LEXICOGRAPHICAL))) {
+            thread.setContextClassLoader(loader);
+            final SchemaProcessor.InMemoryCache cache = new 
SchemaProcessor.InMemoryCache();
+            final Schema schema = new SchemaProcessor()
+                    .mapSchemaFromClass(loader.loadClass(schemaClass.trim()), 
cache);
+            schema.setTitle(title);
+            schema.setDescription(description);
+            if (!cache.getDefinitions().isEmpty()) {
+                schema.setDefinitions(cache.getDefinitions());
+            }
+            if (target.getParent() != null) {
+                Files.createDirectories(target.toPath().getParent());
+            }
+            Files.write(target.toPath(), 
jsonb.toJson(schema).getBytes(StandardCharsets.UTF_8));
+        } catch (final Exception e) {
+            throw new MojoExecutionException(e.getMessage(), e);
+        } finally {
+            thread.setContextClassLoader(old);
+        }
+        if (attach && project != null) {
+            projectHelper.attachArtifact(project, "json", classifier, target);
+        }
+    }
+
+    private URLClassLoader newLoader(final ClassLoader parent) {
+        return new URLClassLoader(
+                Stream.concat(project.getArtifacts().stream()
+                                .map(Artifact::getFile)
+                                .filter(Objects::nonNull),
+                        Stream.of(classesDir))
+                        .filter(File::exists)
+                        .map(it -> {
+                            try {
+                                return it.toURI().toURL();
+                            } catch (final MalformedURLException e) {
+                                throw new IllegalStateException(e);
+                            }
+                        })
+                        .toArray(URL[]::new),
+                parent);
+    }
+}
diff --git 
a/johnzon-maven-plugin/src/test/java/org/apache/johnzon/maven/plugin/PojoToJsonSchemaMojoTest.java
 
b/johnzon-maven-plugin/src/test/java/org/apache/johnzon/maven/plugin/PojoToJsonSchemaMojoTest.java
new file mode 100644
index 0000000..270c96d
--- /dev/null
+++ 
b/johnzon-maven-plugin/src/test/java/org/apache/johnzon/maven/plugin/PojoToJsonSchemaMojoTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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.maven.plugin;
+
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.project.MavenProject;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.List;
+import java.util.Set;
+
+import static java.util.Collections.emptySet;
+import static java.util.stream.Collectors.joining;
+import static org.junit.Assert.assertEquals;
+
+public class PojoToJsonSchemaMojoTest {
+    @Test
+    public void generate() throws MojoExecutionException, IOException {
+        final PojoToJsonSchemaMojo mojo = new PojoToJsonSchemaMojo();
+        mojo.classesDir = new File("target/classes");
+        mojo.target = new 
File("target/workdir-PojoToJsonSchemaMojoTest/output.json");
+        mojo.description = "Test desc";
+        mojo.title = "Test title";
+        mojo.project = new MavenProject() {
+            @Override
+            public Set<Artifact> getArtifacts() {
+                return emptySet();
+            }
+        };
+        mojo.schemaClass = Foo.class.getName();
+        if (mojo.target.exists()) {
+            mojo.target.delete();
+        }
+        mojo.execute();
+        assertEquals("" +
+                        "{\n" +
+                        "  
\"$id\":\"org_apache_johnzon_maven_plugin_PojoToJsonSchemaMojoTest_Foo\",\n" +
+                        "  \"type\":\"object\",\n" +
+                        "  \"title\":\"Test title\",\n" +
+                        "  \"description\":\"Test desc\",\n" +
+                        "  \"properties\":{\n" +
+                        "    \"nested\":{\n" +
+                        "      
\"$id\":\"org_apache_johnzon_maven_plugin_PojoToJsonSchemaMojoTest_Bar\",\n" +
+                        "      \"type\":\"object\",\n" +
+                        "      \"properties\":{\n" +
+                        "        \"simple\":{\n" +
+                        "          \"type\":\"string\"\n" +
+                        "        }\n" +
+                        "      }\n" +
+                        "    },\n" +
+                        "    \"other\":{\n" +
+                        "      \"type\":\"array\",\n" +
+                        "      \"items\":{\n" +
+                        "        \"type\":\"string\"\n" +
+                        "      }\n" +
+                        "    },\n" +
+                        "    \"simple\":{\n" +
+                        "      \"type\":\"string\"\n" +
+                        "    }\n" +
+                        "  }\n" +
+                        "}" +
+                        "",
+                
Files.readAllLines(mojo.target.toPath()).stream().collect(joining("\n")));
+    }
+
+    public static class Foo {
+        public String simple;
+        public Bar nested;
+        public List<String> other;
+    }
+
+    public static class Bar {
+        public String simple;
+    }
+}

Reply via email to