/*
* Copyright 2006-2008 Web Cohesion
*
* 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 org.codehaus.enunciate.modules.csharp;
import freemarker.template.*;
import net.sf.jelly.apt.decorations.JavaDoc;
import net.sf.jelly.apt.freemarker.FreemarkerJavaDoc;
import org.apache.commons.digester.RuleSet;
import org.codehaus.enunciate.EnunciateException;
import org.codehaus.enunciate.apt.EnunciateFreemarkerModel;
import org.codehaus.enunciate.config.SchemaInfo;
import org.codehaus.enunciate.config.WsdlInfo;
import org.codehaus.enunciate.contract.jaxb.TypeDefinition;
import org.codehaus.enunciate.contract.jaxws.EndpointInterface;
import org.codehaus.enunciate.contract.jaxws.WebFault;
import org.codehaus.enunciate.contract.jaxws.WebMethod;
import org.codehaus.enunciate.contract.validation.Validator;
import org.codehaus.enunciate.main.*;
import org.codehaus.enunciate.modules.FacetAware;
import org.codehaus.enunciate.modules.FreemarkerDeploymentModule;
import org.codehaus.enunciate.modules.csharp.config.CSharpRuleSet;
import org.codehaus.enunciate.modules.csharp.config.PackageNamespaceConversion;
import org.codehaus.enunciate.template.freemarker.AccessorOverridesAnotherMethod;
import org.codehaus.enunciate.template.freemarker.ClientPackageForMethod;
import org.codehaus.enunciate.template.freemarker.FindRootElementMethod;
import org.codehaus.enunciate.template.freemarker.SimpleNameWithParamsMethod;
import org.codehaus.enunciate.util.TypeDeclarationComparator;
import java.io.*;
import java.net.URL;
import java.util.*;
/**
* <h1>C# Module</h1>
*
* <p>The C# module generates C# client code for accessing the SOAP endpoints and makes an attempt at compiling the code in a .NET assembly. If the the compile
* attempt is to be successful, then you must have a C# compiler available on your system path, or specify a "compileExecutable" attribute in the Enunciate
* configuration file. If the compile attempt fails, only the C# source code will be made available as a client artifact.</p>
*
* <p>The order of the C# deployment module is 0, as it doesn't depend on any artifacts exported by any other module.</p>
*
* <ul>
* <li><a href="#config">configuration</a></li>
* </ul>
*
* <h1><a name="config">Configuration</a></h1>
*
* <p>The C# module is configured with the "csharp" element under the "modules" element of the enunciate configuration file. It supports the following
* attributes:</p>
*
* <ul>
* <li>The "label" attribute is the label for the C# API. This is the name by which the files will be identified, producing [label].cs and [label].dll.
* By default the label is the same as the Enunciate project label. For a more custom configuration of the generated file names, use the
* "bundleFileName", "DLLFileName", "docXmlFileName", and "sourceFileName" attributes.</li>
* <li>The "compileExecutable" attribute is the executable for invoking the C# compiler. If not supplied, an attempt will be made to find a C# compiler on the
* system path. If the attempt fails, the C# code will not be compiled (but the source code will still be made available as a download artifact).</li>
* <li>The "compileCommand" is a <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/util/Formatter.html#syntax">Java format string</a> that represents the
* full command that is used to invoke the C# compiler. The string will be formatted with the following arguments: compile executable, assembly path,
* doc xml path, source doc path. The default value is "%s /target:library /out:%s /r:System.Web.Services /doc:%s %s"</li>
* <li>The "singleFilePerClass" allows you to specify that Enunciate should generate a file for each C# class. By default (<tt>false</tt>), Enunciate
* puts all C# classes into a single file.</li>
* </ul>
*
* <h3>The "package-conversions" element</h3>
*
* <p>The "package-conversions" subelement of the "csharp" element is used to map packages from
* the original API packages to C# namespaces. This element supports an arbitrary number of
* "convert" child elements that are used to specify the conversions. These "convert" elements support
* the following attributes:</p>
*
* <ul>
* <li>The "from" attribute specifies the package that is to be converted. This package will match
* all classes in the package as well as any subpackages of the package. This means that if "org.enunciate"
* were specified, it would match "org.enunciate", "org.enunciate.api", and "org.enunciate.api.impl".</li>
* <li>The "to" attribute specifies what the package is to be converted to. Only the part of the package
* that matches the "from" attribute will be converted.</li>
* </ul>
*
* <h3>The "facets" element</h3>
*
* <p>The "facets" element is applicable to the C# module to configure which facets are to be included/excluded from the C# artifacts. For
* more information, see <a href="http://docs.codehaus.org/display/ENUNCIATE/Enunciate+API+Facets">API Facets</a></p>
*
* @author Ryan Heaton
* @docFileName module_csharp.html
*/
public class CSharpDeploymentModule extends FreemarkerDeploymentModule implements FacetAware {
private boolean require = false;
private boolean disableCompile = true;
private String label = null;
private String compileExecutable = null;
private String compileCommand = "%s /target:library /out:%s /r:System.Web.Services /doc:%s %s";
private final Map<String, String> packageToNamespaceConversions = new HashMap<String, String>();
private String bundleFileName = null;
private String DLLFileName = null;
private String docXmlFileName = null;
private String sourceFileName = null;
private boolean singleFilePerClass = false;
private Set<String> facetIncludes = new TreeSet<String>();
private Set<String> facetExcludes = new TreeSet<String>();
public CSharpDeploymentModule() {
}
/**
* @return "csharp"
*/
@Override
public String getName() {
return "csharp";
}
@Override
public void init(Enunciate enunciate) throws EnunciateException {
super.init(enunciate);
if (!super.isDisabled()) { //if we're explicitly disabled, we can ignore this...
if (isDisableCompile()) {
info("C# compilation is disabled, but the source code will still be generated.");
setCompileExecutable(null);
}
else {
String compileExectuable = getCompileExecutable();
if (compileExectuable == null) {
String osName = System.getProperty("os.name");
if (osName != null && osName.toUpperCase().contains("WINDOWS")) {
//try the "csc" command on Windows environments.
debug("Attempting to execute command \"csc /help\" for the current environment (%s).", osName);
try {
Process process = new ProcessBuilder("csc", "/help").redirectErrorStream(true).start();
InputStream in = process.getInputStream();
byte[] buffer = new byte[1024];
int len = in.read(buffer);
while (len >- 0) {
len = in.read(buffer);
}
int exitCode = process.waitFor();
if (exitCode != 0) {
debug("Command \"csc /help\" failed with exit code " + exitCode + ".");
}
else {
compileExectuable = "csc";
debug("C# compile executable to be used: csc");
}
}
catch (Throwable e) {
debug("Command \"csc /help\" failed (" + e.getMessage() + ").");
}
}
if (compileExectuable == null) {
//try the "gmcs" command (Mono)
debug("Attempting to execute command \"gmcs /help\" for the current environment (%s).", osName);
try {
Process process = new ProcessBuilder("gmcs", "/help").redirectErrorStream(true).start();
InputStream in = process.getInputStream();
byte[] buffer = new byte[1024];
int len = in.read(buffer);
while (len >- 0) {
len = in.read(buffer);
}
int exitCode = process.waitFor();
if (exitCode != 0) {
debug("Command \"gmcs /help\" failed with exit code " + exitCode + ".");
}
else {
compileExectuable = "gmcs";
debug("C# compile executable to be used: %s", compileExectuable);
}
}
catch (Throwable e) {
debug("Command \"gmcs /help\" failed (" + e.getMessage() + ").");
}
}
if (compileExectuable == null && isRequire()) {
throw new EnunciateException("C# client code generation is required, but there was no valid compile executable found. " +
"Please supply one in the configuration file, or set it up on your system path.");
}
setCompileExecutable(compileExectuable);
}
}
}
}
@Override
public void initModel(EnunciateFreemarkerModel model) {
super.initModel(model);
if (!isDisabled()) {
TreeSet<WebFault> allFaults = new TreeSet<WebFault>(new TypeDeclarationComparator());
for (WsdlInfo wsdlInfo : model.getNamespacesToWSDLs().values()) {
for (EndpointInterface ei : wsdlInfo.getEndpointInterfaces()) {
String pckg = ei.getPackage().getQualifiedName();
if (!this.packageToNamespaceConversions.containsKey(pckg)) {
this.packageToNamespaceConversions.put(pckg, packageToNamespace(pckg));
}
for (WebMethod webMethod : ei.getWebMethods()) {
for (WebFault webFault : webMethod.getWebFaults()) {
allFaults.add(webFault);
}
}
}
}
for (WebFault webFault : allFaults) {
String pckg = webFault.getPackage().getQualifiedName();
if (!this.packageToNamespaceConversions.containsKey(pckg)) {
this.packageToNamespaceConversions.put(pckg, packageToNamespace(pckg));
}
}
for (SchemaInfo schemaInfo : model.getNamespacesToSchemas().values()) {
for (TypeDefinition typeDefinition : schemaInfo.getTypeDefinitions()) {
String pckg = typeDefinition.getPackage().getQualifiedName();
if (!this.packageToNamespaceConversions.containsKey(pckg)) {
this.packageToNamespaceConversions.put(pckg, packageToNamespace(pckg));
}
}
}
}
}
protected String packageToNamespace(String pckg) {
if (pckg == null) {
return null;
}
else {
StringBuilder ns = new StringBuilder();
for (StringTokenizer toks = new StringTokenizer(pckg, "."); toks.hasMoreTokens();) {
String tok = toks.nextToken();
ns.append(Character.toString(tok.charAt(0)).toUpperCase());
if (tok.length() > 1) {
ns.append(tok.substring(1));
}
if (toks.hasMoreTokens()) {
ns.append('.');
}
}
return ns.toString();
}
}
@Override
public void doFreemarkerGenerate() throws IOException, TemplateException {
File genDir = getGenerateDir();
if (!enunciate.isUpToDateWithSources(genDir)) {
EnunciateFreemarkerModel model = getModel();
ClientPackageForMethod namespaceFor = new ClientPackageForMethod(this.packageToNamespaceConversions);
namespaceFor.setUseClientNameConversions(true);
model.put("namespaceFor", namespaceFor);
model.put("findRootElement", new FindRootElementMethod());
model.put("requestDocumentQName", new RequestDocumentQNameMethod());
model.put("responseDocumentQName", new ResponseDocumentQNameMethod());
ClientClassnameForMethod classnameFor = new ClientClassnameForMethod(this.packageToNamespaceConversions);
classnameFor.setUseClientNameConversions(true);
model.put("classnameFor", classnameFor);
model.put("listsAsArraysClassnameFor", new ListsAsArraysClientClassnameForMethod(this.packageToNamespaceConversions));
model.put("simpleNameFor", new SimpleNameWithParamsMethod(classnameFor));
model.put("csFileName", getSourceFileName());
model.put("accessorOverridesAnother", new AccessorOverridesAnotherMethod());
debug("Generating the C# client classes...");
URL apiTemplate = isSingleFilePerClass() ? getTemplateURL("api-multiple-files.fmt") : getTemplateURL("api.fmt");
processTemplate(apiTemplate, model);
}
else {
info("Skipping C# code generation because everything appears up-to-date.");
}
}
@Override
protected void doCompile() throws EnunciateException, IOException {
File compileDir = getCompileDir();
Enunciate enunciate = getEnunciate();
String compileExecutable = getCompileExecutable();
if (getCompileExecutable() != null) {
if (!enunciate.isUpToDateWithSources(compileDir)) {
compileDir.mkdirs();
String compileCommand = getCompileCommand();
if (compileCommand == null) {
throw new IllegalStateException("Somehow the \"compile\" step was invoked on the C# module without a valid compile command.");
}
compileCommand = compileCommand.replace(' ', '\0'); //replace all spaces with the null character, so the command can be tokenized later.
File dll = new File(compileDir, getDLLFileName());
File docXml = new File(compileDir, getDocXmlFileName());
File sourceFile = new File(getGenerateDir(), getSourceFileName());
compileCommand = String.format(compileCommand, compileExecutable,
dll.getAbsolutePath(),
docXml.getAbsolutePath(),
sourceFile.getAbsolutePath());
StringTokenizer tokenizer = new StringTokenizer(compileCommand, "\0"); //tokenize on the null character to preserve the spaces in the command.
List<String> command = new ArrayList<String>();
while (tokenizer.hasMoreElements()) {
command.add((String) tokenizer.nextElement());
}
Process process = new ProcessBuilder(command).redirectErrorStream(true).directory(compileDir).start();
BufferedReader procReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line = procReader.readLine();
while (line != null) {
info(line);
line = procReader.readLine();
}
int procCode;
try {
procCode = process.waitFor();
}
catch (InterruptedException e1) {
throw new EnunciateException("Unexpected inturruption of the C# compile process.");
}
if (procCode != 0) {
throw new EnunciateException("C# compile failed.");
}
enunciate.addArtifact(new FileArtifact(getName(), "csharp.assembly", dll));
if (docXml.exists()) {
enunciate.addArtifact(new FileArtifact(getName(), "csharp.docs.xml", docXml));
}
}
else {
info("Skipping C# compile because everything appears up-to-date.");
}
}
else {
debug("Skipping C# compile because a compile executale was neither found nor provided. The C# bundle will only include the sources.");
}
}
@Override
protected void doBuild() throws EnunciateException, IOException {
Enunciate enunciate = getEnunciate();
File buildDir = getBuildDir();
if (!enunciate.isUpToDateWithSources(buildDir)) {
File compileDir = getCompileDir();
compileDir.mkdirs(); //might not exist if we couldn't actually compile.
//we want to zip up the source file, too, so we'll just copy it to the compile dir.
enunciate.copyDir(getGenerateDir(), compileDir);
buildDir.mkdirs();
File bundle = new File(buildDir, getBundleFileName());
enunciate.zip(bundle, compileDir);
ClientLibraryArtifact artifactBundle = new ClientLibraryArtifact(getName(), "csharp.client.library", ".NET Client Library");
artifactBundle.setPlatform(".NET 2.0");
StringBuilder builder = new StringBuilder("C# source code");
boolean docsExist = new File(compileDir, getDocXmlFileName()).exists();
boolean dllExists = new File(compileDir, getDLLFileName()).exists();
if (docsExist && dllExists) {
builder.append(", the assembly, and the XML docs");
}
else if (dllExists) {
builder.append("and the assembly");
}
//read in the description from file:
String description = readResource("library_description.fmt", builder.toString());
artifactBundle.setDescription(description);
NamedFileArtifact binariesJar = new NamedFileArtifact(getName(), "dotnet.client.bundle", bundle);
binariesJar.setArtifactType(ArtifactType.binaries);
binariesJar.setDescription(String.format("The %s for the .NET client library.", builder.toString()));
binariesJar.setPublic(false);
artifactBundle.addArtifact(binariesJar);
enunciate.addArtifact(artifactBundle);
}
}
/**
* Reads a resource into string form.
*
* @param resource The resource to read.
* @param contains The description of what the bundle contains.
* @return The string form of the resource.
*/
protected String readResource(String resource, String contains) throws IOException, EnunciateException {
HashMap<String, Object> model = new HashMap<String, Object>();
model.put("sample_service_method", getModelInternal().findExampleWebMethod());
model.put("sample_resource", getModelInternal().findExampleResourceMethod());
model.put("bundle_contains", contains);
URL res = CSharpDeploymentModule.class.getResource(resource);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
PrintStream out = new PrintStream(bytes);
try {
processTemplate(res, model, out);
out.flush();
bytes.flush();
return bytes.toString("utf-8");
}
catch (TemplateException e) {
throw new EnunciateException(e);
}
}
/**
* The name of the bundle file.
*
* @return The name of the bundle file.
*/
protected String getBundleFileName() {
if (this.bundleFileName != null) {
return this.bundleFileName;
}
String label = getLabel();
if (label == null) {
label = getEnunciate().getConfig().getLabel();
}
return label + "-dotnet.zip";
}
/**
* The name of the bundle file.
*
* @param bundleFileName The name of the bundle file.
*/
public void setBundleFileName(String bundleFileName) {
this.bundleFileName = bundleFileName;
}
/**
* The name of the generated C# dll.
*
* @return The name of the generated C# file.
*/
protected String getDLLFileName() {
if (this.DLLFileName != null) {
return this.DLLFileName;
}
String label = getLabel();
if (label == null) {
label = getEnunciate().getConfig().getLabel();
}
return label + ".dll";
}
/**
* The name of the generated C# dll.
*
* @param DLLFileName The name of the generated C# dll.
*/
public void setDLLFileName(String DLLFileName) {
this.DLLFileName = DLLFileName;
}
/**
* The name of the generated C# xml documentation.
*
* @return The name of the generated C# xml documentation.
*/
protected String getDocXmlFileName() {
if (this.docXmlFileName != null) {
return this.docXmlFileName;
}
String label = getLabel();
if (label == null) {
label = getEnunciate().getConfig().getLabel();
}
return label + "-docs.xml";
}
/**
* The name of the generated C# xml documentation.
*
* @param docXmlFileName The name of the generated C# xml documentation.
*/
public void setDocXmlFileName(String docXmlFileName) {
this.docXmlFileName = docXmlFileName;
}
/**
* The name of the generated C# source file.
*
* @return The name of the generated C# source file.
*/
protected String getSourceFileName() {
if (this.sourceFileName != null) {
return this.sourceFileName;
}
String label = getLabel();
if (label == null) {
label = getEnunciate().getConfig().getLabel();
}
return label + ".cs";
}
/**
* The name of the generated C# source file.
*
* @param sourceFileName The name of the generated C# source file.
*/
public void setSourceFileName(String sourceFileName) {
this.sourceFileName = sourceFileName;
}
@Override
protected ObjectWrapper getObjectWrapper() {
return new DefaultObjectWrapper() {
@Override
public TemplateModel wrap(Object obj) throws TemplateModelException {
if (obj instanceof JavaDoc) {
return new FreemarkerJavaDoc((JavaDoc) obj);
}
return super.wrap(obj);
}
};
}
/**
* Get a template URL for the template of the given name.
*
* @param template The specified template.
* @return The URL to the specified template.
*/
protected URL getTemplateURL(String template) {
return CSharpDeploymentModule.class.getResource(template);
}
/**
* Whether the generate dir is up-to-date.
*
* @param genDir The generate dir.
* @return Whether the generate dir is up-to-date.
*/
protected boolean isUpToDate(File genDir) {
return enunciate.isUpToDateWithSources(genDir);
}
/**
* Whether to require the C# client code.
*
* @return Whether to require the C# client code.
*/
public boolean isRequire() {
return require;
}
/**
* Whether to require the C# client code.
*
* @param require Whether to require the C# client code.
*/
public void setRequire(boolean require) {
this.require = require;
}
/**
* The label for the C# API.
*
* @return The label for the C# API.
*/
public String getLabel() {
return label;
}
/**
* The label for the C# API.
*
* @param label The label for the C# API.
*/
public void setLabel(String label) {
this.label = label;
}
/**
* The path to the compile executable.
*
* @return The path to the compile executable.
*/
public String getCompileExecutable() {
return compileExecutable;
}
/**
* The path to the compile executable.
*
* @param compileExecutable The path to the compile executable.
*/
public void setCompileExecutable(String compileExecutable) {
this.compileExecutable = compileExecutable;
}
/**
* The C# compile command.
*
* @return The C# compile command.
*/
public String getCompileCommand() {
return compileCommand;
}
/**
* The C# compile command.
*
* @param compileCommand The C# compile command.
*/
public void setCompileCommand(String compileCommand) {
this.compileCommand = compileCommand;
}
/**
* The package-to-namespace conversions.
*
* @return The package-to-namespace conversions.
*/
public Map<String, String> getPackageToNamespaceConversions() {
return packageToNamespaceConversions;
}
/**
* Whether to disable the compile step.
*
* @return Whether to disable the compile step.
*/
public boolean isDisableCompile() {
return disableCompile;
}
/**
* Whether to disable the compile step.
*
* @param disableCompile Whether to disable the compile step.
*/
public void setDisableCompile(boolean disableCompile) {
this.disableCompile = disableCompile;
}
/**
* Whether there should be a single file per class. Default: false (all classes are contained in a single file).
*
* @return Whether there should be a single file per class.
*/
public boolean isSingleFilePerClass() {
return singleFilePerClass;
}
/**
* Whether there should be a single file per class.
*
* @param singleFilePerClass Whether there should be a single file per class.
*/
public void setSingleFilePerClass(boolean singleFilePerClass) {
this.singleFilePerClass = singleFilePerClass;
}
/**
* The set of facets to include.
*
* @return The set of facets to include.
*/
public Set<String> getFacetIncludes() {
return facetIncludes;
}
/**
* Add a facet include.
*
* @param name The name.
*/
public void addFacetInclude(String name) {
if (name != null) {
this.facetIncludes.add(name);
}
}
/**
* The set of facets to exclude.
*
* @return The set of facets to exclude.
*/
public Set<String> getFacetExcludes() {
return facetExcludes;
}
/**
* Add a facet exclude.
*
* @param name The name.
*/
public void addFacetExclude(String name) {
if (name != null) {
this.facetExcludes.add(name);
}
}
/**
* Add a client package conversion.
*
* @param conversion The conversion to add.
*/
public void addClientPackageConversion(PackageNamespaceConversion conversion) {
String from = conversion.getFrom();
String to = conversion.getTo();
if (from == null) {
throw new IllegalArgumentException("A 'from' attribute must be specified on a package-conversion element.");
}
if (to == null) {
throw new IllegalArgumentException("A 'to' attribute must be specified on a package-conversion element.");
}
this.packageToNamespaceConversions.put(from, to);
}
@Override
public RuleSet getConfigurationRules() {
return new CSharpRuleSet();
}
@Override
public Validator getValidator() {
return new CSharpValidator();
}
// Inherited.
@Override
public boolean isDisabled() {
if (super.isDisabled()) {
return true;
}
else if (getModelInternal() != null && getModelInternal().getNamespacesToWSDLs().isEmpty() && getModelInternal().getNamespacesToSchemas().isEmpty()) {
debug("C# module is disabled because there are no endpoint interfaces, nor any XML types.");
return true;
}
return false;
}
}