com.betfair.cougar.codegen.IdlToDSMojo.java Source code

Java tutorial

Introduction

Here is the source code for com.betfair.cougar.codegen.IdlToDSMojo.java

Source

/*
 * Copyright 2014, The Sporting Exchange Limited
 *
 * 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.betfair.cougar.codegen;

import com.betfair.cougar.codegen.except.PluginException;
import com.betfair.cougar.codegen.resolver.DefaultSchemaCatalogSource;
import com.betfair.cougar.codegen.resolver.InterceptingResolver;
import com.betfair.cougar.codegen.resolver.SchemaCatalogSource;
import com.betfair.cougar.codegen.resource.ResourceLoader;
import com.betfair.cougar.transformations.CougarTransformations;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.project.MavenProject;
import org.w3c.dom.Document;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
import java.util.List;

/**
 * A plugin which is responsible for generating Cougar-based services. This encompasses a number
 * of code- and file-generation steps, as well as validation. The intention is for this mojo to
 * do everything needed, keeping the plugins/plugin section of a Cougar service's pom as simple as
 * possible.
 * <p>
 * <h2>NOTE: idd dependencies</h2>
 * IDDs can be read from the file system or as resources. The IDD is expected to be named after the
 * service (see {@link #services} param), suffixed with {@code .xml} for the service and
 * {@code -Extensions.xml} for the extensions definition. If you're using an IDD file then it should
 *  be in {@code /src/main/resources}. Switch between the two modes using the {@link #iddAsResource}
 *  flag.
 * <p>
 * A gotcha exists when accessing IDDs as resources.
 * Since the IDDs are not required at run-time, it would make sense to include the relevant IDD
 * project (jar) as a plugin dependency (ie. in {@code project/plugins/plugin/dependencies} as opposed to a
 * project dependency of {@code project/dependencies}). You can do this <em>unless</em> your
 * service is built as part of a larger project tree, in which multiple services are built. Maven
 * resolves dependencies for the plugin once, so you can't have projectA relying on projectA-idd
 * and project B relying on projectB-idd respectively - you end up with both projects relying on
 * (say) projectA-idd. To work around this, you have to include the IDD as part of the project
 * dependencies.
 * <p>
 * TODO If there's an easy way to fix this, we should do so (maven-savvy volunteers welcome)
 *
 * @goal process
 *
 * @phase generate-sources
 * @requiresDependencyResolution
 */
public class IdlToDSMojo extends AbstractMojo {

    private static final String RESOURCES_DIR = "src/main/resources";

    // =============================================================================================
    //   Mojo params
    // =============================================================================================

    //   this contains only those params which are mandatory or which need to be initialised by
    //   maven
    //
    //   it's not that hard to turn other class members into params, but if users don't need them
    //   and don't know about them, then the simpler things remain.
    //
    //  all access to these MUST be via the associated getters to enable subclasses to work

    /**
     * We may need the runtime classpath to access the idds if we're doing resource-based loading.
     * @parameter default-value="${project.runtimeClasspathElements}"
     * @readonly
     */
    private List<String> runtimeClassPath;

    protected List<String> getRuntimeClassPath() {
        return runtimeClassPath;
    }

    /**
      * name of service.
      * @parameter
      * @required
      */
    private Service[] services;

    protected Service[] getServices() {
        return services;
    }

    /**
     * the base directory of the project
     * @parameter default-value="${basedir}"
     */
    private String baseDir;

    protected String getBaseDir() {
        return baseDir;
    }

    /**
     * Either {@code mvn -o} or in settings.xml
     *
     *  @parameter expression="${settings.offline}"
     */
    private boolean offline;

    protected boolean isOffline() {
        return offline;
    }

    /**
     * @parameter expression="${project}"
     * @required
     * @readonly
     */
    private MavenProject project;

    protected MavenProject getProject() {
        return project;
    }

    /**
     * If set to true, generate a client version of generated code.
     * @parameter
     */

    private boolean client;

    protected boolean isClient() {
        return client;
    }

    /**
     * If set to true, generate a server version of generated code.
     * @parameter
     */

    private boolean server;

    protected boolean isServer() {
        return server;
    }

