All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.flexiblepower.pythoncodegen.PythonCodegen Maven / Gradle / Ivy

/*-
 * #%L
 * dEF-Pi python service creation
 * %%
 * Copyright (C) 2017 - 2018 Flexible Power Alliance Network
 * %%
 * Licensed 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.
 * #L%
 */
package org.flexiblepower.pythoncodegen;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

import org.flexiblepower.codegen.PluginUtils;
import org.flexiblepower.codegen.model.InterfaceDescription;
import org.flexiblepower.codegen.model.InterfaceVersionDescription;
import org.flexiblepower.codegen.model.InterfaceVersionDescription.Type;
import org.flexiblepower.codegen.model.ServiceDescription;
import org.flexiblepower.pythoncodegen.compiler.PyXBCompiler;
import org.flexiblepower.pythoncodegen.compiler.PythonProtoCompiler;

import com.github.fge.jsonschema.core.exceptions.ProcessingException;

import lombok.extern.slf4j.Slf4j;

/**
 * CreateComponentMojo
 *
 * @version 0.1
 * @since Jun 28, 2017
 */
@Slf4j
public class PythonCodegen {

    /**
     * The file where the package declaration should be placed in
     */
    public static final String PACKAGE_DECLARATION = "__init__.py";

    /**
     * Use this version of the protobuf compiler (which is downloaded automatically)
     */
    private static final String PROTOBUF_VERSION = "3.3.0";

    // Constants where the files should be found relative to the working directory
    private static final String DEFAULT_SERVICE_FILE = "service.json";
    private static final String MODULE_DECLARATION = "__main__.py";
    private static final String RESOURCE_LOCATION = "resources";
    private static final String SOURCE_LOCATION = "service";
    private static final String REQUIREMENTS_LOCATION = "";

    /** The location where to find and place all interface resources, relative to {@value #RESOURCE_LOCATION} */
    private static final String PROTO_INPUT_LOCATION = "";
    private static final String PROTO_OUTPUT_LOCATION = "proto";
    private static final String XSD_INPUT_LOCATION = "";
    private static final String XSD_OUTPUT_LOCATION = "xsd";

    /** The location of the docker files also relative to {@value #RESOURCE_LOCATION} */
    private static final String DOCKER_LOCATION = "docker";
    private static final String DOCKER_ARM_LOCATION = "docker-arm";

    /** The location of the model files are placed as a subfolder of the {@value #SOURCE_LOCATION} */
    private static final String MODEL_SOURCE_LOCATION = "model";

    /** The entrypoint of the docker file, i.e. what to run in the process */
    private static final String DOCKER_ENTRY_POINT = "python -m service";

    private final Map hashes = new HashMap<>();

    private PythonProtoCompiler protoCompiler;

    private Path resourcePath;
    private PythonTemplates templates;
    private PyXBCompiler pyXbCompiler;

    /**
     * Main function of the code generator, which will call the several stages of the generation
     *
     * @throws ProcessingException When an invalid service description is provided
     * @throws IOException When unable to read the service description, or writing the output files
     */
    public void run() throws IOException, ProcessingException {
        final File serviceDescriptionFile = Paths.get(PythonCodegen.DEFAULT_SERVICE_FILE).toFile();
        if (!PluginUtils.validateServiceDefinition(serviceDescriptionFile)) {
            throw new IOException("Invalid service description, see message log");
        }

        final ServiceDescription service = PluginUtils.readServiceDefinition(serviceDescriptionFile);
        service.setId(PluginUtils.camelCaps(service.getName()));

        this.resourcePath = Paths.get(PythonCodegen.RESOURCE_LOCATION);
        Files.createDirectories(this.resourcePath);

        final Path pythonSourceFolder = Paths.get(PythonCodegen.SOURCE_LOCATION);
        Files.createDirectories(pythonSourceFolder);

        // Add descriptors and related hashes
        this.compileDescriptors(service);

        // Add templates to generate java code and the dockerfile
        this.templates = new PythonTemplates(service);

        this.createPythonFiles(service, pythonSourceFolder);
        this.createDockerfiles(service);
        this.createRequirements();
    }

