com.phoenixnap.oss.ramlapisync.plugin.SpringMvcEndpointGeneratorMojo.java Source code

Java tutorial

Introduction

Here is the source code for com.phoenixnap.oss.ramlapisync.plugin.SpringMvcEndpointGeneratorMojo.java

Source

/*
 * Copyright 2002-2016 the original author or authors.
 * 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.
 */
package com.phoenixnap.oss.ramlapisync.plugin;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.classworlds.realm.ClassRealm;
import org.jsonschema2pojo.Annotator;
import org.jsonschema2pojo.GenerationConfig;
import org.jsonschema2pojo.Jackson1Annotator;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import com.phoenixnap.oss.ramlapisync.data.ApiBodyMetadata;
import com.phoenixnap.oss.ramlapisync.data.ApiResourceMetadata;
import com.phoenixnap.oss.ramlapisync.generation.RamlParser;
import com.phoenixnap.oss.ramlapisync.generation.rules.ConfigurableRule;
import com.phoenixnap.oss.ramlapisync.generation.rules.Rule;
import com.phoenixnap.oss.ramlapisync.generation.rules.Spring4ControllerStubRule;
import com.phoenixnap.oss.ramlapisync.naming.NamingHelper;
import com.phoenixnap.oss.ramlapisync.naming.SchemaHelper;
import com.phoenixnap.oss.ramlapisync.raml.RamlRoot;
import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.JDefinedClass;

/**
 * Maven Plugin MOJO specific to Generation of Spring MVC Endpoints from RAML documents.
 *
 * @author Kurt Paris
 * @since 0.2.1
 */
@Mojo(name = "generate-springmvc-endpoints", requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, threadSafe = true)
public class SpringMvcEndpointGeneratorMojo extends AbstractMojo {

    /**
     * Maven project - required for project info
     */
    @Parameter(defaultValue = "${project}", required = true, readonly = true)
    protected MavenProject project;

    @Parameter(defaultValue = "${plugin}", readonly = true)
    private PluginDescriptor descriptor;

    /**
     * Path to the raml document to be verified
     */
    @Parameter(required = true, readonly = true, defaultValue = "")
    protected String ramlPath;

    /**
     * Relative file path where the Java files will be saved to
     */
    @Parameter(required = false, readonly = true, defaultValue = "")
    protected String outputRelativePath;

    /**
     * IF this is set to true, we will only parse methods that consume, produce or accept the
     * requested defaultMediaType
     */
    @Parameter(required = false, readonly = true, defaultValue = "false")
    protected Boolean addTimestampFolder;

    /**
     * Java package to be applied to the generated files
     */
    @Parameter(required = true, readonly = true, defaultValue = "")
    protected String basePackage;

    /**
     * The URI or relative path to the folder/network location containing JSON Schemas
     */
    @Parameter(required = false, readonly = true, defaultValue = "")
    protected String schemaLocation;

    /**
     * A boolean indicating whether the POJOs for unreferenced schemas defined in the RAML file
     * should be generated. By default, such schemas are not generated.
     */
    @Parameter(required = false, readonly = true, defaultValue = "false")
    protected Boolean generateUnreferencedSchemas;

    /**
     * The explicit base path under which the rest endpoints should be located.
     * If overrules the baseUri setting in the raml spec.
     */
    @Parameter(required = false, readonly = true)
    protected String baseUri;

    /**
     * If set to true, we will generate seperate methods for different content types in the RAML
     */
    @Parameter(required = false, readonly = true, defaultValue = "false")
    protected Boolean seperateMethodsByContentType;

    /**
     * If set to true, we will generate Jackson 1 annotations inside the model objects
     */
    @Parameter(required = false, readonly = true, defaultValue = "false")
    protected Boolean useJackson1xCompatibility;

    /**
     * The full qualified name of the Rule that should be used for code generation.
     */
    @Parameter(required = false, readonly = true, defaultValue = "com.phoenixnap.oss.ramlapisync.generation.rules.Spring4ControllerStubRule")
    protected String rule;

    /**
     * Map of key/value configuration parameters that can be used to modify behaviour or certain
     * rules
     */
    @Parameter(required = false, readonly = true)
    protected Map<String, String> ruleConfiguration = new LinkedHashMap<>();

    /**
     * Configuration passed to JSONSchema2Pojo for generation of pojos.
     */
    @Parameter(required = false, readonly = true)
    protected PojoGenerationConfig generationConfig = new PojoGenerationConfig();