    /**
     * Read IDDs and files from the file system (as opposed to as resources). Use as transition
     * for existing services, and also as a way to make writing/testing new services a bit simpler.
     *
     * @parameter
     */
    private boolean iddAsResource = false;

    protected boolean isIddAsResource() {
        return iddAsResource;
    }

    // =============================================================================================
    //   POJO stuff
    // =============================================================================================

    /**
     * Location of generated sources (relative to baseDir)
     *
     * This could be a property, but as noted above, not making it one until we need to
     */
    private String generatedSourceDir = "/target/generated-sources/java";

    /**
     * Location (resource) of the wsdl style sheet to be used
     */
    private String wsdlXslResource = "wsdl-xsl/wsdl.xsl";

    /**
     * Location (resource) of the xsd style sheet to be used
     */
    private String xsdXslResource = "xsd-xsl/xsd.xsl";

    /**
    * File location of the on-disk iddstripper.csl, relative to base directory
    */
    private String iddStripperXslResource = "bsidl/iddstripper.xsl";

    /**
     * File location of the on-disk wsdl.xsl, relative to base directory
     */
    private String iddStripperXslFile = "target/wrk/iddstripper.xsl";

    /**
     * File location of the on-disk wsdl.xsl, relative to base directory
     */
    private String wsdlXslFile = "target/wrk/wsdl.xsl";

    /**
     * File location of the on-disk wsdl.xsl, relative to base directory
     */
    private String xsdXslFile = "target/wrk/xsd.xsl";

    /**
     * Location for storing our catalog file and schemas for validation.
     */
    private String schemaDir = "target/wrk/schemas";

    /**
     * Actual file pointing to wsdl.xsl. Initialised by {@link #prepWsdlXsl()}.
     */
    private File wsdlXsl;

    /**
     * Actual file pointing to xsd.xsl. Initialised by {@link #prepXsdXsl()}.
     */
    private File xsdXsl;

    /**
     * Actual file pointing to iddstripper.xsl. Initialised by {@link #prepIddStripperXsl()}.
     */
    private File iddStripperXsl;

    /**
     * Actual file pointing to the catalog.xml used for validation.
     * Initialised by {@link #unwrapSchemas()}.
     */
    private File catalogFile = null;

    private InterceptingResolver resolver;

    private ResourceLoader resourceLoader;

    private XPathExpression namespaceExpr;

    public void execute() throws MojoExecutionException {
        getLog().info("Starting Cougar code generation");
        if (isOffline()) {
            getLog().warn("Maven in offline mode, plugin is NOT validating IDDs against schemas");
        } else {
            getLog().debug("Unbundling schemas for validation");
            catalogFile = unwrapSchemas();
        }
        initResourceLoader();
        initResolver(); // needs the resource loader

        // load wsdl.xsl (as resource) and write (as file) to a working directory
        prepWsdlXsl();

        // load xsd.xsl (as resource) and write (as file) to a working directory
        prepXsdXsl();

        // load iddstripper.xsl (as resource) and write (as file) to a working directory
        prepIddStripperXsl();

        try {
            getLog().debug("Starting IDL to Java");

            for (Service service : getServices()) {
                processService(service);
            }

            // this replaces the functionality of build-helper-maven-plugin
            addSource();
        } catch (Exception e) {
            getLog().error(e);
            throw new MojoExecutionException("Failed processing IDL: " + e, e);
        }

        getLog().info("Completed Cougar code generation");
    }

    private void prepIddStripperXsl() throws MojoExecutionException {
        try {
            iddStripperXsl = new File(getBaseDir(), iddStripperXslFile);
            initOutputDir(iddStripperXsl.getParentFile());
            writeIDDStylesheet(iddStripperXsl);
        } catch (Exception e) {
            throw new MojoExecutionException("Failed to write local copy of IDD stripper stylesheet: " + e, e);
        }

    }

