blob: 5db6239448b5332243126b73579cb37f4c43c066 [file] [log] [blame]
/*
* 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.JsonObject;
import javax.json.JsonString;
import javax.json.JsonValue;
import javax.json.bind.annotation.JsonbProperty;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import static java.util.Collections.singletonMap;
import static java.util.Comparator.comparing;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
// todo: support ref resolution for schema and avoid to generate as much classes as attributes
public class PojoGenerator {
private final PojoConfiguration configuration;
protected final Set<String> imports = new TreeSet<>(String::compareTo);
protected final List<Attribute> attributes = new ArrayList<>();
protected final Map<String, String> nested = new TreeMap<>(String::compareTo);
public PojoGenerator(final PojoConfiguration configuration) {
this.configuration = configuration;
}
public PojoGenerator setNested(final Map<String, String> nested) {
this.nested.putAll(nested);
return this;
}
public Map<String, String> generate() {
final String name = configuration.getPackageName() + '.' + configuration.getClassName();
final String path = name.replace('.', '/') + ".java";
attributes.sort(comparing(a -> a.javaName));
if (!attributes.isEmpty()) {
imports.add(Objects.class.getName());
}
final String content = "" +
"package " + configuration.getPackageName() + ";\n" +
"\n" +
(imports.isEmpty() ? "" : imports.stream()
.sorted()
.map(it -> "import " + it + ";")
.collect(joining("\n", "", "\n\n"))) +
"public class " + configuration.getClassName() + afterClassName() + " {\n" +
(attributes.isEmpty() ?
("" +
" @Override\n" +
" public int hashCode() {\n" +
" return 0;\n" +
" }\n" +
"\n" +
" @Override\n" +
" public boolean equals(final Object other) {\n" +
" return other instanceof " + configuration.getClassName() + ";\n" +
" }\n" +
"}\n") :
(attributes.stream()
.map(a -> "" +
(configuration.isAddJsonbProperty() && !Objects.equals(a.javaName, a.jsonName) ?
" @JsonbProperty(\"" + a.jsonName.replace("\"", "\\\"") + "\")\n" :
"") +
" private " + a.type + " " + a.javaName + ";")
.collect(joining("\n", "", "\n\n")) +
(configuration.isAddAllArgsConstructor() ?
" public " + configuration.getClassName() + "() {\n" +
" // no-op\n" +
" }\n" +
"\n" +
" public " + configuration.getClassName() + "(" +
attributes.stream()
.map(a -> "final " + a.type + " " + a.javaName)
.collect(joining(
",\n" + IntStream.range(
0,
" public (".length() +
configuration.getClassName().length())
.mapToObj(i -> " ")
.collect(joining()),
"",
") {\n" +
" // no-op\n" +
" }\n\n")) :
"") +
attributes.stream()
.map(a -> {
final String marker = Character.toUpperCase(a.javaName.charAt(0)) + a.javaName.substring(1);
return "" +
" public " + a.type + " get" + Character.toUpperCase(a.javaName.charAt(0)) + a.javaName.substring(1) + "() {\n" +
" return " + a.javaName + ";\n" +
" }\n" +
"\n" +
" public " +
(configuration.isFluentSetters() ? a.type : "void") +
" set" + marker + "(final " + a.type + " " + a.javaName + ") {\n" +
" this." + a.javaName + " = " + a.javaName + ";\n" +
(configuration.isFluentSetters() ? " return this;\n" : "") +
" }\n" +
"";
})
.collect(joining("\n", "", "\n")) +
" @Override\n" +
" public int hashCode() {\n" +
" return Objects.hash(\n" +
attributes.stream()
.map(a -> a.javaName)
.collect(joining(",\n ", " ", ");\n")) +
" }\n" +
"\n" +
" @Override\n" +
" public boolean equals(final Object __other) {\n" +
" if (!(__other instanceof " + configuration.getClassName() + ")) {\n" +
" return false;\n" +
" }\n" +
" final " + configuration.getClassName() + " __otherCasted = (" + configuration.getClassName() + ") __other;\n" +
" return " + attributes.stream()
.map(a -> a.javaName)
.map(it -> "Objects.equals(" + it + ", __otherCasted." + it + ")")
.collect(joining(" &&\n ")) + ";\n" +
" }\n"
)) +
beforeClassEnd() +
"}\n";
if (nested.isEmpty()) {
return singletonMap(path, content);
}
return Stream.concat(
nested.entrySet().stream(),
Stream.of(new AbstractMap.SimpleImmutableEntry<>(path, content)))
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, () -> new TreeMap<>(String::compareTo)));
}
public PojoGenerator visitSchema(final JsonObject schema) {
if (!schema.containsKey("properties")) {
throw new IllegalArgumentException("Unsupported schema since it does not contain any properties: " + schema);
}
final JsonObject properties = getValueAs(schema, "properties", JsonObject.class);
final JsonValue required = schema.get("required");
final List<String> requiredAttrs = required != null && required.getValueType() == JsonValue.ValueType.ARRAY ?
required.asJsonArray().stream().map(JsonString.class::cast).map(JsonString::getString).collect(toList()) :
null;
attributes.addAll(properties.entrySet().stream()
.map(e -> {
final String javaName = toJavaName(e.getKey());
return new Attribute(
javaName, e.getKey(),
asType(javaName, e.getValue().asJsonObject(), requiredAttrs != null && requiredAttrs.contains(e.getKey())));
})
.collect(toList()));
return this;
}
/**
* @param ref the reference to resolve.
* @return the reference class name if resolved else null.
*/
protected String onRef(final Ref ref) {
if (configuration.getOnRef() != null) {
return configuration.getOnRef().apply(ref);
}
return null; // todo: check if already in nested for ex
}
protected String beforeEnumEnd() {
return "";
}
protected String beforeClassEnd() {
return "";
}
protected String afterClassName() {
return "";
}
/**
* @param values the enum values (key is json name, value is java name).
* @param valuesAreInjected is there a toString() with json name or is the enum anemic (only values, no method)
* @return the data to add after enum name and before the opening brace in enum declaration.
*/
protected String afterEnumName(final Map<String, String> values, final boolean valuesAreInjected) {
return "";
}
protected String enumImports() {
return "";
}
protected String asType(final String javaName, final JsonObject schema, final boolean required) {
final JsonValue ref = schema.get("$ref");
if (ref != null && ref.getValueType() == JsonValue.ValueType.STRING) {
final String name = onRef(new Ref(
JsonString.class.cast(ref).getString(), configuration.getClassName(), imports, attributes, nested));
if (name != null) {
return name;
}
}
final JsonValue value = schema.get("type");
String type;
if (value == null) {
if (schema.containsKey("properties") || schema.containsKey("additionalProperties")) {
type = "object";
} else if (schema.containsKey("items")) {
type = "array";
} else { // unknown, don't fail for wrongly written schema
imports.add(JsonValue.class.getName());
return JsonValue.class.getSimpleName();
}
} else {
type = JsonString.class.cast(value).getString();
}
final JsonValue formatValue = schema.get("date-time");
if (formatValue != null && formatValue.getValueType() == JsonValue.ValueType.STRING) {
type = JsonString.class.cast(formatValue).getString();
}
switch (type) {
case "array":
final JsonObject items = getValueAs(schema, "items", JsonObject.class);
final String itemType = onItemSchema(javaName, items);
imports.add(List.class.getName());
return List.class.getSimpleName() + "<" + itemType + ">";
case "object":
return onObjectAttribute(javaName, schema);
case "null":
imports.add(JsonValue.class.getName());
return JsonValue.class.getSimpleName();
case "boolean":
return required ? "boolean" : "Boolean";
case "string":
final JsonValue enumList = schema.get("enum");
if (enumList != null && enumList.getValueType() == JsonValue.ValueType.ARRAY) {
return onEnum(javaName, enumList);
}
return "String";
case "number":
case "double": // openapi
return required ? "double" : "Double";
// openapi types
case "int":
case "int32":
case "integer":
return required ? "int" : "Integer";
case "int64":
case "long":
return required ? "long" : "Long";
case "float":
return required ? "float" : "Float";
case "date":
imports.add(LocalDate.class.getName());
return LocalDate.class.getSimpleName();
case "duration":
imports.add(Duration.class.getName());
return Duration.class.getSimpleName();
case "date-time":
case "dateTime":
imports.add(OffsetDateTime.class.getName());
return OffsetDateTime.class.getSimpleName();
case "time":
imports.add(LocalTime.class.getName());
return LocalTime.class.getSimpleName();
case "byte":
return "byte[]";
case "uuid":
case "hostname":
case "idn-hostname":
case "email":
case "idn-email":
case "ipv4":
case "ipv6":
case "uri":
case "uri-reference":
case "iri":
case "iri-reference":
case "uri-template":
case "json-pointer":
case "relative-json-pointer":
case "regex":
case "binary": // base64
case "password":
return "String";
default:
throw new IllegalArgumentException("Unsupported type: " + type);
}
}
protected String onEnum(final String javaName, final JsonValue enumList) {
final String className = configuration.getClassName() + Character.toUpperCase(javaName.charAt(0)) + javaName.substring(1);
final Map<String, String> values = enumList.asJsonArray().stream()
.map(JsonString.class::cast)
.map(JsonString::getString)
.collect(toMap(identity(), this::toJavaName));
final boolean injectValues = !values.keySet().equals(new HashSet<>(values.values())); // java != json
nested.put(
configuration.getPackageName().replace('.', '/') + '/' + className + ".java", "" +
"package " + configuration.getPackageName() + ";\n" +
"\n" +
enumImports() +
"public enum " + className + afterEnumName(values, injectValues) + " {\n" +
values.entrySet().stream()
.map(it -> "" +
(injectValues && configuration.isAddJsonbProperty() ?
" @JsonbProperty(\"" + it.getKey().replace("\"", "\\\"") + "\")\n" :
"") +
" " + it.getValue() +
(injectValues ? "(\"" + it.getKey().replace("\"", "\\\"") + "\")" : ""))
.collect(joining(",\n", "", injectValues ? ";\n\n" : "\n")) +
(injectValues ?
"" +
" private String value;\n" +
"\n" +
" " + className + "(final String value) {\n" +
" this.value = value;\n" +
" }\n" +
"\n" +
" public String toString() {\n" +
" return value;\n" +
" }\n" +
"" :
"") +
beforeEnumEnd() +
"}\n");
return className;
}
protected String onObjectAttribute(final String javaName, final JsonObject schema) {
final JsonValue additionalProperties = schema.get("additionalProperties");
final JsonValue properties = schema.get("properties");
final boolean hasProperties = properties != null && properties.getValueType() == JsonValue.ValueType.OBJECT;
if (!hasProperties &&
additionalProperties != null &&
additionalProperties.getValueType() == JsonValue.ValueType.OBJECT) {
final JsonObject propSchema = additionalProperties.asJsonObject();
final JsonValue propTypeValue = propSchema.get("type");
if (propTypeValue != null && propTypeValue.getValueType() == JsonValue.ValueType.STRING) {
String propType = JsonString.class.cast(propTypeValue).getString();
final JsonValue formatValue = schema.get("date-time");
if (formatValue != null && formatValue.getValueType() == JsonValue.ValueType.STRING) {
propType = JsonString.class.cast(formatValue).getString();
}
switch (propType) {
case "uuid":
case "hostname":
case "idn-hostname":
case "email":
case "idn-email":
case "ipv4":
case "ipv6":
case "uri":
case "uri-reference":
case "iri":
case "iri-reference":
case "uri-template":
case "json-pointer":
case "relative-json-pointer":
case "regex":
case "string":
case "binary":
case "password":
imports.add(Map.class.getName());
return "Map<String, String>";
case "boolean":
imports.add(Map.class.getName());
return "Map<String, Boolean>";
case "number":
case "double":
imports.add(Map.class.getName());
return "Map<String, Double>";
case "int":
case "int32":
case "integer":
imports.add(Map.class.getName());
return "Map<String, Integer>";
case "int64":
case "long":
imports.add(Map.class.getName());
return "Map<String, Long>";
case "float":
imports.add(Map.class.getName());
return "Map<String, Float>";
case "date":
imports.add(Map.class.getName());
imports.add(LocalDate.class.getName());
return "Map<String, LocalDate>";
case "dateTime":
case "date-time":
imports.add(Map.class.getName());
imports.add(OffsetDateTime.class.getName());
return "Map<String, OffsetDateTime>";
case "duration":
imports.add(Map.class.getName());
imports.add(Duration.class.getName());
return "Map<String, Duration>";
case "time":
imports.add(Map.class.getName());
imports.add(LocalTime.class.getName());
return "Map<String, LocalTime>";
default:
// todo: case array, object
}
}
} else if (hasProperties) {
final String className = configuration.getClassName() + Character.toUpperCase(javaName.charAt(0)) + javaName.substring(1);
nested.putAll(newSubPojoGenerator(new PojoConfiguration()
.setPackageName(configuration.getPackageName())
.setClassName(className)
.setAddJsonbProperty(configuration.isAddJsonbProperty())
.setAddAllArgsConstructor(configuration.isAddAllArgsConstructor())
.setOnRef(configuration.getOnRef()), schema)
.visitSchema(schema)
.generate());
return className;
}
imports.add(JsonObject.class.getName());
return JsonObject.class.getSimpleName();
}
protected PojoGenerator newSubPojoGenerator(final PojoConfiguration pojoConfiguration, final JsonObject schema) {
return new PojoGenerator(pojoConfiguration);
}
protected String onItemSchema(final String javaName, final JsonObject schema) {
final JsonValue ref = schema.get("$ref");
if (ref != null && ref.getValueType() == JsonValue.ValueType.STRING) {
final String name = onRef(new Ref(
JsonString.class.cast(ref).getString(), configuration.getClassName(),
imports, attributes, nested));
if (name != null) {
return name;
}
}
final JsonValue propTypeValue = schema.get("type");
if (propTypeValue != null && propTypeValue.getValueType() == JsonValue.ValueType.STRING) {
String type = JsonString.class.cast(propTypeValue).getString();
final JsonValue formatValue = schema.get("date-time");
if (formatValue != null && formatValue.getValueType() == JsonValue.ValueType.STRING) {
type = JsonString.class.cast(formatValue).getString();
}
switch (type) {
case "array":
throw new IllegalStateException("Array of array unsupported");
case "object":
final String className = configuration.getClassName() + Character.toUpperCase(javaName.charAt(0)) + javaName.substring(1);
nested.putAll(newSubPojoGenerator(new PojoConfiguration()
.setPackageName(configuration.getPackageName())
.setClassName(className)
.setAddJsonbProperty(configuration.isAddJsonbProperty())
.setAddAllArgsConstructor(configuration.isAddAllArgsConstructor())
.setOnRef(configuration.getOnRef()), schema)
.visitSchema(schema)
.generate());
return className;
case "null":
imports.add(JsonValue.class.getName());
return JsonValue.class.getSimpleName();
case "boolean":
return "Boolean";
case "uuid":
case "hostname":
case "idn-hostname":
case "email":
case "idn-email":
case "ipv4":
case "ipv6":
case "uri":
case "uri-reference":
case "iri":
case "iri-reference":
case "uri-template":
case "json-pointer":
case "relative-json-pointer":
case "regex":
case "string":
case "binary":
case "password":
return "String";
case "number":
case "double":
return "Double";
case "int":
case "int32":
case "integer":
return "Integer";
case "int64":
case "long":
return "Long";
case "float":
return "Float";
case "date":
imports.add(LocalDate.class.getName());
return LocalDate.class.getSimpleName();
case "dateTime":
imports.add(OffsetDateTime.class.getName());
return OffsetDateTime.class.getSimpleName();
case "duration":
imports.add(Duration.class.getName());
return Duration.class.getSimpleName();
case "time":
imports.add(LocalTime.class.getName());
return LocalTime.class.getSimpleName();
case "byte":
return "byte[]";
default:
throw new IllegalArgumentException("Unsupported type: " + type);
}
}
imports.add(JsonValue.class.getName());
return JsonValue.class.getSimpleName();
}
protected String toJavaName(final String key) {
String name = key.chars()
.mapToObj(i -> Character.toString(!Character.isJavaIdentifierPart(i) ? '_' : (char) i))
.collect(joining());
if (Character.isDigit(name.charAt(0))) {
name = "a" + name;
}
while (!name.isEmpty() && (!Character.isJavaIdentifierStart(name.charAt(0)) || name.charAt(0) == '_' || name.charAt(0) == '$')) {
name = name.substring(1);
}
if (name.isEmpty()) {
throw new IllegalArgumentException("Can't find a name for '" + key + "'");
}
if (isReserved(name)) {
name += "Value";
}
if (name.contains("_")) {
name = toCamelCase(name);
}
if (!Objects.equals(key, name) && configuration.isAddJsonbProperty()) {
imports.add(JsonbProperty.class.getName());
}
return name;
}
protected String toCamelCase(final String name) {
final StringBuilder out = new StringBuilder(name.length());
boolean up = false;
for (final char c : name.toCharArray()) {
if (up) {
out.append(Character.toUpperCase(c));
up = false;
} else if (c == '_') {
up = true;
} else {
out.append(c);
}
}
return out.toString();
}
protected boolean isReserved(final String name) {
return "continue".equals(name) || "break".equals(name) || "default".equals(name) ||
"do".equals(name) || "while".equals(name) ||
"for".equals(name) ||
"if".equals(name) || "else".equals(name) ||
"enum".equals(name) ||
"int".equals(name) ||
"long".equals(name) ||
"float".equals(name) ||
"double".equals(name) ||
"boolean".equals(name) ||
"byte".equals(name) ||
"char".equals(name) ||
"short".equals(name) ||
"String".equals(name);
}
private static <T> T getValueAs(final JsonObject schema, final String attribute, final Class<T> type) {
final JsonValue value = schema.get(attribute);
if (value == null) {
throw new IllegalArgumentException("No \"" + attribute + "\" value in " + schema);
}
return valueAs(schema, type, value);
}
private static <T> T valueAs(final JsonObject schema, final Class<T> type, final JsonValue value) {
if (!type.isInstance(value)) {
throw new IllegalArgumentException("\"items\" not an object: " + schema);
}
return type.cast(value);
}
public static class PojoConfiguration {
private String packageName = "org.apache.johnzon.generated.pojo";
private String className;
private boolean addJsonbProperty = true;
private boolean addAllArgsConstructor = true;
private boolean fluentSetters = false;
private Function<Ref, String> onRef;
public Function<Ref, String> getOnRef() {
return onRef;
}
public PojoConfiguration setOnRef(final Function<Ref, String> onRef) {
this.onRef = onRef;
return this;
}
public boolean isFluentSetters() {
return fluentSetters;
}
public PojoConfiguration setFluentSetters(final boolean fluentSetters) {
this.fluentSetters = fluentSetters;
return this;
}
public boolean isAddAllArgsConstructor() {
return addAllArgsConstructor;
}
public PojoConfiguration setAddAllArgsConstructor(final boolean addAllArgsConstructor) {
this.addAllArgsConstructor = addAllArgsConstructor;
return this;
}
public boolean isAddJsonbProperty() {
return addJsonbProperty;
}
public PojoConfiguration setAddJsonbProperty(final boolean addJsonbProperty) {
this.addJsonbProperty = addJsonbProperty;
return this;
}
public String getClassName() {
return className;
}
public PojoConfiguration setClassName(final String className) {
this.className = className;
return this;
}
public String getPackageName() {
return packageName;
}
public PojoConfiguration setPackageName(final String packageName) {
this.packageName = packageName;
return this;
}
}
protected static class Attribute {
protected final String javaName;
protected final String jsonName;
protected final String type;
protected Attribute(final String javaName, final String jsonName, final String type) {
this.javaName = javaName;
this.jsonName = jsonName;
this.type = type;
}
public String getJavaName() {
return javaName;
}
public String getJsonName() {
return jsonName;
}
public String getType() {
return type;
}
}
public static class Ref {
private final String ref;
private final String enclosingClass;
private final Set<String> imports;
private final List<Attribute> attributes;
private final Map<String, String> nested;
private Ref(final String ref, final String enclosingClass, final Set<String> imports,
final List<Attribute> attributes, final Map<String, String> nested) {
this.ref = ref;
this.enclosingClass = enclosingClass;
this.imports = imports;
this.attributes = attributes;
this.nested = nested;
}
public String getEnclosingClass() {
return enclosingClass;
}
public String getRef() {
return ref;
}
public Set<String> getImports() {
return imports;
}
public List<Attribute> getAttributes() {
return attributes;
}
public Map<String, String> getNested() {
return nested;
}
}
}