    /**
     * If set to true, we will generate methods with HttpHeaders as a parameter
     */
    @Parameter(required = false, readonly = true, defaultValue = "false")
    protected Boolean injectHttpHeadersParameter;

    private ClassRealm classRealm;

    private String resolvedSchemaLocation;

    protected void generateEndpoints() throws MojoExecutionException, MojoFailureException, IOException {

        String resolvedPath = project.getBasedir().getAbsolutePath();
        if (resolvedPath.endsWith(File.separator) || resolvedPath.endsWith("/")) {
            resolvedPath = resolvedPath.substring(0, resolvedPath.length() - 1);
        }
        String resolvedRamlPath = project.getBasedir().getAbsolutePath();
        if (!ramlPath.startsWith(File.separator) && !ramlPath.startsWith("/")) {
            resolvedRamlPath += File.separator + ramlPath;
        } else {
            resolvedRamlPath += ramlPath;
        }

        // Resolve schema location and add to classpath
        resolvedSchemaLocation = getSchemaLocation();

        RamlRoot loadRamlFromFile = RamlParser.loadRamlFromFile(new File(resolvedRamlPath).toURI().toString());
        RamlParser par = new RamlParser(basePackage, getBasePath(loadRamlFromFile), seperateMethodsByContentType,
                injectHttpHeadersParameter);
        Set<ApiResourceMetadata> controllers = par.extractControllers(loadRamlFromFile);

        if (StringUtils.hasText(outputRelativePath)) {
            if (!outputRelativePath.startsWith(File.separator) && !outputRelativePath.startsWith("/")) {
                resolvedPath += File.separator;
            }
            resolvedPath += outputRelativePath;
        } else {
            resolvedPath += "/target/generated-sources/spring-mvc";
        }

        File rootDir = new File(
                resolvedPath + (addTimestampFolder == true ? System.currentTimeMillis() : "") + "/");

        if (!rootDir.exists() && !rootDir.mkdirs()) {
            throw new IOException("Could not create directory:" + rootDir.getAbsolutePath());
        }

        generateCode(controllers, rootDir);
        generateUnreferencedSchemas(resolvedRamlPath, loadRamlFromFile, rootDir);
    }

    private void generateUnreferencedSchemas(String resolvedRamlPath, RamlRoot loadRamlFromFile, File rootDir) {

        if (this.generateUnreferencedSchemas) {

            if (loadRamlFromFile.getSchemas() != null && !loadRamlFromFile.getSchemas().isEmpty()) {
                for (Map<String, String> map : loadRamlFromFile.getSchemas()) {
                    for (String schemaName : map.keySet()) {
                        this.getLog().info("Generating POJO for unreferenced schema " + schemaName);
                        ApiBodyMetadata tempBodyMetadata = SchemaHelper.mapSchemaToPojo(loadRamlFromFile,
                                schemaName, resolvedRamlPath, schemaName, this.resolvedSchemaLocation);
                        // TODO Check if this already has been written to disk
                        generateModelSources(tempBodyMetadata, rootDir, this.generationConfig,
                                this.useJackson1xCompatibility == true ? new Jackson1Annotator() : null);
                    }
                }
            }
        }
    }

    private void generateCode(Set<ApiResourceMetadata> controllers, File rootDir) {
        for (ApiResourceMetadata met : controllers) {
            this.getLog().debug("");
            this.getLog().debug("-----------------------------------------------------------");
            this.getLog().debug(met.getName());
            this.getLog().debug("");

            Set<ApiBodyMetadata> dependencies = met.getDependencies();
            for (ApiBodyMetadata body : dependencies) {
                generateModelSources(body, rootDir, generationConfig,
                        useJackson1xCompatibility == true ? new Jackson1Annotator() : null);
            }

            generateControllerSource(met, rootDir);
        }
    }

    /*
     * @return The configuration property <baseUri> (if set) or the baseUri from the RAML spec.
     */
    private String getBasePath(RamlRoot loadRamlFromFile) {
        // we take the given baseUri from raml spec by default.
        String basePath = loadRamlFromFile.getBaseUri();

        // If the baseUri is explicitly set by the plugin configuration we take it.
        if (baseUri != null) {
            basePath = baseUri;
        }

        // Because we can't load an empty string parameter value from maven config
        // the user needs to set a single "/", to overrule the raml spec.
        if (basePath != null && basePath.equals("/")) {
            // We remove a single "/" cause the leading slash will be generated by the raml
            // endpoints.
            basePath = "";
        }

        return basePath;
    }

    @SuppressWarnings("unchecked")
    private Rule<JCodeModel, JDefinedClass, ApiResourceMetadata> loadRule() {
        Rule<JCodeModel, JDefinedClass, ApiResourceMetadata> ruleInstance = new Spring4ControllerStubRule();
        try {
            ruleInstance = (Rule<JCodeModel, JDefinedClass, ApiResourceMetadata>) getClassRealm().loadClass(rule)
                    .newInstance();
            this.getLog().debug(StringUtils.collectionToCommaDelimitedString(ruleConfiguration.keySet()));
            this.getLog().debug(StringUtils.collectionToCommaDelimitedString(ruleConfiguration.values()));

            if (ruleInstance instanceof ConfigurableRule<?, ?, ?> && !CollectionUtils.isEmpty(ruleConfiguration)) {
                this.getLog().debug("SETTING CONFIG");
                ((ConfigurableRule<?, ?, ?>) ruleInstance).applyConfiguration(ruleConfiguration);
            }
        } catch (Exception e) {
            getLog().error("Could not instantiate Rule " + this.rule
                    + ". The default Rule will be used for code generation.", e);
        }
        return ruleInstance;
    }

    private ClassRealm getClassRealm() throws DependencyResolutionRequiredException, MalformedURLException {
        if (classRealm == null) {
            List<String> runtimeClasspathElements = project.getRuntimeClasspathElements();

            classRealm = descriptor.getClassRealm();

            if (classRealm == null) {
                classRealm = project.getClassRealm();
            }

            for (String element : runtimeClasspathElements) {
                File elementFile = new File(element);
                classRealm.addURL(elementFile.toURI().toURL());
            }
        }
        return classRealm;
    }

    private void generateModelSources(ApiBodyMetadata body, File rootDir, GenerationConfig config,
            Annotator annotator) {
        try {
            JCodeModel codeModel;
            if (config == null && annotator == null) {
                codeModel = body.getCodeModel();
            } else {
                codeModel = body.getCodeModel(resolvedSchemaLocation,
                        basePackage + NamingHelper.getDefaultModelPackage(), config, annotator);
            }
            if (codeModel != null) {
                codeModel.build(rootDir);
            }
        } catch (IOException e) {
            e.printStackTrace();
            this.getLog().error("Could not build code model for " + body.getName(), e);
        }
    }

    private String getSchemaLocation() {

        if (StringUtils.hasText(schemaLocation)) {

            if (!schemaLocation.contains(":")) {
                String resolvedPath = project.getBasedir().getAbsolutePath();
                if (resolvedPath.endsWith(File.separator) || resolvedPath.endsWith("/")) {
                    resolvedPath = resolvedPath.substring(0, resolvedPath.length() - 1);
                }

                if (!schemaLocation.startsWith(File.separator) && !schemaLocation.startsWith("/")) {
                    resolvedPath += File.separator;
                }

                resolvedPath += schemaLocation;

                if (!schemaLocation.endsWith(File.separator) && !schemaLocation.endsWith("/")) {
                    resolvedPath += File.separator;
                }
                resolvedPath = resolvedPath.replace(File.separator, "/").replace("\\", "/");
                try {
                    URLClassLoader urlClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
                    Class<?> urlClass = URLClassLoader.class;
                    Method method = urlClass.getDeclaredMethod("addURL", new Class[] { URL.class });
                    method.setAccessible(true);
                    method.invoke(urlClassLoader, new Object[] { new File(resolvedPath).toURI().toURL() });
                    return "classpath:/"; // since we have added this folder to the classpath this
                                          // should be used by the plugin
                } catch (Exception ex) {
                    this.getLog().error("Could not add schema location to classpath", ex);
                    return new File(resolvedPath).toURI().toString();
                }
            }
            return schemaLocation;
        }
        return null;
    }

    private void generateControllerSource(ApiResourceMetadata met, File dir) {
        JCodeModel codeModel = new JCodeModel();
        loadRule().apply(met, codeModel);
        try {
            codeModel.build(dir);
        } catch (IOException e) {
            e.printStackTrace();
            this.getLog().error("Could not build code model for " + met.getName(), e);
        }
    }

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        long startTime = System.currentTimeMillis();

        try {
            generateEndpoints();
        } catch (IOException e) {
            ClassLoaderUtils.restoreOriginalClassLoader();
            throw new MojoExecutionException(e,
                    "Unexpected exception while executing Spring MVC Endpoint Generation Plugin.", e.toString());
        }

        this.getLog().info("Endpoint Generation Complete in:" + (System.currentTimeMillis() - startTime) + "ms");
    }

}