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;
+ }
+}