| /* |
| * 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); |
| } |
| } |