    /**
     * Read a wsdl.xsl (stylesheet) from a resource, and write it to a working directory.
     *
     * @return the on-disk wsdl.xsl file
     */
    private void prepWsdlXsl() throws MojoExecutionException {
        try {
            wsdlXsl = new File(getBaseDir(), wsdlXslFile);
            initOutputDir(wsdlXsl.getParentFile());
            writeWsdlStylesheet(wsdlXsl);
        } catch (Exception e) {
            throw new MojoExecutionException("Failed to write local copy of WSDL stylesheet: " + e, e);
        }
    }

    /**
     * Read a wsdl.xsl (stylesheet) from a resource, and write it to a working directory.
     *
     * @return the on-disk wsdl.xsl file
     */
    private void prepXsdXsl() throws MojoExecutionException {
        try {
            xsdXsl = new File(getBaseDir(), xsdXslFile);
            initOutputDir(xsdXsl.getParentFile());
            writeXsdStylesheet(xsdXsl);
        } catch (Exception e) {
            throw new MojoExecutionException("Failed to write local copy of XSD stylesheet: " + e, e);
        }
    }

    /**
     * Various steps needing to be done for each IDD
     */
    private void processService(Service service) throws Exception {

        getLog().info("  Service: " + service.getServiceName());

        Document iddDoc = parseIddFile(service.getServiceName());

        // 1. validate
        if (!isOffline()) {
            getLog().debug("Validating XML..");
            new XmlValidator(resolver).validate(iddDoc);
        }

        // 2. generate outputs
        generateJavaCode(service, iddDoc);
    }

    private void generateExposedIDD(Document iddDoc, String serviceName, String version) throws Exception {

        File iddFile = new File(getBaseDir(),
                "target/generated-resources/idd/" + serviceName + "_" + version.replace("_", ".") + "_Exposed.idd");
        getLog().debug("Writing to idd file " + iddFile);
        initOutputDir(iddFile.getParentFile());

        ExposedIDDGenerator.transform(iddDoc, iddStripperXsl, iddFile);

    }

    /**
     * Find, open and parse the IDD implied by the specified service name. Reads either an explicit
     * file or else a resource based on {@link #iddAsResource} flag.
     * <p>
     * The implied name is simply the service name + ".xml".
     */
    private Document parseIddFile(String serviceName) {

        String iddFileName = serviceName + ".xml";

        if (isIddAsResource()) {
            InputStream is = resourceLoader.getResourceAsStream(iddFileName);
            if (is == null) {
                throw new RuntimeException("Cannot open IDD resource named '" + iddFileName + "'");
            }
            return XmlUtil.parse(is, resolver);
        } else {
            File iddFile = new File(new File(getBaseDir(), RESOURCES_DIR), iddFileName);
            if (!iddFile.exists()) {
                throw new RuntimeException("Cannot open IDD file named '" + iddFileName + "'");
            }
            return XmlUtil.parse(iddFile, resolver);
        }
    }

    /**
    * Find, open and parse the IDD implied by the specified service name. Reads either an explicit
    * file or else a resource based on {@link #iddAsResource} flag.
    * <p>
    * The implied name is simply the service name + ".xml".
    */
    private Document parseIddFromString(String iddContent) {

        return XmlUtil.parse(new ByteArrayInputStream(iddContent.getBytes()), resolver);

    }

    /**
      * Find, open and parse the extensions xml file or null if it doesn't exist. Reads from
      * file or resource based on {@link #iddAsResource} flag.
      * <p>
      * Name of extensions file should be ServiceName-Extensions.xml.
      */
    private Document parseExtensionFile(String serviceName) {

        String extensionFileName = serviceName + "-Extensions.xml";

        if (isIddAsResource()) {
            InputStream is = resourceLoader.getResourceAsStream(extensionFileName);
            if (is != null) {
                return XmlUtil.parse(is, resolver);
            } else {
                return null;
            }
        } else {
            File extensionsFile = new File(new File(getBaseDir(), RESOURCES_DIR), extensionFileName);
            if (extensionsFile.exists()) {
                return XmlUtil.parse(extensionsFile, resolver);
            } else {
                return null;
            }
        }
    }

