Skip to content

Commit 99a203f

Browse files
authored
Merge pull request #5 from github/support-jaxrs
Support JAX-RS
2 parents c54f617 + 13211d6 commit 99a203f

34 files changed

+898
-24
lines changed

README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ It supports the generation of Java based servers with the following flavours sup
77

88
+ [Spring Boot/Spring MVC](https://spring.io/projects/spring-boot "Spring Boot")
99
+ [Undertow](http://undertow.io/ "Undertow")
10+
+ JAX-RS ([Jersey](https://eclipse-ee4j.github.io/jersey/), [Apache CFX](http://cxf.apache.org/))
1011

1112
## Building & Running
1213

1314
### Requirements
1415

15-
The build has been tested with [Oracle's JDK](http://www.oracle.com/technetwork/java/javase/downloads/index.html "JDK Downloads") (version 1.8)
16+
The build has been tested with [Zulu's OpenJDK](https://www.azul.com/downloads/zulu-community/?architecture=x86-64-bit&package=jdk "JDK Downloads") (version 11)
1617

1718
The build uses gradle to generate the artifacts. No installation is required as the project uses the
1819
[gradle wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html "gradle wrapper") setup.
@@ -27,6 +28,7 @@ The project is split into the following modules:
2728
|:------------------|:------------------------------------------------------|
2829
| `plugin` | The `protoc` plugin |
2930
| `runtime:core` | Core functionality required by generated code |
31+
| `runtime:jaxrs` | Runtime library for JAX-RS servers |
3032
| `runtime:spring` | Runtime library for Spring MVC/Boot servers |
3133
| `runtime:undertow`| Runtime library for Undertow servers |
3234

@@ -64,11 +66,11 @@ The plugin is executed as part of a protoc compilation step:
6466

6567
The flit plugin accepts the following plugin parameters:
6668

67-
| Name | Required | Type | Description |
68-
|:--------------|:---------:|:------------------------------|:----------------------------------------------------------|
69-
| `target` | Y | `enum[server]` | The type of target to generate e.g. server, client etc |
70-
| `type` | Y | `enum[spring,undertow,boot]` | Type of target to generate |
71-
| `context` | N | `string` | Base context for routing, default is `/twirp` |
69+
| Name | Required | Type | Description |
70+
|:--------------|:---------:|:----------------------------------|:----------------------------------------------------------|
71+
| `target` | Y | `enum[server]` | The type of target to generate e.g. server, client etc |
72+
| `type` | Y | `enum[spring,undertow,boot,jaxrs]`| Type of target to generate |
73+
| `context` | N | `string` | Base context for routing, default is `/twirp` |
7274

7375
# Development
7476

plugin/gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
version=1.1.0
1+
version=1.2.0

plugin/src/main/java/com/flit/protoc/Plugin.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.flit.protoc.gen.Generator;
44
import com.flit.protoc.gen.GeneratorException;
5+
import com.flit.protoc.gen.server.jaxrs.JaxrsGenerator;
56
import com.flit.protoc.gen.server.spring.SpringGenerator;
67
import com.flit.protoc.gen.server.undertow.UndertowGenerator;
78
import com.google.protobuf.compiler.PluginProtos.CodeGeneratorRequest;
@@ -22,7 +23,7 @@ public Plugin(CodeGeneratorRequest request) {
2223

2324
public CodeGeneratorResponse process() {
2425
if (!request.hasParameter()) {
25-
return CodeGeneratorResponse.newBuilder().setError("Usage: --flit_out=target=server,type=[spring|undertow]:<PATH>").build();
26+
return CodeGeneratorResponse.newBuilder().setError("Usage: --flit_out=target=server,type=[spring|undertow|jaxrs]:<PATH>").build();
2627
}
2728

2829
Map<String, Parameter> params = Parameter.of(request.getParameter());
@@ -50,6 +51,8 @@ private Generator resolveGenerator(Map<String, Parameter> params) {
5051
return new SpringGenerator();
5152
case "undertow":
5253
return new UndertowGenerator();
54+
case "jaxrs":
55+
return new JaxrsGenerator();
5356
default:
5457
throw new GeneratorException("Unknown server type: " + params.get(PARAM_TYPE).getValue());
5558
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.flit.protoc.gen.server.jaxrs;
2+
3+
import com.flit.protoc.gen.server.BaseGenerator;
4+
import com.flit.protoc.gen.server.BaseServerGenerator;
5+
import com.flit.protoc.gen.server.TypeMapper;
6+
import com.google.protobuf.DescriptorProtos.FileDescriptorProto;
7+
import com.google.protobuf.DescriptorProtos.ServiceDescriptorProto;
8+
9+
public class JaxrsGenerator extends BaseServerGenerator {
10+
11+
@Override
12+
protected BaseGenerator getRpcGenerator(FileDescriptorProto proto, ServiceDescriptorProto service,
13+
String context, TypeMapper mapper) {
14+
return new RpcGenerator(proto, service, context, mapper);
15+
}
16+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package com.flit.protoc.gen.server.jaxrs;
2+
3+
import com.flit.protoc.gen.server.BaseGenerator;
4+
import com.flit.protoc.gen.server.TypeMapper;
5+
import com.flit.protoc.gen.server.Types;
6+
import com.google.common.net.MediaType;
7+
import com.google.protobuf.DescriptorProtos;
8+
import com.google.protobuf.DescriptorProtos.MethodDescriptorProto;
9+
import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse.File;
10+
import com.squareup.javapoet.AnnotationSpec;
11+
import com.squareup.javapoet.ClassName;
12+
import com.squareup.javapoet.FieldSpec;
13+
import com.squareup.javapoet.MethodSpec;
14+
import com.squareup.javapoet.ParameterSpec;
15+
import com.squareup.javapoet.TypeSpec;
16+
import com.squareup.javapoet.TypeSpec.Builder;
17+
import java.util.Collections;
18+
import java.util.List;
19+
import javax.lang.model.element.Modifier;
20+
21+
public class RpcGenerator extends BaseGenerator {
22+
23+
public static final ClassName PATH = ClassName.bestGuess("javax.ws.rs.Path");
24+
public static final ClassName POST = ClassName.bestGuess("javax.ws.rs.POST");
25+
public static final ClassName PRODUCES = ClassName.bestGuess("javax.ws.rs.Produces");
26+
public static final ClassName CONSUMES = ClassName.bestGuess("javax.ws.rs.Consumes");
27+
public static final ClassName CONTEXT = ClassName.bestGuess("javax.ws.rs.core.Context");
28+
public static final ClassName HttpServletRequest = ClassName.bestGuess("javax.servlet.http.HttpServletRequest");
29+
public static final ClassName HttpServletResponse = ClassName.bestGuess("javax.servlet.http.HttpServletResponse");
30+
private final String context;
31+
private final Builder rpcResource;
32+
33+
RpcGenerator(DescriptorProtos.FileDescriptorProto proto,
34+
DescriptorProtos.ServiceDescriptorProto service, String context, TypeMapper mapper) {
35+
super(proto, service, mapper);
36+
this.context = getContext(context);
37+
this.rpcResource = TypeSpec.classBuilder(getResourceName(service))
38+
.addModifiers(Modifier.PUBLIC)
39+
.addAnnotation(
40+
AnnotationSpec.builder(PATH).addMember("value", "$S",
41+
this.context + "/" + (proto.hasPackage() ? proto.getPackage() + "." : "") + service
42+
.getName()).build());
43+
addInstanceFields();
44+
addConstructor();
45+
service.getMethodList().forEach(this::addHandleMethod);
46+
}
47+
48+
private void addConstructor() {
49+
rpcResource.addMethod(MethodSpec.constructorBuilder()
50+
.addModifiers(Modifier.PUBLIC)
51+
.addParameter(getServiceInterface(), "service")
52+
.addStatement("this.service = service").build());
53+
}
54+
55+
private void addHandleMethod(MethodDescriptorProto mdp) {
56+
ClassName inputType = mapper.get(mdp.getInputType());
57+
ClassName outputType = mapper.get(mdp.getOutputType());
58+
rpcResource.addMethod(MethodSpec.methodBuilder("handle" + mdp.getName())
59+
.addModifiers(Modifier.PUBLIC)
60+
.addAnnotation(POST)
61+
.addAnnotation(AnnotationSpec.builder(PATH)
62+
.addMember("value", "$S", "/" + mdp.getName())
63+
.build())
64+
.addParameter(ParameterSpec.builder(HttpServletRequest, "request")
65+
.addAnnotation(CONTEXT).build())
66+
.addParameter(ParameterSpec.builder(HttpServletResponse, "response")
67+
.addAnnotation(CONTEXT).build())
68+
.addException(Types.Exception)
69+
.addStatement("boolean json = false")
70+
.addStatement("final $T data", inputType)
71+
.beginControlFlow("if (request.getContentType().equals($S))", MediaType.PROTOBUF.toString())
72+
.addStatement("data = $T.parseFrom(request.getInputStream())", inputType)
73+
.nextControlFlow("else if (request.getContentType().startsWith($S))", "application/json")
74+
.addStatement("json = true")
75+
.addStatement("$T.Builder builder = $T.newBuilder()", inputType, inputType)
76+
.addStatement("$T.parser().merge(new $T(request.getInputStream(), $T.UTF_8), builder)",
77+
Types.JsonFormat,
78+
Types.InputStreamReader,
79+
Types.StandardCharsets)
80+
.addStatement("data = builder.build()")
81+
.nextControlFlow("else")
82+
.addStatement("response.setStatus(415)")
83+
.addStatement("response.flushBuffer()")
84+
.addStatement("return")
85+
.endControlFlow()
86+
// route to the service
87+
.addStatement("$T retval = service.handle$L(data)", outputType, mdp.getName())
88+
.addStatement("response.setStatus(200)")
89+
// send the response
90+
.beginControlFlow("if (json)")
91+
.addStatement("response.setContentType($S)", MediaType.JSON_UTF_8.toString())
92+
.addStatement("response.getOutputStream().write($T.printer().omittingInsignificantWhitespace().print(retval).getBytes($T.UTF_8))",
93+
Types.JsonFormat,
94+
Types.StandardCharsets)
95+
.nextControlFlow("else")
96+
.addStatement("response.setContentType($S)", MediaType.PROTOBUF.toString())
97+
.addStatement("retval.writeTo(response.getOutputStream())")
98+
.endControlFlow()
99+
.addStatement("response.flushBuffer()")
100+
.build());
101+
}
102+
103+
private ClassName getResourceName(DescriptorProtos.ServiceDescriptorProto service) {
104+
return ClassName.get(javaPackage, "Rpc" + service.getName() + "Resource");
105+
}
106+
107+
private void addInstanceFields() {
108+
rpcResource.addField(FieldSpec.builder(getServiceInterface(), "service")
109+
.addModifiers(Modifier.PRIVATE, Modifier.FINAL).build());
110+
}
111+
112+
@Override
113+
public List<File> getFiles() {
114+
return Collections.singletonList(toFile(getResourceName(service), rpcResource.build()));
115+
}
116+
}

plugin/src/main/java/com/flit/protoc/gen/server/spring/RpcGenerator.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.flit.protoc.gen.server.BaseGenerator;
44
import com.flit.protoc.gen.server.TypeMapper;
55
import com.flit.protoc.gen.server.Types;
6+
import com.google.common.net.MediaType;
67
import com.google.protobuf.DescriptorProtos;
78
import com.google.protobuf.compiler.PluginProtos;
89
import com.squareup.javapoet.*;
@@ -47,7 +48,7 @@ private void addHandleMethod(DescriptorProtos.MethodDescriptorProto m) {
4748
.addAnnotation(AnnotationSpec.builder(PostMapping).addMember("value", "$S", route).build())
4849
.addStatement("boolean json = false")
4950
.addStatement("final $T data", inputType)
50-
.beginControlFlow("if (request.getContentType().equals($S))", "application/protobuf")
51+
.beginControlFlow("if (request.getContentType().equals($S))", MediaType.PROTOBUF.toString())
5152
.addStatement("data = $T.parseFrom(request.getInputStream())", inputType)
5253
.nextControlFlow("else if (request.getContentType().startsWith($S))", "application/json")
5354
.addStatement("json = true")
@@ -66,12 +67,12 @@ private void addHandleMethod(DescriptorProtos.MethodDescriptorProto m) {
6667
.addStatement("response.setStatus(200)")
6768
// send the response
6869
.beginControlFlow("if (json)")
69-
.addStatement("response.setContentType($S)", "application/json;charset=UTF-8")
70+
.addStatement("response.setContentType($S)", MediaType.JSON_UTF_8.toString())
7071
.addStatement("response.getOutputStream().write($T.printer().omittingInsignificantWhitespace().print(retval).getBytes($T.UTF_8))",
7172
Types.JsonFormat,
7273
Types.StandardCharsets)
7374
.nextControlFlow("else")
74-
.addStatement("response.setContentType($S)", "application/protobuf")
75+
.addStatement("response.setContentType($S)", MediaType.PROTOBUF.toString())
7576
.addStatement("retval.writeTo(response.getOutputStream())")
7677
.endControlFlow()
7778
.build());

plugin/src/main/java/com/flit/protoc/gen/server/undertow/RpcGenerator.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.flit.protoc.gen.server.BaseGenerator;
44
import com.flit.protoc.gen.server.TypeMapper;
55
import com.flit.protoc.gen.server.Types;
6+
import com.google.common.net.MediaType;
67
import com.google.protobuf.DescriptorProtos;
78
import com.google.protobuf.compiler.PluginProtos;
89
import com.squareup.javapoet.*;
@@ -110,7 +111,7 @@ private void writeHandleMethod(DescriptorProtos.MethodDescriptorProto m) {
110111
.addStatement("boolean json = false")
111112
.addStatement("final $T data", inputType)
112113
.addStatement("final String contentType = exchange.getRequestHeaders().get($T.CONTENT_TYPE).getFirst()", Headers)
113-
.beginControlFlow("if (contentType.equals($S))", "application/protobuf")
114+
.beginControlFlow("if (contentType.equals($S))", MediaType.PROTOBUF.toString())
114115
.addStatement("data = $T.parseFrom(exchange.getInputStream())", inputType)
115116
.nextControlFlow("else if (contentType.startsWith($S))", "application/json")
116117
.addStatement("json = true")
@@ -126,10 +127,10 @@ private void writeHandleMethod(DescriptorProtos.MethodDescriptorProto m) {
126127
.addStatement("exchange.setStatusCode(200)")
127128
// put the result on the wire
128129
.beginControlFlow("if (json)")
129-
.addStatement("exchange.getResponseHeaders().put($T.CONTENT_TYPE, $S)", Headers, "application/json;charset=UTF-8")
130+
.addStatement("exchange.getResponseHeaders().put($T.CONTENT_TYPE, $S)", Headers, MediaType.JSON_UTF_8.toString())
130131
.addStatement("exchange.getResponseSender().send($T.printer().omittingInsignificantWhitespace().print(response))", JsonFormat)
131132
.nextControlFlow("else")
132-
.addStatement("exchange.getResponseHeaders().put($T.CONTENT_TYPE, $S)", Headers, "application/protobuf")
133+
.addStatement("exchange.getResponseHeaders().put($T.CONTENT_TYPE, $S)", Headers, MediaType.PROTOBUF.toString())
133134
.addStatement("response.writeTo(exchange.getOutputStream())")
134135
.endControlFlow()
135136
.build());

plugin/src/test/java/com/flit/protoc/PluginTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public class PluginTest {
1414
PluginProtos.CodeGeneratorResponse response = plugin.process();
1515

1616
assertTrue("Expected an error for no parameters", response.hasError());
17-
assertEquals("Incorrect error message", "Usage: --flit_out=target=server,type=[spring|undertow]:<PATH>", response.getError());
17+
assertEquals("Incorrect error message", "Usage: --flit_out=target=server,type=[spring|undertow|jaxrs]:<PATH>", response.getError());
1818
}
1919

2020
@Test public void test_NoTargetSpecified() {
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package com.flit.protoc.gen.server.jaxrs;
2+
3+
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertNotNull;
5+
import static org.junit.Assert.assertTrue;
6+
7+
import com.flit.protoc.Plugin;
8+
import com.flit.protoc.gen.BaseGeneratorTest;
9+
import com.google.protobuf.compiler.PluginProtos;
10+
import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse.File;
11+
import java.util.Map;
12+
import java.util.function.Function;
13+
import java.util.stream.Collectors;
14+
import org.junit.Test;
15+
16+
/**
17+
* Tests the generation of a service that has core definition imported from another file
18+
*/
19+
public class ContextGeneratorTest extends BaseGeneratorTest {
20+
21+
@Test
22+
public void test_GenerateWithMissingRoot() throws Exception {
23+
test_Route("context.missing.jaxrs.json", "/twirp/com.example.context.NullService");
24+
}
25+
26+
@Test
27+
public void test_GenerateWithEmptyRoot() throws Exception {
28+
test_Route("context.empty.jaxrs.json", "/twirp/com.example.context.NullService");
29+
}
30+
31+
@Test
32+
public void test_GenerateWithSlashOnlyRoot() throws Exception {
33+
test_Route("context.slash.jaxrs.json", "/com.example.context.NullService");
34+
}
35+
36+
@Test
37+
public void test_GenerateWithSlashRoot() throws Exception {
38+
test_Route("context.root.jaxrs.json", "/root/com.example.context.NullService");
39+
}
40+
41+
@Test
42+
public void test_GenerateWithNameRoot() throws Exception {
43+
test_Route("context.name.jaxrs.json", "/fibble/com.example.context.NullService");
44+
}
45+
46+
private void test_Route(String file, String route) throws Exception {
47+
PluginProtos.CodeGeneratorRequest request = loadJson(file);
48+
49+
Plugin plugin = new Plugin(request);
50+
PluginProtos.CodeGeneratorResponse response = plugin.process();
51+
52+
assertNotNull(response);
53+
assertEquals(2, response.getFileCount());
54+
55+
Map<String, File> files = response.getFileList()
56+
.stream()
57+
.collect(Collectors
58+
.toMap(PluginProtos.CodeGeneratorResponse.File::getName, Function.identity()));
59+
60+
assertTrue(files.containsKey("com/example/context/rpc/RpcNullService.java"));
61+
assertTrue(files.containsKey("com/example/context/rpc/RpcNullServiceResource.java"));
62+
63+
assertTrue(files.get("com/example/context/rpc/RpcNullServiceResource.java")
64+
.getContent()
65+
.contains(String.format("@Path(\"%s\")", route)));
66+
}
67+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.flit.protoc.gen.server.jaxrs;
2+
3+
import static java.util.stream.Collectors.toList;
4+
import static org.junit.Assert.assertEquals;
5+
import static org.junit.Assert.assertNotNull;
6+
7+
import com.flit.protoc.Plugin;
8+
import com.flit.protoc.gen.BaseGeneratorTest;
9+
import com.google.protobuf.compiler.PluginProtos;
10+
import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse.File;
11+
import org.approvaltests.Approvals;
12+
import org.junit.Test;
13+
14+
public class HelloworldGeneratorTest extends BaseGeneratorTest {
15+
16+
@Test
17+
public void test_Generate() throws Exception {
18+
PluginProtos.CodeGeneratorRequest request = loadJson("helloworld.jaxrs.json");
19+
20+
Plugin plugin = new Plugin(request);
21+
PluginProtos.CodeGeneratorResponse response = plugin.process();
22+
23+
assertNotNull(response);
24+
assertEquals(2, response.getFileCount());
25+
assertEquals(response.getFile(0).getName(), "com/example/helloworld/RpcHelloWorld.java");
26+
assertEquals(response.getFile(1).getName(), "com/example/helloworld/RpcHelloWorldResource.java");
27+
28+
Approvals.verifyAll("", response.getFileList().stream().map(File::getContent).collect(toList()));
29+
response.getFileList().forEach(BaseGeneratorTest::assertParses);
30+
}
31+
}

0 commit comments

Comments
 (0)