org.codehaus.enunciate.modules.ruby.RubyDeploymentModule Maven / Gradle / Ivy
Show all versions of enunciate-ruby Show documentation
/*
* 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.ruby;
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.template.freemarker.ClientPackageForMethod;
import org.codehaus.enunciate.template.freemarker.SimpleNameWithParamsMethod;
import org.codehaus.enunciate.apt.EnunciateClasspathListener;
import org.codehaus.enunciate.apt.EnunciateFreemarkerModel;
import org.codehaus.enunciate.config.SchemaInfo;
import org.codehaus.enunciate.contract.jaxb.TypeDefinition;
import org.codehaus.enunciate.contract.validation.Validator;
import org.codehaus.enunciate.contract.jaxrs.RootResource;
import org.codehaus.enunciate.contract.jaxrs.ResourceMethod;
import org.codehaus.enunciate.main.NamedFileArtifact;
import org.codehaus.enunciate.main.ClientLibraryArtifact;
import org.codehaus.enunciate.main.ArtifactType;
import org.codehaus.enunciate.modules.FreemarkerDeploymentModule;
import org.codehaus.enunciate.modules.ruby.config.PackageModuleConversion;
import org.codehaus.enunciate.modules.ruby.config.RubyRuleSet;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.net.URL;
import java.util.*;
import com.sun.mirror.type.ClassType;
/**
* Ruby Module
*
* The Ruby module generates Ruby data types that can be used in conjunction with the Ruby JSON implementation
* to (de)serialize the REST resources as they are represented as JSON data.
*
* The order of the Ruby deployment module is 0, as it doesn't depend on any artifacts exported by any other module.
*
*
* - configuration
*
*
* Configuration
*
* The Ruby module is configured with the "ruby" element under the "modules" element of the enunciate configuration file. It supports the following
* attributes:
*
*
* - The "label" attribute is the label for the Ruby API. This is the name by which the file will be identified (producing [label].rb).
* By default the label is the same as the Enunciate project label.
* - The "forceEnable" attribute is used to force-enable the Ruby module. By default, the Ruby module is
* enabled only when both of these conditions are met:
*
* - Jackson-XC is on the claspath.
* - There exists a JAX-RS resource method that consumes or produces JSON.
*
*
*
*
* The "package-conversions" element
*
* The "package-conversions" subelement of the "ruby" element is used to map packages from
* the original API packages to Ruby modules. This element supports an arbitrary number of
* "convert" child elements that are used to specify the conversions. These "convert" elements support
* the following attributes:
*
*
* - 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".
* - 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.
*
*
* The "facets" element
*
* The "facets" element is applicable to the Ruby module to configure which facets are to be included/excluded from the Ruby artifacts. For
* more information, see API Facets
*
* @author Ryan Heaton
* @docFileName module_ruby.html
*/
public class RubyDeploymentModule extends FreemarkerDeploymentModule implements EnunciateClasspathListener {
private boolean forceEnable = false;
private String label = null;
private final Map packageToModuleConversions = new HashMap();
private boolean jacksonXcAvailable = false;
private Set facetIncludes = new TreeSet();
private Set facetExcludes = new TreeSet();
/**
* @return "ruby"
*/
@Override
public String getName() {
return "ruby";
}
public void onClassesFound(Set classes) {
jacksonXcAvailable |= classes.contains("org.codehaus.jackson.xc.JaxbAnnotationIntrospector");
}
@Override
public void initModel(EnunciateFreemarkerModel model) {
super.initModel(model);
if (!isDisabled()) {
for (SchemaInfo schemaInfo : model.getNamespacesToSchemas().values()) {
for (TypeDefinition typeDefinition : schemaInfo.getTypeDefinitions()) {
String pckg = typeDefinition.getPackage().getQualifiedName();
if (!this.packageToModuleConversions.containsKey(pckg)) {
this.packageToModuleConversions.put(pckg, packageToModule(pckg));
}
}
}
}
}
protected String packageToModule(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, EnunciateException {
File genDir = getGenerateDir();
if (!enunciate.isUpToDateWithSources(genDir)) {
EnunciateFreemarkerModel model = getModel();
List schemaTypes = new ArrayList();
ExtensionDepthComparator comparator = new ExtensionDepthComparator();
for (SchemaInfo schemaInfo : model.getNamespacesToSchemas().values()) {
for (TypeDefinition typeDefinition : schemaInfo.getTypeDefinitions()) {
int position = Collections.binarySearch(schemaTypes, typeDefinition, comparator);
if (position < 0) {
position = -position - 1;
}
schemaTypes.add(position, typeDefinition);
}
}
model.put("schemaTypes", schemaTypes);
model.put("packages2modules", this.packageToModuleConversions);
ClientPackageForMethod moduleFor = new ClientPackageForMethod(this.packageToModuleConversions);
moduleFor.setUseClientNameConversions(true);
model.put("moduleFor", moduleFor);
ClientClassnameForMethod classnameFor = new ClientClassnameForMethod(this.packageToModuleConversions);
classnameFor.setUseClientNameConversions(true);
model.put("classnameFor", classnameFor);
SimpleNameWithParamsMethod simpleNameFor = new SimpleNameWithParamsMethod(classnameFor);
model.put("simpleNameFor", simpleNameFor);
model.put("rubyFileName", getSourceFileName());
debug("Generating the Ruby data classes...");
URL apiTemplate = getTemplateURL("api.fmt");
processTemplate(apiTemplate, model);
}
else {
info("Skipping Ruby code generation because everything appears up-to-date.");
}
ClientLibraryArtifact artifactBundle = new ClientLibraryArtifact(getName(), "ruby.client.library", "Ruby Client Library");
artifactBundle.setPlatform("Ruby");
NamedFileArtifact sourceScript = new NamedFileArtifact(getName(), "ruby.client", new File(getGenerateDir(), getSourceFileName()));
sourceScript.setArtifactType(ArtifactType.binaries); //binaries and sources are the same thing in ruby
sourceScript.setPublic(false);
String description = readResource("library_description.fmt"); //read in the description from file
artifactBundle.setDescription(description);
artifactBundle.addArtifact(sourceScript);
getEnunciate().addArtifact(artifactBundle);
}
/**
* Reads a resource into string form.
*
* @param resource The resource to read.
* @return The string form of the resource.
*/
protected String readResource(String resource) throws IOException, EnunciateException {
HashMap model = new HashMap();
model.put("sample_resource", getModelInternal().findExampleResourceMethod());
URL res = RubyDeploymentModule.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 generated Ruby source file.
*
* @return The name of the generated Ruby source file.
*/
protected String getSourceFileName() {
String label = getLabel();
if (label == null) {
label = getEnunciate().getConfig().getLabel();
}
return label + ".rb";
}
@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 RubyDeploymentModule.class.getResource(template);
}
/**
* Whether to force-enable the Ruby module.
*
* @return Whether to force-enable the Ruby module.
*/
public boolean isForceEnable() {
return forceEnable;
}
/**
* Whether to force-enable the Ruby module.
*
* @param forceEnable Whether to force-enable the Ruby module.
*/
public void setForceEnable(boolean forceEnable) {
this.forceEnable = forceEnable;
}
/**
* The label for the Ruby API.
*
* @return The label for the Ruby API.
*/
public String getLabel() {
return label;
}
/**
* The label for the Ruby API.
*
* @param label The label for the Ruby API.
*/
public void setLabel(String label) {
this.label = label;
}
/**
* The package-to-module conversions.
*
* @return The package-to-module conversions.
*/
public Map getPackageToModuleConversions() {
return packageToModuleConversions;
}
/**
* Add a client package conversion.
*
* @param conversion The conversion to add.
*/
public void addClientPackageConversion(PackageModuleConversion 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.packageToModuleConversions.put(from, to);
}
@Override
public RuleSet getConfigurationRules() {
return new RubyRuleSet();
}
@Override
public Validator getValidator() {
return new RubyValidator();
}
/**
* The set of facets to include.
*
* @return The set of facets to include.
*/
public Set 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 getFacetExcludes() {
return facetExcludes;
}
/**
* Add a facet exclude.
*
* @param name The name.
*/
public void addFacetExclude(String name) {
if (name != null) {
this.facetExcludes.add(name);
}
}
// Inherited.
@Override
public boolean isDisabled() {
if (isForceEnable()) {
debug("Ruby module is force-enabled via the 'forceEnable' attribute in the config.");
return false;
}
else if (super.isDisabled()) {
return true;
}
else if (!jacksonXcAvailable) {
debug("Ruby module is disabled because Jackson XC was not found on the Enunciate classpath.");
return true;
}
else if (getModelInternal() != null && getModelInternal().getNamespacesToSchemas().isEmpty()) {
debug("Ruby module is disabled because there are no schema types.");
return true;
}
else if (getModelInternal() != null && getModelInternal().getRootResources().isEmpty()) {
debug("Ruby module is disabled because there are no JAX-RS root resources.");
return true;
}
else if (getModelInternal() != null && !existsAnyJsonResourceMethod(getModelInternal().getRootResources())) {
debug("Ruby module is disabled because there are no JAX-RS root resource methods that produce or consume json.");
return true;
}
return false;
}
/**
* Whether any root resources exist that produce json.
*
* @param rootResources The root resources.
* @return Whether any root resources exist that produce json.
*/
protected boolean existsAnyJsonResourceMethod(List rootResources) {
for (RootResource rootResource : rootResources) {
for (ResourceMethod resourceMethod : rootResource.getResourceMethods(true)) {
for (String mime : resourceMethod.getProducesMime()) {
if ("*/*".equals(mime)) {
return true;
}
else if (mime.toLowerCase().contains("json")) {
return true;
}
}
for (String mime : resourceMethod.getConsumesMime()) {
if ("*/*".equals(mime)) {
return true;
}
else if (mime.toLowerCase().contains("json")) {
return true;
}
}
}
}
return false;
}
private static final class ExtensionDepthComparator implements Comparator {
public int compare(TypeDefinition t1, TypeDefinition t2) {
int depth1 = 0;
int depth2 = 0;
ClassType superClass = t1.getSuperclass();
while (superClass != null && superClass.getDeclaration() != null && !Object.class.getName().equals(superClass.getDeclaration().getQualifiedName())) {
depth1++;
superClass = superClass.getDeclaration().getSuperclass();
}
superClass = t2.getSuperclass();
while (superClass != null && superClass.getDeclaration() != null && !Object.class.getName().equals(superClass.getDeclaration().getQualifiedName())) {
depth2++;
superClass = superClass.getDeclaration().getSuperclass();
}
return depth1 - depth2;
}
}
}