    /**
     * Create stubs for the service implementation. By using the templates in
     * the Template object.
     *
     * @throws IOException
     */
    private void createPythonFiles(final ServiceDescription serviceDescription, final Path dest) throws IOException {
        PythonCodegen.log.debug("Creating stubs");

        final String ext = ".py";
        Files.createDirectories(dest);

        final Path moduleMain = dest.resolve(PythonCodegen.MODULE_DECLARATION);
        if (moduleMain.toFile().exists()) {
            PythonCodegen.log.debug("Skipping existing file " + moduleMain.toString());
        } else {
            Files.write(moduleMain, this.templates.generateServiceMain().getBytes());
        }

        final Path serviceImpl = dest.resolve(PythonCodegenUtils.serviceImplClass(serviceDescription) + ext);
        if (serviceImpl.toFile().exists()) {
            PythonCodegen.log.debug("Skipping existing file " + serviceImpl.toString());
        } else {
            Files.write(serviceImpl, this.templates.generateServiceImplementation().getBytes());
        }

        for (final InterfaceDescription itf : serviceDescription.getInterfaces()) {
            final Path interfacePath = Files
                    .createDirectories(dest.resolve(PythonCodegenUtils.getInterfacePackage(itf)));
            if (Files.notExists(interfacePath.resolve(PythonCodegen.PACKAGE_DECLARATION))) {
                Files.createFile(interfacePath.resolve(PythonCodegen.PACKAGE_DECLARATION));
            }
            final Path manager = interfacePath.resolve(PythonCodegenUtils.managerInterface(itf) + ext);
            final Path managerImpl = interfacePath.resolve(PythonCodegenUtils.managerClass(itf) + ext);

            // Write interface files
            Files.write(manager, this.templates.generateManagerInterface(itf).getBytes());

            if (managerImpl.toFile().exists()) {
                PythonCodegen.log.debug("Skipping existing file " + managerImpl.toString());
            } else {
                Files.write(managerImpl, this.templates.generateManagerImplementation(itf).getBytes());
            }

            for (final InterfaceVersionDescription version : itf.getInterfaceVersions()) {
                final Path interfaceVersionPath = Files
                        .createDirectories(interfacePath.resolve(PythonCodegenUtils.getVersion(version)));
                if (Files.notExists(interfaceVersionPath.resolve(PythonCodegen.PACKAGE_DECLARATION))) {
                    Files.createFile(interfaceVersionPath.resolve(PythonCodegen.PACKAGE_DECLARATION));
                }
                // Create intermediate directories
                final Path connectionHandler = interfaceVersionPath
                        .resolve(PythonCodegenUtils.connectionHandlerInterface(itf, version) + ext);
                final Path connectionHandlerImpl = interfaceVersionPath
                        .resolve(PythonCodegenUtils.connectionHandlerClass(itf, version) + ext);

                // Write files
                Files.write(connectionHandler, this.templates.generateHandlerInterface(itf, version).getBytes());

                if (connectionHandlerImpl.toFile().exists()) {
                    PythonCodegen.log.debug("Skipping existing file " + connectionHandlerImpl.toString());
                } else {
                    Files.write(connectionHandlerImpl,
                            this.templates.generateHandlerImplementation(itf, version).getBytes());
                }

            }
        }
    }

    /**
     * Create the Dockerfile
     *
     * @param service
     *            The current ServiceDescription object
     * @throws IOException
     * @throws HashComputeException
     */
    private void createDockerfiles(final ServiceDescription service) throws IOException {
        PythonCodegen.log.debug("Creating Dockerfiles");

        final Path dockerFolder = Files.createDirectories(this.resourcePath.resolve(PythonCodegen.DOCKER_LOCATION));
        final Path dockerArmFolder = Files
                .createDirectories(this.resourcePath.resolve(PythonCodegen.DOCKER_ARM_LOCATION));

        Files.write(dockerFolder.resolve("Dockerfile"),
                this.templates.generateDockerfile("x86", PythonCodegen.DOCKER_ENTRY_POINT).getBytes());

        Files.write(dockerArmFolder.resolve("Dockerfile"),
                this.templates.generateDockerfile("arm", PythonCodegen.DOCKER_ENTRY_POINT).getBytes());
    }

    private void createRequirements() throws IOException {
        final Path requirements = Paths.get(PythonCodegen.REQUIREMENTS_LOCATION).resolve("requirements.txt");
        if (requirements.toFile().exists()) {
            PythonCodegen.log.debug("Skipping existing file " + requirements.toString());
        } else {
            Files.write(requirements, this.templates.generateRequirements().getBytes());
        }
    }

