/*
* Copyright 2013, 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();
}
}