ClassGenerator.java

/*
 * MIT License
 *
 * Copyright (c) 2024 Helvethink
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 */

package ch.helvethink.odoo4java.generator;

import gg.jte.ContentType;
import gg.jte.TemplateEngine;
import gg.jte.output.FileOutput;
import org.codehaus.plexus.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;

import static ch.helvethink.odoo4java.generator.ClassTools.getJavaType;
import static ch.helvethink.odoo4java.generator.ClassTools.sanitizeClassName;
import static ch.helvethink.odoo4java.generator.OdooConstants.RELATION_FIELD_NAME;
import static ch.helvethink.odoo4java.tools.FieldUtils.formatFieldName;
import static org.apache.commons.lang3.StringUtils.capitalize;

/**
 * Main class to generate models.
 */
public final class ClassGenerator {

    /**
     * Simple Logger
     */
    public static final Logger LOG = LoggerFactory.getLogger(ClassGenerator.class.getName());

    /**
     * ClassGenerator is a tooling class, no visible constructor
     */
    private ClassGenerator() {
        // Tooling class
    }

    /**
     * Main method to generate a class for a model
     * All attributes of the object are public (no getters and setters), this is very conscious
     *
     * @param pathToGenerate Path where we want to generate files - default will be "target"
     * @param modelName      Name of the model we want to generate a class
     * @param fields         Fields that have been returned from Odoo for this model
     * @throws IOException If we can't write to the destination directory
     */
    public static void generateClassNG(final List<String> excludedPrefixes, final List<String> includedPrefixes, final String pathToGenerate, final String generatedRootPackage, final String modelName, final Map<String, Map<String, Object>> fields) throws IOException {
        LOG.debug("generating from {}", modelName);

        final List<FieldTemplate> fieldsToCreate = new ArrayList<>();
        // Import list is used as string to avoid double line breaks from jte
        final Set<String> listOfImports = new HashSet<>();
        listOfImports.add("import ch.helvethink.odoo4java.models.OdooObject;");
        listOfImports.add("import ch.helvethink.odoo4java.models.OdooObj;");
        listOfImports.add("import com.fasterxml.jackson.annotation.JsonProperty;");

        final String rootPackage = StringUtils.isEmpty(generatedRootPackage) ? "ch.helvethink.odoo4java.models.generated." : (generatedRootPackage + ".");
        final String classfqdn = computeClassFQDN(modelName);
        if (!StringUtils.isEmpty(classfqdn) && (includedPrefixes.isEmpty() || includedPrefixes.stream().anyMatch(classfqdn::startsWith) || !excludedPrefixes.isEmpty() && excludedPrefixes.stream().noneMatch(classfqdn::startsWith))) {
            final String currentClassPackageName = classfqdn.substring(0, classfqdn.lastIndexOf("."));
            final String className = StringUtils.capitaliseAllWords(classfqdn.substring(classfqdn.lastIndexOf(".") + 1));
            LOG.info("Generating {}{}.{}", rootPackage, currentClassPackageName, className);
            LOG.debug("From modelName, packageName is {} and className is {}", currentClassPackageName, className);

            final String directory = pathToGenerate + "/" + rootPackage.replace(".", "/") + currentClassPackageName.replace(".", "/");
            Files.createDirectories(Path.of(directory));

            for (final Map.Entry<String, Map<String, Object>> entry : fields.entrySet()) {
                final Map<String, Object> fieldProps = entry.getValue();
                final String fieldRelation = fieldProps.get(RELATION_FIELD_NAME) != null ? computeClassFQDN(fieldProps.get(RELATION_FIELD_NAME).toString()) : null;
                final boolean fieldMustBeCreated = fieldRelation == null || (includedPrefixes.isEmpty() || includedPrefixes.stream().anyMatch(fieldRelation::startsWith)) || !excludedPrefixes.isEmpty() && excludedPrefixes.stream().noneMatch(fieldRelation::startsWith);
                final String fieldType = getJavaType(fieldProps.get("type").toString());
                final String fieldNameSanitized = "boolean".equals(fieldType) ? "is" + capitalize(entry.getKey()) : entry.getKey();

                if (fieldMustBeCreated) {
                    if("Date".equals(fieldType)) {
                        listOfImports.add("import java.util.Date;");
                    }

                    FieldTemplate newField = new FieldTemplate(entry.getKey(), formatFieldName(fieldNameSanitized), fieldType);

                    if (fieldRelation != null) {
                        listOfImports.add("import ch.helvethink.odoo4java.models.FieldRelation;");
                        listOfImports.add("import ch.helvethink.odoo4java.models.OdooModel;");

                        final String fieldRelationImport = rootPackage + fieldRelation;

                        newField.setFieldModel(fieldRelation);
                        newField.setFieldRelation(fieldRelationImport);
                        // If generated class and its field are in the same package there's no need to import
                        if (!fieldRelationImport.equals(currentClassPackageName)) {
                            listOfImports.add("import " + fieldRelationImport + ";");
                        }

                        switch (fieldType) {
                            case "OdooId":
                                newField.setOdooId(true);
                                listOfImports.add("import ch.helvethink.odoo4java.models.OdooId;");
                                fieldsToCreate.add(new FieldTemplate(newField));
                                break;
                            case "List<Integer>":
                                listOfImports.add("import java.util.List;");
                                newField.setOdooListIds(true);
                                fieldsToCreate.add(new FieldTemplate(newField));
                                break;
                            default:
                                break;
                        }
                    }
                    fieldsToCreate.add(newField);
                }

                TemplateEngine templateEngine = TemplateEngine.createPrecompiled(ContentType.Plain);
                try (final FileOutput output = new FileOutput(Path.of(directory + "/" + className + ".java"))) {
                    templateEngine.render("JavaClass", new TemplateData(rootPackage + currentClassPackageName, modelName, className, String.join("\n", listOfImports), fieldsToCreate), output);
                }
            }
        }

    }

    public record TemplateData(String packageName, String modelName, String className, String importsList, List<FieldTemplate> fields) {
    }

    /**
     * Compute the class full qualified name from an Odoo model name
     *
     * @param modelName The name of the Odoo model as it is returned by the Odoo instance
     * @return The class FQDN, i.e. packageName + class
     */
    static String computeClassFQDN(final String modelName) {
        final String[] splittedModelName = modelName.split("\\.");
        if (modelName.split("\\.").length < 2) {
            return computeClassFQDN("generic." + modelName);
        }
        if (Arrays.asList(modelName.split("\\.")).contains("import")) {
            //specific for ImportModule (because "import" totally sucked as object name)
            return computeClassFQDN(modelName.replace("import", "odooimport"));
        }
        final StringBuilder classfqdnSB = new StringBuilder();
        // There's at least one element as package name into the model
        classfqdnSB.append(splittedModelName[0]);
        for (int i = 1; i < splittedModelName.length; i++) {
            if (i == splittedModelName.length - 1) {
                // 2 use cases to manage: AccountAccount and AccountXXX for Instance
                if (splittedModelName[i - 1].equals(splittedModelName[i].toLowerCase())) {
                    classfqdnSB.append(".").append(capitalize(splittedModelName[i].toLowerCase()));
                } else {
                    classfqdnSB.append(".").append(sanitizeClassName(modelName));
                }
            } else {
                classfqdnSB.append(".").append(splittedModelName[i].toLowerCase());
            }
        }
        return classfqdnSB.toString();
    }
}