    /**
      * The original concept of the IDLReader (other devs) has gone away a bit, so there could be
      * some refactoring around this.
      */
    private void generateJavaCode(Service service, Document iddDoc) throws Exception {

        IDLReader reader = new IDLReader();

        Document extensionDoc = parseExtensionFile(service.getServiceName());

        String packageName = derivePackageName(service, iddDoc);

        reader.init(iddDoc, extensionDoc, service.getServiceName(), packageName, getBaseDir(), generatedSourceDir,
                getLog(), service.getOutputDir(), isClient(), isServer());

        runMerge(reader);

        // also create the stripped down, combined version of the IDD doc
        getLog().debug("Generating combined IDD sans comments...");
        Document combinedIDDDoc = parseIddFromString(reader.serialize());
        // WARNING: this absolutely has to be run after a call to reader.runMerge (called by runMerge above) as otherwise the version will be null...
        generateExposedIDD(combinedIDDDoc, reader.getInterfaceName(), reader.getInterfaceMajorMinorVersion());

        // generate WSDL/XSD
        getLog().debug("Generating wsdl...");
        generateWsdl(iddDoc, reader.getInterfaceName(), reader.getInterfaceMajorMinorVersion());
        getLog().debug("Generating xsd...");
        generateXsd(iddDoc, reader.getInterfaceName(), reader.getInterfaceMajorMinorVersion());
    }

    /**
    * Package name comes from explicit plugin param (if set), else the namespace definition, else
    * skream and die.
    * <p>
    * Having the plugin override allows backwards compatibility as well as being useful for
    * fiddling and tweaking.
    */
    private String derivePackageName(Service service, Document iddDoc) {

        String packageName = service.getPackageName();
        if (packageName == null) {
            packageName = readNamespaceAttr(iddDoc);
            if (packageName == null) {
                throw new PluginException(
                        "Cannot find a package name " + "(not specified in plugin and no namespace in IDD");
            }
        }
        return packageName;
    }

    private void generateWsdl(Document iddDoc, String serviceName, String version) throws Exception {

        File wsdlFile = new File(getBaseDir(),
                "target/generated-resources/wsdl/" + serviceName + "_" + version.replace("_", ".") + ".wsdl");
        getLog().debug("Writing to wsdl file " + wsdlFile);
        initOutputDir(wsdlFile.getParentFile());

        new XmlGenerator().transform(iddDoc, wsdlXsl, wsdlFile);
    }

    private void generateXsd(Document iddDoc, String serviceName, String version) throws Exception {

        File xsdFile = new File(getBaseDir(),
                "target/generated-resources/xsd/" + serviceName + "_" + version.replace("_", ".") + ".xsd");
        getLog().debug("Writing to xsd file " + xsdFile);
        initOutputDir(xsdFile.getParentFile());

        new XmlGenerator().transform(iddDoc, xsdXsl, xsdFile);
    }

    private void writeWsdlStylesheet(File xslFile) throws Exception {

        if (wsdlXslResource == null) {
            throw new MojoExecutionException("wsdl resource not specified");
        }

        FileUtil.resourceToFile(wsdlXslResource, xslFile, getClass());

        getLog().debug("Wrote wsdl stylesheet from resource " + wsdlXslResource + " to " + xslFile);
    }

    private void writeXsdStylesheet(File xslFile) throws Exception {

        if (xsdXslResource == null) {
            throw new MojoExecutionException("xsd resource not specified");
        }

        FileUtil.resourceToFile(xsdXslResource, xslFile, getClass());

        getLog().debug("Wrote xsd stylesheet from resource " + xsdXslResource + " to " + xslFile);
    }

    private void writeIDDStylesheet(File xslFile) throws Exception {

        if (iddStripperXslResource == null) {
            throw new MojoExecutionException("wsdl resource not specified");
        }

        FileUtil.resourceToFile(iddStripperXslResource, xslFile, getClass());

        getLog().debug("Wrote IDD stylesheet from resource " + iddStripperXslResource + " to " + xslFile);
    }

    private File unwrapSchemas() {

        File dir = new File(getBaseDir(), schemaDir);
        dir.mkdirs();
        return getCatalogSource().getCatalog(dir, getLog());
    }

    protected Transformations getTransformations() {
        return new CougarTransformations();
    }