    /**
     * @param service
     * @throws IOException
     */
    private void compileDescriptors(final ServiceDescription service) throws IOException {
        PythonCodegen.log.debug("Compiling descriptors definitions to java code");

        this.protoCompiler = new PythonProtoCompiler(PythonCodegen.PROTOBUF_VERSION);
        this.pyXbCompiler = new PyXBCompiler();

        for (final InterfaceDescription iface : service.getInterfaces()) {
            for (final InterfaceVersionDescription versionDescription : iface.getInterfaceVersions()) {

                if (versionDescription.getType().equals(Type.PROTO)) {
                    this.compileProtoDescriptor(iface, versionDescription);

                } else if (versionDescription.getType().equals(Type.XSD)) {
                    this.compileXSDDescriptor(iface, versionDescription);
                }
            }
        }
    }

    /**
     * @param itf
     * @param versionDescription
     * @throws IOException
     */
    private void compileXSDDescriptor(final InterfaceDescription itf, final InterfaceVersionDescription vitf)
            throws IOException {
        // First get the hash of the input file
        final Path xsdSourceFilePath = PluginUtils.downloadFileOrResolve(vitf.getLocation(),
                Paths.get(PythonCodegen.XSD_INPUT_LOCATION));

        // Compute hash and store in interface
        final String interfaceHash = PluginUtils.SHA256(xsdSourceFilePath);
        vitf.setHash(interfaceHash);

        if (this.hashes.containsKey(interfaceHash)) {
            vitf.setModelPackageName(this.hashes.get(interfaceHash).getModelPackageName());
            return;
        }

        // Get the package name and add the hash
        final String versionedName = PythonCodegenUtils.getVersionedName(itf, vitf);

        // Store for later reference
        vitf.setModelPackageName(PythonCodegen.SOURCE_LOCATION + "." + PythonCodegen.MODEL_SOURCE_LOCATION + "."
                + versionedName + ".xsd");
        this.hashes.put(interfaceHash, vitf);

        // Append additional compilation info to the proto file and compile the java code
        final Path xsdResourceFolder = Files
                .createDirectories(this.resourcePath.resolve(PythonCodegen.XSD_OUTPUT_LOCATION));
        final Path xsdDestFilePath = xsdResourceFolder.resolve(versionedName + ".xsd");

        // Copy the descriptor and start compilation
        Files.copy(xsdSourceFilePath, xsdDestFilePath, StandardCopyOption.REPLACE_EXISTING);

        this.pyXbCompiler.compile(xsdDestFilePath,
                Paths.get(PythonCodegen.SOURCE_LOCATION).resolve(PythonCodegen.MODEL_SOURCE_LOCATION));
    }

    /**
     * @param itf
     * @param vitf
     * @throws IOException
     */
    private void compileProtoDescriptor(final InterfaceDescription itf, final InterfaceVersionDescription vitf)
            throws IOException {
        final Path protoSourceFilePath = PluginUtils.downloadFileOrResolve(vitf.getLocation(),
                Paths.get(PythonCodegen.PROTO_INPUT_LOCATION));

        // Compute hash and store in interface
        final String interfaceHash = PluginUtils.SHA256(protoSourceFilePath);
        vitf.setHash(interfaceHash);

        if (this.hashes.containsKey(interfaceHash)) {
            // If we already have it, just copy the package name
            vitf.setModelPackageName(this.hashes.get(interfaceHash).getModelPackageName());
            return;
        }

        // Get the package name and add the hash
        final String packagePath = Paths.get(PythonCodegen.SOURCE_LOCATION)
                .resolve(PythonCodegen.MODEL_SOURCE_LOCATION)
                .toString();
        final String versionedName = PythonCodegenUtils.getVersionedName(itf, vitf);
        final String protoClassName = versionedName + "_pb2";

        // Store for later reference
        vitf.setModelPackageName(packagePath + "." + protoClassName);
        this.hashes.put(interfaceHash, vitf);

        // Append additional compilation info to the proto file and compile the java code
        final Path protoDestFilePath = Files
                .createDirectories(this.resourcePath.resolve(PythonCodegen.PROTO_OUTPUT_LOCATION))
                .resolve(versionedName + ".proto");
        Files.copy(protoSourceFilePath, protoDestFilePath, StandardCopyOption.REPLACE_EXISTING);

        try (final Scanner scanner = new Scanner(new FileInputStream(protoSourceFilePath.toFile()), "UTF-8")) {
            Files.write(protoDestFilePath,
                    ("syntax = \"proto2\";\n\npackage " + versionedName + ";\n" + scanner.useDelimiter("\\A").next())
                            .getBytes());
        }

        this.protoCompiler.compile(protoDestFilePath,
                Paths.get(PythonCodegen.SOURCE_LOCATION).resolve(PythonCodegen.MODEL_SOURCE_LOCATION));
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy