├── settings.gradle ├── .gitignore ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src └── main │ ├── java │ └── nl │ │ └── toefel │ │ ├── server │ │ ├── ServerMain.java │ │ ├── ReservationRepository.java │ │ └── ReservationController.java │ │ └── client │ │ └── ClientMain.java │ └── proto │ └── reservation_service.proto ├── envoy-config.yml ├── gradlew.bat ├── start-envoy.sh ├── gradlew └── README.md /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'transcoding-grpc-to-http-json' 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | product-review.iml 3 | .gradle/ 4 | out/ 5 | transcoding-grpc-to-http-json.iml 6 | build 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toefel18/transcoding-grpc-to-http-json/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Oct 08 21:02:31 CEST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.8-all.zip 7 | -------------------------------------------------------------------------------- /src/main/java/nl/toefel/server/ServerMain.java: -------------------------------------------------------------------------------- 1 | package nl.toefel.server; 2 | 3 | import io.grpc.Server; 4 | import io.grpc.ServerBuilder; 5 | 6 | import java.io.IOException; 7 | 8 | public class ServerMain { 9 | public static void main(String[] args) throws IOException, InterruptedException { 10 | 11 | ReservationRepository repository = new ReservationRepository(); 12 | 13 | Server service = ServerBuilder.forPort(53000) 14 | .addService(new ReservationController(repository)) 15 | .build() 16 | .start(); 17 | 18 | Runtime.getRuntime().addShutdownHook(new Thread(service::shutdownNow)); 19 | System.out.println("Started listening for rpc calls on 53000..."); 20 | service.awaitTermination(); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/nl/toefel/server/ReservationRepository.java: -------------------------------------------------------------------------------- 1 | package nl.toefel.server; 2 | 3 | import nl.toefel.reservations.v1.Reservation; 4 | 5 | import java.util.*; 6 | 7 | public class ReservationRepository { 8 | 9 | private Map reservationById = new HashMap<>(); 10 | 11 | public Optional findReservation(String id) { 12 | return Optional.ofNullable(reservationById.get(id)); 13 | } 14 | 15 | public Reservation createReservation(Reservation reservation) { 16 | String id = UUID.randomUUID().toString(); 17 | Reservation reservationWithId = reservation.toBuilder() 18 | .setId(id) 19 | .build(); 20 | 21 | reservationById.put(id, reservationWithId); 22 | 23 | return reservationWithId; 24 | } 25 | 26 | public List listReservations() { 27 | return List.copyOf(reservationById.values()); 28 | } 29 | 30 | public void deleteReservation(String id) { 31 | reservationById.remove(id); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /envoy-config.yml: -------------------------------------------------------------------------------- 1 | admin: 2 | access_log_path: /tmp/admin_access.log 3 | address: 4 | socket_address: { address: 0.0.0.0, port_value: 9901 } #1 5 | 6 | static_resources: 7 | listeners: 8 | - name: main-listener 9 | address: 10 | socket_address: { address: 0.0.0.0, port_value: 51051 } #2 11 | filter_chains: 12 | - filters: 13 | - name: envoy.http_connection_manager 14 | config: 15 | stat_prefix: grpc_json 16 | codec_type: AUTO 17 | route_config: 18 | name: local_route 19 | virtual_hosts: 20 | - name: local_service 21 | domains: ["*"] 22 | routes: 23 | - match: { prefix: "/" , grpc: {}} # 3a grpc:{} means that requests are only forwarded if they are found in the grpc service definition, returning 404 for others 24 | route: { cluster: grpc-backend-services, timeout: { seconds: 60 } } #3b 25 | http_filters: 26 | - name: envoy.grpc_json_transcoder 27 | # configuration docs: https://github.com/envoyproxy/envoy/blob/master/api/envoy/config/filter/http/transcoder/v2/transcoder.proto 28 | config: 29 | proto_descriptor: "/data/reservation_service_definition.pb" #4 30 | services: ["reservations.v1.ReservationService"] #5 31 | print_options: 32 | add_whitespace: true 33 | always_print_primitive_fields: true 34 | always_print_enums_as_ints: false 35 | preserve_proto_field_names: false #6 36 | - name: envoy.router 37 | 38 | clusters: 39 | - name: grpc-backend-services #7 40 | connect_timeout: 1.25s 41 | type: logical_dns 42 | lb_policy: round_robin 43 | dns_lookup_family: V4_ONLY 44 | http2_protocol_options: {} 45 | hosts: 46 | - socket_address: 47 | address: 127.0.0.1 #8 48 | port_value: 53000 49 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /start-envoy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if ! [ -x "$(command -v protoc)" ] ; then 4 | echo "you do not seem to have the protoc executable on your path" 5 | echo "we need protoc to generate a service defintion (*.pb file) that envoy can understand" 6 | echo "download the precompiled protoc executable and place it in somewhere in your systems PATH!" 7 | echo "goto: https://github.com/protocolbuffers/protobuf/releases/latest" 8 | echo "choose:" 9 | echo " for linux: protoc-3.6.1-linux-x86_64.zip" 10 | echo " for windows: protoc-3.6.1-win32.zip" 11 | echo " for mac: protoc-3.6.1-osx-x86_64.zip" 12 | exit 1 13 | fi 14 | 15 | # generate the reservation_service_definition.pb file that we can pass to envoy so that knows the grpc service 16 | # we want to expose 17 | protoc -I. -Ibuild/extracted-include-protos/main --include_imports \ 18 | --include_source_info \ 19 | --descriptor_set_out=reservation_service_definition.pb \ 20 | src/main/proto/reservation_service.proto 21 | 22 | if ! [ $? -eq 0 ]; then 23 | echo "protobuf compilation failed" 24 | exit 1 25 | fi 26 | 27 | # now we can start envoy in a docker container and map the configuration and service definition inside 28 | # we use --network="host" so that envoy can access the grpc service at localhost: 29 | # the envoy-config.yml has configured envoy to run at port 51051, so you can access the HTTP/JSON 30 | # api at localhost:51051 31 | 32 | 33 | if ! [ -x "$(command -v docker)" ] ; then 34 | echo "docker command is not available, please install docker" 35 | echo "Install docker: https://store.docker.com/search?offering=community&type=edition" 36 | exit 1 37 | fi 38 | 39 | # check if sudo is required to run docker 40 | if [ "$(groups | grep -c docker)" -gt "0" ]; then 41 | echo "Envoy will run at port 51051 (see envoy-config.yml)" 42 | docker run -it --rm --name envoy --network="host" \ 43 | -v "$(pwd)/reservation_service_definition.pb:/data/reservation_service_definition.pb:ro" \ 44 | -v "$(pwd)/envoy-config.yml:/etc/envoy/envoy.yaml:ro" \ 45 | envoyproxy/envoy 46 | else 47 | echo "you are not in the docker group, running with sudo" 48 | echo "Envoy will run at port 51051 (see envoy-config.yml)" 49 | sudo docker run -it --rm --name envoy --network="host"\ 50 | -v "$(pwd)/reservation_service_definition.pb:/data/reservation_service_definition.pb:ro" \ 51 | -v "$(pwd)/envoy-config.yml:/etc/envoy/envoy.yaml:ro" \ 52 | envoyproxy/envoy 53 | fi 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/main/proto/reservation_service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package reservations.v1; 4 | 5 | // Creates separate .java files for message and service 6 | // instead of creating them inside the class defined by 7 | // java_outer_classname 8 | option java_multiple_files = true; 9 | 10 | // Class that will contain descriptor 11 | option java_outer_classname = "ReservationServiceProto"; 12 | 13 | // The package where the generated classes will reside 14 | option java_package = "nl.toefel.reservations.v1"; 15 | 16 | // required to add annotations to the rpc calls 17 | import "google/api/annotations.proto"; 18 | import "google/protobuf/empty.proto"; 19 | 20 | service ReservationService { 21 | 22 | rpc CreateReservation(CreateReservationRequest) returns (Reservation) { 23 | option(google.api.http) = { 24 | post: "/v1/reservations" 25 | body: "reservation" 26 | }; 27 | } 28 | 29 | rpc GetReservation(GetReservationRequest) returns (Reservation) { 30 | // {id} is mapped into the GetReservationRequest.id field! 31 | option (google.api.http) = { 32 | get: "/v1/reservations/{id}" 33 | }; 34 | } 35 | 36 | // lists all the reservations, use query params on venue or timestamp to filter the resultset. 37 | rpc ListReservations(ListReservationsRequest) returns (stream Reservation) { 38 | // use query parameter to specify filters, example: ?venue=UtrechtHomeoffice 39 | // these query parameters will be automatically mapped to the ListReservationRequest object 40 | option (google.api.http) = { 41 | get: "/v1/reservations" 42 | }; 43 | } 44 | 45 | rpc DeleteReservation(DeleteReservationRequest) returns (google.protobuf.Empty) { 46 | // {id} is mapped into the DeleteReservationRequest.id field! 47 | option (google.api.http) = { 48 | delete: "/v1/reservations/{id}" 49 | }; 50 | } 51 | 52 | } 53 | 54 | message Reservation { 55 | string id = 1; 56 | string title = 2; 57 | string venue = 3; 58 | string room = 4; 59 | string timestamp = 5; 60 | repeated Person attendees = 6; 61 | } 62 | 63 | message Person { 64 | string ssn = 1; 65 | string firstName = 2; 66 | string lastName = 3; 67 | } 68 | 69 | message CreateReservationRequest { 70 | Reservation reservation = 2; 71 | } 72 | 73 | message CreateReservationResponse { 74 | Reservation reservation = 1; 75 | } 76 | 77 | message GetReservationRequest { 78 | string id = 1; 79 | } 80 | 81 | message ListReservationsRequest { 82 | string venue = 1; 83 | string timestamp = 2; 84 | string room = 3; 85 | 86 | Attendees attendees = 4; 87 | 88 | message Attendees { 89 | repeated string lastName = 1; 90 | } 91 | } 92 | 93 | message DeleteReservationRequest { 94 | string id = 1; 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/nl/toefel/server/ReservationController.java: -------------------------------------------------------------------------------- 1 | package nl.toefel.server; 2 | 3 | import com.google.protobuf.Empty; 4 | import io.grpc.Status; 5 | import io.grpc.stub.StreamObserver; 6 | import nl.toefel.reservations.v1.*; 7 | 8 | import java.util.List; 9 | import java.util.Optional; 10 | import java.util.function.Predicate; 11 | import java.util.stream.Collectors; 12 | import java.util.stream.Stream; 13 | 14 | class ReservationController extends ReservationServiceGrpc.ReservationServiceImplBase { 15 | 16 | private ReservationRepository reservationRepository; 17 | 18 | public ReservationController(ReservationRepository reservationRepository) { 19 | this.reservationRepository = reservationRepository; 20 | } 21 | 22 | @Override 23 | public void createReservation(CreateReservationRequest request, StreamObserver responseObserver) { 24 | System.out.println("createReservation() called"); 25 | Reservation createdReservation = reservationRepository.createReservation(request.getReservation()); 26 | responseObserver.onNext(createdReservation); 27 | responseObserver.onCompleted(); 28 | } 29 | 30 | @Override 31 | public void deleteReservation(DeleteReservationRequest request, StreamObserver responseObserver) { 32 | System.out.println("deleteReservation() called"); 33 | reservationRepository.deleteReservation(request.getId()); 34 | responseObserver.onNext(Empty.newBuilder().build()); 35 | responseObserver.onCompleted(); 36 | } 37 | 38 | @Override 39 | public void getReservation(GetReservationRequest request, StreamObserver responseObserver) { 40 | System.out.println("getReservation() called"); 41 | Optional optionalReservation = reservationRepository.findReservation(request.getId()); 42 | if (optionalReservation.isPresent()) { 43 | responseObserver.onNext(optionalReservation.orElse(Reservation.newBuilder().build())); 44 | responseObserver.onCompleted(); 45 | } else { 46 | responseObserver.onError(Status.NOT_FOUND 47 | .withDescription("no reservation with id " + request.getId()) 48 | .asRuntimeException()); 49 | } 50 | } 51 | 52 | @Override 53 | public void listReservations(ListReservationsRequest request, StreamObserver responseObserver) { 54 | System.out.println("listReservations() called with " + request); 55 | if ("error".equals(request.getRoom())) { 56 | responseObserver.onError(Status.UNAUTHENTICATED.asRuntimeException()); 57 | } else if ("throw".equals(request.getRoom())) { 58 | throw Status.UNAUTHENTICATED.asRuntimeException(); 59 | } else { 60 | // nothing, empty response should yield [] 61 | responseObserver.onCompleted(); 62 | } 63 | } 64 | 65 | private boolean hasAttendeeLastNames(Reservation it, List requiredAttendeeLastNames) { 66 | List reservationAttendeeLastNames = it.getAttendeesList().stream().map(Person::getLastName).collect(Collectors.toList()); 67 | return reservationAttendeeLastNames.containsAll(requiredAttendeeLastNames); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/nl/toefel/client/ClientMain.java: -------------------------------------------------------------------------------- 1 | package nl.toefel.client; 2 | 3 | import io.grpc.ManagedChannel; 4 | import io.grpc.ManagedChannelBuilder; 5 | import io.grpc.StatusRuntimeException; 6 | import nl.toefel.reservations.v1.*; 7 | 8 | import java.util.concurrent.atomic.AtomicInteger; 9 | 10 | public class ClientMain { 11 | public static void main(String[] args) { 12 | ManagedChannel channel = ManagedChannelBuilder 13 | .forAddress("localhost", 53000) 14 | .usePlaintext() // disable TLS which is enabled by default and requires certificates 15 | .build(); 16 | 17 | System.out.println("--- Getting reservation 123 which does not exist"); 18 | ReservationServiceGrpc.ReservationServiceBlockingStub reservationClient = ReservationServiceGrpc.newBlockingStub(channel); 19 | 20 | try { 21 | Reservation nonExistingReservation = reservationClient.getReservation(GetReservationRequest.newBuilder() 22 | .setId("123") 23 | .build()); 24 | System.out.println("response:" + nonExistingReservation); 25 | } catch (StatusRuntimeException e) { 26 | System.out.println(e.getStatus().getDescription() + " " + e.getMessage()); 27 | System.out.println(e); 28 | } 29 | 30 | String createdReservationId = createReservation(reservationClient, "JDriven Coltbaan 3", "2018-10-10T11:12:13", "meeting-room"); 31 | 32 | System.out.println("--- Getting reservation with id " + createdReservationId); 33 | Reservation existingReservation = reservationClient.getReservation(GetReservationRequest.newBuilder() 34 | .setId(createdReservationId) 35 | .build()); 36 | System.out.println("response: " + existingReservation); 37 | 38 | 39 | // deleteReservation(reservationClient, createdReservationId); 40 | 41 | 42 | System.out.println("--- Getting reservation with id " + createdReservationId); 43 | Reservation existingReservation2 = reservationClient.getReservation(GetReservationRequest.newBuilder() 44 | .setId(createdReservationId) 45 | .build()); 46 | System.out.println("response: " + existingReservation2); 47 | 48 | 49 | createReservation(reservationClient, "JDriven Coltbaan 3", "2018-11-10T12:12:13", "meeting-room"); 50 | createReservation(reservationClient, "JDriven Coltbaan 3", "2019-11-12T11:30:13", "meeting-room"); 51 | createReservation(reservationClient, "Vandervalk Hotel", "2019-11-12T11:30:13", "meeting-room"); 52 | 53 | AtomicInteger counterWithoutFilters = new AtomicInteger(); 54 | reservationClient.listReservations(ListReservationsRequest.newBuilder().build()).forEachRemaining(it -> counterWithoutFilters.getAndIncrement()); 55 | System.out.println("Received " +counterWithoutFilters.get() + " reservations without filter params"); 56 | 57 | AtomicInteger counterWithVenueFilter = new AtomicInteger(); 58 | reservationClient.listReservations(ListReservationsRequest.newBuilder().setVenue("JDriven Coltbaan 3").build()).forEachRemaining(it -> counterWithVenueFilter.getAndIncrement()); 59 | System.out.println("Received " +counterWithVenueFilter.get() + " reservations with venue filter params"); 60 | 61 | AtomicInteger counterWithTimestampFilter = new AtomicInteger(); 62 | reservationClient.listReservations(ListReservationsRequest.newBuilder().setTimestamp("2019-11-12T11:30:13").build()).forEachRemaining(it -> counterWithTimestampFilter.getAndIncrement()); 63 | System.out.println("Received " +counterWithTimestampFilter.get() + " reservations with timestamp filter params"); 64 | } 65 | 66 | private static void deleteReservation(ReservationServiceGrpc.ReservationServiceBlockingStub reservationClient, String createdReservationId) { 67 | System.out.println("--- Deleting reservation with id " + createdReservationId); 68 | reservationClient.deleteReservation(DeleteReservationRequest.newBuilder() 69 | .setId(createdReservationId) 70 | .build()); 71 | System.out.println("response: empty" ); 72 | } 73 | 74 | private static String createReservation(ReservationServiceGrpc.ReservationServiceBlockingStub reservationClient, String venue, String timestamp, String room) { 75 | System.out.println("--- Creating reservation "); 76 | Reservation newReservation = Reservation.newBuilder() 77 | .setTitle("Lunchmeeting") 78 | .addAttendees(Person.newBuilder() 79 | .setSsn("1234567890") 80 | .setFirstName("Jimmy") 81 | .setLastName("Jones").build()) 82 | .addAttendees(Person.newBuilder() 83 | .setSsn("9999999999") 84 | .setFirstName("Dennis") 85 | .setLastName("Richie").build()) 86 | .setVenue(venue) 87 | .setRoom(room) 88 | .setTimestamp(timestamp) 89 | .build(); 90 | 91 | Reservation createdReservationResponse = reservationClient.createReservation( 92 | CreateReservationRequest.newBuilder() 93 | .setReservation(newReservation) 94 | .build()); 95 | 96 | String createdReservationId = createdReservationResponse.getId(); 97 | System.out.println("response: " + createdReservationResponse); 98 | return createdReservationId; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Transcoding gRPC to HTTP/JSON 2 | 3 | Sample project showing how to expose a gRPC service as a HTTP/JSON api. 4 | 5 | There is a blogpost containing an in-depth explanation of this project 6 | 7 | [Transcoding gRPC to HTTP JSON using Envoy](https://blog.jdriven.com/2018/11/transcoding-grpc-to-http-json-using-envoy/) 8 | 9 | Built with Java 11, but 1.8 should also be supported, change `sourceCompatibility` in the build.gradle to 1.8 10 | 11 | Requirements: docker 12 | 13 | ## Getting started 14 | 15 | 1. Open the project in your favourite IDE 16 | * run `./gradlew idea` or `./gradlew eclipse` to configure your IDE to detect the generated code directories. 17 | 18 | 1. Run `./gradlew build` on Linux/Mac, or `gradlew.bat build` on Windows to run a build. 19 | 20 | 1. Run `ServerMain` to start the gRPC server! 21 | 22 | 1. Run `ClientMain` to test some calls using the generated gRPC client 23 | 24 | 1. follow the steps below 25 | 26 | If you want to Run `./gradlew generateProto` generates sources from your .proto files 27 | 28 | ## exposing the gRPC service as HTTP/JSON using Envoy proxy 29 | 30 | Requirements: 31 | * protoc (to generate a service definition that envoy understands) 32 | * docker (envoy comes in a docker container) 33 | 34 | ### Installing protoc 35 | 1. Goto: https://github.com/protocolbuffers/protobuf/releases/latest" + 36 | 2. download choose the precompiled version " + 37 | 38 | for linux: protoc-3.6.1-linux-x86_64.zip" + 39 | for windows: protoc-3.6.1-win32.zip" + 40 | for mac: protoc-3.6.1-osx-x86_64.zip" + 41 | 42 | 3. extract it somewhere in your PATH 43 | 44 | ### Installing docker 45 | 46 | Check out this page: [Install docker](https://store.docker.com/search?offering=community&type=edition) 47 | 48 | ### Running Envoy to transcode our service 49 | 50 | The script start-envoy.sh automates the tasks below for linux and mac: 51 | 52 | ./start-envoy.sh 53 | 54 | #### Manual steps if script does not work 55 | 56 | 1. Run the protoc command from within this project's root directory! 57 | notice that build/extracted-include-protos/main contains proto files from 58 | jar files on the classpath, for example: `com.google.api.grpc:googleapis-common-protos:0.0.3` 59 | this dependency contains the sources for `google.api.http` options we use 60 | in the .proto file 61 | 62 | protoc -I. -Ibuild/extracted-include-protos/main --include_imports --include_source_info --descriptor_set_out=reservation_service_definition.pb src/main/proto/reservation_service.proto 63 | 64 | 1. Run envoy (docker container) from within this directory 65 | 66 | docker run -it --rm --name envoy --network="host" -v "$(pwd)/reservation_service_definition.pb:/tmp/reservation_service_definition.pb:ro" -v "$(pwd)/envoy-config.yml:/etc/envoy/envoy.yaml:ro" envoyproxy/envoy 67 | 68 | ### Testing the REST API 69 | 70 | 1. Create a reservation 71 | 72 | curl -X POST \ 73 | http://localhost:51051/v1/reservations \ 74 | -H 'Content-Type: application/json' \ 75 | -d '{ 76 | "title": "Lunchmeeting2", 77 | "venue": "JDriven Coltbaan 3", 78 | "room": "atrium", 79 | "timestamp": "2018-10-10T11:12:13", 80 | "attendees": [ 81 | { 82 | "ssn": "1234567890", 83 | "firstName": "Jimmy", 84 | "lastName": "Jones" 85 | }, 86 | { 87 | "ssn": "9999999999", 88 | "firstName": "Dennis", 89 | "lastName": "Richie" 90 | } 91 | ] 92 | }' 93 | 94 | Example output: 95 | 96 | ```json 97 | { 98 | "id": "2cec91a7-d2d6-4600-8cc3-4ebf5417ac4b", 99 | "title": "Lunchmeeting2", 100 | "venue": "JDriven Coltbaan 3", 101 | "room": "atrium", 102 | "timestamp": "2018-10-10T11:12:13", 103 | "attendees": [ 104 | { 105 | "ssn": "1234567890", 106 | "firstName": "Jimmy", 107 | "lastName": "Jones" 108 | }, 109 | { 110 | "ssn": "9999999999", 111 | "firstName": "Dennis", 112 | "lastName": "Richie" 113 | } 114 | ] 115 | } 116 | ``` 117 | 118 | 119 | 1. Retrieve it (substitute ID in url): 120 | 121 | curl -X GET http://localhost:51051/v1/reservations/ 122 | 123 | example output 124 | ```json 125 | { 126 | "id": "2cec91a7-d2d6-4600-8cc3-4ebf5417ac4b", 127 | "title": "Lunchmeeting2", 128 | "venue": "JDriven Coltbaan 3", 129 | "room": "atrium", 130 | "timestamp": "2018-10-10T11:12:13", 131 | "attendees": [ 132 | { 133 | "ssn": "1234567890", 134 | "firstName": "Jimmy", 135 | "lastName": "Jones" 136 | }, 137 | { 138 | "ssn": "9999999999", 139 | "firstName": "Dennis", 140 | "lastName": "Richie" 141 | } 142 | ] 143 | } 144 | ``` 145 | 146 | 1. Delete it (substitute ID in url): 147 | 148 | curl -X DELETE http://localhost:51051/v1/reservations/ 149 | 150 | 1. Create several reservations (vary the fields), and then list them with 151 | 152 | curl -X GET http://localhost:51051/v1/reservations 153 | 154 | 1. Then list them with a search on venue only 155 | 156 | curl -X GET "http://localhost:51051/v1/reservations?venue=JDriven%20Coltbaan%203" 157 | 158 | example output: 159 | ```json 160 | [ 161 | { 162 | "id": "2cec91a7-d2d6-4600-8cc3-4ebf5417ac4b", 163 | "title": "Lunchmeeting2", 164 | "venue": "JDriven Coltbaan 3", 165 | "room": "atrium", 166 | "timestamp": "2018-10-10T11:12:13", 167 | "attendees": [ 168 | { 169 | "ssn": "1234567890", 170 | "firstName": "Jimmy", 171 | "lastName": "Jones" 172 | }, 173 | { 174 | "ssn": "9999999999", 175 | "firstName": "Dennis", 176 | "lastName": "Richie" 177 | } 178 | ] 179 | }, 180 | { 181 | "id": "2f23c05a-c0ed-4b60-9b21-479d640030cc", 182 | "title": "Lunchmeeting", 183 | "venue": "JDriven Coltbaan 3", 184 | "timestamp": "2018-10-10T11:12:13", 185 | "attendees": [ 186 | { 187 | "ssn": "1234567890", 188 | "firstName": "Jimmy", 189 | "lastName": "Jones" 190 | }, 191 | { 192 | "ssn": "9999999999", 193 | "firstName": "Dennis", 194 | "lastName": "Richie" 195 | } 196 | ] 197 | } 198 | ] 199 | ``` 200 | 201 | --------------------------------------------------------------------------------