    @SuppressWarnings("unchecked")
    private void runMerge(IDLReader reader) throws Exception {
        Transformations transformations = getTransformations();

        // First let's mangle the document if need be.
        if (transformations.getManglers() != null) {
            getLog().debug("mangling IDL using " + transformations.getManglers().size() + " manglers");
            for (DocumentMangler m : transformations.getManglers()) {
                getLog().debug(m.getName());
                reader.mangle(m);
            }
        }

        if (transformations.getPreValidations() != null) {
            getLog().debug(
                    "Pre validating IDL using " + transformations.getPreValidations().size() + " pre validations");
            for (Validator v : transformations.getPreValidations()) {
                getLog().debug(v.getName());
                reader.validate(v);
            }

        }

        for (Transformation t : transformations.getTransformations()) {
            getLog().debug(t.toString());
        }
        reader.runMerge(transformations.getTransformations());

        reader.writeResult();

    }

    /**
     * Set up and validate the creation of the specified output directory
     */
    private void initOutputDir(File outputDir) {

        if (!outputDir.exists()) {
            if (!outputDir.mkdirs()) {
                throw new IllegalArgumentException("Output Directory " + outputDir + " could not be created");
            }
        }
        if (!outputDir.isDirectory() || (!outputDir.canWrite())) {
            throw new IllegalArgumentException(
                    "Output Directory " + outputDir + " is not a directory or cannot be written to.");
        }
    }

    /**
     * Add the generated-sources directory to the classpath.
     * <p>
     * This one-liner is nicked from build-helper-maven-plugin v1.4, AddSourceMojo.java.
     */
    private void addSource() {

        // TODO this should be shared between here and IDLReader
        File generatedSources = new File(getBaseDir(), generatedSourceDir);

        this.getProject().addCompileSourceRoot(generatedSources.getAbsolutePath());
        this.getLog().debug("Source directory " + generatedSources + " added.");
    }

    private void initResolver() {

        // catalogs aren't needed if we're offline because we don't validate
        String[] catalogs = isOffline() ? new String[0] : new String[] { catalogFile.getAbsolutePath() };

        resolver = new InterceptingResolver(getLog(), (isIddAsResource() ? resourceLoader : null), catalogs);
    }

    private void initResourceLoader() throws MojoExecutionException {

        try {
            if (isIddAsResource()) {
                // we need this classLoader because it's the only way to get to the project dependencies
                resourceLoader = new ResourceLoader(getRuntimeClassPath());
            } else {
                resourceLoader = new ResourceLoader();
            }
        } catch (Exception e) {
            throw new MojoExecutionException("Error initialising classloader: " + e, e);
        }
    }

    private XPathExpression initNamespaceAttrExpression() {

        XPathFactory xfactory = XPathFactory.newInstance();
        XPath xpath = xfactory.newXPath();
        try {
            return xpath.compile("/interface/@namespace");
        } catch (XPathExpressionException e) {
            throw new PluginException("Error compiling namespace XPath expression: " + e, e);
        }
    }

    /**
     * Retrieve 'namespace' attr of interface definition or null if not found
     */
    private String readNamespaceAttr(Document doc) {

        // lazy loading is mostly pointless but it keeps things together
        if (namespaceExpr == null) {
            namespaceExpr = initNamespaceAttrExpression();
        }

        String s;
        try {
            s = namespaceExpr.evaluate(doc);
        } catch (XPathExpressionException e) {
            throw new PluginException("Error evaluating namespace XPath expression: " + e, e);
        }
        // xpath returns an empty string if not found, null is cleaner for callers
        return (s == null || s.length() == 0) ? null : s;
    }

    /**
     * For tests
     */
    void setBaseDir(String s) {
        this.baseDir = s;
    }

    /**
     * For tests
     */
    void setWsdlXslResource(String s) {
        this.wsdlXslResource = s;
    }

    /**
     * For tests
     */
    void setXsdXslResource(String xsdXslResource) {
        this.xsdXslResource = xsdXslResource;
    }

    /**
     * For tests
     */
    void setServices(Service[] services) {//NOSONAR
        this.services = services;
    }

    protected SchemaCatalogSource getCatalogSource() {
        return new DefaultSchemaCatalogSource();
    }
}