Skip to content

Commit

Permalink
Merge pull request #5 from github/support-jaxrs
Browse files Browse the repository at this point in the history
Support JAX-RS
  • Loading branch information
jlisam authored Aug 25, 2020
2 parents c54f617 + 13211d6 commit 99a203f
Show file tree
Hide file tree
Showing 34 changed files with 898 additions and 24 deletions.
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ It supports the generation of Java based servers with the following flavours sup

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

## Building & Running

### Requirements

The build has been tested with [Oracle's JDK](http://www.oracle.com/technetwork/java/javase/downloads/index.html "JDK Downloads") (version 1.8)
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)

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

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

The flit plugin accepts the following plugin parameters:

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

# Development

Expand Down
2 changes: 1 addition & 1 deletion plugin/gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version=1.1.0
version=1.2.0
5 changes: 4 additions & 1 deletion plugin/src/main/java/com/flit/protoc/Plugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.flit.protoc.gen.Generator;
import com.flit.protoc.gen.GeneratorException;
import com.flit.protoc.gen.server.jaxrs.JaxrsGenerator;
import com.flit.protoc.gen.server.spring.SpringGenerator;
import com.flit.protoc.gen.server.undertow.UndertowGenerator;
import com.google.protobuf.compiler.PluginProtos.CodeGeneratorRequest;
Expand All @@ -22,7 +23,7 @@ public Plugin(CodeGeneratorRequest request) {

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

Map<String, Parameter> params = Parameter.of(request.getParameter());
Expand Down Expand Up @@ -50,6 +51,8 @@ private Generator resolveGenerator(Map<String, Parameter> params) {
return new SpringGenerator();
case "undertow":
return new UndertowGenerator();
case "jaxrs":
return new JaxrsGenerator();
default:
throw new GeneratorException("Unknown server type: " + params.get(PARAM_TYPE).getValue());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.flit.protoc.gen.server.jaxrs;

import com.flit.protoc.gen.server.BaseGenerator;
import com.flit.protoc.gen.server.BaseServerGenerator;
import com.flit.protoc.gen.server.TypeMapper;
import com.google.protobuf.DescriptorProtos.FileDescriptorProto;
import com.google.protobuf.DescriptorProtos.ServiceDescriptorProto;

public class JaxrsGenerator extends BaseServerGenerator {

@Override
protected BaseGenerator getRpcGenerator(FileDescriptorProto proto, ServiceDescriptorProto service,
String context, TypeMapper mapper) {
return new RpcGenerator(proto, service, context, mapper);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package com.flit.protoc.gen.server.jaxrs;

import com.flit.protoc.gen.server.BaseGenerator;
import com.flit.protoc.gen.server.TypeMapper;
import com.flit.protoc.gen.server.Types;
import com.google.common.net.MediaType;
import com.google.protobuf.DescriptorProtos;
import com.google.protobuf.DescriptorProtos.MethodDescriptorProto;
import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse.File;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeSpec.Builder;
import java.util.Collections;
import java.util.List;
import javax.lang.model.element.Modifier;

public class RpcGenerator extends BaseGenerator {

public static final ClassName PATH = ClassName.bestGuess("javax.ws.rs.Path");
public static final ClassName POST = ClassName.bestGuess("javax.ws.rs.POST");
public static final ClassName PRODUCES = ClassName.bestGuess("javax.ws.rs.Produces");
public static final ClassName CONSUMES = ClassName.bestGuess("javax.ws.rs.Consumes");
public static final ClassName CONTEXT = ClassName.bestGuess("javax.ws.rs.core.Context");
public static final ClassName HttpServletRequest = ClassName.bestGuess("javax.servlet.http.HttpServletRequest");
public static final ClassName HttpServletResponse = ClassName.bestGuess("javax.servlet.http.HttpServletResponse");
private final String context;
private final Builder rpcResource;

RpcGenerator(DescriptorProtos.FileDescriptorProto proto,
DescriptorProtos.ServiceDescriptorProto service, String context, TypeMapper mapper) {
super(proto, service, mapper);
this.context = getContext(context);
this.rpcResource = TypeSpec.classBuilder(getResourceName(service))
.addModifiers(Modifier.PUBLIC)
.addAnnotation(
AnnotationSpec.builder(PATH).addMember("value", "$S",
this.context + "/" + (proto.hasPackage() ? proto.getPackage() + "." : "") + service
.getName()).build());
addInstanceFields();
addConstructor();
service.getMethodList().forEach(this::addHandleMethod);
}

private void addConstructor() {
rpcResource.addMethod(MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(getServiceInterface(), "service")
.addStatement("this.service = service").build());
}

private void addHandleMethod(MethodDescriptorProto mdp) {
ClassName inputType = mapper.get(mdp.getInputType());
ClassName outputType = mapper.get(mdp.getOutputType());
rpcResource.addMethod(MethodSpec.methodBuilder("handle" + mdp.getName())
.addModifiers(Modifier.PUBLIC)
.addAnnotation(POST)
.addAnnotation(AnnotationSpec.builder(PATH)
.addMember("value", "$S", "/" + mdp.getName())
.build())
.addParameter(ParameterSpec.builder(HttpServletRequest, "request")
.addAnnotation(CONTEXT).build())
.addParameter(ParameterSpec.builder(HttpServletResponse, "response")
.addAnnotation(CONTEXT).build())
.addException(Types.Exception)
.addStatement("boolean json = false")
.addStatement("final $T data", inputType)
.beginControlFlow("if (request.getContentType().equals($S))", MediaType.PROTOBUF.toString())
.addStatement("data = $T.parseFrom(request.getInputStream())", inputType)
.nextControlFlow("else if (request.getContentType().startsWith($S))", "application/json")
.addStatement("json = true")
.addStatement("$T.Builder builder = $T.newBuilder()", inputType, inputType)
.addStatement("$T.parser().merge(new $T(request.getInputStream(), $T.UTF_8), builder)",
Types.JsonFormat,
Types.InputStreamReader,
Types.StandardCharsets)
.addStatement("data = builder.build()")
.nextControlFlow("else")
.addStatement("response.setStatus(415)")
.addStatement("response.flushBuffer()")
.addStatement("return")
.endControlFlow()
// route to the service
.addStatement("$T retval = service.handle$L(data)", outputType, mdp.getName())
.addStatement("response.setStatus(200)")
// send the response
.beginControlFlow("if (json)")
.addStatement("response.setContentType($S)", MediaType.JSON_UTF_8.toString())
.addStatement("response.getOutputStream().write($T.printer().omittingInsignificantWhitespace().print(retval).getBytes($T.UTF_8))",
Types.JsonFormat,
Types.StandardCharsets)
.nextControlFlow("else")
.addStatement("response.setContentType($S)", MediaType.PROTOBUF.toString())
.addStatement("retval.writeTo(response.getOutputStream())")
.endControlFlow()
.addStatement("response.flushBuffer()")
.build());
}

private ClassName getResourceName(DescriptorProtos.ServiceDescriptorProto service) {
return ClassName.get(javaPackage, "Rpc" + service.getName() + "Resource");
}

private void addInstanceFields() {
rpcResource.addField(FieldSpec.builder(getServiceInterface(), "service")
.addModifiers(Modifier.PRIVATE, Modifier.FINAL).build());
}

@Override
public List<File> getFiles() {
return Collections.singletonList(toFile(getResourceName(service), rpcResource.build()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.flit.protoc.gen.server.BaseGenerator;
import com.flit.protoc.gen.server.TypeMapper;
import com.flit.protoc.gen.server.Types;
import com.google.common.net.MediaType;
import com.google.protobuf.DescriptorProtos;
import com.google.protobuf.compiler.PluginProtos;
import com.squareup.javapoet.*;
Expand Down Expand Up @@ -47,7 +48,7 @@ private void addHandleMethod(DescriptorProtos.MethodDescriptorProto m) {
.addAnnotation(AnnotationSpec.builder(PostMapping).addMember("value", "$S", route).build())
.addStatement("boolean json = false")
.addStatement("final $T data", inputType)
.beginControlFlow("if (request.getContentType().equals($S))", "application/protobuf")
.beginControlFlow("if (request.getContentType().equals($S))", MediaType.PROTOBUF.toString())
.addStatement("data = $T.parseFrom(request.getInputStream())", inputType)
.nextControlFlow("else if (request.getContentType().startsWith($S))", "application/json")
.addStatement("json = true")
Expand All @@ -66,12 +67,12 @@ private void addHandleMethod(DescriptorProtos.MethodDescriptorProto m) {
.addStatement("response.setStatus(200)")
// send the response
.beginControlFlow("if (json)")
.addStatement("response.setContentType($S)", "application/json;charset=UTF-8")
.addStatement("response.setContentType($S)", MediaType.JSON_UTF_8.toString())
.addStatement("response.getOutputStream().write($T.printer().omittingInsignificantWhitespace().print(retval).getBytes($T.UTF_8))",
Types.JsonFormat,
Types.StandardCharsets)
.nextControlFlow("else")
.addStatement("response.setContentType($S)", "application/protobuf")
.addStatement("response.setContentType($S)", MediaType.PROTOBUF.toString())
.addStatement("retval.writeTo(response.getOutputStream())")
.endControlFlow()
.build());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.flit.protoc.gen.server.BaseGenerator;
import com.flit.protoc.gen.server.TypeMapper;
import com.flit.protoc.gen.server.Types;
import com.google.common.net.MediaType;
import com.google.protobuf.DescriptorProtos;
import com.google.protobuf.compiler.PluginProtos;
import com.squareup.javapoet.*;
Expand Down Expand Up @@ -110,7 +111,7 @@ private void writeHandleMethod(DescriptorProtos.MethodDescriptorProto m) {
.addStatement("boolean json = false")
.addStatement("final $T data", inputType)
.addStatement("final String contentType = exchange.getRequestHeaders().get($T.CONTENT_TYPE).getFirst()", Headers)
.beginControlFlow("if (contentType.equals($S))", "application/protobuf")
.beginControlFlow("if (contentType.equals($S))", MediaType.PROTOBUF.toString())
.addStatement("data = $T.parseFrom(exchange.getInputStream())", inputType)
.nextControlFlow("else if (contentType.startsWith($S))", "application/json")
.addStatement("json = true")
Expand All @@ -126,10 +127,10 @@ private void writeHandleMethod(DescriptorProtos.MethodDescriptorProto m) {
.addStatement("exchange.setStatusCode(200)")
// put the result on the wire
.beginControlFlow("if (json)")
.addStatement("exchange.getResponseHeaders().put($T.CONTENT_TYPE, $S)", Headers, "application/json;charset=UTF-8")
.addStatement("exchange.getResponseHeaders().put($T.CONTENT_TYPE, $S)", Headers, MediaType.JSON_UTF_8.toString())
.addStatement("exchange.getResponseSender().send($T.printer().omittingInsignificantWhitespace().print(response))", JsonFormat)
.nextControlFlow("else")
.addStatement("exchange.getResponseHeaders().put($T.CONTENT_TYPE, $S)", Headers, "application/protobuf")
.addStatement("exchange.getResponseHeaders().put($T.CONTENT_TYPE, $S)", Headers, MediaType.PROTOBUF.toString())
.addStatement("response.writeTo(exchange.getOutputStream())")
.endControlFlow()
.build());
Expand Down
2 changes: 1 addition & 1 deletion plugin/src/test/java/com/flit/protoc/PluginTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class PluginTest {
PluginProtos.CodeGeneratorResponse response = plugin.process();

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

@Test public void test_NoTargetSpecified() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.flit.protoc.gen.server.jaxrs;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import com.flit.protoc.Plugin;
import com.flit.protoc.gen.BaseGeneratorTest;
import com.google.protobuf.compiler.PluginProtos;
import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse.File;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.junit.Test;

/**
* Tests the generation of a service that has core definition imported from another file
*/
public class ContextGeneratorTest extends BaseGeneratorTest {

@Test
public void test_GenerateWithMissingRoot() throws Exception {
test_Route("context.missing.jaxrs.json", "/twirp/com.example.context.NullService");
}

@Test
public void test_GenerateWithEmptyRoot() throws Exception {
test_Route("context.empty.jaxrs.json", "/twirp/com.example.context.NullService");
}

@Test
public void test_GenerateWithSlashOnlyRoot() throws Exception {
test_Route("context.slash.jaxrs.json", "/com.example.context.NullService");
}

@Test
public void test_GenerateWithSlashRoot() throws Exception {
test_Route("context.root.jaxrs.json", "/root/com.example.context.NullService");
}

@Test
public void test_GenerateWithNameRoot() throws Exception {
test_Route("context.name.jaxrs.json", "/fibble/com.example.context.NullService");
}

private void test_Route(String file, String route) throws Exception {
PluginProtos.CodeGeneratorRequest request = loadJson(file);

Plugin plugin = new Plugin(request);
PluginProtos.CodeGeneratorResponse response = plugin.process();

assertNotNull(response);
assertEquals(2, response.getFileCount());

Map<String, File> files = response.getFileList()
.stream()
.collect(Collectors
.toMap(PluginProtos.CodeGeneratorResponse.File::getName, Function.identity()));

assertTrue(files.containsKey("com/example/context/rpc/RpcNullService.java"));
assertTrue(files.containsKey("com/example/context/rpc/RpcNullServiceResource.java"));

assertTrue(files.get("com/example/context/rpc/RpcNullServiceResource.java")
.getContent()
.contains(String.format("@Path(\"%s\")", route)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.flit.protoc.gen.server.jaxrs;

import static java.util.stream.Collectors.toList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

import com.flit.protoc.Plugin;
import com.flit.protoc.gen.BaseGeneratorTest;
import com.google.protobuf.compiler.PluginProtos;
import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse.File;
import org.approvaltests.Approvals;
import org.junit.Test;

public class HelloworldGeneratorTest extends BaseGeneratorTest {

@Test
public void test_Generate() throws Exception {
PluginProtos.CodeGeneratorRequest request = loadJson("helloworld.jaxrs.json");

Plugin plugin = new Plugin(request);
PluginProtos.CodeGeneratorResponse response = plugin.process();

assertNotNull(response);
assertEquals(2, response.getFileCount());
assertEquals(response.getFile(0).getName(), "com/example/helloworld/RpcHelloWorld.java");
assertEquals(response.getFile(1).getName(), "com/example/helloworld/RpcHelloWorldResource.java");

Approvals.verifyAll("", response.getFileList().stream().map(File::getContent).collect(toList()));
response.getFileList().forEach(BaseGeneratorTest::assertParses);
}
}
Loading

0 comments on commit 99a203f

Please sign in to comment.