Skip to content

Commit

Permalink
Issue 5431 Support jaxrs client api interfaces (#6412)
Browse files Browse the repository at this point in the history
* Adds the ability to create code for an interface-based jaxrs client.

* Adds shell script and sample files for jaxrs-spec-interface

* rebase into adds shell

* Fixes bug in creation of Produces/Consumes in method annotation. Allows for instance "application/json; charset=utf-8"

* Fixes generated pom.xml

* Generate pom.xml by default

* Prettier output from api.mustache

* Fixes bug in mediatype, allowing charset-specification in swagger.yaml.

* Merges generation of interface-based jaxrs client/api into jaxrs-spec.

* Moves jaxrs-spec server interface to match location of jaxrs-spec server

* Makes Generated-annotation in genereated classes slightly prettier.
  • Loading branch information
jarlesat authored and wing328 committed Oct 8, 2017
1 parent cdc83ff commit 8a7940f
Show file tree
Hide file tree
Showing 58 changed files with 6,054 additions and 172 deletions.
33 changes: 33 additions & 0 deletions bin/jaxrs-spec-petstore-server-interface.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/bin/sh

SCRIPT="$0"

while [ -h "$SCRIPT" ] ; do
ls=`ls -ld "$SCRIPT"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
SCRIPT="$link"
else
SCRIPT=`dirname "$SCRIPT"`/"$link"
fi
done

if [ ! -d "${APP_DIR}" ]; then
APP_DIR=`dirname "$SCRIPT"`/..
APP_DIR=`cd "${APP_DIR}"; pwd`
fi

executable="./modules/swagger-codegen-cli/target/swagger-codegen-cli.jar"

if [ ! -f "$executable" ]
then
mvn clean package
fi

# if you've executed sbt assembly previously it will use that instead.
export JAVA_OPTS="${JAVA_OPTS} -XX:MaxPermSize=256M -Xmx1024M -DloggerPath=conf/log4j.properties"
ags="$@ generate -i modules/swagger-codegen/src/test/resources/2_0/petstore-with-fake-endpoints-models-for-testing.yaml -l jaxrs-spec -o samples/server/petstore/jaxrs-spec-interface
-DhideGenerationTimestamp=true
-DinterfaceOnly=true"

java $JAVA_OPTS -jar $executable $ags
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@
import io.swagger.codegen.CodegenOperation;
import io.swagger.codegen.CodegenProperty;
import io.swagger.codegen.SupportingFile;
import io.swagger.codegen.languages.features.BeanValidationFeatures;
import io.swagger.models.Operation;
import io.swagger.models.Swagger;
import io.swagger.models.properties.Property;
import io.swagger.util.Json;

public class JavaJAXRSSpecServerCodegen extends AbstractJavaJAXRSServerCodegen
{

private static final String INTERFACE_ONLY = "interfaceOnly";
public static final String INTERFACE_ONLY = "interfaceOnly";
public static final String GENERATE_POM = "generatePom";

protected boolean interfaceOnly = false;
private boolean interfaceOnly = false;
private boolean generatePom = true;

public JavaJAXRSSpecServerCodegen()
{
Expand All @@ -36,7 +36,6 @@ public JavaJAXRSSpecServerCodegen()
outputFolder = "generated-code/JavaJaxRS-Spec";

modelTemplateFiles.put("model.mustache", ".java");

apiTemplateFiles.put("api.mustache", ".java");
apiPackage = "io.swagger.api";
modelPackage = "io.swagger.model";
Expand Down Expand Up @@ -74,25 +73,30 @@ public JavaJAXRSSpecServerCodegen()
library.setEnum(supportedLibraries);

cliOptions.add(library);
cliOptions.add(CliOption.newBoolean(INTERFACE_ONLY, "Whether to generate only API interface stubs without the server files."));
cliOptions.add(CliOption.newBoolean(GENERATE_POM, "Whether to generate pom.xml if the file does not already exist.").defaultValue(String.valueOf(generatePom)));
cliOptions.add(CliOption.newBoolean(INTERFACE_ONLY, "Whether to generate only API interface stubs without the server files.").defaultValue(String.valueOf(interfaceOnly)));
}

@Override
public void processOpts()
{
super.processOpts();

supportingFiles.clear(); // Don't need extra files provided by AbstractJAX-RS & Java Codegen

if (additionalProperties.containsKey(GENERATE_POM)) {
generatePom = Boolean.valueOf(additionalProperties.get(GENERATE_POM).toString());
}
if (additionalProperties.containsKey(INTERFACE_ONLY)) {
this.setInterfaceOnly(Boolean.valueOf(additionalProperties.get(INTERFACE_ONLY).toString()));
if (!interfaceOnly) {
additionalProperties.remove(INTERFACE_ONLY);
}
interfaceOnly = Boolean.valueOf(additionalProperties.get(INTERFACE_ONLY).toString());
}
if (interfaceOnly) {
// Change default artifactId if genereating interfaces only, before command line options are applied in base class.
artifactId = "swagger-jaxrs-client";
}

writeOptional(outputFolder, new SupportingFile("pom.mustache", "", "pom.xml"));
super.processOpts();

supportingFiles.clear(); // Don't need extra files provided by AbstractJAX-RS & Java Codegen
if (generatePom) {
writeOptional(outputFolder, new SupportingFile("pom.mustache", "", "pom.xml"));
}
if (!interfaceOnly) {
writeOptional(outputFolder, new SupportingFile("RestApplication.mustache",
(sourceFolder + '/' + invokerPackage).replace(".", "/"), "RestApplication.java"));
Expand All @@ -106,8 +110,6 @@ public String getName()
return "jaxrs-spec";
}

public void setInterfaceOnly(boolean interfaceOnly) { this.interfaceOnly = interfaceOnly; }

@Override
public void addOperationToGroup(String tag, String resourcePath, Operation operation, CodegenOperation co, Map<String, List<CodegenOperation>> operations) {
String basePath = resourcePath;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,39 +10,18 @@ import io.swagger.annotations.*;

import java.util.Map;
import java.util.List;
{{#useBeanValidation}}
import javax.validation.constraints.*;
import javax.validation.Valid;
{{/useBeanValidation}}
import java.lang.Exception;
{{#useBeanValidation}}import javax.validation.constraints.*;
import javax.validation.Valid;{{/useBeanValidation}}

@Path("/{{{baseName}}}")

@Api(description = "the {{{baseName}}} API")
{{#hasConsumes}}@Consumes({ {{#consumes}}"{{{mediaType}}}"{{#hasMore}}, {{/hasMore}}{{/consumes}} }){{/hasConsumes}}
{{#hasProduces}}@Produces({ {{#produces}}"{{{mediaType}}}"{{#hasMore}}, {{/hasMore}}{{/produces}} }){{/hasProduces}}
{{>generatedAnnotation}}

public {{#interfaceOnly}}interface{{/interfaceOnly}}{{^interfaceOnly}}class{{/interfaceOnly}} {{classname}} {
@Api(description = "the {{{baseName}}} API"){{#hasConsumes}}
@Consumes({ {{#consumes}}"{{{mediaType}}}"{{#hasMore}}, {{/hasMore}}{{/consumes}} }){{/hasConsumes}}{{#hasProduces}}
@Produces({ {{#produces}}"{{{mediaType}}}"{{#hasMore}}, {{/hasMore}}{{/produces}} }){{/hasProduces}}
{{>generatedAnnotation}}public {{#interfaceOnly}}interface{{/interfaceOnly}}{{^interfaceOnly}}class{{/interfaceOnly}} {{classname}} {
{{#operations}}
{{#operation}}

@{{httpMethod}}
{{#subresourceOperation}}@Path("{{{path}}}"){{/subresourceOperation}}
{{#hasConsumes}}@Consumes({ {{#consumes}}"{{mediaType}}"{{#hasMore}}, {{/hasMore}}{{/consumes}} }){{/hasConsumes}}
{{#hasProduces}}@Produces({ {{#produces}}"{{mediaType}}"{{#hasMore}}, {{/hasMore}}{{/produces}} }){{/hasProduces}}
@ApiOperation(value = "{{{summary}}}", notes = "{{{notes}}}", response = {{{returnBaseType}}}.class{{#returnContainer}}, responseContainer = "{{{returnContainer}}}"{{/returnContainer}}{{#hasAuthMethods}}, authorizations = {
{{#authMethods}}@Authorization(value = "{{name}}"{{#isOAuth}}, scopes = {
{{#scopes}}@AuthorizationScope(scope = "{{scope}}", description = "{{description}}"){{#hasMore}},
{{/hasMore}}{{/scopes}}
}{{/isOAuth}}){{#hasMore}},
{{/hasMore}}{{/authMethods}}
}{{/hasAuthMethods}}, tags={ {{#vendorExtensions.x-tags}}"{{tag}}"{{#hasMore}}, {{/hasMore}}{{/vendorExtensions.x-tags}} })
@ApiResponses(value = { {{#responses}}
@ApiResponse(code = {{{code}}}, message = "{{{message}}}", response = {{{baseType}}}.class{{#containerType}}, responseContainer = "{{{containerType}}}"{{/containerType}}){{#hasMore}},{{/hasMore}}{{/responses}} })
public Response {{nickname}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{#hasMore}},{{/hasMore}}{{/allParams}}) throws Exception{{#interfaceOnly}};{{/interfaceOnly}}{{^interfaceOnly}} {
return Response.ok().entity("magic!").build();
}{{/interfaceOnly}}
{{#interfaceOnly}}{{>apiInterface}}{{/interfaceOnly}}{{^interfaceOnly}}{{>apiMethod}}{{/interfaceOnly}}
{{/operation}}
}
{{/operations}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@{{httpMethod}}{{#subresourceOperation}}
@Path("{{{path}}}"){{/subresourceOperation}}{{#hasConsumes}}
@Consumes({ {{#consumes}}"{{{mediaType}}}"{{#hasMore}}, {{/hasMore}}{{/consumes}} }){{/hasConsumes}}{{#hasProduces}}
@Produces({ {{#produces}}"{{{mediaType}}}"{{#hasMore}}, {{/hasMore}}{{/produces}} }){{/hasProduces}}
@ApiOperation(value = "{{{summary}}}", notes = "{{{notes}}}"{{#hasAuthMethods}}, authorizations = {
{{#authMethods}}@Authorization(value = "{{name}}"{{#isOAuth}}, scopes = {
{{#scopes}}@AuthorizationScope(scope = "{{scope}}", description = "{{description}}"){{#hasMore}},
{{/hasMore}}{{/scopes}}
}{{/isOAuth}}){{#hasMore}},
{{/hasMore}}{{/authMethods}}
}{{/hasAuthMethods}}, tags={ {{#vendorExtensions.x-tags}}"{{tag}}"{{#hasMore}}, {{/hasMore}}{{/vendorExtensions.x-tags}} })
@ApiResponses(value = { {{#responses}}
@ApiResponse(code = {{{code}}}, message = "{{{message}}}", response = {{{baseType}}}.class{{#returnContainer}}, responseContainer = "{{{returnContainer}}}"{{/returnContainer}}){{#hasMore}},{{/hasMore}}{{/responses}} })
{{>returnTypeInterface}} {{nickname}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{#hasMore}},{{/hasMore}}{{/allParams}});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@{{httpMethod}}{{#subresourceOperation}}
@Path("{{{path}}}"){{/subresourceOperation}}{{#hasConsumes}}
@Consumes({ {{#consumes}}"{{{mediaType}}}"{{#hasMore}}, {{/hasMore}}{{/consumes}} }){{/hasConsumes}}{{#hasProduces}}
@Produces({ {{#produces}}"{{{mediaType}}}"{{#hasMore}}, {{/hasMore}}{{/produces}} }){{/hasProduces}}
@ApiOperation(value = "{{{summary}}}", notes = "{{{notes}}}", response = {{{returnBaseType}}}.class{{#returnContainer}}, responseContainer = "{{{returnContainer}}}"{{/returnContainer}}{{#hasAuthMethods}}, authorizations = {
{{#authMethods}}@Authorization(value = "{{name}}"{{#isOAuth}}, scopes = {
{{#scopes}}@AuthorizationScope(scope = "{{scope}}", description = "{{description}}"){{#hasMore}},
{{/hasMore}}{{/scopes}}
}{{/isOAuth}}){{#hasMore}},
{{/hasMore}}{{/authMethods}}
}{{/hasAuthMethods}}, tags={ {{#vendorExtensions.x-tags}}"{{tag}}"{{#hasMore}}, {{/hasMore}}{{/vendorExtensions.x-tags}} })
@ApiResponses(value = { {{#responses}}
@ApiResponse(code = {{{code}}}, message = "{{{message}}}", response = {{{baseType}}}.class{{#containerType}}, responseContainer = "{{{containerType}}}"{{/containerType}}){{#hasMore}},{{/hasMore}}{{/responses}} })
public Response {{nickname}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{#hasMore}},{{/hasMore}}{{/allParams}}) {
return Response.ok().entity("magic!").build();
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@
<modelVersion>4.0.0</modelVersion>
<groupId>{{groupId}}</groupId>
<artifactId>{{artifactId}}</artifactId>
<packaging>war</packaging>
<packaging>{{#interfaceOnly}}jar{{/interfaceOnly}}{{^interfaceOnly}}war{{/interfaceOnly}}</packaging>
<name>{{artifactId}}</name>
<version>{{artifactVersion}}</version>
<build>
<sourceDirectory>src/main/java</sourceDirectory>
<plugins>
<plugins>{{#interfaceOnly}}
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.2</version>
</plugin>{{/interfaceOnly}}{{^interfaceOnly}}
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
Expand All @@ -27,7 +32,7 @@
</goals>
</execution>
</executions>
</plugin>
</plugin>{{/interfaceOnly}}
</plugins>
</build>
<dependencies>
Expand All @@ -48,7 +53,7 @@
<artifactId>junit</artifactId>
<version>${junit-version}</version>
<scope>test</scope>
</dependency>
</dependency>{{^interfaceOnly}}
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
Expand All @@ -68,7 +73,7 @@
<groupId>org.beanshell</groupId>
</exclusion>
</exclusions>
</dependency>
</dependency>{{/interfaceOnly}}
{{#useBeanValidation}}
<!-- Bean Validation API support -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#returnContainer}}{{{returnContainer}}}<{{/returnContainer}}{{{returnType}}}{{#returnContainer}}>{{/returnContainer}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package io.swagger.codegen.languages;

import io.swagger.codegen.CliOption;
import io.swagger.codegen.SupportingFile;
import io.swagger.models.properties.BooleanProperty;
import org.junit.Test;
import org.testng.Assert;

public class JavaJAXRSSpecServerCodegenTest {

private JavaJAXRSSpecServerCodegen generator = new JavaJAXRSSpecServerCodegen();

@Test
public void do_not_process_RestApplication_when_interfaceOnly_is_true() {
generator.additionalProperties().put(JavaJAXRSSpecServerCodegen.INTERFACE_ONLY, "true");
generator.processOpts();
for (SupportingFile file : generator.supportingFiles()) {
Assert.assertNotEquals("RestApplication.mustache", file.templateFile);
}
}

@Test
public void do_process_pom_by_default() {
generator.processOpts();
for (SupportingFile file : generator.supportingFiles()) {
if ("pom.mustache".equals(file.templateFile)) {
return;
}
}
Assert.fail("Missing pom.mustache");
}

@Test
public void process_pom_if_generatePom_is_true() {
generator.additionalProperties().put(JavaJAXRSSpecServerCodegen.GENERATE_POM, "true");
generator.processOpts();
for (SupportingFile file : generator.supportingFiles()) {
if ("pom.mustache".equals(file.templateFile)) {
return;
}
}
Assert.fail("Missing pom.mustache");
}

@Test
public void do_not_process_pom_if_generatePom_is_false() {
generator.additionalProperties().put(JavaJAXRSSpecServerCodegen.GENERATE_POM, "false");
generator.processOpts();
for (SupportingFile file : generator.supportingFiles()) {
Assert.assertNotEquals("pom.mustache", file.templateFile);
}
}

@Test
public void verify_that_generatePom_exists_as_a_parameter_with_default_true() {
for (CliOption option : generator.cliOptions()) {
if (option.getOpt().equals(JavaJAXRSSpecServerCodegen.GENERATE_POM)) {
Assert.assertEquals(BooleanProperty.TYPE, option.getType());
Assert.assertEquals("true", option.getDefault());
return;
}
}
Assert.fail("Missing " + JavaJAXRSSpecServerCodegen.GENERATE_POM);
}

@Test
public void verify_that_interfaceOnly_exists_as_a_parameter_with_default_false() {
for (CliOption option : generator.cliOptions()) {
if (option.getOpt().equals(JavaJAXRSSpecServerCodegen.INTERFACE_ONLY)) {
Assert.assertEquals(BooleanProperty.TYPE, option.getType());
Assert.assertEquals("false", option.getDefault());
return;
}
}
Assert.fail("Missing " + JavaJAXRSSpecServerCodegen.INTERFACE_ONLY);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Swagger Codegen Ignore
# Generated by swagger-codegen https://github.com/swagger-api/swagger-codegen

# Use this file to prevent files from being overwritten by the generator.
# The patterns follow closely to .gitignore or .dockerignore.

# As an example, the C# client generator defines ApiClient.cs.
# You can make changes and tell Swagger Codgen to ignore just this file by uncommenting the following line:
#ApiClient.cs

# You can match any string of characters against a directory, file or extension with a single asterisk (*):
#foo/*/qux
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux

# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
#foo/**/qux
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux

# You can also negate patterns with an exclamation (!).
# For example, you can ignore all files in a docs folder with the file extension .md:
#docs/*.md
# Then explicitly reverse the ignore rule for a single file:
#!docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2.3.0-SNAPSHOT
Loading

0 comments on commit 8a7940f

Please sign in to comment.