blob: 2ce7ee73152533528806f5165c06750df6801e7a [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.maven.plugin;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonReader;
import javax.json.JsonReaderFactory;
import javax.json.JsonStructure;
import javax.json.JsonValue;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.TreeSet;
import static java.util.Arrays.asList;
import static org.apache.maven.plugins.annotations.LifecyclePhase.GENERATE_SOURCES;
@Mojo(name = "example-to-model", defaultPhase = GENERATE_SOURCES)
public class ExampleToModelMojo extends AbstractMojo {
// not strictly forbidden but kind of file to java convertion
private static final List<Character> FORBIDDEN_JAVA_NAMES = asList('-', '_', '.', '{', '}');
@Parameter(property = "johnzon.source", defaultValue = "${project.basedir}/src/main/johnzon")
protected File source;
@Parameter(property = "johnzon.target", defaultValue = "${project.build.directory}/generated-sources/johnzon")
protected File target;
@Parameter(property = "johnzon.package", defaultValue = "com.johnzon.generated")
protected String packageBase;
@Parameter
protected String header;
@Parameter(defaultValue = "${project}", readonly = true)
protected MavenProject project;
@Parameter(property = "johnzon.attach", defaultValue = "true")
protected boolean attach;
@Parameter(property = "johnzon.useRecord", defaultValue = "false")
protected boolean useRecord;
@Parameter(property = "johnzon.useJsonb", defaultValue = "false")
protected boolean useJsonb;
@Parameter(property = "johnzon.ignoreNull", defaultValue = "false")
protected boolean ignoreNull;
@Override
public void execute() throws MojoExecutionException {
final JsonReaderFactory readerFactory = Json.createReaderFactory(Collections.emptyMap());
if (source.isFile()) {
generateFile(readerFactory, source);
} else {
final File[] children = source.listFiles((dir, name) -> name.endsWith(".json"));
if (children == null || children.length == 0) {
throw new MojoExecutionException("No json file found in " + source);
}
for (final File child : children) {
generateFile(readerFactory, child);
}
}
if (attach && project != null) {
project.addCompileSourceRoot(target.getAbsolutePath());
}
}
// TODO: unicity of field name, better nested array/object handling
private void generate(final JsonReaderFactory readerFactory, final File source, final Writer writer, final String javaName) throws MojoExecutionException {
try (final JsonReader reader = readerFactory.createReader(new FileReader(source))) {
final JsonStructure structure = reader.read();
if (JsonArray.class.isInstance(structure) || !JsonObject.class.isInstance(structure)) { // quite redundant for now but to avoid surprises in future
throw new MojoExecutionException("This plugin doesn't support array generation, generate the model (generic) and handle arrays in your code");
}
final JsonObject object = JsonObject.class.cast(structure);
final Collection<String> imports = new TreeSet<>();
// while we browse the example tree just store imports as well, avoids a 2 passes processing duplicating imports logic
final StringWriter memBuffer = new StringWriter();
generateFieldsAndMethods(memBuffer, object, " ", imports);
if (header != null) {
writer.write(header);
writer.write('\n');
}
writer.write("package " + packageBase + ";\n\n");
if (!imports.isEmpty()) {
for (final String imp : imports) {
writer.write("import " + imp + ";\n");
}
writer.write('\n');
}
if (useRecord) {
writer.write("public record " + javaName + "(\n");
writer.write(memBuffer.toString());
} else {
writer.write("public class " + javaName + " {\n");
writer.write(memBuffer.toString());
}
writer.write("}\n");
} catch (final IOException e) {
throw new MojoExecutionException(e.getMessage(), e);
}
}
private void generateFieldsAndMethods(final StringWriter writer, final JsonObject object, final String prefix,
final Collection<String> imports) throws IOException {
final Map<String, JsonObject> nestedTypes = new TreeMap<>();
{
final Iterator<Map.Entry<String, JsonValue>> iterator = object.entrySet().iterator();
while (iterator.hasNext()) {
final Map.Entry<String, JsonValue> entry = iterator.next();
final String key = entry.getKey();
final String fieldName = toJavaFieldName(key);
final boolean hasNext = iterator.hasNext();
switch (entry.getValue().getValueType()) {
case ARRAY:
imports.add("java.util.List");
handleArray(writer, prefix, nestedTypes, entry.getValue(), key, fieldName, 1, imports, !hasNext);
break;
case OBJECT:
final String type = toJavaName(fieldName);
nestedTypes.put(type, JsonObject.class.cast(entry.getValue()));
fieldGetSetMethods(writer, key, fieldName, type, prefix, 0, imports, !hasNext);
break;
case TRUE:
case FALSE:
fieldGetSetMethods(writer, key, fieldName, "Boolean", prefix, 0, imports, !hasNext);
break;
case NUMBER:
fieldGetSetMethods(writer, key, fieldName, "Double", prefix, 0, imports, !hasNext);
break;
case STRING:
fieldGetSetMethods(writer, key, fieldName, "String", prefix, 0, imports, !hasNext);
break;
case NULL:
default:
if (ignoreNull) {
if (useRecord && writer.getBuffer().length() > 0) {
writer.getBuffer().setLength(writer.getBuffer().length() - 2);
writer.write("\n");
}
} else {
throw new UnsupportedOperationException("Unsupported " + entry.getValue() + ".");
}
}
if (hasNext) {
writer.write("\n");
} else if (useRecord) {
writer.write(") {\n");
}
}
}
if (!object.isEmpty() && !nestedTypes.isEmpty()) {
writer.write("\n");
}
final Iterator<Map.Entry<String, JsonObject>> entries = nestedTypes.entrySet().iterator();
while (entries.hasNext()) {
final Map.Entry<String, JsonObject> entry = entries.next();
if (useRecord) {
writer.write(prefix + "public static record " + entry.getKey() + "(\n");
} else {
writer.write(prefix + "public static class " + entry.getKey() + " {\n");
}
generateFieldsAndMethods(writer, entry.getValue(), " " + prefix, imports);
writer.write(prefix + "}\n");
if (entries.hasNext()) {
writer.write("\n");
}
}
}
private void handleArray(final Writer writer, final String prefix,
final Map<String, JsonObject> nestedTypes,
final JsonValue value,
final String jsonField, final String fieldName,
final int arrayLevel,
final Collection<String> imports,
final boolean last) throws IOException {
final JsonArray array = JsonArray.class.cast(value);
if (array.size() > 0) { // keep it simple for now - 1 level, we can have an awesome recursive algo later if needed
final JsonValue jsonValue = array.get(0);
switch (jsonValue.getValueType()) {
case OBJECT:
final String javaName = toJavaName(fieldName);
nestedTypes.put(javaName, JsonObject.class.cast(jsonValue));
fieldGetSetMethods(writer, jsonField, fieldName, javaName, prefix, arrayLevel, imports, last);
break;
case TRUE:
case FALSE:
fieldGetSetMethods(writer, jsonField, fieldName, "Boolean", prefix, arrayLevel, imports, last);
break;
case NUMBER:
fieldGetSetMethods(writer, jsonField, fieldName, "Double", prefix, arrayLevel, imports, last);
break;
case STRING:
fieldGetSetMethods(writer, jsonField, fieldName, "String", prefix, arrayLevel, imports, last);
break;
case ARRAY:
handleArray(writer, prefix, nestedTypes, jsonValue, jsonField, fieldName, arrayLevel + 1, imports, last);
break;
case NULL:
default:
throw new UnsupportedOperationException("Unsupported " + value + ".");
}
} else {
getLog().warn("Empty arrays are ignored");
}
}
private void fieldGetSetMethods(final Writer writer,
final String jsonField, final String field,
final String type, final String prefix, final int arrayLevel,
final Collection<String> imports, final boolean last) throws IOException {
final String actualType = buildArrayType(arrayLevel, type);
final String actualField = buildValidFieldName(jsonField);
final String methodName = capitalize(actualField);
if (!jsonField.equals(field)) {
if (useJsonb) {
imports.add("javax.json.bind.annotation.JsonbProperty");
writer.append(prefix).append("@JsonbProperty(\"").append(jsonField).append("\")");
} else {
imports.add("org.apache.johnzon.mapper.JohnzonProperty");
writer.append(prefix).append("@JohnzonProperty(\"").append(jsonField).append("\")");
}
if (useRecord) {
writer.append(" ");
} else {
writer.append("\n").append(prefix);
}
} else {
writer.append(prefix);
}
if (!useRecord) {
writer.append("private ");
}
writer.append(actualType).append(" ").append(actualField);
if (!useRecord) {
writer.append(";\n");
writer.append(prefix).append("public ").append(actualType).append(" get").append(methodName).append("() {\n");
writer.append(prefix).append(" return ").append(actualField).append(";\n");
writer.append(prefix).append("}\n");
writer.append(prefix).append("public void set").append(methodName).append("(final ").append(actualType).append(" newValue) {\n");
writer.append(prefix).append(" this.").append(actualField).append(" = newValue;\n");
writer.append(prefix).append("}\n");
} else if (!last) {
writer.append(",");
}
}
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;
}
final StringBuilder builder = new StringBuilder();
for (int i = 0; i < arrayLevel; i++) {
builder.append("List<");
}
builder.append(type);
for (int i = 0; i < arrayLevel; i++) {
builder.append(">");
}
return builder.toString();
}
private void visit(final JsonStructure structure, final Visitor visitor) {
if (JsonObject.class.isInstance(structure)) {
visitor.visitObject(JsonObject.class.cast(structure));
} else if (JsonArray.class.isInstance(structure)) {
visitor.visitArray(JsonArray.class.cast(structure));
} else {
throw new UnsupportedOperationException("Can't visit " + structure);
}
}
private void generateFile(final JsonReaderFactory readerFactory, final File source) throws MojoExecutionException {
final String javaName = capitalize(toJavaName(source.getName()));
final String jsonToClass = packageBase + '.' + javaName;
final File outputFile = new File(target, jsonToClass.replace('.', '/') + ".java");
outputFile.getParentFile().mkdirs();
try (final FileWriter writer = new FileWriter(outputFile)) {
generate(readerFactory, source, writer, javaName);
} catch (IOException e) {
throw new MojoExecutionException(e.getMessage(), e);
}
// no-op
}
private String buildValidFieldName(final String jsonField) {
String val = toJavaFieldName(jsonField);
if (Character.isDigit(jsonField.charAt(0))) {
val = "_" + jsonField;
}
return val.replace(".", "");
}
private String toJavaFieldName(final String key) {
final String javaName = toJavaName(key);
return Character.toLowerCase(javaName.charAt(0)) + javaName.substring(1);
}
private String toJavaName(final String file) {
final StringBuilder builder = new StringBuilder();
boolean nextUpper = false;
for (final char anIn : file.replace(".json", "").toCharArray()) {
if (FORBIDDEN_JAVA_NAMES.contains(anIn)) {
nextUpper = true;
} else if (nextUpper) {
builder.append(Character.toUpperCase(anIn));
nextUpper = false;
} else {
builder.append(anIn);
}
}
return capitalize(builder.toString());
}
private interface Visitor {
void visitObject(JsonObject structure);
void visitArray(JsonArray structure);
}
}