diff --git a/README.md b/README.md index 73ea777..879fc26 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,139 @@ -[![Build Status](https://travis-ci.org/teris-io/rpc.svg?branch=master)](https://travis-ci.org/teris-io/rpc) -[![Code Coverage](https://img.shields.io/codecov/c/github/teris-io/rpc.svg)](https://codecov.io/gh/teris-io/rpc) - - -# rpc - service-based RPC and public APIs in Java - -The `rpc` library aims to simplify the definition and implementation of public APIs and -their use for RPC. It draws a clear cut between the invocation, serialization and transport -yielding a definition of the invocation flow by composition of proxy, serialization and -transport. - -The `io.teris.rpc:rpc` core library is plain Java-8 with no further dependencies. On the -client side, it implements the service instantiation remote call logic passing service -method arguments through the bound serializer and transport layer. On the server side, -it implements the dispatching mechanism that takes incoming requests from the bound -transport layer, passes the data through a content-type specific bound deserializer and -dispatches to a service implementation. - -## Obtaining the library +[![Build Status](https://travis-ci.org/teris-io/kite.svg?branch=master)](https://travis-ci.org/teris-io/kite) +[![Code Coverage](https://img.shields.io/codecov/c/github/teris-io/kite.svg)](https://codecov.io/gh/teris-io/kite) + + +# kite - service-based RPC, public APIs and PubSub in Java + +`Kite` is a collection of reactive application messaging libraries that aim at +providing high level abstractions for PubSub, RPC and public API definitions with +minimal dependencies. Both PubSub and RPC (of which API defintion is a subset) provide +a clean separation between the service and message definition, arguments/result/message +(de)serialization and actual transport layer. The latter two injectable at the +application wiring stage making client ans server implementations agnostic to +serialization and transport and relying on POJO service and message defintions only. + +All interfaces and implementations are fully reactive including the RPC and (de)serialization. + +The collections consist of the following production-ready libraries (released to +`jcenter`): + +* `kite` -- the core defintion library. Provides `@Service`, `@Name`, `Context` as well as + `Serializer` and `Deserializer` interfaces; +* `kite-rpc` -- the RPC and public API implementation with pluggable serialization and + transport. Provides the `ServiceFactory` interface and implementation to obtain client + side service proxies, the `ServiceExporter` interface and implementation to export + service implementation on the server side to a given transport and the `ServiceInvoker` + interface used in conjuncion with the given transport to construct instances of + `ServiceFactory`. Plain Java 8 w/o further dependencies; +* `kite-gson` -- the (default) implementation of JSON (de)serialization with `Gson. +* `kite-rpc-vertx` -- the Vert.x based HTTP transport layer for the client and server + sides. Provides `HttpServiceInvoker` that implements `ServiceInvoker` over HTTP(S) + and `HttpServiceExporter` that exports service implementations bound to one or + a few generic `ServiceExporter`s as HTTPS POST endpoints; + +The following libraries are under development or not yet ready for production +(only available as source from GitHub, not released to `jcenter`): + +* `kite-fasterxml` -- a JSON (de)serializer implementation based on the FasterXML + `ObjectMapper` rather than `Gson`. The implementation has two outstanding gaps: + enum name-based deserialzation does not work without a `toString` method, encodings + other than UTF-8 are not supported for strings; +* `kite-rpc-amqp` -- a provisional RPC implementation for the RabbitMQ variant of `AMQP`. + The implementation is fully functional, however, more work needs to be done on + the reconnection and parameter tuning; +* `kite-rpc-jms` -- a provisional RPC implementation for the JMS1.1. The integration test + is performed with ActiveMQ. +* `kite-pubsub` -- under development, not yet publicly available (ETA March 2018); +* `kite-pubsub-vertx` -- under development, not yet publicly available (ETA April 2018); +* `kite-pubsub-amqp` -- under development, not yet publicly available (ETA April 2018); +* `kite-pubsub-vertx` -- under development, not yet publicly available (ETA March 2018); + + +### Obtaining the library Get it with gradle: repositories.jcenter() dependencies { - compile("io.teris.rpc:rpc:+") - compile("io.teris.rpc:serialization-json:+") - compile("io.teris.rpc:vertx:+") + compile("io.teris.kite:kite:+") + compile("io.teris.kite:kite-rpc:+") + // add these to inject implementation when wiring up + compile("io.teris.kite:kite-gson:+") + compile("io.teris.kite:kite-rpc-vertx:+") } -Or download the `rpc`, `vertx` and `serialization-json` jars directly from the -[jcenter repository](http://jcenter.bintray.com/io/teris/rpc/). +Or download the jars directly from the [jcenter repository](http://jcenter.bintray.com/io/teris/kite/). -## Building and testing +### Building and testing -In order to run all integration tests one needs to have `rabbitmq` broker running on `localhost:5672` -using the default guest account. The easiest way to get it deployed is by using an official `rabbitmq` -docker image: +In order to run all integration tests a `rabbitmq` broker is expected to be running +on `localhost:5672` using the default guest account. The easiest way to get it +deployed is by using the official `rabbitmq` alpine docker image (this is exactly +what the Travis CI deployment script is doing): docker run -it -p 5672:5672 rabbitmq:alpine -Vert.x and ActiveMQ tests will run their server side deployments from within the tests. With rabbit deployed -one can run the full build with unit and integration tests and a deployment to the local `~/.m2` maven -repository using: +Vert.x and ActiveMQ integration tests will start their server/broker from within +the corresponding tests. Given a deployed `rabbitmq` broker one can build, test +and deploy the binaries to a local Maven repository for reuse with (drop tasks which +you do not want to execute): - ./gradlew build test integration coverage install + ./gradlew clean build test integration coverage install -Without rabbit installed, add `--continue` to continue upon encounterring a failed test. +Without the `rabbitmq` installed, add `--continue` to continue upon encounterring +a failed test. +## RPC and public APIs -## Service declaration +APIs are defined by declaring public interfaces annotated with `@io.teris.kite.Service`. +In order to enable predictable and user-friend payload definition for public APIs, +service method arguments must also be annotated with `@io.teris.kite.Name`. Java +compiler by default erases actual argument names and to have no dependency on compiler +settings the `@Name` annotaton has to be used instead. -APIs are defined by declaring public interfaces annotated with `@io.teris.rpc.Service`. Service -method arguments must also be annotated with `@io.teris.rpc.Name`. The two annotations -control the way invocation routes are composed (e.g. HTTP endpoints or AMQP routing keys), -and how the payload structure looks like. +The two annotations also control the way invocation routes are composed yielding +a route per method of each service. The routes are dot-separated lower-case +strings that serve as routing keys for AMQP, message filters for JMS or HTTP URIs +having substituted dots for forward slashes). The library support two types of service methods. Asynchronous methods return a -`CompletableFuture` of generic type extending `Serializable` or being `Void`. -Synchronous methods return an instance of a type that extends `Serializable` directly, or -are declared void. Internally the library itself is fully asynchronous and will benefit -from non-blocking execution of the implementations. - -Service method arguments start with `io.teris.rpc.Context` that serves to pass request -headers (bi-directionally). Therefore, the most simple signature of a service method reads -`void call(Context context);`. Further arguments must explicitly implement `Serializable`. -The same requirement is applied to generic type, array and vararg parameters. Wildcards -are not supported. All method arguments after the context, must be annotated with -`@io.teris.rpc.Name` even if the compiler options `-parameters` is on. +`CompletableFuture` of `Void` or a type extending `Serializable`. Synchronous +methods return an instance of a type extending `Serializable` directly, or +can be declared void. + +Internally the library is fully reactive and will benefit from non-blocking +execution of the implementations where possible. + +**Important**: any service method returning a `CompletableFuture` must not block +in the implementation as the library will *not* spawn a thread for such methods, +synchronous methods are always executed via `ExecutorService` even if they are +returning void and are internally non blocking. + +Service method arguments must take at least one argument, `io.teris.rpc.Context`, +that serves to pass request headers (bi-directionally). Further arguments must +explicitly implement `Serializable` and be concrete classes. The same requirement +is applied to generic types, arrays and varargs, all of which are supported +as concrete serializable types. Wildcards in generic are not supported. + +All method arguments after the context, must be annotated with `@io.teris.kite.Name` +even if the compiler options `-parameters` is on. + +Services are assumed to accept and return serializable POJOs, therefore interfaces +are not supported, both as arguments and as return types (this is the case even +for standard collections, of which the interfaces are in fact not serializable +even though most JSON deserializers support them). + +The compliance of the declaration to above rules is checked on the server side +within the `ServiceExporter` builder when attaching services for export. On the +client side, it is checked when obtaining a proxy instance from the factory. +Further checks are performed during the invocation. Any exception in such a +check will result in a client side exception or an exceptionally completed future. + +For plain HTTP clients contacting the public API declared using the above method, +technical errors will result in HTTP500, authentication errors in HTTP403 and +all busines errors (exceptions during the service implementation invocation) in +HTTP200 with an error field in the payload. The following defines a service with two endpoints, a synchronous and an asynchronous ones: @@ -82,214 +149,222 @@ public interface DataService { } ``` -Each service method receives an independent route composed, by default, from the package -name, service class name (w/o the Service suffix) and a method name, all lower case and -dot-separated. There is a mechanism to override those defaults redefining the routes fully -or partially. The declaration above yields the following routes, `api.data.upload` and -`api.data.download`, respectively. Public inner interfaces are fully supported for -service declaration and their holder class name becomes a part of the route (preserving -the `Service` suffix on the holder if any). +Each service method receives an independent route. By default it is composed from +the package name, service class name (w/o the Service suffix) and method name, +all lower case and dot-separated. + +There is a mechanism to override these defaults redefining the routes fully or +partially. The `@Service` annotation takes two optional arguements `replace` +(what to replace in the default route) and `value` (what to replace it for). By +default nothing is replaced. If only `replace` is provided, the matching part +will be removed (replaced with nothing). If only the `value` is provided, the +fully route will be replaced with ìt. Thus, the above declaration yields the +following routes, `api.data.upload` and `api.data.download`, respectively. The +`@Name` annotation applied to a method allows to fully replace the last part +of the route (the method name) with a new one. E.g. +`@Name("import") void importPrices(...` will be exported as `{prefix}.import`. -All provided transport implementations are aware of those route definitions and will -automatically expose service endpoints at the server side and route to the correct ones -on the client. +Public inner interfaces are fully supported for service declaration and their +holder class name becomes a part of the route (preserving the `Service` suffix +on the holder if any). -The client side `ServiceFactory` and the server sie `ServiceDispatcher` -will validate method/service declaration before the invocation and at the time of binding -s service implementation. +All provided transport implementations are aware of those route definitions and +will automatically expose service endpoints at the server side and route to the +correct ones on the client. For the HTTP implementation an optional prefix +can be added to the URI on both server and client sides. +The client side `ServiceFactory` and the server sie `ServiceExporter` will +validate method/service declaration before the invocation and at the time of +binding a service implementation. -## Client-side invocation + +### Client-side invocation Service proxy instances are obtained from an instance of `ServiceFactory`, -which is parametrized using an instance of `Serializer` that transfers method arguments -into a byte array of data transporable over the wire and an instance of `ServiceInvoker` -that implements that transport layer on the client side. Content type specific deserializers -can additionally be registered for turning the response byte array into a response structure. -Assuming the content type of the response is the same as of the request, the default -deserializer is provided by the registered serializer. +which is parametrized using an instance of `Serializer` that transfers service +method arguments into a byte array and a transport specific client side instance +of `ServiceInvoker`. Content type specific deserializers can additionally be +registered for turning the response byte array into a response structure based +on the reported content type. Assuming the content type of the response is the +same as of the request, the default deserializer is provided by the registered +serializer. `ServiceFactory` instances can be constructed for example in the following manner: ```java -ServiceFactory factory = ServiceFactory.builder() - .serviceInvoker(invoker) - .serializer(serializer) +ServiceFactory factory = ServiceFactory.invoker(httpServiceInvoker) + .serializer(gsonSerializer) + .deserializer("application/json", fasterXmlDeserializer) + .uidGenerator(() -> UUID.randomUUID().toString()) .build(); ``` -Service proxies are then obtained by calling `newInstance` on the factory: - -```java -DataService dataService = factory.newInstance(DataService.class); - -Double value = dataService.download(new Context(), "key"); -``` - -## Serialization - -Serialization and deserialization are intergral parts of the remote invocation workflow. -The client serializes outgoing requests and deserializes incoming responses while the server -deserializes incoming requests and serializes outgoing responses. - -Serialization is pluggable into the client and server sides via instances implementing the -`io.teris.rpc.Serializer` +The only two required arguments are the invoker and serializer though: ```java -@Nonnull - CompletableFuture serialize(@Nonnull CT value); +ServiceFactory factory = ServiceFactory.invoker(httpServiceInvoker) + .serializer(JsonSerializer.builder().build()) + .build(); ``` -and `io.teris.rpc.Deserializer` interfaces +Service proxies are then obtained by calling `newInstance` on the factory: ```java -@Nonnull - CompletableFuture deserialize(@Nonnull byte[] data, @Nonnull Class clazz); +DataService dataService = factory.newInstance(DataService.class); -@Nonnull - CompletableFuture deserialize(@Nonnull byte[] data, @Nonnull Type type); +Double value = dataService.download(new Context(), "key"); ``` -When sending out a message (client request or server response), the serializer on the -sending side defines and sets the content type of the data to transfer. This value is read -from the headers on the receiving side and a matching deserializer is used to -deserialize the content (in case no matching one found, the deserializer of the registered -serializer is tried). - -The package provides JSON serializer and deserializer in `io.teris.rpc:serialization-json` -based on GSON and released along with the core library. The serializer and deserializer -are configurable with further GSON options beyond default. - -*Note*: Custom deserializers must satisfy a requirements to deliver a deserializable slice -of the original or transformed byte array for every explicit `Serializable` parameter. -Due to the necessity to dynamically put heterogeneous method arguments together, the server -side will first deserialize the data into a `HashMap`and then, in a -second iteration, it will use the same deserializer to process byte arrays behind each -`Serializable` value into a concrete type as declared on the method. - -## Transport and server-side invocation +### Transport and server-side invocation -Transport layers implement the `ServiceInvoker` interface for the client side and -`ServiceDispatcher` for the server side. Both interfaces, although not related via any -parent-child relation, provide the following same method that is central for the bi-directional -data flow: +Transport implementations must provide a client side `ServiceInvoker` implementation +and a server side service exporter that accept instances of `ServiceExporter` +and export transport specific endpoints for each route found in each service exporter. + +Both interfaces, although not related by any parent-child relation, provide the +following same method that is central for the bi-directional data flow: CompletableFuture> call(@Nonnull String route, @Nonnull Context context, @Nullable byte[] data); Invoking proxies first compose the method route; then copy the context and put new unique -request Id and the correct content type there; finally, collect method arguments and serialize -them into byte arrays. The three values are then passed into an instance of the above -`ServiceInvoker` interface implementation. This performs an asynchronous network transport -of the data and waits for the completion of the returned future by the server side. - -The transport layer transfers the data and context (in form of request/message headers) to -the destination route (which for protocol comptibility may be transformed into a URI on a -given host/port or anything else). The server side transport implementation receives the -request/message, passes its route, headers and data into an instance of the `ServiceDispatcher` -implementation. Based on the route, it finds the matching service implementation instance and -the method to call, deserializes the data correspondingly, creates a new context from the -headers and invokes the method. The same asynchronous process is repeated in reverse for the -response. - -The package provides the following three transport layer implementations: - -* HTTP(s) POST using `vert.x` in `io.teris.rpc:vertx` (released together with the core) -* JMS using `ActiveMQ` in `io.teris.rpc:jms` (not yet finalized, not released, can be used from source only) -* AMQP using `RabbitMQ` in `io.teris.rpc:amqp` (not yet finalized, not released, can be used from source only) - -Using the `vert.x` HTTP implementation and the GSON serializer, the fully configured client-side -service factory that will invoke service methods over HTTP on a corresponding HTTP server -can be instantiated as follows (adding further optional parameters for completeness): +request Id and the correct content type there; collect method arguments and serialize +them into byte arrays; finally, the three values are passed into a transport specific +instance of `ServiceInvoker` for sending it to the server (directly or via some sort +of broker or reverse proxy). The future is completed when the transport receives a +corresponding response from the server. In case of HTTP implementation the HTTP +request itself is synchronous and is kept open for the duration of execution. JMS and +AMQP implementations are fully asynchrnous and are based on message publishing on a +shared corrlation Id. + +The transport layer transfers the data and the context (in form of request/message +headers) to the destination route (which for protocol compatibility may be +transformed into a URI on a given host/port or anything else). The server side transport +receives the request/message and passes its route, headers and data into an instance of +`ServiceExporter`. Based on the route, the latter finds the matching service +implementation instance and method to call, deserializes the data, creates a new +context from the headers and invokes the method. + +The same process is repeated in reverse for the response. + +Using the `vert.x` HTTP implementation and the GSON serializer, the fully configured +client side service factory that invokes service methods over HTTP can be instantiated +as follows (adding further optional parameters for completeness): ```java -Vertx vertx = Vertx.vertx(); - -HttpClient httpClient = vertx.createHttpClient(new HttpClientOptions() +HttpClient httpClient = Vertx.vertx().createHttpClient(new HttpClientOptions() .setDefaultHost("localhost") .setDefaultPort(8080) .setMaxPoolSize(50)); -ServiceInvoker invoker = VertxServiceInvoker.builder(httpClient).build(); - -Serializer serializer = GsonSerializer.builder().build(); +ServiceInvoker invoker = HttpServiceInvoker.httpClient(httpClient).build(); -Supplier uidGenerator = () -> UUID.randomUUID().toString(); - -ServiceFactory factory = ServiceFactory.builder() - .serviceInvoker(invoker) - .serializer(serializer) - .uidGenerator(uidGenerator) // optional (default same as above) - .deserialier("application/json", serializer.deserializer()) // optional +ServiceFactory factory = ServiceFactory.invoker(invoker) + .serializer(JsonSerializer.builder().build()) .build(); ``` -On the server side, one needs to register services along with implementing instances in -an instance of the `ServiceDispatcher` implementation. Initializing an `VertxServiceRouter` -instance with the dispatcher instance and a `vert.x` router will register HTTP POST -endpoint for all services and their methods on every dispatcher used: +The server side, correspondingly: ```java -Serializer serializer = GsonSerializer.builder().build(); - -ServiceDispatcher dispatcher = ServiceDispatcher.builder() - .serializer(serializer) - .bind(DataService.class, new DataServiceImpl(dataServiceDependency)) - .bind(OtherService.class, new OtherServiceImpl(otherServiceDependency)) - .build(); +ServiceExporter exporter1 = ServiceExporter.serializer(JsonSerializer.builder().build()) + .preprocessor(authenticator1) + .export(DataService1.class, new DataService1Impl(dataServiceDependency)) + .export(DataService2.class, new DataService2Impl(dataServiceDependency)); -Vertx vertx = Vertx.vertx(); -Router router = Router.router(vertx); +ServiceExporter exporter2 = ServiceExporter.serializer(JsonSerializer.builder().build()) + .preprocessor(authenticator2) + .export(OtherService.class, new OtherServiceImpl(otherServiceDependency)); -VertxServiceRouter.builder(router).build() - .preconditioner(authHandler) - .route(dispatcher); +HttpServiceExporter httpExporter = HttpServiceExporter.router(Vertx.vertx()) + .export(exporter1) + .export(exporter2); vertx.createHttpServer(httpServerOptions) - .requestHandler(router::accept) + .requestHandler(httpExporter.router()::accept) .listen()); ``` -Exception thrown during the invocation process are wrapped into `io.teris.rpc.InvocationException` -or `io.teris.rpc.BusinessExcpeption` in case the exception occurs when invoking the actual -service method. References to the cause are dropped from the transport, however, their original -stacktraces are preserved. Exception data will be delivered serialized in the response payload. -The only exception from this case is when the serialization of an (exceptional or normal) response on the -server side throws itself an exception. This exception will be delivered to the client by different -means depending on the transport. In the `vert.x` implementation it will result in the server -500 HTTP code and a text error message. Any middleware (preprocessor) exception should -result in a similar behavior. The resuling future is then completed exceptionally on the transport -layer interface. +Here, the `exporter1` and `exporter2` are exported via the same `HttpServiceExporter`, +but each define a different authenticating preprocessor (e.g. two different +authentication methods). + +Exception thrown during the invocation process are wrapped into `io.teris.kite.rpc.InvocationException` +or `io.teris.kite.rpc.BusinessExcpeption`. Their constructors are not publicly +exported and can only be used from within the RPC mechanism. These exceptions are +fully transportable over the serialization mechanism and are rethrown on +the client side with the original stack trace. If the client is a non-library +component, e.g. a CURL HTTP request to the public API, these excpetions are +transformed in the payload field "exception" (assuming JSON payload) and +additionally the field "errorMessage" contains the actual text message only. The +HTTP status code is in this case < 400. + +Exceptions occurring in the preprocessors or during the invocation processing, but +not related to service definition correctness, deserialization or business logic, +are delivered via different means (that is not serialized in the payload) and are +transport specific. The following cases are supported: + +* a public `AuthenticationException` is delivered as a text message and is rethrown + on the client side as `AuthenticationException`. For HTTP it is signalled by HTTP403; +* a server-side `NotFoundException` is delivered as a text message and is rethrown + on the client side as `NotFoundException`. Currently it is only implemented by the + HTTP transport and is signlled by HTTP404; +* other exceptions are delivered as a text message and are rethrown on the client + side as `TechnicalException`. For HTTP they are signalled by HTTP500. It is essential to note that _no checked_ exceptions will cross the remote invocation -boundary and all runtime exceptions will descend from `InvocationException` or `BusinessException`. -This is true even for the case when the service declares and throws an exception of a -particular type. The reason for this is to guarantee that exceptions are always deserializable -on the client side! +boundary and all runtime exceptions will descend from `InvocationException` or +`BusinessException`. This is true even for the case when the service declares and +throws an exception of a particular type. The reason for this is to guarantee that +exceptions are always deserializable on the client side without futher dependencies! ## Generic preprocessing (and authentication) -The service dispatcher allows for registration of a series of preprocessors that are executed -(asynchronously) before dispatching to the actual service implementation. Preprocessors are -functions that satisfy the following declaration: +The service exporter allows for the registration of a series of preprocessors that +are executed before dispatching to the actual service implementation. Preprocessors are +functions that satisfy the following declaration and must be non-blocking: BiFunction, CompletableFuture> -Each preprocessor asynchronously receives the context from the previous iteration and can -use its values and the original data to generate new values for the context and/or -validate permissions. The pair of route and incoming data is passed into every preprocesor -along with the context. - - -## Public APIs - -Using the provided GSON-based serializer and the `vert.x` HTTP transport layer implementation, -every service becomes an API that can be easily consumed. For example, using the server side -initialization as shown above, The API endpoint for the `DataService.download` method declared +Each preprocessor asynchronously receives the context, the route and raw data from +the previous iteration and can use them to generate new values for the context and/or +validate permissions. The pair of the route and incoming data is passed into every +preprocesor along with the evolving context. + +The preprocessors are executed sequentially (as a next completion stage) in the +order of their registration on the exporter. Any preprocessor completing exceptionally +will result in aborting the chain and responding to the original request before the +actual method call can be made. With one exception all exceptions will be rethrown +on the client side as `TechnicalException` using the original exception message. + +One can use preprocessor to implement request authentication and authorisation. +It is advisable to throw an `AuthenticationException` in case of authentication +errors in this case as they have a special treatment in the transport. + + +### Public APIs + +Using the provided GSON-based serializer and the `vert.x` HTTP transport layer +implementation, every service becomes an API that has an easily deductable structure: + +* all requests use the POST method +* the URI is defined by the service method route (substituting . for /) with an + optional prefix +* context is written as is into request headers, returning context into response headers +* the request payload is a JSON object with attributes being method arguments as + they are named in the definition +* the response is HTTP200 with JSON payload containing the "payload" field of the same type + as method return type (or null if `void` or `CompletableFuture`) +* in case of `BusinessException` or `TechnicalException` the response is HTTP200 + with JSON payload containing a non-null "errorMessage" (and "exception") field +* in case of any other exception the response is one of HTTP403, HTTP404, HTTP500 + or other HTTP error status codes as generated by the generic HTTP protocol. + +For example, the API endpoint for the `DataService.download` method declared above will be defined as follows: REQUEST: POST /api/data/download - HEADERS: X-Request-Id, Content-Type (automatically set, the rest from Context) + HEADERS: X-Request-Id, Content-Type + context JSON { "key": } @@ -309,15 +384,52 @@ Unless the client is Java and is using this library to consume the response, the for `CompletableFuture` and for `MyType` will be identical with `MyType` data under the `payload` field. -The server will only return non-Ok HTTP codes if it fails to serialize the error response into -a payload structure shown above. Otherwise, the response is always 200, or 3xx if overwritten -by a proxy. - Service requests that contain no arguments beside `Context` will be performed with an empty request body. Service responses that respond to `void` or `CompletableFuture` and contain no exception will contain an empty response body. Both are treated as normal, non-erroneous cases. +## Serialization in RPC and PubSub + +Serialization and deserialization are intergral parts of remote messaging. In the RPC +the client serializes arguments of outgoing requests and deserializes incoming results +while the server deserializes incoming requests and serializes outgoing responses. +In PubSub the publisher serializes the message while the subscriber deserializes it. + +Serialization is pluggable via instances implementing the `io.teris.kite.Serializer` + +```java +@Nonnull + CompletableFuture serialize(@Nonnull CT value); +``` + +and `io.teris.kite.Deserializer` interfaces + +```java +@Nonnull + CompletableFuture deserialize(@Nonnull byte[] data, @Nonnull Class clazz); + +@Nonnull + CompletableFuture deserialize(@Nonnull byte[] data, @Nonnull Type type); +``` + +For the case of multiple registered deserializers, the content type transmitted in +`context` (or in the header) is used to decide which deserializer to use. Serialization, +on the other hand, is only possible with one and the same serialize registered at +the time of constructing the trasport element. + +**Important**: for RPC custom deserializers must satisfy a requirements to deliver +a fully deserializable slice of the original or transformed byte array for every +explicit `Serializable` declaration. This is the only exception from the response +type: wherever it contains the `Serializable` interface the original slice of byte +array will be delivered. This functionality is used to deserialize RPC service +method arguments when they arrive on the server side. The structure of arguments +is dynamic and cannot be described by a specific class (even if all the individual +element types are known at runtime), so the deserialization is performed into +a map of argument name to `Serializable` and then every argument is deserialized +into its concrete type independently. + + ### License and copyright - Copyright (c) 2017. Oleg Sklyar and teris.io. MIT license applies. All rights reserved. + Copyright (c) 2017-2018. Oleg Sklyar and teris.io. All rights reserved. MIT license applies diff --git a/amqp/build.gradle b/amqp/build.gradle deleted file mode 100644 index 6265c40..0000000 --- a/amqp/build.gradle +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved - */ - -plugins.apply(JavaPlugin) - -dependencies { - compileOnly(findbugsModule) - compile(slf4jModule) - compile("com.rabbitmq:amqp-client:5.0.0") - compile(project(":rpc")) - - testCompile(junitModule) - testCompile(activemqBrokerModule) - testCompile(project(":serialization-json")) - - testRuntime(logbackModule) -} diff --git a/amqp/src/main/java/io/teris/rpc/amqp/AmqpServiceRouter.java b/amqp/src/main/java/io/teris/rpc/amqp/AmqpServiceRouter.java deleted file mode 100644 index 545a1f4..0000000 --- a/amqp/src/main/java/io/teris/rpc/amqp/AmqpServiceRouter.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved - */ - -package io.teris.rpc.amqp; - - -import java.io.IOException; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeoutException; -import javax.annotation.Nonnull; - -import com.rabbitmq.client.ConnectionFactory; - -import io.teris.rpc.ServiceDispatcher; - - -public interface AmqpServiceRouter { - - @Nonnull - CompletableFuture close(); - - @Nonnull - static Builder builder(@Nonnull ConnectionFactory connectionFactory) { - return new AmqpServiceRouterImpl.BuilderImpl(connectionFactory); - } - - interface Builder { - - Builder exchangeName(String exchangeName); - - @Nonnull - Router build() throws IOException, TimeoutException; - } - - interface Router { - - @Nonnull - Router route(@Nonnull ServiceDispatcher serviceDispatcher) throws IOException; - - AmqpServiceRouter start(); - } -} diff --git a/build.gradle b/build.gradle index 2a9c3c4..aec7b76 100644 --- a/build.gradle +++ b/build.gradle @@ -8,10 +8,13 @@ ext { activemqBrokerModule = "org.apache.activemq:activemq-broker:5.15.2" activemqClientModule = "org.apache.activemq:activemq-client:5.15.2" activemqKahaDbModule = "org.apache.activemq:activemq-kahadb-store:5.15.2" - findbugsModule = "com.google.code.findbugs:annotations:3.0.1" + findbugsModule = "com.google.code.findbugs:jsr305:3.0.1" geronimoJmsModule = "org.apache.geronimo.specs:geronimo-jms_1.1_spec:1.1.1" gsonModule = "com.google.code.gson:gson:2.8.2" + jacksonDatabindModule = "com.fasterxml.jackson.core:jackson-databind:2.9.2" + jacksonDatatypeJSR310Module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.2" logbackModule = "ch.qos.logback:logback-classic:1.2.3" + rabbitmqModule = "com.rabbitmq:amqp-client:5.1.2" slf4jModule = "org.slf4j:slf4j-api:1.7.25" vertxWebModule = "io.vertx:vertx-web:3.5.0" vertxTestModule = "io.vertx:vertx-unit:3.5.0" @@ -22,8 +25,8 @@ ext { } allprojects { - version = "0.4.0" - group = "io.teris.rpc" + version = "0.5.0" + group = "io.teris.kite" plugins.apply(JacocoPlugin) diff --git a/integration-tests/build.gradle b/integration-tests/build.gradle index e4f3bc2..5a0078b 100644 --- a/integration-tests/build.gradle +++ b/integration-tests/build.gradle @@ -7,13 +7,13 @@ plugins.apply(JavaPlugin) dependencies { testCompile(junitModule) testCompile(mockitoModule) - testCompile(project(":serialization-json")) - - testCompile(project(":vertx")) - - testCompile(project(":amqp")) - - testCompile(project(":jms")) + testCompile(project(":kite")) + testCompile(project(":kite-gson")) + testCompile(project(":kite-fasterxml")) + testCompile(project(":kite-rpc")) + testCompile(project(":kite-rpc-vertx")) + testCompile(project(":kite-rpc-amqp")) + testCompile(project(":kite-rpc-jms")) testCompile(activemqClientModule) testCompile(activemqBrokerModule) // amq persistence diff --git a/integration-tests/src/integration/java/io/teris/rpc/AbstractInvocationTestsuite.java b/integration-tests/src/integration/java/io/teris/kite/rpc/AbstractInvocationTestsuite.java similarity index 59% rename from integration-tests/src/integration/java/io/teris/rpc/AbstractInvocationTestsuite.java rename to integration-tests/src/integration/java/io/teris/kite/rpc/AbstractInvocationTestsuite.java index 969d759..dbe7109 100644 --- a/integration-tests/src/integration/java/io/teris/rpc/AbstractInvocationTestsuite.java +++ b/integration-tests/src/integration/java/io/teris/kite/rpc/AbstractInvocationTestsuite.java @@ -2,11 +2,13 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc; +package io.teris.kite.rpc; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import java.io.IOException; +import java.net.ServerSocket; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; @@ -20,12 +22,25 @@ import org.junit.Test; import org.junit.rules.ExpectedException; +import io.teris.kite.Context; +import io.teris.kite.Serializer; +import io.teris.kite.gson.JsonSerializer; +import io.teris.kite.rpc.impl.AsyncServiceImpl; +import io.teris.kite.rpc.impl.SyncServiceImpl; +import io.teris.kite.rpc.impl.ThrowingServiceImpl; + public abstract class AbstractInvocationTestsuite { @Rule public ExpectedException exception = ExpectedException.none(); + static int port; + + static ServiceExporter exporter1; + + static ServiceExporter exporter2; + static SyncService syncService; static AsyncService asyncService; @@ -36,6 +51,43 @@ public abstract class AbstractInvocationTestsuite { private final int nrequests = 200; + static void preInit() { + while (true) { + try (ServerSocket socket = new ServerSocket((int) (49152 + Math.random() * (65535 - 49152)))) { + port = socket.getLocalPort(); + break; + } + catch (IOException e) { + // repeat + } + } + + Serializer serializer = JsonSerializer.builder().build(); + + exporter1 = ServiceExporter.serializer(serializer) + .preprocessor((context, data) -> { + CompletableFuture res = new CompletableFuture<>(); + if (context.containsKey("x-technical-error")) { + res.completeExceptionally(new RuntimeException("BOOM")); + } + else if (context.containsKey("x-auth-error")) { + res.completeExceptionally(new AuthenticationException("BOOM")); + } + else { + res.complete(context); + } + return res; + }) + .export(SyncService.class, new SyncServiceImpl("1")) + .export(AsyncService.class, new AsyncServiceImpl("2")) + .build(); + + exporter2 = ServiceExporter.serializer(serializer) + .export(ThrowingService.class, new ThrowingServiceImpl("3")) + .build(); + } + + @Test public void roundtrip_single_sync_success() { Context context = new Context(); @@ -75,7 +127,7 @@ public void roundtrip_dual_async_success() throws Exception { } @Test - public void roundtrip_exceptional_success() { + public void roundtrip_businessException() { Context context = new Context(); CompletableFuture boomPromise = throwingService.boomThen(context); try { @@ -96,6 +148,49 @@ public void roundtrip_exceptional_success() { } } + @Test + public void roundtrip_technicalException() { + Context context = new Context(); + context.put("x-technical-error", "yes"); + try { + syncService.plus(context, Double.valueOf(341.2), Double.valueOf(359.3)); + throw new AssertionError("unreachable code"); + } + catch (TechnicalException ex) { + assertTrue(ex.getMessage().contains("BOOM")); + } + CompletableFuture promise = asyncService.plus(context, Double.valueOf(341.2), Double.valueOf(359.3)); + try { + promise.get(); + throw new AssertionError("unreachable code"); + } + catch (ExecutionException | InterruptedException ex) { + assertTrue(ex.getMessage().contains("TechnicalException: BOOM")); + } + } + + @Test + public void roundtrip_authenticationException() { + Context context = new Context(); + context.put("x-auth-error", "yes"); + try { + syncService.plus(context, Double.valueOf(341.2), Double.valueOf(359.3)); + throw new AssertionError("unreachable code"); + } + catch (AuthenticationException ex) { + assertEquals("BOOM", ex.getMessage()); + } + CompletableFuture promise = asyncService.plus(context, Double.valueOf(341.2), Double.valueOf(359.3)); + try { + promise.get(); + throw new AssertionError("unreachable code"); + } + catch (ExecutionException | InterruptedException ex) { + assertTrue(ex.getCause() instanceof AuthenticationException); + assertEquals("BOOM", ex.getCause().getMessage()); + } + } + @Test public void benchmark_async_invocationsNThreadsxMRequests() throws Exception { List> callables = new ArrayList<>(); diff --git a/integration-tests/src/integration/java/io/teris/rpc/AsyncService.java b/integration-tests/src/integration/java/io/teris/kite/rpc/AsyncService.java similarity index 75% rename from integration-tests/src/integration/java/io/teris/rpc/AsyncService.java rename to integration-tests/src/integration/java/io/teris/kite/rpc/AsyncService.java index cad7905..49fa564 100644 --- a/integration-tests/src/integration/java/io/teris/rpc/AsyncService.java +++ b/integration-tests/src/integration/java/io/teris/kite/rpc/AsyncService.java @@ -2,10 +2,14 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc; +package io.teris.kite.rpc; import java.util.concurrent.CompletableFuture; +import io.teris.kite.Context; +import io.teris.kite.Name; +import io.teris.kite.Service; + @Service public interface AsyncService { diff --git a/integration-tests/src/integration/java/io/teris/rpc/SyncService.java b/integration-tests/src/integration/java/io/teris/kite/rpc/SyncService.java similarity index 69% rename from integration-tests/src/integration/java/io/teris/rpc/SyncService.java rename to integration-tests/src/integration/java/io/teris/kite/rpc/SyncService.java index 402b4cf..0ce8d98 100644 --- a/integration-tests/src/integration/java/io/teris/rpc/SyncService.java +++ b/integration-tests/src/integration/java/io/teris/kite/rpc/SyncService.java @@ -2,7 +2,12 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc; +package io.teris.kite.rpc; + +import io.teris.kite.Context; +import io.teris.kite.Name; +import io.teris.kite.Service; + @Service public interface SyncService { diff --git a/integration-tests/src/integration/java/io/teris/kite/rpc/TestAmqpInvocationRoundtrip.java b/integration-tests/src/integration/java/io/teris/kite/rpc/TestAmqpInvocationRoundtrip.java new file mode 100644 index 0000000..1999d34 --- /dev/null +++ b/integration-tests/src/integration/java/io/teris/kite/rpc/TestAmqpInvocationRoundtrip.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved + */ + +package io.teris.kite.rpc; + +import org.junit.AfterClass; +import org.junit.BeforeClass; + +import com.rabbitmq.client.ConnectionFactory; + +import io.teris.kite.gson.JsonSerializer; +import io.teris.kite.rpc.amqp.AmqpServiceInvoker; +import io.teris.kite.rpc.amqp.AmqpServiceExporter; + +//@Ignore +public class TestAmqpInvocationRoundtrip extends AbstractInvocationTestsuite { + + private static final String requestExchange = "RPC"; + + private static AmqpServiceInvoker invoker; + + private static AmqpServiceExporter provider; + + @BeforeClass + public static void init() throws Exception { + preInit(); + port = 5672; + + ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.setHost("127.0.0.1"); + connectionFactory.setPort(port); + + invoker = AmqpServiceInvoker.connectionFactory(connectionFactory) + .requestExchange(requestExchange) + .start(); + + ServiceFactory factory = ServiceFactory.invoker(invoker) + .serializer(JsonSerializer.builder().build()) + .build(); + + syncService = factory.newInstance(SyncService.class); + asyncService = factory.newInstance(AsyncService.class); + throwingService = factory.newInstance(ThrowingService.class); + + provider = AmqpServiceExporter.connectionFactory(connectionFactory) + .requestExchange(requestExchange) + .export(exporter1) + .export(exporter2) + .start(); + } + + @AfterClass + public static void teardown() throws Exception { + invoker.close().get(); + provider.close().get(); + } +} diff --git a/integration-tests/src/integration/java/io/teris/kite/rpc/TestJmsInvocationRountrip.java b/integration-tests/src/integration/java/io/teris/kite/rpc/TestJmsInvocationRountrip.java new file mode 100644 index 0000000..7d38985 --- /dev/null +++ b/integration-tests/src/integration/java/io/teris/kite/rpc/TestJmsInvocationRountrip.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved + */ + +package io.teris.kite.rpc; + +import org.apache.activemq.ActiveMQConnectionFactory; +import org.apache.activemq.broker.BrokerService; +import org.junit.AfterClass; +import org.junit.BeforeClass; + +import io.teris.kite.gson.JsonSerializer; +import io.teris.kite.rpc.jms.JmsServiceInvoker; +import io.teris.kite.rpc.jms.JmsServiceExporter; + + +public class TestJmsInvocationRountrip extends AbstractInvocationTestsuite { + + private static final String requestTopic = "RPC"; + + private static BrokerService broker; + + private static JmsServiceInvoker invoker; + + private static JmsServiceExporter provider; + + @BeforeClass + public static void init() throws Exception { + preInit(); + + String brokerUrl = String.format("tcp://localhost:%d", Integer.valueOf(port)); + String clientUrl = brokerUrl + "?jms.useAsyncSend=true"; + + broker = new BrokerService(); + broker.setUseJmx(true); + broker.addConnector(brokerUrl); + broker.start(); + + invoker = JmsServiceInvoker.connectionFactory(new ActiveMQConnectionFactory(clientUrl)) + .requestTopic(requestTopic) + .start(); + + ServiceFactory factory = ServiceFactory.invoker(invoker) + .serializer(JsonSerializer.builder().build()) + .build(); + + syncService = factory.newInstance(SyncService.class); + asyncService = factory.newInstance(AsyncService.class); + throwingService = factory.newInstance(ThrowingService.class); + + provider = JmsServiceExporter.connectionFactory(new ActiveMQConnectionFactory(clientUrl)) + .requestTopic(requestTopic) + .export(exporter1) + .export(exporter2) + .start(); + } + + @AfterClass + public static void teardown() throws Exception { + invoker.close().get(); + provider.close().get(); + broker.stop(); + } +} diff --git a/integration-tests/src/integration/java/io/teris/rpc/TestVertxInvocationLongBlocking.java b/integration-tests/src/integration/java/io/teris/kite/rpc/TestVertxInvocationLongBlocking.java similarity index 80% rename from integration-tests/src/integration/java/io/teris/rpc/TestVertxInvocationLongBlocking.java rename to integration-tests/src/integration/java/io/teris/kite/rpc/TestVertxInvocationLongBlocking.java index 60e2333..786ebe5 100644 --- a/integration-tests/src/integration/java/io/teris/rpc/TestVertxInvocationLongBlocking.java +++ b/integration-tests/src/integration/java/io/teris/kite/rpc/TestVertxInvocationLongBlocking.java @@ -2,7 +2,7 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc; +package io.teris.kite.rpc; import java.io.IOException; import java.net.ServerSocket; @@ -21,9 +21,13 @@ import org.junit.Ignore; import org.junit.Test; -import io.teris.rpc.http.vertx.VertxServiceInvoker; -import io.teris.rpc.http.vertx.VertxServiceRouter; -import io.teris.rpc.serialization.json.GsonSerializer; +import io.teris.kite.Context; +import io.teris.kite.Name; +import io.teris.kite.Serializer; +import io.teris.kite.Service; +import io.teris.kite.fasterxml.JsonSerializer; +import io.teris.kite.rpc.vertx.HttpServiceInvoker; +import io.teris.kite.rpc.vertx.HttpServiceExporter; import io.vertx.core.Vertx; import io.vertx.core.http.HttpClient; import io.vertx.core.http.HttpClientOptions; @@ -78,6 +82,8 @@ public static void init() throws Exception { HttpServerOptions httpServerOptions = new HttpServerOptions().setHost("0.0.0.0").setPort(port); + Serializer serializer = JsonSerializer.builder().build(); + Vertx vertx = Vertx.vertx(); HttpClient httpClient = vertx.createHttpClient(new HttpClientOptions() @@ -85,25 +91,21 @@ public static void init() throws Exception { .setDefaultPort(port) .setMaxPoolSize(50)); - VertxServiceInvoker invoker = VertxServiceInvoker.builder(httpClient).build(); + HttpServiceInvoker invoker = HttpServiceInvoker.httpClient(httpClient).build(); - ServiceFactory creator = ServiceFactory.builder() - .serviceInvoker(invoker) - .serializer(GsonSerializer.builder().build()) + ServiceFactory factory = ServiceFactory.invoker(invoker) + .serializer(serializer) .build(); - service = creator.newInstance(LongBlocking.class); - - - Router router = Router.router(vertx); + service = factory.newInstance(LongBlocking.class); - ServiceDispatcher dispatcher = ServiceDispatcher.builder() - .serializer(GsonSerializer.builder().build()) - .bind(LongBlocking.class, new LongBlockingImpl()) + ServiceExporter provider = ServiceExporter.serializer(serializer) + .export(LongBlocking.class, new LongBlockingImpl()) .build(); - VertxServiceRouter.builder(router).build() - .route(dispatcher); + Router router = HttpServiceExporter.router(vertx) + .export(provider) + .router(); CompletableFuture promise = new CompletableFuture<>(); CompletableFuture.runAsync(() -> diff --git a/integration-tests/src/integration/java/io/teris/rpc/TestVertxInvocationRountrip.java b/integration-tests/src/integration/java/io/teris/kite/rpc/TestVertxInvocationRountrip.java similarity index 50% rename from integration-tests/src/integration/java/io/teris/rpc/TestVertxInvocationRountrip.java rename to integration-tests/src/integration/java/io/teris/kite/rpc/TestVertxInvocationRountrip.java index 1428269..bec6279 100644 --- a/integration-tests/src/integration/java/io/teris/rpc/TestVertxInvocationRountrip.java +++ b/integration-tests/src/integration/java/io/teris/kite/rpc/TestVertxInvocationRountrip.java @@ -2,46 +2,39 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc; +package io.teris.kite.rpc; + +import static org.junit.Assert.assertTrue; -import java.io.IOException; -import java.net.ServerSocket; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import org.junit.AfterClass; import org.junit.BeforeClass; +import org.junit.Test; -import io.teris.rpc.http.vertx.VertxServiceInvoker; -import io.teris.rpc.http.vertx.VertxServiceRouter; -import io.teris.rpc.impl.AsyncServiceImpl; -import io.teris.rpc.impl.SyncServiceImpl; -import io.teris.rpc.impl.ThrowingServiceImpl; -import io.teris.rpc.serialization.json.GsonSerializer; +import io.teris.kite.Context; +import io.teris.kite.Service; +import io.teris.kite.gson.JsonSerializer; +import io.teris.kite.rpc.vertx.HttpServiceInvoker; +import io.teris.kite.rpc.vertx.HttpServiceExporter; import io.vertx.core.Vertx; import io.vertx.core.http.HttpClient; import io.vertx.core.http.HttpClientOptions; import io.vertx.core.http.HttpServer; import io.vertx.core.http.HttpServerOptions; -import io.vertx.ext.web.Router; public class TestVertxInvocationRountrip extends AbstractInvocationTestsuite { private static HttpServer server; + private static ServiceFactory creator; + @BeforeClass public static void init() throws Exception { - int port; - while (true) { - try (ServerSocket socket = new ServerSocket((int) (49152 + Math.random() * (65535 - 49152)))) { - port = socket.getLocalPort(); - break; - } - catch (IOException e) { - // repeat - } - } + preInit(); HttpServerOptions httpServerOptions = new HttpServerOptions().setHost("0.0.0.0").setPort(port); @@ -52,39 +45,24 @@ public static void init() throws Exception { .setDefaultPort(port) .setMaxPoolSize(200)); - VertxServiceInvoker invoker = VertxServiceInvoker.builder(httpClient).build(); + HttpServiceInvoker invoker = HttpServiceInvoker.httpClient(httpClient).build(); - ServiceFactory creator = ServiceFactory.builder() - .serviceInvoker(invoker) - .serializer(GsonSerializer.builder().build()) + creator = ServiceFactory.invoker(invoker) + .serializer(JsonSerializer.builder().build()) .build(); syncService = creator.newInstance(SyncService.class); asyncService = creator.newInstance(AsyncService.class); throwingService = creator.newInstance(ThrowingService.class); - - Router router = Router.router(vertx); - - ServiceDispatcher dispatcher1 = ServiceDispatcher.builder() - .serializer(GsonSerializer.builder().build()) - .bind(SyncService.class, new SyncServiceImpl("1")) - .bind(AsyncService.class, new AsyncServiceImpl("2")) - .build(); - - ServiceDispatcher dispatcher2 = ServiceDispatcher.builder() - .serializer(GsonSerializer.builder().build()) - .bind(ThrowingService.class, new ThrowingServiceImpl("3")) - .build(); - - VertxServiceRouter.builder(router).build() - .route(dispatcher1) - .route(dispatcher2); + HttpServiceExporter exporter = HttpServiceExporter.router(vertx) + .export(exporter1) + .export(exporter2); CompletableFuture promise = new CompletableFuture<>(); CompletableFuture.runAsync(() -> vertx.createHttpServer(httpServerOptions) - .requestHandler(router::accept) + .requestHandler(exporter.router()::accept) .listen(handler -> { if (handler.failed()) { promise.completeExceptionally(handler.cause()); @@ -100,4 +78,33 @@ public static void init() throws Exception { public static void teardown() { server.close(); } + + @Service + public interface NotServedService { + void dosync(Context context); + + CompletableFuture doasync(Context context); + } + + @Test + public void roundtrip_notFoundException() { + Context context = new Context(); + NotServedService service = creator.newInstance(NotServedService.class); + try { + service.dosync(context); + throw new AssertionError("unreachable code"); + } + catch (NotFoundException ex) { + assertTrue(ex.getMessage().contains("Not Found")); + } + CompletableFuture promise = service.doasync(context); + try { + promise.get(); + throw new AssertionError("unreachable code"); + } + catch (ExecutionException | InterruptedException ex) { + assertTrue(ex.getMessage().contains("NotFoundException: Not Found")); + } + } + } diff --git a/integration-tests/src/integration/java/io/teris/rpc/TestVertxPublicAPI.java b/integration-tests/src/integration/java/io/teris/kite/rpc/TestVertxPublicAPI.java similarity index 86% rename from integration-tests/src/integration/java/io/teris/rpc/TestVertxPublicAPI.java rename to integration-tests/src/integration/java/io/teris/kite/rpc/TestVertxPublicAPI.java index b1714a7..570cef6 100644 --- a/integration-tests/src/integration/java/io/teris/rpc/TestVertxPublicAPI.java +++ b/integration-tests/src/integration/java/io/teris/kite/rpc/TestVertxPublicAPI.java @@ -2,7 +2,7 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc; +package io.teris.kite.rpc; import static org.junit.Assert.assertTrue; @@ -20,8 +20,12 @@ import org.junit.BeforeClass; import org.junit.Test; -import io.teris.rpc.http.vertx.VertxServiceRouter; -import io.teris.rpc.serialization.json.GsonSerializer; +import io.teris.kite.Context; +import io.teris.kite.Name; +import io.teris.kite.Serializer; +import io.teris.kite.Service; +import io.teris.kite.gson.JsonSerializer; +import io.teris.kite.rpc.vertx.HttpServiceExporter; import io.vertx.core.Vertx; import io.vertx.core.http.HttpClient; import io.vertx.core.http.HttpClientOptions; @@ -33,7 +37,7 @@ public class TestVertxPublicAPI { - @Service(replace = "io.teris.rpc.TestVertxPublicAPI") + @Service(replace = "io.teris.kite.rpc.TestVertxPublicAPI") public interface ComputeService { HashMap sum(Context context, @Name("data") HashMap> data); } @@ -69,6 +73,8 @@ public static void init() throws Exception { } } + Serializer serializer = JsonSerializer.builder().build(); + HttpServerOptions httpServerOptions = new HttpServerOptions().setHost("0.0.0.0").setPort(port); Vertx vertx = Vertx.vertx(); @@ -78,15 +84,13 @@ public static void init() throws Exception { .setDefaultPort(port) .setMaxPoolSize(50)); - Router router = Router.router(vertx); - - ServiceDispatcher dispatcher = ServiceDispatcher.builder() - .serializer(GsonSerializer.builder().build()) - .bind(ComputeService.class, new ComputeServiceImpl()) + ServiceExporter dispatcher = ServiceExporter.serializer(serializer) + .export(ComputeService.class, new ComputeServiceImpl()) .build(); - VertxServiceRouter.builder(router).build() - .route(dispatcher); + Router router = HttpServiceExporter.router(vertx) + .export(dispatcher) + .router(); CompletableFuture promise = new CompletableFuture<>(); CompletableFuture.runAsync(() -> diff --git a/integration-tests/src/integration/java/io/teris/rpc/ThrowingService.java b/integration-tests/src/integration/java/io/teris/kite/rpc/ThrowingService.java similarity index 74% rename from integration-tests/src/integration/java/io/teris/rpc/ThrowingService.java rename to integration-tests/src/integration/java/io/teris/kite/rpc/ThrowingService.java index ced4f89..e616f15 100644 --- a/integration-tests/src/integration/java/io/teris/rpc/ThrowingService.java +++ b/integration-tests/src/integration/java/io/teris/kite/rpc/ThrowingService.java @@ -2,10 +2,13 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc; +package io.teris.kite.rpc; import java.util.concurrent.CompletableFuture; +import io.teris.kite.Context; +import io.teris.kite.Service; + @Service public interface ThrowingService { diff --git a/integration-tests/src/integration/java/io/teris/rpc/impl/AsyncServiceImpl.java b/integration-tests/src/integration/java/io/teris/kite/rpc/impl/AsyncServiceImpl.java similarity index 87% rename from integration-tests/src/integration/java/io/teris/rpc/impl/AsyncServiceImpl.java rename to integration-tests/src/integration/java/io/teris/kite/rpc/impl/AsyncServiceImpl.java index b236b48..117568a 100644 --- a/integration-tests/src/integration/java/io/teris/rpc/impl/AsyncServiceImpl.java +++ b/integration-tests/src/integration/java/io/teris/kite/rpc/impl/AsyncServiceImpl.java @@ -2,12 +2,12 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc.impl; +package io.teris.kite.rpc.impl; import java.util.concurrent.CompletableFuture; -import io.teris.rpc.AsyncService; -import io.teris.rpc.Context; +import io.teris.kite.Context; +import io.teris.kite.rpc.AsyncService; public class AsyncServiceImpl implements AsyncService { diff --git a/integration-tests/src/integration/java/io/teris/rpc/impl/SyncServiceImpl.java b/integration-tests/src/integration/java/io/teris/kite/rpc/impl/SyncServiceImpl.java similarity index 84% rename from integration-tests/src/integration/java/io/teris/rpc/impl/SyncServiceImpl.java rename to integration-tests/src/integration/java/io/teris/kite/rpc/impl/SyncServiceImpl.java index a0700d2..9e11fc4 100644 --- a/integration-tests/src/integration/java/io/teris/rpc/impl/SyncServiceImpl.java +++ b/integration-tests/src/integration/java/io/teris/kite/rpc/impl/SyncServiceImpl.java @@ -2,10 +2,10 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc.impl; +package io.teris.kite.rpc.impl; -import io.teris.rpc.SyncService; -import io.teris.rpc.Context; +import io.teris.kite.Context; +import io.teris.kite.rpc.SyncService; public class SyncServiceImpl implements SyncService { diff --git a/integration-tests/src/integration/java/io/teris/rpc/impl/ThrowingServiceImpl.java b/integration-tests/src/integration/java/io/teris/kite/rpc/impl/ThrowingServiceImpl.java similarity index 85% rename from integration-tests/src/integration/java/io/teris/rpc/impl/ThrowingServiceImpl.java rename to integration-tests/src/integration/java/io/teris/kite/rpc/impl/ThrowingServiceImpl.java index c33ac7e..4c5e622 100644 --- a/integration-tests/src/integration/java/io/teris/rpc/impl/ThrowingServiceImpl.java +++ b/integration-tests/src/integration/java/io/teris/kite/rpc/impl/ThrowingServiceImpl.java @@ -2,12 +2,12 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc.impl; +package io.teris.kite.rpc.impl; import java.util.concurrent.CompletableFuture; -import io.teris.rpc.Context; -import io.teris.rpc.ThrowingService; +import io.teris.kite.Context; +import io.teris.kite.rpc.ThrowingService; public class ThrowingServiceImpl implements ThrowingService { diff --git a/integration-tests/src/integration/java/io/teris/rpc/TestAmqpInvocationRoundtrip.java b/integration-tests/src/integration/java/io/teris/rpc/TestAmqpInvocationRoundtrip.java deleted file mode 100644 index 239e33d..0000000 --- a/integration-tests/src/integration/java/io/teris/rpc/TestAmqpInvocationRoundtrip.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved - */ - -package io.teris.rpc; - -import org.junit.AfterClass; -import org.junit.BeforeClass; - -import com.rabbitmq.client.ConnectionFactory; - -import io.teris.rpc.amqp.AmqpServiceInvoker; -import io.teris.rpc.amqp.AmqpServiceRouter; -import io.teris.rpc.impl.AsyncServiceImpl; -import io.teris.rpc.impl.SyncServiceImpl; -import io.teris.rpc.impl.ThrowingServiceImpl; -import io.teris.rpc.serialization.json.GsonSerializer; - -//@Ignore -public class TestAmqpInvocationRoundtrip extends AbstractInvocationTestsuite { - - private static final String exchangeName = "RPC"; - - private static AmqpServiceInvoker invoker; - - private static AmqpServiceRouter router; - - private static final int port = 5672; - - @BeforeClass - public static void init() throws Exception { - - ConnectionFactory connectionFactory = new ConnectionFactory(); - connectionFactory.setHost("127.0.0.1"); - connectionFactory.setPort(port); - - invoker = AmqpServiceInvoker.builder(connectionFactory) - .exchangeName(exchangeName) - .build() - .start(); - - ServiceFactory creator = ServiceFactory.builder() - .serviceInvoker(invoker) - .serializer(GsonSerializer.builder().build()) - .build(); - - syncService = creator.newInstance(SyncService.class); - asyncService = creator.newInstance(AsyncService.class); - throwingService = creator.newInstance(ThrowingService.class); - - ServiceDispatcher dispatcher1 = ServiceDispatcher.builder() - .serializer(GsonSerializer.builder().build()) - .bind(SyncService.class, new SyncServiceImpl("1")) - .bind(AsyncService.class, new AsyncServiceImpl("2")) - .build(); - - ServiceDispatcher dispatcher2 = ServiceDispatcher.builder() - .serializer(GsonSerializer.builder().build()) - .bind(ThrowingService.class, new ThrowingServiceImpl("3")) - .build(); - - router = AmqpServiceRouter.builder(connectionFactory) - .exchangeName(exchangeName) - .build() - .route(dispatcher1) - .route(dispatcher2) - .start(); - } - - @AfterClass - public static void teardown() { - invoker.close(); - router.close(); - } -} diff --git a/integration-tests/src/integration/java/io/teris/rpc/TestJmsInvocationRountrip.java b/integration-tests/src/integration/java/io/teris/rpc/TestJmsInvocationRountrip.java deleted file mode 100644 index 77c59a5..0000000 --- a/integration-tests/src/integration/java/io/teris/rpc/TestJmsInvocationRountrip.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved - */ - -package io.teris.rpc; - -import java.io.IOException; -import java.net.ServerSocket; - -import org.apache.activemq.ActiveMQConnectionFactory; -import org.apache.activemq.broker.BrokerService; -import org.junit.AfterClass; -import org.junit.BeforeClass; - -import io.teris.rpc.impl.AsyncServiceImpl; -import io.teris.rpc.impl.SyncServiceImpl; -import io.teris.rpc.impl.ThrowingServiceImpl; -import io.teris.rpc.jms.JmsServiceInvoker; -import io.teris.rpc.jms.JmsServiceRouter; -import io.teris.rpc.serialization.json.GsonSerializer; - - -public class TestJmsInvocationRountrip extends AbstractInvocationTestsuite { - - private static final String topicName = "RPC"; - - private static BrokerService broker; - - @BeforeClass - public static void init() throws Exception { - int port; - while (true) { - try (ServerSocket socket = new ServerSocket((int) (49152 + Math.random() * (65535 - 49152)))) { - port = socket.getLocalPort(); - break; - } - catch (IOException e) { - // repeat - } - } - - String brokerUrl = String.format("tcp://localhost:%d", Integer.valueOf(port)); - String clientUrl = brokerUrl + "?jms.useAsyncSend=true"; - - broker = new BrokerService(); - broker.setUseJmx(true); - broker.addConnector(brokerUrl); - broker.start(); - - JmsServiceInvoker invoker = JmsServiceInvoker.builder(new ActiveMQConnectionFactory(clientUrl)) - .topicName(topicName) - .build() - .start(); - - ServiceFactory creator = ServiceFactory.builder() - .serviceInvoker(invoker) - .serializer(GsonSerializer.builder().build()) - .build(); - - syncService = creator.newInstance(SyncService.class); - asyncService = creator.newInstance(AsyncService.class); - throwingService = creator.newInstance(ThrowingService.class); - - ServiceDispatcher dispatcher1 = ServiceDispatcher.builder() - .serializer(GsonSerializer.builder().build()) - .bind(SyncService.class, new SyncServiceImpl("1")) - .bind(AsyncService.class, new AsyncServiceImpl("2")) - .build(); - - ServiceDispatcher dispatcher2 = ServiceDispatcher.builder() - .serializer(GsonSerializer.builder().build()) - .bind(ThrowingService.class, new ThrowingServiceImpl("3")) - .build(); - - JmsServiceRouter.builder(new ActiveMQConnectionFactory(clientUrl)) - .topicName(topicName) - .build() - .route(dispatcher1) - .route(dispatcher2) - .start(); - } - - @AfterClass - public static void teardown() throws Exception { - broker.stop(); - } -} diff --git a/jms/build.gradle b/jms/build.gradle deleted file mode 100644 index 42336aa..0000000 --- a/jms/build.gradle +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved - */ - -plugins.apply(JavaPlugin) - -dependencies { - compileOnly(findbugsModule) - compile(slf4jModule) - compile(geronimoJmsModule) - compile(project(":rpc")) - - testCompile(junitModule) - testCompile(mockitoModule) - testCompile(activemqClientModule) - testCompile(activemqBrokerModule) - testCompile(project(":serialization-json")) - - // amq persistence - testRuntime(activemqKahaDbModule) - testRuntime(logbackModule) -} diff --git a/jms/src/main/java/io/teris/rpc/jms/JmsServiceRouter.java b/jms/src/main/java/io/teris/rpc/jms/JmsServiceRouter.java deleted file mode 100644 index db4a1bc..0000000 --- a/jms/src/main/java/io/teris/rpc/jms/JmsServiceRouter.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved - */ - -package io.teris.rpc.jms; - - -import java.util.concurrent.CompletableFuture; -import javax.annotation.Nonnull; -import javax.jms.ConnectionFactory; -import javax.jms.JMSException; - -import io.teris.rpc.ServiceDispatcher; - - -public interface JmsServiceRouter { - - - @Nonnull - CompletableFuture close(); - - @Nonnull - static Builder builder(@Nonnull ConnectionFactory connectionFactory) { - return new JmsServiceRouterImpl.BuilderImpl(connectionFactory); - } - - interface Builder { - - Builder topicName(String topicName); - - @Nonnull - Router build() throws JMSException; - } - - interface Router { - - @Nonnull - Router route(@Nonnull ServiceDispatcher serviceDispatcher) throws JMSException; - - JmsServiceRouter start() throws JMSException; - } -} diff --git a/kite-fasterxml/build.gradle b/kite-fasterxml/build.gradle new file mode 100644 index 0000000..47492dc --- /dev/null +++ b/kite-fasterxml/build.gradle @@ -0,0 +1,16 @@ +/* + * Copyright (c) Oleg Sklyar & teris.io, 2018. All rights reserved. + */ + +plugins.apply(JavaPlugin) + +dependencies { + compileOnly(findbugsModule) + compile(project(":kite")) + compile(jacksonDatabindModule) + compile(jacksonDatatypeJSR310Module) { + transitive = false + } + + testCompile(junitModule) +} diff --git a/kite-fasterxml/src/main/java/io/teris/kite/fasterxml/JsonDeserializer.java b/kite-fasterxml/src/main/java/io/teris/kite/fasterxml/JsonDeserializer.java new file mode 100644 index 0000000..1b058b6 --- /dev/null +++ b/kite-fasterxml/src/main/java/io/teris/kite/fasterxml/JsonDeserializer.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) Oleg Sklyar & teris.io, 2018. All rights reserved. + */ + +package io.teris.kite.fasterxml; + +import java.io.IOException; +import java.io.Serializable; +import java.lang.reflect.Type; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; +import javax.annotation.Nonnull; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.teris.kite.Deserializer; + + +public class JsonDeserializer implements Deserializer { + + private final ObjectMapper mapper; + + public JsonDeserializer(ObjectMapper mapper) { + this(mapper, StandardCharsets.UTF_8); + } + + public JsonDeserializer(ObjectMapper mapper, Charset charset) { + this.mapper = mapper; + } + + @Nonnull + @Override + public CompletableFuture deserialize(@Nonnull byte[] data, @Nonnull Class clazz) { + + return CompletableFuture.supplyAsync(() -> { + try { + return mapper.readValue(data, clazz); + } + catch (IOException ex) { + throw new IllegalArgumentException(ex.getCause() != null ? ex.getCause() : ex); + } + }); + } + + @Nonnull + @Override + public CompletableFuture deserialize(@Nonnull byte[] data, @Nonnull Type type) { + return CompletableFuture.supplyAsync(() -> { + try { + return mapper.readValue(data, mapper.constructType(type)); + } + catch (IOException ex) { + throw new IllegalArgumentException(ex.getCause() != null ? ex.getCause() : ex); + } + }); + } +} diff --git a/kite-fasterxml/src/main/java/io/teris/kite/fasterxml/JsonSerializer.java b/kite-fasterxml/src/main/java/io/teris/kite/fasterxml/JsonSerializer.java new file mode 100644 index 0000000..828165d --- /dev/null +++ b/kite-fasterxml/src/main/java/io/teris/kite/fasterxml/JsonSerializer.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) Oleg Sklyar & teris.io, 2018. All rights reserved. + */ + +package io.teris.kite.fasterxml; + +import java.io.IOException; +import java.io.Serializable; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; +import javax.annotation.Nonnull; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.teris.kite.Deserializer; +import io.teris.kite.Serializer; + + +public class JsonSerializer implements Serializer { + + private static final String CONTENT_TYPE = "application/json"; + + private final Deserializer deserializer; + + private final ObjectMapper mapper; + + private final Charset charset; + + public JsonSerializer(ObjectMapper mapper) { + this(mapper, StandardCharsets.UTF_8); + } + + public JsonSerializer(ObjectMapper mapper, Charset charset) { + this.mapper = mapper; + this.charset = charset; + deserializer = new JsonDeserializer(mapper, charset); + } + + public static JsonSerializerBuilder builder() { + return new JsonSerializerBuilder(); + } + + @Nonnull + @Override + public CompletableFuture serialize(@Nonnull CT value) { + return CompletableFuture.supplyAsync(() -> { + try { + return mapper.writeValueAsString(value).getBytes(charset); + } + catch (IOException ex) { + throw new IllegalArgumentException(ex.getCause() != null ? ex.getCause() : ex); + } + }); + } + + @Nonnull + @Override + public String contentType() { + return CONTENT_TYPE; + } + + @Nonnull + @Override + public Deserializer deserializer() { + return deserializer; + } +} diff --git a/kite-fasterxml/src/main/java/io/teris/kite/fasterxml/JsonSerializerBuilder.java b/kite-fasterxml/src/main/java/io/teris/kite/fasterxml/JsonSerializerBuilder.java new file mode 100644 index 0000000..a535d30 --- /dev/null +++ b/kite-fasterxml/src/main/java/io/teris/kite/fasterxml/JsonSerializerBuilder.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) Oleg Sklyar & teris.io, 2018. All rights reserved. + */ + +package io.teris.kite.fasterxml; + +import java.io.IOException; +import java.io.Serializable; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + + +public class JsonSerializerBuilder { + + private final ObjectMapper mapper; + + private Charset charset = StandardCharsets.UTF_8; + + JsonSerializerBuilder() { + mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.USE_LONG_FOR_INTS, true); + + mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + + // mapper.configure(DeserializationFeature.READ_ENUMS_USING_TO_STRING, true); + // mapper.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true); + + mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + mapper.configure(SerializationFeature.WRITE_DATES_WITH_ZONE_ID, true); + mapper.registerModule(new JavaTimeModule()); + + mapper.setSerializationInclusion(Include.NON_ABSENT); + } + + public JsonSerializerBuilder withCharset(Charset charset) { + this.charset = charset; + return this; + } + + public ObjectMapper rawMapper() { + return mapper; + } + + public JsonSerializer build() { + mapper.registerModule(new SimpleModule() + .addDeserializer(Serializable.class, new SerializableDeserializer(charset))); + return new JsonSerializer(mapper, charset); + } + + private static class SerializableDeserializer extends StdScalarDeserializer { + + private final Charset charset; + + SerializableDeserializer(Charset charset) { + super((Class)null); + this.charset = charset; + } + + @Override + public byte[] deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + return jp.getCodec().readTree(jp).toString().getBytes(charset); + } + } +} diff --git a/kite-fasterxml/src/test/java/io/teris/kite/fasterxml/JsonSerializerTest.java b/kite-fasterxml/src/test/java/io/teris/kite/fasterxml/JsonSerializerTest.java new file mode 100644 index 0000000..b63c5f1 --- /dev/null +++ b/kite-fasterxml/src/test/java/io/teris/kite/fasterxml/JsonSerializerTest.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) Oleg Sklyar & teris.io, 2018. All rights reserved. + */ +package io.teris.kite.fasterxml; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import io.teris.kite.Deserializer; +import io.teris.kite.Serializer; + + +public class JsonSerializerTest { + + @Rule + public ExpectedException exception = ExpectedException.none(); + + private final Serializer serializer = JsonSerializer.builder().build(); + + private final Deserializer deserializer = serializer.deserializer(); + + static class WithArray implements Serializable { + private static final long serialVersionUID = 5491293815791739629L; + public ArrayList values; + } + + static class MissingVals implements Serializable { + private static final long serialVersionUID = -4487032678271049099L; + public String name; + } + + @Test + public void missingValues_ignoredInDeserialize() throws Exception { + MissingVals res = deserializer.deserialize("{}".getBytes(), MissingVals.class).get(); + assertNull(res.name); + } + + @Test + public void missingList_deserializedAsNull() throws Exception { + WithArray res = deserializer.deserialize("{}".getBytes(), WithArray.class).get(); + assertNull(res.values); + } + + @Test + public void emptyArray_deserializedEmpty() throws Exception { + WithArray res = deserializer.deserialize("{\"values\": []}".getBytes(), WithArray.class).get(); + assertEquals(Collections.emptyList(), res.values); + } + + @Test + public void emptyArray_preservedInSerialization() throws Exception { + WithArray res = new WithArray(); + res.values = new ArrayList<>(); + assertEquals("{\"values\":[]}", new String(serializer.serialize(res).get())); + } + + public enum TestEnum { one, two } + + @Test + public void enum_serializedAsStrings() throws Exception { + assertEquals("\"one\"", new String(serializer.serialize(TestEnum.one).get())); + } + + @Test + @Ignore("need a deserializer that goes onto values()[i].name") + public void enum_deserializedFromStrings() throws Exception { + assertEquals(TestEnum.two, deserializer.deserialize("two".getBytes(), TestEnum.class).get()); + } + + @Test + public void missingValues_droppedInSerialization() throws Exception { + MissingVals res = new MissingVals(); + assertEquals("{}", new String(serializer.serialize(res).get())); + } + + static class WithLocalDateTime implements Serializable { + public LocalDateTime field; + } + + @Test + public void serialize_LocalDateTime_ok() throws Exception { + WithLocalDateTime underTest = new WithLocalDateTime(); + underTest.field = LocalDateTime.of(2016, 2, 29, 12, 34, 56, 234234); + assertEquals("{\"field\":\"2016-02-29T12:34:56.000234234\"}", new String(serializer.serialize(underTest).get())); + } + + @Test + public void deserialize_LocalDateTime_ok() throws Exception { + WithLocalDateTime actual = deserializer.deserialize("{\"field\":\"2016-02-29T12:34:56.000234234\"}".getBytes(), WithLocalDateTime.class).get(); + assertEquals(LocalDateTime.of(2016, 2, 29, 12, 34, 56, 234234), actual.field); + } + + @Test + public void deserialize_LocalDateTimeShortFormat_ok() throws Exception { + WithLocalDateTime actual = deserializer.deserialize("{\"field\":\"2016-02-29T12:34:56\"}".getBytes(), WithLocalDateTime.class).get(); + assertEquals(LocalDateTime.of(2016, 2, 29, 12, 34, 56, 0), actual.field); + } + + static class WithZonedDateTime implements Serializable { + public ZonedDateTime field; + } + + @Test + public void deserialize_zonedDateTime_okWithTimeZone() throws Exception { + WithZonedDateTime actual = deserializer.deserialize("{\"field\":\"2016-02-29T12:34:56Z\"}".getBytes(), WithZonedDateTime.class).get(); + assertEquals(ZonedDateTime.of(LocalDateTime.of(2016, 2, 29, 12, 34, 56, 0), ZoneId.of("UTC")), actual.field); + } + + @Test + public void deserialize_zonedDateTime_throwsWithoutTimeZone() throws Exception { + exception.expect(ExecutionException.class); + exception.expectMessage("Text '2016-02-29T12:34:56' could not be parsed at index 19"); + deserializer.deserialize("{\"field\":\"2016-02-29T12:34:56\"}".getBytes(), WithZonedDateTime.class).get(); + } + + private static class TypedefOuter extends HashMap {} + + private static class TypedefInner extends HashSet {} + + @Test + public void deserialize_Serializable_asByteArrray() throws Exception { + HashMap data = new HashMap<>(); + + HashSet dates = new HashSet<>(); + LocalDateTime date1 = LocalDateTime.of(2016, 2, 29, 12, 34, 56); + dates.add(date1); + LocalDateTime date2 = LocalDateTime.of(2016, 2, 28, 12, 34, 56); + dates.add(date2); + data.put("dates", dates); + byte[] payload = serializer.serialize(data).get(); + + CompletableFuture> actual = deserializer.deserialize(payload, TypedefOuter.class.getGenericSuperclass()); + CompletableFuture> actualDates = deserializer.deserialize((byte[]) actual.get().get("dates"), TypedefInner.class.getGenericSuperclass()); + assertEquals(dates, actualDates.get()); + } + + @Test + @Ignore("need a byte to string deserializer") + public void roundtrip_iso8859_1_ok() throws Exception { + String value = "äüöабв"; + Serializer serializer = JsonSerializer.builder().withCharset(StandardCharsets.ISO_8859_1).build(); + byte[] data = serializer.serialize(value).get(5, TimeUnit.SECONDS); + assertEquals(8, data.length); + assertEquals("äüö???", serializer.deserializer().deserialize(data, String.class).get(5, TimeUnit.SECONDS)); + } + + @Test + public void roundtrip_utf8_asDefault_ok() throws Exception { + String value = "äüöабв"; + Serializer serializer = JsonSerializer.builder().build(); + byte[] data = serializer.serialize(value).get(5, TimeUnit.SECONDS); + assertEquals(14, data.length); + assertEquals(value, serializer.deserializer().deserialize(data, String.class).get(5, TimeUnit.SECONDS)); + } +} diff --git a/serialization-json/build.gradle b/kite-gson/build.gradle similarity index 87% rename from serialization-json/build.gradle rename to kite-gson/build.gradle index e37eadd..0df176c 100644 --- a/serialization-json/build.gradle +++ b/kite-gson/build.gradle @@ -6,7 +6,7 @@ plugins.apply(JavaPlugin) dependencies { compileOnly(findbugsModule) - compile(project(":rpc")) + compile(project(":kite")) compile(gsonModule) testCompile(junitModule) diff --git a/serialization-json/src/main/java/io/teris/rpc/serialization/json/GsonDeserializer.java b/kite-gson/src/main/java/io/teris/kite/gson/JsonDeserializer.java similarity index 76% rename from serialization-json/src/main/java/io/teris/rpc/serialization/json/GsonDeserializer.java rename to kite-gson/src/main/java/io/teris/kite/gson/JsonDeserializer.java index 1e1fca0..e3436c4 100644 --- a/serialization-json/src/main/java/io/teris/rpc/serialization/json/GsonDeserializer.java +++ b/kite-gson/src/main/java/io/teris/kite/gson/JsonDeserializer.java @@ -1,8 +1,8 @@ /* - * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved + * Copyright (c) Oleg Sklyar & teris.io, 2018. All rights reserved. */ -package io.teris.rpc.serialization.json; +package io.teris.kite.gson; import java.io.Serializable; import java.lang.reflect.Type; @@ -14,24 +14,23 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonParseException; -import io.teris.rpc.Deserializer; +import io.teris.kite.Deserializer; -public class GsonDeserializer implements Deserializer { +public class JsonDeserializer implements Deserializer { private final Gson gson; private final Charset charset; - public GsonDeserializer(GsonBuilder builder) { + public JsonDeserializer(GsonBuilder builder) { this(builder, StandardCharsets.UTF_8); } - public GsonDeserializer(GsonBuilder builder, Charset charset) { + public JsonDeserializer(GsonBuilder builder, Charset charset) { gson = builder .registerTypeAdapter(Serializable.class, new SerializableDeserializer(charset)) .create(); @@ -50,7 +49,7 @@ public CompletableFuture deserialize(@Nonnull byte return CompletableFuture.supplyAsync(() -> gson.fromJson(new String(data, charset), type)); } - private static class SerializableDeserializer implements JsonDeserializer { + private static class SerializableDeserializer implements com.google.gson.JsonDeserializer { private final Charset charset; diff --git a/serialization-json/src/main/java/io/teris/rpc/serialization/json/GsonSerializer.java b/kite-gson/src/main/java/io/teris/kite/gson/JsonSerializer.java similarity index 65% rename from serialization-json/src/main/java/io/teris/rpc/serialization/json/GsonSerializer.java rename to kite-gson/src/main/java/io/teris/kite/gson/JsonSerializer.java index c8d95c0..252b505 100644 --- a/serialization-json/src/main/java/io/teris/rpc/serialization/json/GsonSerializer.java +++ b/kite-gson/src/main/java/io/teris/kite/gson/JsonSerializer.java @@ -1,8 +1,8 @@ /* - * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved + * Copyright (c) Oleg Sklyar & teris.io, 2018. All rights reserved. */ -package io.teris.rpc.serialization.json; +package io.teris.kite.gson; import java.io.Serializable; import java.nio.charset.Charset; @@ -13,11 +13,11 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import io.teris.rpc.Deserializer; -import io.teris.rpc.Serializer; +import io.teris.kite.Deserializer; +import io.teris.kite.Serializer; -public class GsonSerializer implements Serializer { +public class JsonSerializer implements Serializer { private static final String CONTENT_TYPE = "application/json"; @@ -27,18 +27,18 @@ public class GsonSerializer implements Serializer { private final Charset charset; - public GsonSerializer(GsonBuilder builder) { + public JsonSerializer(GsonBuilder builder) { this(builder, StandardCharsets.UTF_8); } - public GsonSerializer(GsonBuilder builder, Charset charset) { + public JsonSerializer(GsonBuilder builder, Charset charset) { gson = builder.create(); this.charset = charset; - deserializer = new GsonDeserializer(builder, charset); + deserializer = new JsonDeserializer(builder, charset); } - public static GsonSerializerBuilder builder() { - return new GsonSerializerBuilder(); + public static JsonSerializerBuilder builder() { + return new JsonSerializerBuilder(); } @Nonnull diff --git a/serialization-json/src/main/java/io/teris/rpc/serialization/json/GsonSerializerBuilder.java b/kite-gson/src/main/java/io/teris/kite/gson/JsonSerializerBuilder.java similarity index 91% rename from serialization-json/src/main/java/io/teris/rpc/serialization/json/GsonSerializerBuilder.java rename to kite-gson/src/main/java/io/teris/kite/gson/JsonSerializerBuilder.java index 976e0de..45a7893 100644 --- a/serialization-json/src/main/java/io/teris/rpc/serialization/json/GsonSerializerBuilder.java +++ b/kite-gson/src/main/java/io/teris/kite/gson/JsonSerializerBuilder.java @@ -1,8 +1,8 @@ /* - * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved + * Copyright (c) Oleg Sklyar & teris.io, 2018. All rights reserved. */ -package io.teris.rpc.serialization.json; +package io.teris.kite.gson; import java.lang.reflect.Type; import java.nio.charset.Charset; @@ -22,7 +22,7 @@ import com.google.gson.JsonSerializationContext; -public class GsonSerializerBuilder { +public class JsonSerializerBuilder { private final GsonBuilder builder = new GsonBuilder() .registerTypeAdapter(LocalDate.class, new LocalDateSerializer()) @@ -36,14 +36,14 @@ public class GsonSerializerBuilder { private Charset charset = StandardCharsets.UTF_8; - GsonSerializerBuilder() {} + JsonSerializerBuilder() {} - public GsonSerializerBuilder registerTypeAdapter(Type type, Object typeAdapter) { + public JsonSerializerBuilder registerTypeAdapter(Type type, Object typeAdapter) { builder.registerTypeAdapter(type, typeAdapter); return this; } - public GsonSerializerBuilder withCharset(Charset charset) { + public JsonSerializerBuilder withCharset(Charset charset) { this.charset = charset; return this; } @@ -52,8 +52,8 @@ public GsonBuilder rawBuilder() { return builder; } - public GsonSerializer build() { - return new GsonSerializer(builder, charset); + public JsonSerializer build() { + return new JsonSerializer(builder, charset); } private static class LocalDateDeserializer implements JsonDeserializer { diff --git a/serialization-json/src/test/java/io/teris/rpc/serialization/json/GsonSerializerTest.java b/kite-gson/src/test/java/io/teris/kite/gson/JsonSerializerTest.java similarity index 93% rename from serialization-json/src/test/java/io/teris/rpc/serialization/json/GsonSerializerTest.java rename to kite-gson/src/test/java/io/teris/kite/gson/JsonSerializerTest.java index 8b678b0..5b02533 100644 --- a/serialization-json/src/test/java/io/teris/rpc/serialization/json/GsonSerializerTest.java +++ b/kite-gson/src/test/java/io/teris/kite/gson/JsonSerializerTest.java @@ -1,7 +1,7 @@ /* - * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved + * Copyright (c) Oleg Sklyar & teris.io, 2018. All rights reserved. */ -package io.teris.rpc.serialization.json; +package io.teris.kite.gson; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; @@ -23,16 +23,16 @@ import org.junit.Test; import org.junit.rules.ExpectedException; -import io.teris.rpc.Deserializer; -import io.teris.rpc.Serializer; +import io.teris.kite.Deserializer; +import io.teris.kite.Serializer; -public class GsonSerializerTest { +public class JsonSerializerTest { @Rule public ExpectedException exception = ExpectedException.none(); - private final Serializer serializer = GsonSerializer.builder().build(); + private final Serializer serializer = JsonSerializer.builder().build(); private final Deserializer deserializer = serializer.deserializer(); @@ -153,7 +153,7 @@ public void deserialize_Serializable_asByteArrray() throws Exception { @Test public void roundtrip_iso8859_1_ok() throws Exception { String value = "äüöабв"; - Serializer serializer = GsonSerializer.builder().withCharset(StandardCharsets.ISO_8859_1).build(); + Serializer serializer = JsonSerializer.builder().withCharset(StandardCharsets.ISO_8859_1).build(); byte[] data = serializer.serialize(value).get(5, TimeUnit.SECONDS); assertEquals(8, data.length); assertEquals("äüö???", serializer.deserializer().deserialize(data, String.class).get(5, TimeUnit.SECONDS)); @@ -162,7 +162,7 @@ public void roundtrip_iso8859_1_ok() throws Exception { @Test public void roundtrip_utf8_asDefault_ok() throws Exception { String value = "äüöабв"; - Serializer serializer = GsonSerializer.builder().build(); + Serializer serializer = JsonSerializer.builder().build(); byte[] data = serializer.serialize(value).get(5, TimeUnit.SECONDS); assertEquals(14, data.length); assertEquals(value, serializer.deserializer().deserialize(data, String.class).get(5, TimeUnit.SECONDS)); diff --git a/kite-rpc-amqp/build.gradle b/kite-rpc-amqp/build.gradle new file mode 100644 index 0000000..54def8d --- /dev/null +++ b/kite-rpc-amqp/build.gradle @@ -0,0 +1,19 @@ +/* + * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved + */ + +plugins.apply(JavaPlugin) + +dependencies { + compileOnly(findbugsModule) + compile(slf4jModule) + compile(project(":kite")) + compile(project(":kite-rpc")) + compile(rabbitmqModule) { + transitive = false + } + + // testCompile(junitModule) + // testCompile(project(":kite-gson")) + // testRuntime(logbackModule) +} diff --git a/kite-rpc-amqp/src/main/java/io/teris/kite/rpc/amqp/AmqpServiceBase.java b/kite-rpc-amqp/src/main/java/io/teris/kite/rpc/amqp/AmqpServiceBase.java new file mode 100644 index 0000000..268ec8d --- /dev/null +++ b/kite-rpc-amqp/src/main/java/io/teris/kite/rpc/amqp/AmqpServiceBase.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) Oleg Sklyar & teris.io, 2018. All rights reserved. + */ + +package io.teris.kite.rpc.amqp; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeoutException; +import javax.annotation.Nonnull; + +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; + + +abstract class AmqpServiceBase { + + static final String MSGTYPE_REQUEST = "request"; + + static final String MSGTYPE_RESPONSE = "response"; + + static final String MSGTYPE_ERROR = "error"; + + static final String MSGTYPE_ERROR_AUTH = "error:auth"; + + static final String MSGTYPE_ERROR_NOTFOUND = "error:not-found"; + + final Connection connection; + + final Channel channel; + + + AmqpServiceBase(Connection connection, Channel channel) { + this.connection = connection; + this.channel = channel; + } + + @Nonnull + public CompletableFuture close() { + CompletableFuture result = new CompletableFuture<>(); + CompletableFuture.runAsync(() -> { + try { + channel.close(); + connection.close(); + result.complete(null); + } + catch (IOException | TimeoutException ex) { + result.completeExceptionally(ex); + } + }); + return result; + } +} diff --git a/kite-rpc-amqp/src/main/java/io/teris/kite/rpc/amqp/AmqpServiceExporter.java b/kite-rpc-amqp/src/main/java/io/teris/kite/rpc/amqp/AmqpServiceExporter.java new file mode 100644 index 0000000..cd2f93f --- /dev/null +++ b/kite-rpc-amqp/src/main/java/io/teris/kite/rpc/amqp/AmqpServiceExporter.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved + */ + +package io.teris.kite.rpc.amqp; + + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeoutException; +import javax.annotation.Nonnull; + +import com.rabbitmq.client.ConnectionFactory; + +import io.teris.kite.rpc.ServiceExporter; +import io.teris.kite.rpc.amqp.AmqpServiceExporterImpl.ConfiguratorImpl; + + +public interface AmqpServiceExporter { + + @Nonnull + AmqpServiceExporter export(@Nonnull ServiceExporter serviceExporter) throws IOException; + + @Nonnull + AmqpServiceExporter start(); + + @Nonnull + CompletableFuture close(); + + @Nonnull + static Configurator connectionFactory(@Nonnull ConnectionFactory connectionFactory) { + return new ConfiguratorImpl(connectionFactory); + } + + interface Configurator { + + @Nonnull + AmqpServiceExporter requestExchange(String requestExchange) throws IOException, TimeoutException; + } +} diff --git a/amqp/src/main/java/io/teris/rpc/amqp/AmqpServiceRouterImpl.java b/kite-rpc-amqp/src/main/java/io/teris/kite/rpc/amqp/AmqpServiceExporterImpl.java similarity index 70% rename from amqp/src/main/java/io/teris/rpc/amqp/AmqpServiceRouterImpl.java rename to kite-rpc-amqp/src/main/java/io/teris/kite/rpc/amqp/AmqpServiceExporterImpl.java index 6654314..bc5442e 100644 --- a/amqp/src/main/java/io/teris/rpc/amqp/AmqpServiceRouterImpl.java +++ b/kite-rpc-amqp/src/main/java/io/teris/kite/rpc/amqp/AmqpServiceExporterImpl.java @@ -2,15 +2,16 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc.amqp; +package io.teris.kite.rpc.amqp; import java.io.IOException; import java.util.Collections; import java.util.Map; import java.util.Map.Entry; import java.util.UUID; -import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import javax.annotation.Nonnull; @@ -26,30 +27,31 @@ import com.rabbitmq.client.DefaultConsumer; import com.rabbitmq.client.Envelope; -import io.teris.rpc.Context; -import io.teris.rpc.ServiceDispatcher; +import io.teris.kite.Context; +import io.teris.kite.rpc.AuthenticationException; +import io.teris.kite.rpc.NotFoundException; +import io.teris.kite.rpc.ServiceExporter; -class AmqpServiceRouterImpl implements AmqpServiceRouter, AmqpServiceRouter.Router { +class AmqpServiceExporterImpl extends AmqpServiceBase implements AmqpServiceExporter { - private static final Logger logger = LoggerFactory.getLogger(AmqpServiceRouter.class); - - private final Connection connection; - - private final Channel channel; + private static final Logger logger = LoggerFactory.getLogger(AmqpServiceExporter.class); private final String exchangeName; private final String requestQueue = "request-" + UUID.randomUUID().toString(); // FIXME pass in - private final Map serviceDispatchers = new ConcurrentHashMap<>(); + private final Map serviceDispatchers = new ConcurrentHashMap<>(); - AmqpServiceRouterImpl(ConnectionFactory connectionFactory, String exchangeName) throws IOException, TimeoutException { - this.exchangeName = exchangeName; - connection = connectionFactory.newConnection(); - channel = connection.createChannel(); + AmqpServiceExporterImpl(ConnectionFactory connectionFactory, String exchangeName) throws IOException, TimeoutException { + this(connectionFactory.newConnection(), exchangeName); + } + + AmqpServiceExporterImpl(Connection connection, String exchangeName) throws IOException { + super(connection, connection.createChannel()); + this.exchangeName = exchangeName; channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC); channel.queueDeclare(requestQueue, true, false, true, Collections.emptyMap()); @@ -57,34 +59,26 @@ class AmqpServiceRouterImpl implements AmqpServiceRouter, AmqpServiceRouter.Rout new RequestConsumer(channel, exchangeName, serviceDispatchers)); } - static class BuilderImpl implements Builder { + static class ConfiguratorImpl implements Configurator { private final ConnectionFactory connectionFactory; - private String exchangeName; - - BuilderImpl(ConnectionFactory connectionFactory) { + ConfiguratorImpl(ConnectionFactory connectionFactory) { this.connectionFactory = connectionFactory; } - @Override - public Builder exchangeName(String exchangeName) { - this.exchangeName = exchangeName; - return this; - } - @Nonnull @Override - public Router build() throws IOException, TimeoutException { - return new AmqpServiceRouterImpl(connectionFactory, exchangeName); + public AmqpServiceExporter requestExchange(String requestExchange) throws IOException, TimeoutException { + return new AmqpServiceExporterImpl(connectionFactory, requestExchange); } } @Nonnull @Override - public Router route(@Nonnull ServiceDispatcher serviceDispatcher) throws IOException { - for (String route :serviceDispatcher.dispatchRoutes()) { - serviceDispatchers.put(route, serviceDispatcher); + public AmqpServiceExporter export(@Nonnull ServiceExporter serviceExporter) throws IOException { + for (String route : serviceExporter.routes()) { + serviceDispatchers.put(route, serviceExporter); channel.queueBind(requestQueue, exchangeName, route); } return this; @@ -95,9 +89,9 @@ static class RequestConsumer extends DefaultConsumer implements Consumer { private final String exchangeName; - private final Map serviceDispatchers; + private final Map serviceDispatchers; - RequestConsumer(Channel channel, String exchangeName, Map serviceDispatchers) { + RequestConsumer(Channel channel, String exchangeName, Map serviceDispatchers) { super(channel); this.exchangeName = exchangeName; // do not copy content, assign reference @@ -114,10 +108,10 @@ public void handleDelivery(String consumerTag, Envelope envelope, BasicPropertie logger.debug("server received request {} on '{}'", props.getCorrelationId(), exchangeName); - if (AmqpServiceInvokerImpl.MSGTYPE_REQUEST.equals(props.getType())) { + if (MSGTYPE_REQUEST.equals(props.getType())) { String route = envelope.getRoutingKey(); - ServiceDispatcher serviceDispatcher = serviceDispatchers.get(route); - if (serviceDispatcher == null) { + ServiceExporter serviceExporter = serviceDispatchers.get(route); + if (serviceExporter == null) { throw new IOException(String.format("no service for route %s", route)); } Context context = new Context(); @@ -128,7 +122,7 @@ public void handleDelivery(String consumerTag, Envelope envelope, BasicPropertie context.put(prop.getKey(), String.valueOf(prop.getValue())); } - serviceDispatcher.call(route, context, body) + serviceExporter.call(route, context, body) .whenComplete((entry, t) -> { if (t != null) { respond(props, envelope, t); @@ -152,11 +146,13 @@ else if (entry == null) { private void respond(BasicProperties props, Envelope envelope, Throwable t) { try { + t = t.getCause() != null && (t instanceof CompletionException || t instanceof ExecutionException) ? t.getCause() : t; String replyTo = props.getReplyTo(); props = new BasicProperties.Builder() .correlationId(props.getCorrelationId()) .contentEncoding("UTF-8") - .type(AmqpServiceInvokerImpl.MSGTYPE_ERROR) + .type(t instanceof AuthenticationException ? MSGTYPE_ERROR_AUTH : + t instanceof NotFoundException ? MSGTYPE_ERROR_NOTFOUND : MSGTYPE_ERROR) .build(); if (replyTo != null) { @@ -186,7 +182,7 @@ private void respond(BasicProperties props, Envelope envelope, Context context, .contentType(context.get(Context.CONTENT_TYPE_KEY)) .contentEncoding("UTF-8") .headers(headers) - .type(AmqpServiceInvokerImpl.MSGTYPE_RESPONSE) + .type(MSGTYPE_RESPONSE) .build(); getChannel().basicPublish(exchangeName, replyTo, props, data); @@ -200,25 +196,9 @@ private void respond(BasicProperties props, Envelope envelope, Context context, } } - @Override - public AmqpServiceRouter start() { - return this; - } - @Nonnull @Override - public CompletableFuture close() { - CompletableFuture result = new CompletableFuture<>(); - CompletableFuture.runAsync(() -> { - try { - channel.close(); - connection.close(); - result.complete(null); - } - catch (IOException | TimeoutException ex) { - result.completeExceptionally(ex); - } - }); - return result; + public AmqpServiceExporter start() { + return this; } } diff --git a/amqp/src/main/java/io/teris/rpc/amqp/AmqpServiceInvoker.java b/kite-rpc-amqp/src/main/java/io/teris/kite/rpc/amqp/AmqpServiceInvoker.java similarity index 52% rename from amqp/src/main/java/io/teris/rpc/amqp/AmqpServiceInvoker.java rename to kite-rpc-amqp/src/main/java/io/teris/kite/rpc/amqp/AmqpServiceInvoker.java index 6a124bf..1b9298f 100644 --- a/amqp/src/main/java/io/teris/rpc/amqp/AmqpServiceInvoker.java +++ b/kite-rpc-amqp/src/main/java/io/teris/kite/rpc/amqp/AmqpServiceInvoker.java @@ -2,7 +2,7 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc.amqp; +package io.teris.kite.rpc.amqp; import java.io.IOException; import java.util.concurrent.CompletableFuture; @@ -11,26 +11,26 @@ import com.rabbitmq.client.ConnectionFactory; -import io.teris.rpc.ServiceInvoker; +import io.teris.kite.rpc.ServiceInvoker; +import io.teris.kite.rpc.amqp.AmqpServiceInvokerImpl.ConfiguratorImpl; public interface AmqpServiceInvoker extends ServiceInvoker { + @Nonnull AmqpServiceInvoker start(); @Nonnull CompletableFuture close(); @Nonnull - static Builder builder(@Nonnull ConnectionFactory connectionFactory) { - return new AmqpServiceInvokerImpl.BuilderImpl(connectionFactory); + static Configurator connectionFactory(@Nonnull ConnectionFactory connectionFactory) { + return new ConfiguratorImpl(connectionFactory); } - interface Builder { - - Builder exchangeName(String exchangeName); + interface Configurator { @Nonnull - AmqpServiceInvoker build() throws IOException, TimeoutException; + AmqpServiceInvoker requestExchange(String exchangeName) throws IOException, TimeoutException; } } diff --git a/amqp/src/main/java/io/teris/rpc/amqp/AmqpServiceInvokerImpl.java b/kite-rpc-amqp/src/main/java/io/teris/kite/rpc/amqp/AmqpServiceInvokerImpl.java similarity index 78% rename from amqp/src/main/java/io/teris/rpc/amqp/AmqpServiceInvokerImpl.java rename to kite-rpc-amqp/src/main/java/io/teris/kite/rpc/amqp/AmqpServiceInvokerImpl.java index 1ea6071..6952ccb 100644 --- a/amqp/src/main/java/io/teris/rpc/amqp/AmqpServiceInvokerImpl.java +++ b/kite-rpc-amqp/src/main/java/io/teris/kite/rpc/amqp/AmqpServiceInvokerImpl.java @@ -2,7 +2,7 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc.amqp; +package io.teris.kite.rpc.amqp; import java.io.IOException; import java.util.AbstractMap.SimpleEntry; @@ -29,35 +29,30 @@ import com.rabbitmq.client.DefaultConsumer; import com.rabbitmq.client.Envelope; -import io.teris.rpc.Context; -import io.teris.rpc.InvocationException; +import io.teris.kite.Context; +import io.teris.kite.rpc.AuthenticationException; +import io.teris.kite.rpc.NotFoundException; +import io.teris.kite.rpc.TechnicalException; -class AmqpServiceInvokerImpl implements AmqpServiceInvoker { +class AmqpServiceInvokerImpl extends AmqpServiceBase implements AmqpServiceInvoker { private static final Logger log = LoggerFactory.getLogger(AmqpServiceInvoker.class); - static final String MSGTYPE_REQUEST = "request"; - - static final String MSGTYPE_RESPONSE = "response"; - - static final String MSGTYPE_ERROR = "error"; - private final String exchangeName; - private final Connection connection; - - private final Channel channel; - private final Map>>> requestStore = new ConcurrentHashMap<>(); private final String clientId; AmqpServiceInvokerImpl(ConnectionFactory connectionFactory, String exchangeName) throws IOException, TimeoutException { + this(connectionFactory.newConnection(), exchangeName); + } + + AmqpServiceInvokerImpl(Connection connection, String exchangeName) throws IOException { + super(connection, connection.createChannel()); this.exchangeName = exchangeName; - connection = connectionFactory.newConnection(); - channel = connection.createChannel(); channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC); @@ -69,25 +64,17 @@ class AmqpServiceInvokerImpl implements AmqpServiceInvoker { channel.basicConsume(responseQueueName, true, clientId, new ResponseReceiver(channel, clientId, requestStore)); } - static class BuilderImpl implements AmqpServiceInvoker.Builder { + static class ConfiguratorImpl implements Configurator { private final ConnectionFactory connectionFactory; - private String exchangeName = "RPC"; - - BuilderImpl(ConnectionFactory connectionFactory) { + ConfiguratorImpl(ConnectionFactory connectionFactory) { this.connectionFactory = connectionFactory; } - @Override - public Builder exchangeName(String exchangeName) { - this.exchangeName = exchangeName; - return this; - } - @Nonnull @Override - public AmqpServiceInvoker build() throws IOException, TimeoutException { + public AmqpServiceInvoker requestExchange(String exchangeName) throws IOException, TimeoutException { return new AmqpServiceInvokerImpl(connectionFactory, exchangeName); } } @@ -159,10 +146,16 @@ public void handleDelivery(String consumerTag, Envelope envelope, BasicPropertie promise.complete(new SimpleEntry<>(context, body)); } else if (MSGTYPE_ERROR.equals(props.getType())) { - promise.completeExceptionally(new InvocationException(new String(body))); + promise.completeExceptionally(new TechnicalException(new String(body))); + } + else if (MSGTYPE_ERROR_AUTH.equals(props.getType())) { + promise.completeExceptionally(new AuthenticationException(new String(body))); + } + else if (MSGTYPE_ERROR_NOTFOUND.equals(props.getType())) { + promise.completeExceptionally(new NotFoundException(new String(body))); } else { - promise.completeExceptionally(new InvocationException("Unsupported message type: " + new String(body))); + promise.completeExceptionally(new TechnicalException("Unsupported message type: " + new String(body))); } } catch (IOException ex) { @@ -171,25 +164,9 @@ else if (MSGTYPE_ERROR.equals(props.getType())) { } } + @Nonnull @Override public AmqpServiceInvoker start() { return this; } - - @Nonnull - @Override - public CompletableFuture close() { - CompletableFuture result = new CompletableFuture<>(); - CompletableFuture.runAsync(() -> { - try { - channel.close(); - connection.close(); - result.complete(null); - } - catch (IOException | TimeoutException ex) { - result.completeExceptionally(ex); - } - }); - return result; - } } diff --git a/kite-rpc-jms/build.gradle b/kite-rpc-jms/build.gradle new file mode 100644 index 0000000..4306a45 --- /dev/null +++ b/kite-rpc-jms/build.gradle @@ -0,0 +1,17 @@ +/* + * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved + */ + +plugins.apply(JavaPlugin) + +dependencies { + compileOnly(findbugsModule) + compile(slf4jModule) + compile(project(":kite")) + compile(project(":kite-rpc")) + compile(geronimoJmsModule) + + // testCompile(junitModule) + // testCompile(mockitoModule) + // testRuntime(logbackModule) +} diff --git a/kite-rpc-jms/src/main/java/io/teris/kite/rpc/jms/JmsServiceBase.java b/kite-rpc-jms/src/main/java/io/teris/kite/rpc/jms/JmsServiceBase.java new file mode 100644 index 0000000..aa9ae4c --- /dev/null +++ b/kite-rpc-jms/src/main/java/io/teris/kite/rpc/jms/JmsServiceBase.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) Oleg Sklyar & teris.io, 2018. All rights reserved. + */ + +package io.teris.kite.rpc.jms; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.Nonnull; +import javax.jms.Connection; +import javax.jms.JMSException; +import javax.jms.Session; + + +abstract class JmsServiceBase { + + static final String JMS_ROUTE = "JMS_ROUTE"; + + static final String ACCESS_DENIED = "Access denied: "; + + final Connection connection; + + final Session requestSession; + + final Session responseSession; + + JmsServiceBase(Connection connection, Session requestSession, Session responseSession) { + this.connection = connection; + this.requestSession = requestSession; + this.responseSession = responseSession; + } + + @Nonnull + public CompletableFuture close() { + CompletableFuture result = new CompletableFuture<>(); + CompletableFuture.runAsync(() -> { + try { + requestSession.close(); + connection.close(); + responseSession.close(); + result.complete(null); + } + catch (JMSException ex) { + result.completeExceptionally(ex); + } + }); + return result; + } +} diff --git a/kite-rpc-jms/src/main/java/io/teris/kite/rpc/jms/JmsServiceExporter.java b/kite-rpc-jms/src/main/java/io/teris/kite/rpc/jms/JmsServiceExporter.java new file mode 100644 index 0000000..34f03ed --- /dev/null +++ b/kite-rpc-jms/src/main/java/io/teris/kite/rpc/jms/JmsServiceExporter.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved + */ + +package io.teris.kite.rpc.jms; + + +import java.util.concurrent.CompletableFuture; +import javax.annotation.Nonnull; +import javax.jms.ConnectionFactory; +import javax.jms.JMSException; + +import io.teris.kite.rpc.ServiceExporter; +import io.teris.kite.rpc.jms.JmsServiceExporterImpl.ConfiguratorImpl; + + +public interface JmsServiceExporter { + + @Nonnull + JmsServiceExporter export(@Nonnull ServiceExporter serviceExporter) throws JMSException; + + @Nonnull + JmsServiceExporter start() throws JMSException; + + @Nonnull + CompletableFuture close(); + + @Nonnull + static Configurator connectionFactory(@Nonnull ConnectionFactory connectionFactory) { + return new ConfiguratorImpl(connectionFactory); + } + + interface Configurator { + + @Nonnull + JmsServiceExporter requestTopic(String requestTopic) throws JMSException; + } +} diff --git a/jms/src/main/java/io/teris/rpc/jms/JmsServiceRouterImpl.java b/kite-rpc-jms/src/main/java/io/teris/kite/rpc/jms/JmsServiceExporterImpl.java similarity index 65% rename from jms/src/main/java/io/teris/rpc/jms/JmsServiceRouterImpl.java rename to kite-rpc-jms/src/main/java/io/teris/kite/rpc/jms/JmsServiceExporterImpl.java index 28ca99f..1f7b348 100644 --- a/jms/src/main/java/io/teris/rpc/jms/JmsServiceRouterImpl.java +++ b/kite-rpc-jms/src/main/java/io/teris/kite/rpc/jms/JmsServiceExporterImpl.java @@ -2,13 +2,14 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc.jms; +package io.teris.kite.rpc.jms; import java.util.Enumeration; import java.util.Map; import java.util.Map.Entry; -import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; import javax.annotation.Nonnull; import javax.jms.BytesMessage; @@ -27,67 +28,54 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.teris.rpc.Context; -import io.teris.rpc.ServiceDispatcher; +import io.teris.kite.Context; +import io.teris.kite.rpc.AuthenticationException; +import io.teris.kite.rpc.ServiceExporter; -class JmsServiceRouterImpl implements JmsServiceRouter, JmsServiceRouter.Router { +class JmsServiceExporterImpl extends JmsServiceBase implements JmsServiceExporter { - private static final Logger log = LoggerFactory.getLogger(JmsServiceRouter.class); - - static final String JMS_ROUTE = "JMS_ROUTE"; - - private final Connection connection; - - private final Session requestSession; + private static final Logger log = LoggerFactory.getLogger(JmsServiceExporter.class); private final Topic requestTopic; - private final Session responseSession; + private final Map serviceDispatchers = new ConcurrentHashMap<>(); - private final Map serviceDispatchers = new ConcurrentHashMap<>(); + JmsServiceExporterImpl(ConnectionFactory connectionFactory, String topicName) throws JMSException { + this(connectionFactory.createConnection(), topicName); + } - JmsServiceRouterImpl(ConnectionFactory connectionFactory, String topicName) throws JMSException { - connection = connectionFactory.createConnection(); + JmsServiceExporterImpl(Connection connection, String topicName) throws JMSException { + super(connection, connection.createSession(false, Session.CLIENT_ACKNOWLEDGE), + connection.createSession(false, Session.AUTO_ACKNOWLEDGE)); - requestSession = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE); requestTopic = requestSession.createTopic(topicName); - - responseSession = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); } - static class BuilderImpl implements Builder { + static class ConfiguratorImpl implements Configurator { private final ConnectionFactory connectionFactory; - private String topicName; - - BuilderImpl(ConnectionFactory connectionFactory) { + ConfiguratorImpl(ConnectionFactory connectionFactory) { this.connectionFactory = connectionFactory; } - @Override - public Builder topicName(String topicName) { - this.topicName = topicName; - return this; - } - @Nonnull @Override - public Router build() throws JMSException { - return new JmsServiceRouterImpl(connectionFactory, topicName); + public JmsServiceExporter requestTopic(String requestTopic) throws JMSException { + return new JmsServiceExporterImpl(connectionFactory, requestTopic); } } @Nonnull @Override - public Router route(@Nonnull ServiceDispatcher serviceDispatcher) throws JMSException { + public JmsServiceExporter export(@Nonnull ServiceExporter serviceExporter) throws JMSException { StringBuilder sb = new StringBuilder(JMS_ROUTE); sb.append(" IN ("); AtomicBoolean found = new AtomicBoolean(false); - for (String route :serviceDispatcher.dispatchRoutes()) { - serviceDispatchers.put(route, serviceDispatcher); + for (String route : serviceExporter.routes()) { + serviceDispatchers.put(route, serviceExporter); if (found.getAndSet(true)) { sb.append(","); @@ -98,11 +86,12 @@ public Router route(@Nonnull ServiceDispatcher serviceDispatcher) throws JMSExce } sb.append(")"); - String filter = found.get() ? sb.toString() : null; // FIXME should never be null as this will consume all, add UUID? - - requestSession - .createConsumer(requestTopic, filter) - .setMessageListener(new RequestConsumer(requestTopic.getTopicName(), serviceDispatchers, responseSession)); + // no consumer if no routes + if (found.get()) { + requestSession + .createConsumer(requestTopic, sb.toString()) + .setMessageListener(new RequestConsumer(requestTopic.getTopicName(), serviceDispatchers, responseSession)); + } return this; } @@ -111,11 +100,11 @@ static class RequestConsumer implements MessageListener { private final String topicName; - private final Map serviceDispatchers; + private final Map serviceDispatchers; private final Session responseSession; - RequestConsumer(String topicName, Map serviceDispatchers, Session responseSession) { + RequestConsumer(String topicName, Map serviceDispatchers, Session responseSession) { this.topicName = topicName; // do not copy content, assign reference this.serviceDispatchers = serviceDispatchers; @@ -134,8 +123,8 @@ public void onMessage(Message message) { if (message instanceof BytesMessage) { String route = message.getStringProperty(JMS_ROUTE); - ServiceDispatcher serviceDispatcher = serviceDispatchers.get(route); - if (serviceDispatcher == null) { + ServiceExporter serviceExporter = serviceDispatchers.get(route); + if (serviceExporter == null) { throw new JMSException(String.format("no service for route %s", route)); } Context context = new Context(); @@ -151,8 +140,9 @@ public void onMessage(Message message) { data = new byte[(int) byteMessage.getBodyLength()]; byteMessage.readBytes(data); } - serviceDispatcher.call(route, context, data) - .whenComplete((entry, t) -> { + serviceExporter + .call(route, context, data) + .handle((entry, t) -> { if (t != null) { respond(message, t); } @@ -162,6 +152,7 @@ else if (entry == null) { else { respond(message, entry.getKey(), entry.getValue()); } + return null; }); } else { @@ -176,7 +167,12 @@ else if (entry == null) { private void respond(Message message, Throwable t) { try { if (message.getJMSReplyTo() != null) { - TextMessage responseMessage = responseSession.createTextMessage(t.getMessage()); // FIXME message null + t = t.getCause() != null && (t instanceof CompletionException || t instanceof ExecutionException) ? t.getCause() : t; + String errorMessage = t.getMessage(); + if (t instanceof AuthenticationException) { + errorMessage = ACCESS_DENIED + t.getMessage(); + } + TextMessage responseMessage = responseSession.createTextMessage(errorMessage); // FIXME message null responseMessage.setJMSCorrelationID(message.getJMSCorrelationID()); responseSession.createProducer(message.getJMSReplyTo()).send(responseMessage); message.acknowledge(); @@ -214,27 +210,10 @@ private void respond(Message message, Context context, byte[] data) { } } + @Nonnull @Override - public JmsServiceRouter start() throws JMSException { + public JmsServiceExporter start() throws JMSException { connection.start(); return this; } - - @Nonnull - @Override - public CompletableFuture close() { - CompletableFuture result = new CompletableFuture<>(); - CompletableFuture.runAsync(() -> { - try { - requestSession.close(); - connection.close(); - responseSession.close(); - result.complete(null); - } - catch (JMSException ex) { - result.completeExceptionally(ex); - } - }); - return result; - } } diff --git a/jms/src/main/java/io/teris/rpc/jms/JmsServiceInvoker.java b/kite-rpc-jms/src/main/java/io/teris/kite/rpc/jms/JmsServiceInvoker.java similarity index 51% rename from jms/src/main/java/io/teris/rpc/jms/JmsServiceInvoker.java rename to kite-rpc-jms/src/main/java/io/teris/kite/rpc/jms/JmsServiceInvoker.java index 238c510..96b064f 100644 --- a/jms/src/main/java/io/teris/rpc/jms/JmsServiceInvoker.java +++ b/kite-rpc-jms/src/main/java/io/teris/kite/rpc/jms/JmsServiceInvoker.java @@ -2,33 +2,33 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc.jms; +package io.teris.kite.rpc.jms; import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; import javax.jms.ConnectionFactory; import javax.jms.JMSException; -import io.teris.rpc.ServiceInvoker; +import io.teris.kite.rpc.ServiceInvoker; +import io.teris.kite.rpc.jms.JmsServiceInvokerImpl.ConfiguratorImpl; public interface JmsServiceInvoker extends ServiceInvoker { + @Nonnull JmsServiceInvoker start() throws JMSException; @Nonnull CompletableFuture close(); @Nonnull - static Builder builder(@Nonnull ConnectionFactory connectionFactory) { - return new JmsServiceInvokerImpl.BuilderImpl(connectionFactory); + static Configurator connectionFactory(@Nonnull ConnectionFactory connectionFactory) { + return new ConfiguratorImpl(connectionFactory); } - interface Builder { - - Builder topicName(String topicName); + interface Configurator { @Nonnull - JmsServiceInvoker build() throws JMSException; + JmsServiceInvoker requestTopic(String requestTopic) throws JMSException; } } diff --git a/jms/src/main/java/io/teris/rpc/jms/JmsServiceInvokerImpl.java b/kite-rpc-jms/src/main/java/io/teris/kite/rpc/jms/JmsServiceInvokerImpl.java similarity index 76% rename from jms/src/main/java/io/teris/rpc/jms/JmsServiceInvokerImpl.java rename to kite-rpc-jms/src/main/java/io/teris/kite/rpc/jms/JmsServiceInvokerImpl.java index 25fc1bf..372f0ea 100644 --- a/jms/src/main/java/io/teris/rpc/jms/JmsServiceInvokerImpl.java +++ b/kite-rpc-jms/src/main/java/io/teris/kite/rpc/jms/JmsServiceInvokerImpl.java @@ -2,7 +2,7 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc.jms; +package io.teris.kite.rpc.jms; import java.util.AbstractMap.SimpleEntry; import java.util.Enumeration; @@ -29,63 +29,54 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.teris.rpc.Context; -import io.teris.rpc.InvocationException; +import io.teris.kite.Context; +import io.teris.kite.rpc.AuthenticationException; +import io.teris.kite.rpc.TechnicalException; -class JmsServiceInvokerImpl implements JmsServiceInvoker { +class JmsServiceInvokerImpl extends JmsServiceBase implements JmsServiceInvoker { private static final Logger log = LoggerFactory.getLogger(JmsServiceInvoker.class); - private final Connection connection; - - private final Session requestSession; - private final Topic requestTopic; private final MessageProducer requestProducer; - private final Session responseSession; - private final Queue responseQueue; private final Map>>> requestStore = new ConcurrentHashMap<>(); JmsServiceInvokerImpl(ConnectionFactory connectionFactory, String topicName) throws JMSException { - connection = connectionFactory.createConnection(); + this(connectionFactory.createConnection(), topicName); + + } + + JmsServiceInvokerImpl(Connection connection, String topicName) throws JMSException { + super(connection, connection.createSession(false, Session.CLIENT_ACKNOWLEDGE), + connection.createSession(false, Session.AUTO_ACKNOWLEDGE)); - requestSession = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE); requestTopic = requestSession.createTopic(topicName); requestProducer = requestSession.createProducer(requestTopic); requestProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT); - responseSession = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); responseQueue = responseSession.createTemporaryQueue(); responseSession .createConsumer(responseQueue) .setMessageListener(new ResponseReceiver(responseQueue.toString(), requestStore)); } - static class BuilderImpl implements JmsServiceInvoker.Builder { + static class ConfiguratorImpl implements Configurator { private final ConnectionFactory connectionFactory; - private String topicName = "RPC"; - - BuilderImpl(ConnectionFactory connectionFactory) { + ConfiguratorImpl(ConnectionFactory connectionFactory) { this.connectionFactory = connectionFactory; } - @Override - public Builder topicName(String topicName) { - this.topicName = topicName; - return this; - } - @Nonnull @Override - public JmsServiceInvoker build() throws JMSException { + public JmsServiceInvoker requestTopic(String topicName) throws JMSException { return new JmsServiceInvokerImpl(connectionFactory, topicName); } } @@ -106,7 +97,7 @@ public CompletableFuture> call(@Nonnull String route, @No if (outgoing != null) { message.writeBytes(outgoing); } - message.setStringProperty(JmsServiceRouterImpl.JMS_ROUTE, route); + message.setStringProperty(JMS_ROUTE, route); message.setStringProperty(Context.CONTENT_TYPE_KEY, context.get(Context.CONTENT_TYPE_KEY)); requestStore.put(correlationId, new SimpleEntry<>(context, promise)); requestProducer.send(requestTopic, message); @@ -152,7 +143,10 @@ public void onMessage(Message message) { Enumeration e = message.getPropertyNames(); while (e.hasMoreElements()) { String name = String.valueOf(e.nextElement()); - context.put(name, message.getStringProperty(name)); // FIXME not null + String property = message.getStringProperty(name); + if (property != null) { + context.put(name, property); + } } byte[] data = null; @@ -164,10 +158,17 @@ public void onMessage(Message message) { promise.complete(new SimpleEntry<>(context, data)); } else if (message instanceof TextMessage) { - promise.completeExceptionally(new InvocationException(((TextMessage) message).getText())); + String errorMessage = ((TextMessage) message).getText(); + errorMessage = errorMessage == null ? "unknown error" : errorMessage; + if (errorMessage.startsWith(ACCESS_DENIED)) { + promise.completeExceptionally(new AuthenticationException(errorMessage.replace(ACCESS_DENIED, ""))); + } + else { + promise.completeExceptionally(new TechnicalException(errorMessage)); + } } else { - promise.completeExceptionally(new InvocationException("Unsupported message type")); + promise.completeExceptionally(new TechnicalException("Unsupported message type")); } } catch (JMSException ex) { @@ -181,28 +182,10 @@ else if (message instanceof TextMessage) { } } + @Nonnull @Override public JmsServiceInvoker start() throws JMSException { connection.start(); return this; } - - @Nonnull - @Override - public CompletableFuture close() { - CompletableFuture result = new CompletableFuture<>(); - CompletableFuture.runAsync(() -> { - try { - requestSession.close(); - connection.close(); - responseSession.close(); - result.complete(null); - } - catch (JMSException ex) { - result.completeExceptionally(ex); - } - }); - return result; - } - } diff --git a/vertx/build.gradle b/kite-rpc-vertx/build.gradle similarity index 74% rename from vertx/build.gradle rename to kite-rpc-vertx/build.gradle index 315658a..9cacb7f 100644 --- a/vertx/build.gradle +++ b/kite-rpc-vertx/build.gradle @@ -8,11 +8,12 @@ dependencies { compileOnly(findbugsModule) compile(slf4jModule) compile(vertxWebModule) - compile(project(":rpc")) + compile(project(":kite")) + compile(project(":kite-rpc")) testCompile(junitModule) testCompile(mockitoModule) - testCompile(project(":serialization-json")) + testCompile(project(":kite-gson")) testRuntime(logbackModule) } diff --git a/vertx/src/main/java/io/teris/rpc/http/vertx/VertxDispatchingHandler.java b/kite-rpc-vertx/src/main/java/io/teris/kite/rpc/vertx/ExportedServiceHandler.java similarity index 73% rename from vertx/src/main/java/io/teris/rpc/http/vertx/VertxDispatchingHandler.java rename to kite-rpc-vertx/src/main/java/io/teris/kite/rpc/vertx/ExportedServiceHandler.java index 8340efe..3177666 100644 --- a/vertx/src/main/java/io/teris/rpc/http/vertx/VertxDispatchingHandler.java +++ b/kite-rpc-vertx/src/main/java/io/teris/kite/rpc/vertx/ExportedServiceHandler.java @@ -1,38 +1,40 @@ /* - * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved + * Copyright (c) Oleg Sklyar & teris.io, 2018. All rights reserved. */ -package io.teris.rpc.http.vertx; +package io.teris.kite.rpc.vertx; import java.util.Collections; import java.util.Map.Entry; import java.util.Set; +import java.util.concurrent.CompletionException; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.teris.rpc.Context; -import io.teris.rpc.ServiceDispatcher; +import io.teris.kite.Context; +import io.teris.kite.rpc.AuthenticationException; +import io.teris.kite.rpc.ServiceExporter; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpServerResponse; import io.vertx.ext.web.RoutingContext; -class VertxDispatchingHandler extends RoutingBase implements Handler { +class ExportedServiceHandler extends RoutingBase implements Handler { - private static final Logger log = LoggerFactory.getLogger(VertxDispatchingHandler.class); + private static final Logger log = LoggerFactory.getLogger(ExportedServiceHandler.class); - private final ServiceDispatcher serviceDispatcher; + private final ServiceExporter serviceExporter; - VertxDispatchingHandler(String uriPrefix, ServiceDispatcher serviceDispatcher) { + ExportedServiceHandler(String uriPrefix, ServiceExporter serviceExporter) { super(uriPrefix); - this.serviceDispatcher = serviceDispatcher; + this.serviceExporter = serviceExporter; } Set dispatchUris() { - return Collections.unmodifiableSet(serviceDispatcher.dispatchRoutes() + return Collections.unmodifiableSet(serviceExporter.routes() .stream() .map(this::routeToUri) .collect(Collectors.toSet())); @@ -55,16 +57,18 @@ public void handle(RoutingContext httpContext) { String corrId = incomingContext.get(Context.X_REQUEST_ID_KEY); log.trace("status=SERVER-EXECUTING, corrId={}, target={}", corrId, uri); - serviceDispatcher + serviceExporter .call(route, incomingContext, incomingData) .handleAsync((entry, t) -> { HttpServerResponse httpResponse = httpContext.response(); // it is expected that all exceptions are serialized as normal response (unless exactly that failed) if (t instanceof Exception || entry == null) { + t = t instanceof CompletionException && t.getCause() != null ? t.getCause() : t; String message = t != null ? t.getMessage() : "Server error: null response in the service future"; + int statusCode = t instanceof AuthenticationException ? 403 : 500; log.trace("status=SERVER-ERROR, corrId={}, target={}, message={}", corrId, uri, message); httpResponse - .setStatusCode(500) + .setStatusCode(statusCode) .setStatusMessage(message) .end(); return null; diff --git a/kite-rpc-vertx/src/main/java/io/teris/kite/rpc/vertx/HttpServiceExporter.java b/kite-rpc-vertx/src/main/java/io/teris/kite/rpc/vertx/HttpServiceExporter.java new file mode 100644 index 0000000..8df2b78 --- /dev/null +++ b/kite-rpc-vertx/src/main/java/io/teris/kite/rpc/vertx/HttpServiceExporter.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) Oleg Sklyar & teris.io, 2018. All rights reserved. + */ + +package io.teris.kite.rpc.vertx; + + +import javax.annotation.Nonnull; + +import io.teris.kite.rpc.ServiceExporter; +import io.teris.kite.rpc.vertx.HttpServiceExporterImpl.ServiceRouterImpl; +import io.vertx.core.Vertx; +import io.vertx.ext.web.Router; + + +/** + * Provides the mechanism to register HTTP endpoints and relay the incoming requests to + * matching service dispatchers, for which endpoints are registered. + */ +public interface HttpServiceExporter { + + /** + * Registers HTTP endpoints for every service method bound to the dispatcher using + * all the preconditions of the router. + */ + @Nonnull + HttpServiceExporter export(@Nonnull ServiceExporter serviceExporter); + + @Nonnull + Router router(); + + @Nonnull + static ServiceRouter router(@Nonnull Router router) { + return new ServiceRouterImpl(router); + } + + @Nonnull + static ServiceRouter router(@Nonnull Vertx vertx) { + return router(Router.router(vertx)); + } + + interface ServiceRouter { + + /** + * Adds a URI prefix to all routes generated automatically from @Service annotations. + * The prefix may contain slashes, e.g. `api`, `/api` and `/api/v2` are all accepted values. + */ + @Nonnull + ServiceRouter uriPrefix(@Nonnull String uriPrefix); + + /** + * Should URIs be case sensitive (the library provides routes in lower case) and + * match exactly (default: false). This leads to faster route matching due to lack of + * regex operations at the cost of flexibility. + */ + @Nonnull + ServiceRouter caseSensitive(); + + /** + * Registers HTTP endpoints for every service method bound to the dispatcher using + * all the preconditions of the router. + */ + @Nonnull + HttpServiceExporter export(@Nonnull ServiceExporter serviceExporter); + } +} diff --git a/vertx/src/main/java/io/teris/rpc/http/vertx/VertxServiceRouterImpl.java b/kite-rpc-vertx/src/main/java/io/teris/kite/rpc/vertx/HttpServiceExporterImpl.java similarity index 56% rename from vertx/src/main/java/io/teris/rpc/http/vertx/VertxServiceRouterImpl.java rename to kite-rpc-vertx/src/main/java/io/teris/kite/rpc/vertx/HttpServiceExporterImpl.java index b5e33b3..69213b9 100644 --- a/vertx/src/main/java/io/teris/rpc/http/vertx/VertxServiceRouterImpl.java +++ b/kite-rpc-vertx/src/main/java/io/teris/kite/rpc/vertx/HttpServiceExporterImpl.java @@ -1,14 +1,14 @@ /* - * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved + * Copyright (c) Oleg Sklyar & teris.io, 2018. All rights reserved. */ -package io.teris.rpc.http.vertx; +package io.teris.kite.rpc.vertx; import java.util.ArrayList; import java.util.List; import javax.annotation.Nonnull; -import io.teris.rpc.ServiceDispatcher; +import io.teris.kite.rpc.ServiceExporter; import io.vertx.core.Handler; import io.vertx.ext.web.Route; import io.vertx.ext.web.Router; @@ -16,7 +16,7 @@ import io.vertx.ext.web.handler.BodyHandler; -class VertxServiceRouterImpl implements VertxServiceRouter { +class HttpServiceExporterImpl implements HttpServiceExporter { private final Router router; @@ -26,7 +26,7 @@ class VertxServiceRouterImpl implements VertxServiceRouter { private final boolean caseSensitive; - VertxServiceRouterImpl(Router router, String uriPrefix, List> preconditioners, boolean caseSensitive) { + HttpServiceExporterImpl(Router router, String uriPrefix, List> preconditioners, boolean caseSensitive) { this.router = router; this.uriPrefix = uriPrefix; this.caseSensitive = caseSensitive; @@ -34,7 +34,7 @@ class VertxServiceRouterImpl implements VertxServiceRouter { this.preconditioners.addAll(preconditioners); } - static class BuilderImpl implements Builder { + static class ServiceRouterImpl implements ServiceRouter { private final Router router; @@ -45,49 +45,36 @@ static class BuilderImpl implements Builder { private boolean caseSensitive = false; - BuilderImpl(Router router) { + ServiceRouterImpl(Router router) { this.router = router; } @Nonnull @Override - public Builder uriPrefix(@Nonnull String uriPrefix) { + public ServiceRouter uriPrefix(@Nonnull String uriPrefix) { this.uriPrefix = uriPrefix.startsWith("/") ? uriPrefix : "/" + uriPrefix; return this; } @Nonnull @Override - public Builder preconditioner(@Nonnull Handler preconditioner) { - this.preconditioners.add(preconditioner); - return this; - } - - @Nonnull - @Override - public Builder preconditioners(@Nonnull List> preconditioners) { - this.preconditioners.addAll(preconditioners); - return this; - } - - @Nonnull - @Override - public Builder caseSensitive() { + public ServiceRouter caseSensitive() { this.caseSensitive = true; return this; } @Nonnull @Override - public VertxServiceRouter build() { - return new VertxServiceRouterImpl(router, uriPrefix, preconditioners, caseSensitive); + public HttpServiceExporter export(@Nonnull ServiceExporter serviceExporter) { + return new HttpServiceExporterImpl(router, uriPrefix, preconditioners, caseSensitive) + .export(serviceExporter); } } @Nonnull @Override - public VertxServiceRouter route(@Nonnull ServiceDispatcher serviceDispatcher) { - VertxDispatchingHandler dispatchingHandler = new VertxDispatchingHandler(uriPrefix, serviceDispatcher); + public HttpServiceExporter export(@Nonnull ServiceExporter serviceExporter) { + ExportedServiceHandler dispatchingHandler = new ExportedServiceHandler(uriPrefix, serviceExporter); for (String uri: dispatchingHandler.dispatchUris()) { Route route; @@ -104,4 +91,10 @@ public VertxServiceRouter route(@Nonnull ServiceDispatcher serviceDispatcher) { } return this; } + + @Nonnull + @Override + public Router router() { + return router; + } } diff --git a/kite-rpc-vertx/src/main/java/io/teris/kite/rpc/vertx/HttpServiceInvoker.java b/kite-rpc-vertx/src/main/java/io/teris/kite/rpc/vertx/HttpServiceInvoker.java new file mode 100644 index 0000000..a39f67d --- /dev/null +++ b/kite-rpc-vertx/src/main/java/io/teris/kite/rpc/vertx/HttpServiceInvoker.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) Oleg Sklyar & teris.io, 2018. All rights reserved. + */ + +package io.teris.kite.rpc.vertx; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.Nonnull; + +import io.vertx.core.http.HttpClient; + + +public interface HttpServiceInvoker extends io.teris.kite.rpc.ServiceInvoker { + + @Nonnull + CompletableFuture close(); + + @Nonnull + static Builder httpClient(@Nonnull HttpClient httpClient) { + return new HttpServiceInvokerImpl.BuilderImpl(httpClient); + } + + interface Builder { + + @Nonnull + Builder uriPrefix(@Nonnull String uriPrefix); + + @Nonnull + HttpServiceInvoker build(); + } +} diff --git a/vertx/src/main/java/io/teris/rpc/http/vertx/VertxServiceInvokerImpl.java b/kite-rpc-vertx/src/main/java/io/teris/kite/rpc/vertx/HttpServiceInvokerImpl.java similarity index 63% rename from vertx/src/main/java/io/teris/rpc/http/vertx/VertxServiceInvokerImpl.java rename to kite-rpc-vertx/src/main/java/io/teris/kite/rpc/vertx/HttpServiceInvokerImpl.java index f71cbfa..b58d124 100644 --- a/vertx/src/main/java/io/teris/rpc/http/vertx/VertxServiceInvokerImpl.java +++ b/kite-rpc-vertx/src/main/java/io/teris/kite/rpc/vertx/HttpServiceInvokerImpl.java @@ -1,8 +1,8 @@ /* - * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved + * Copyright (c) Oleg Sklyar & teris.io, 2018. All rights reserved. */ -package io.teris.rpc.http.vertx; +package io.teris.kite.rpc.vertx; import java.util.AbstractMap.SimpleEntry; import java.util.Map.Entry; @@ -13,25 +13,27 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.teris.rpc.Context; -import io.teris.rpc.InvocationException; +import io.teris.kite.Context; +import io.teris.kite.rpc.AuthenticationException; +import io.teris.kite.rpc.NotFoundException; +import io.teris.kite.rpc.TechnicalException; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpClient; import io.vertx.core.http.HttpClientRequest; -class VertxServiceInvokerImpl extends RoutingBase implements VertxServiceInvoker { +class HttpServiceInvokerImpl extends RoutingBase implements HttpServiceInvoker { - private static final Logger log = LoggerFactory.getLogger(VertxServiceInvoker.class); + private static final Logger log = LoggerFactory.getLogger(HttpServiceInvoker.class); private final HttpClient httpClient; - VertxServiceInvokerImpl(HttpClient httpClient, String uriPrefix) { + HttpServiceInvokerImpl(HttpClient httpClient, String uriPrefix) { super(uriPrefix); this.httpClient = httpClient; } - static class BuilderImpl implements VertxServiceInvoker.Builder { + static class BuilderImpl implements HttpServiceInvoker.Builder { private final HttpClient httpClient; @@ -50,8 +52,8 @@ public Builder uriPrefix(@Nonnull String uriPrefix) { @Nonnull @Override - public VertxServiceInvoker build() { - return new VertxServiceInvokerImpl(httpClient, uriPrefix); + public HttpServiceInvoker build() { + return new HttpServiceInvokerImpl(httpClient, uriPrefix); } } @@ -69,8 +71,20 @@ public CompletableFuture> call(@Nonnull String route, @No log.trace("status=CLIENT-SENDING, corrId={}, target={}", corrId, uri); CompletableFuture> promise = new CompletableFuture<>(); HttpClientRequest httpRequest = httpClient.post(uri, httpResponse -> { - if (httpResponse.statusCode() >= 400) { - promise.completeExceptionally(new InvocationException(httpResponse.statusMessage())); + if (httpResponse.statusCode() == 403) { + promise.completeExceptionally(new AuthenticationException(httpResponse.statusMessage())); + log.error("status=CLIENT-ERROR AUTH, corrId={}, target={}, httpcode=403, message={}", corrId, uri, + httpResponse.statusMessage()); + return; + } + else if (httpResponse.statusCode() == 404) { + promise.completeExceptionally(new NotFoundException(httpResponse.statusMessage())); + log.error("status=CLIENT-ERROR NOT-FOUND, corrId={}, target={}, httpcode=404, message={}", corrId, uri, + httpResponse.statusMessage()); + return; + } + else if (httpResponse.statusCode() >= 400) { + promise.completeExceptionally(new TechnicalException(httpResponse.statusMessage())); log.error("status=CLIENT-ERROR, corrId={}, target={}, httpcode={}, message={}", corrId, uri, Integer.valueOf(httpResponse.statusCode()), httpResponse.statusMessage()); return; @@ -89,7 +103,7 @@ public CompletableFuture> call(@Nonnull String route, @No httpRequest.putHeader(entry.getKey(), entry.getValue()); } httpRequest.exceptionHandler(t -> { - promise.completeExceptionally(new InvocationException("request exception", t)); + promise.completeExceptionally(new TechnicalException("request exception", t)); log.error(String.format("status=CLIENT-ERROR, corrId=%s, target=%s", corrId, uri), t); }); diff --git a/vertx/src/main/java/io/teris/rpc/http/vertx/RoutingBase.java b/kite-rpc-vertx/src/main/java/io/teris/kite/rpc/vertx/RoutingBase.java similarity index 87% rename from vertx/src/main/java/io/teris/rpc/http/vertx/RoutingBase.java rename to kite-rpc-vertx/src/main/java/io/teris/kite/rpc/vertx/RoutingBase.java index 921af87..7dca8cd 100644 --- a/vertx/src/main/java/io/teris/rpc/http/vertx/RoutingBase.java +++ b/kite-rpc-vertx/src/main/java/io/teris/kite/rpc/vertx/RoutingBase.java @@ -1,8 +1,8 @@ /* - * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved + * Copyright (c) Oleg Sklyar & teris.io, 2018. All rights reserved. */ -package io.teris.rpc.http.vertx; +package io.teris.kite.rpc.vertx; abstract class RoutingBase { diff --git a/vertx/src/test/java/io/teris/rpc/http/vertx/VertxServiceRouterTest.java b/kite-rpc-vertx/src/test/java/io/teris/kite/rpc/vertx/HttpServiceExporterTest.java similarity index 66% rename from vertx/src/test/java/io/teris/rpc/http/vertx/VertxServiceRouterTest.java rename to kite-rpc-vertx/src/test/java/io/teris/kite/rpc/vertx/HttpServiceExporterTest.java index 56b7461..f949bdf 100644 --- a/vertx/src/test/java/io/teris/rpc/http/vertx/VertxServiceRouterTest.java +++ b/kite-rpc-vertx/src/test/java/io/teris/kite/rpc/vertx/HttpServiceExporterTest.java @@ -1,8 +1,8 @@ /* - * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved + * Copyright (c) Oleg Sklyar & teris.io, 2018. All rights reserved. */ -package io.teris.rpc.http.vertx; +package io.teris.kite.rpc.vertx; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -14,16 +14,16 @@ import org.junit.Test; -import io.teris.rpc.Context; -import io.teris.rpc.Service; -import io.teris.rpc.ServiceDispatcher; -import io.teris.rpc.serialization.json.GsonSerializer; +import io.teris.kite.Context; +import io.teris.kite.Service; +import io.teris.kite.rpc.ServiceExporter; +import io.teris.kite.gson.JsonSerializer; import io.vertx.core.Vertx; import io.vertx.ext.web.Router; import io.vertx.ext.web.impl.RouteImpl; -public class VertxServiceRouterTest { +public class HttpServiceExporterTest { @Service("upstream") public interface PingService { @@ -40,15 +40,13 @@ public boolean ping(Context context) { @Test public void route_registersCaseInsensiticeEndpointsByDefault_success() throws Exception { - ServiceDispatcher dispatcher = ServiceDispatcher.builder() - .serializer(GsonSerializer.builder().build()) - .bind(PingService.class, new PingServiceImpl()) + ServiceExporter provider = ServiceExporter.serializer(JsonSerializer.builder().build()) + .export(PingService.class, new PingServiceImpl()) .build(); - Router router = Router.router(Vertx.vertx()); - - VertxServiceRouter.builder(router).build() - .route(dispatcher); + Router router = HttpServiceExporter.router(Vertx.vertx()) + .export(provider) + .router(); RouteImpl route = (RouteImpl) router.getRoutes().get(0); Field field = route.getClass().getDeclaredField("pattern"); @@ -63,15 +61,14 @@ public void route_registersCaseInsensiticeEndpointsByDefault_success() throws Ex @Test public void route_registersCaseSensiticeEndpointsWhenRequested_success() throws Exception { - ServiceDispatcher dispatcher = ServiceDispatcher.builder() - .serializer(GsonSerializer.builder().build()) - .bind(PingService.class, new PingServiceImpl()) + ServiceExporter dispatcher = ServiceExporter.serializer(JsonSerializer.builder().build()) + .export(PingService.class, new PingServiceImpl()) .build(); Router router = Router.router(Vertx.vertx()); - VertxServiceRouter.builder(router).caseSensitive().build() - .route(dispatcher); + HttpServiceExporter.router(router).caseSensitive() + .export(dispatcher); RouteImpl route = (RouteImpl) router.getRoutes().get(0); Field field = route.getClass().getDeclaredField("pattern"); diff --git a/vertx/src/test/java/io/teris/rpc/http/vertx/RoutingBaseTest.java b/kite-rpc-vertx/src/test/java/io/teris/kite/rpc/vertx/RoutingBaseTest.java similarity index 95% rename from vertx/src/test/java/io/teris/rpc/http/vertx/RoutingBaseTest.java rename to kite-rpc-vertx/src/test/java/io/teris/kite/rpc/vertx/RoutingBaseTest.java index ee76c80..e2089a8 100644 --- a/vertx/src/test/java/io/teris/rpc/http/vertx/RoutingBaseTest.java +++ b/kite-rpc-vertx/src/test/java/io/teris/kite/rpc/vertx/RoutingBaseTest.java @@ -1,8 +1,8 @@ /* - * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved + * Copyright (c) Oleg Sklyar & teris.io, 2018. All rights reserved. */ -package io.teris.rpc.http.vertx; +package io.teris.kite.rpc.vertx; import static org.junit.Assert.assertEquals; diff --git a/rpc/build.gradle b/kite-rpc/build.gradle similarity index 90% rename from rpc/build.gradle rename to kite-rpc/build.gradle index df03b68..8b24bfb 100644 --- a/rpc/build.gradle +++ b/kite-rpc/build.gradle @@ -6,6 +6,7 @@ plugins.apply(JavaPlugin) dependencies { compileOnly(findbugsModule) + compile(project(":kite")) testCompileOnly(findbugsModule) testCompile(junitModule) diff --git a/kite-rpc/src/main/java/io/teris/kite/rpc/AuthenticationException.java b/kite-rpc/src/main/java/io/teris/kite/rpc/AuthenticationException.java new file mode 100644 index 0000000..2806beb --- /dev/null +++ b/kite-rpc/src/main/java/io/teris/kite/rpc/AuthenticationException.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) Oleg Sklyar & teris.io, 2018. All rights reserved. + */ + +package io.teris.kite.rpc; + +import javax.annotation.Nonnull; + + +/** + * Similar to TechnicalException, but should be used in preprocessor to + * identify authentication problems. + */ +public class AuthenticationException extends RuntimeException { + + private static final long serialVersionUID = 1545013728297877453L; + + /** + * Constructs an InvocationException with the provided detail message. + * + * @param message the detailed exception message. + */ + public AuthenticationException(@Nonnull String message) { + super(message); + } + + /** + * Constructs an InvocationException with the given detail message and cause. + * + * @param message the detailed exception message. + * @param cause the exception cause. + */ + public AuthenticationException(@Nonnull String message, @Nonnull Throwable cause) { + super(String.format("%s [caused by %s%s]", message, cause.getClass().getSimpleName(), + cause.getMessage() != null ? ": " + cause.getMessage() : "")); + setStackTrace(cause.getStackTrace()); + } +} diff --git a/rpc/src/main/java/io/teris/rpc/BusinessException.java b/kite-rpc/src/main/java/io/teris/kite/rpc/BusinessException.java similarity index 88% rename from rpc/src/main/java/io/teris/rpc/BusinessException.java rename to kite-rpc/src/main/java/io/teris/kite/rpc/BusinessException.java index 0da303c..d31e937 100644 --- a/rpc/src/main/java/io/teris/rpc/BusinessException.java +++ b/kite-rpc/src/main/java/io/teris/kite/rpc/BusinessException.java @@ -1,8 +1,8 @@ /* - * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved + * Copyright (c) teris.io & Oleg Sklyar, 2018. All rights reserved */ -package io.teris.rpc; +package io.teris.kite.rpc; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -19,7 +19,7 @@ */ public class BusinessException extends RuntimeException { - private static final long serialVersionUID = 23489765234056L; + private static final long serialVersionUID = 351065990620300587L; /** * Constructs a BusinessException with the provided cause. diff --git a/rpc/src/main/java/io/teris/rpc/ExceptionDataHolder.java b/kite-rpc/src/main/java/io/teris/kite/rpc/ExceptionDataHolder.java similarity index 83% rename from rpc/src/main/java/io/teris/rpc/ExceptionDataHolder.java rename to kite-rpc/src/main/java/io/teris/kite/rpc/ExceptionDataHolder.java index f32013d..c57a83e 100644 --- a/rpc/src/main/java/io/teris/rpc/ExceptionDataHolder.java +++ b/kite-rpc/src/main/java/io/teris/kite/rpc/ExceptionDataHolder.java @@ -1,8 +1,8 @@ /* - * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved + * Copyright (c) teris.io & Oleg Sklyar, 2018. All rights reserved */ -package io.teris.rpc; +package io.teris.kite.rpc; import java.io.Serializable; import javax.annotation.Nonnull; @@ -16,7 +16,7 @@ public class ExceptionDataHolder implements Serializable { public String type = InvocationException.class.getSimpleName(); - public StackTraceElement[] stackTrace = new StackTraceElement[]{}; + StackTraceElement[] stackTrace = new StackTraceElement[]{}; // required for deserialization protected ExceptionDataHolder() {} @@ -34,7 +34,7 @@ protected ExceptionDataHolder() {} } @Nonnull - RuntimeException exception() { + public RuntimeException exception() { if (BusinessException.class.getSimpleName().equalsIgnoreCase(type)) { return new BusinessException(message, stackTrace); } diff --git a/rpc/src/main/java/io/teris/rpc/InvocationException.java b/kite-rpc/src/main/java/io/teris/kite/rpc/InvocationException.java similarity index 81% rename from rpc/src/main/java/io/teris/rpc/InvocationException.java rename to kite-rpc/src/main/java/io/teris/kite/rpc/InvocationException.java index d2c8036..775249e 100644 --- a/rpc/src/main/java/io/teris/rpc/InvocationException.java +++ b/kite-rpc/src/main/java/io/teris/kite/rpc/InvocationException.java @@ -1,8 +1,8 @@ /* - * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved + * Copyright (c) teris.io & Oleg Sklyar, 2018. All rights reserved */ -package io.teris.rpc; +package io.teris.kite.rpc; import javax.annotation.Nonnull; @@ -14,14 +14,14 @@ */ public class InvocationException extends RuntimeException { - static final long serialVersionUID = 4563467345675L; + private static final long serialVersionUID = -6793878671436341755L; /** * Constructs an InvocationException with the provided detail message. * * @param message the detailed exception message. */ - public InvocationException(@Nonnull String message) { + InvocationException(@Nonnull String message) { super(message); } @@ -31,7 +31,7 @@ public InvocationException(@Nonnull String message) { * @param message the detailed exception message. * @param cause the exception cause. */ - public InvocationException(@Nonnull String message, @Nonnull Throwable cause) { + InvocationException(@Nonnull String message, @Nonnull Throwable cause) { super(String.format("%s [caused by %s%s]", message, cause.getClass().getSimpleName(), cause.getMessage() != null ? ": " + cause.getMessage() : "")); setStackTrace(cause.getStackTrace()); diff --git a/kite-rpc/src/main/java/io/teris/kite/rpc/NotFoundException.java b/kite-rpc/src/main/java/io/teris/kite/rpc/NotFoundException.java new file mode 100644 index 0000000..7ad59c1 --- /dev/null +++ b/kite-rpc/src/main/java/io/teris/kite/rpc/NotFoundException.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) Oleg Sklyar & teris.io, 2018. All rights reserved. + */ + +package io.teris.kite.rpc; + +import javax.annotation.Nonnull; + + +/** + * Similar to TechnicalException, but can be thrown on the client side if + * the route is not reachable (provided the transport reports unreachable + * routes, e.g. HTTP does and JMS does not). + */ +public class NotFoundException extends RuntimeException { + + private static final long serialVersionUID = -7944545553830906074L; + + /** + * Constructs an InvocationException with the provided detail message. + * + * @param message the detailed exception message. + */ + public NotFoundException(@Nonnull String message) { + super(message); + } + + /** + * Constructs an InvocationException with the given detail message and cause. + * + * @param message the detailed exception message. + * @param cause the exception cause. + */ + public NotFoundException(@Nonnull String message, @Nonnull Throwable cause) { + super(String.format("%s [caused by %s%s]", message, cause.getClass().getSimpleName(), + cause.getMessage() != null ? ": " + cause.getMessage() : "")); + setStackTrace(cause.getStackTrace()); + } +} diff --git a/kite-rpc/src/main/java/io/teris/kite/rpc/ResponseFields.java b/kite-rpc/src/main/java/io/teris/kite/rpc/ResponseFields.java new file mode 100644 index 0000000..64c0c8b --- /dev/null +++ b/kite-rpc/src/main/java/io/teris/kite/rpc/ResponseFields.java @@ -0,0 +1,17 @@ +/* + * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved + */ + +package io.teris.kite.rpc; + +/** + * Defines field names for composing response objects. + */ +class ResponseFields { + + static final String PAYLOAD = "payload"; + + static final String EXCEPTION = "exception"; + + static final String ERROR_MESSAGE = "errorMessage"; +} diff --git a/rpc/src/main/java/io/teris/rpc/ServiceDispatcher.java b/kite-rpc/src/main/java/io/teris/kite/rpc/ServiceExporter.java similarity index 75% rename from rpc/src/main/java/io/teris/rpc/ServiceDispatcher.java rename to kite-rpc/src/main/java/io/teris/kite/rpc/ServiceExporter.java index e2c45f1..da9a62c 100644 --- a/rpc/src/main/java/io/teris/rpc/ServiceDispatcher.java +++ b/kite-rpc/src/main/java/io/teris/kite/rpc/ServiceExporter.java @@ -2,7 +2,7 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc; +package io.teris.kite.rpc; import java.util.Map; import java.util.Map.Entry; @@ -14,12 +14,18 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import io.teris.kite.Context; +import io.teris.kite.Deserializer; +import io.teris.kite.Serializer; + /** - * Provides a mechanism of dispatching incoming data onto concrete service and service method - * implementations bound to this dispatcher. + * Provides the server side mechanism of dispatching incoming data onto concrete + * service and service method implementations bound to this provider. The transport + * layer is expected to refer calls to an instance of RemoteProvider for actual + * service call execution. */ -public interface ServiceDispatcher { +public interface ServiceExporter { /** * The call method is called by the RPC invocation layer supplying route, context and @@ -33,24 +39,18 @@ public interface ServiceDispatcher { * service) */ @Nonnull - Set dispatchRoutes(); + Set routes(); /** * Creates a new builder for the ServiceDispatcher. */ @Nonnull - static Builder builder() { - return new ServiceDispatcherImpl.BuilderImpl(); + static Builder serializer(@Nonnull Serializer serializer) { + return new ServiceExporterImpl.BuilderImpl(serializer); } interface Builder { - /** - * Binds a serializer used to serialize service method arguments for the remote caller. - */ - @Nonnull - Builder serializer(@Nonnull Serializer serializer); - /** * Binds a content type specific deserializer used to deserialize data received in * response from the server based on the content type of the response. @@ -87,9 +87,9 @@ interface Builder { * Binds an implementation of a service and registers all dispatching routes. */ @Nonnull - Builder bind(@Nonnull Class serviceClass, @Nonnull S service) throws InvocationException; + Builder export(@Nonnull Class serviceClass, @Nonnull S service) throws InvocationException; @Nonnull - ServiceDispatcher build(); + ServiceExporter build(); } } diff --git a/rpc/src/main/java/io/teris/rpc/ServiceDispatcherImpl.java b/kite-rpc/src/main/java/io/teris/kite/rpc/ServiceExporterImpl.java similarity index 82% rename from rpc/src/main/java/io/teris/rpc/ServiceDispatcherImpl.java rename to kite-rpc/src/main/java/io/teris/kite/rpc/ServiceExporterImpl.java index 5ba476b..dc28411 100644 --- a/rpc/src/main/java/io/teris/rpc/ServiceDispatcherImpl.java +++ b/kite-rpc/src/main/java/io/teris/kite/rpc/ServiceExporterImpl.java @@ -1,7 +1,7 @@ /* * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc; +package io.teris.kite.rpc; import java.io.Serializable; import java.lang.reflect.InvocationTargetException; @@ -31,32 +31,32 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; -import io.teris.rpc.internal.ProxyMethodUtil; +import io.teris.kite.Context; +import io.teris.kite.Deserializer; +import io.teris.kite.Name; +import io.teris.kite.Serializer; -class ServiceDispatcherImpl implements ServiceDispatcher { +class ServiceExporterImpl implements ServiceExporter { private final Supplier uidGenerator; - static class BuilderImpl implements ServiceDispatcher.Builder { + static class BuilderImpl implements ServiceExporter.Builder { final Map> endpoints = new HashMap<>(); final List, CompletableFuture>> preprocessors = new ArrayList<>(); - private Serializer serializer = null; + private final Serializer serializer; private final Map deserializerMap = new HashMap<>(); private ExecutorService executors = null; - private Supplier uidGenerator = () -> UUID.randomUUID().toString();; + private Supplier uidGenerator = () -> UUID.randomUUID().toString(); - @Nonnull - @Override - public Builder serializer(@Nonnull Serializer serializer) { + BuilderImpl(Serializer serializer) { this.serializer = serializer; - return this; } @Nonnull @@ -95,10 +95,10 @@ public Builder preprocessor(BiFunction, Completab } @Nonnull - public Builder bind(@Nonnull Class serviceClass, @Nonnull S service) throws InvocationException { + public Builder export(@Nonnull Class serviceClass, @Nonnull S service) throws InvocationException { ServiceValidator.validate(serviceClass); for (Method method : serviceClass.getDeclaredMethods()) { - String route = ProxyMethodUtil.route(method); + String route = ServiceProxyUtil.route(method); this.endpoints.put(route, new SimpleEntry<>(service, method)); } return this; @@ -106,8 +106,8 @@ public Builder bind(@Nonnull Class serviceClass, @Nonnull S service) thro @Nonnull @Override - public ServiceDispatcher build() { - return new ServiceDispatcherImpl(endpoints, preprocessors, serializer, deserializerMap, executors, uidGenerator); + public ServiceExporter build() { + return new ServiceExporterImpl(endpoints, preprocessors, serializer, deserializerMap, executors, uidGenerator); } } @@ -121,7 +121,7 @@ public ServiceDispatcher build() { private final ExecutorService executors; - ServiceDispatcherImpl(Map> endpoints, List, CompletableFuture>> preprocessors, Serializer serializer, Map deserializerMap, ExecutorService executors, Supplier uidGenerator) { + ServiceExporterImpl(Map> endpoints, List, CompletableFuture>> preprocessors, Serializer serializer, Map deserializerMap, ExecutorService executors, Supplier uidGenerator) { this.endpoints.putAll(endpoints); this.preprocessors.addAll(preprocessors); this.serializer = Objects.requireNonNull(serializer, "Serializer is required"); @@ -132,7 +132,7 @@ public ServiceDispatcher build() { @Nonnull @Override - public Set dispatchRoutes() { + public Set routes() { return Collections.unmodifiableSet(new TreeSet<>(endpoints.keySet())); } @@ -144,9 +144,9 @@ public CompletableFuture> call(@Nonnull String route, @No context.put(Context.X_REQUEST_ID_KEY, uidGenerator.get()); } CompletableFuture promise = CompletableFuture.completedFuture(context); - Entry routeAndDate = new SimpleEntry<>(route, incomingData); + Entry routeAndData = new SimpleEntry<>(route, incomingData); for (BiFunction, CompletableFuture> preprocessor : preprocessors) { - promise = promise.thenCompose((c) -> preprocessor.apply(c, routeAndDate)); + promise = promise.thenCompose((c) -> preprocessor.apply(c, routeAndData)); } AtomicReference contextHolder = new AtomicReference<>(context); @@ -164,15 +164,16 @@ public CompletableFuture> call(@Nonnull String route, @No Object service = endpoint.getKey(); if (CompletableFuture.class.isAssignableFrom(method.getReturnType())) { try { - return ((CompletableFuture) method.invoke(service, args)) - .handle((obj, t) -> { - if (t != null) { - throw new BusinessException(t instanceof CompletionException ? t.getCause() : t); - } - return obj; - }); + @SuppressWarnings({"unchecked"}) + CompletableFuture invocationResult = (CompletableFuture) method.invoke(service, args); + return invocationResult.handle((obj, t) -> { + if (t != null) { + throw new BusinessException(t instanceof CompletionException ? t.getCause() : t); + } + return obj; + }); } - catch (InvocationTargetException | IllegalAccessException ex) { // IAE unlikely by design + catch (InvocationTargetException | IllegalAccessException | ClassCastException ex) { // IAE unlikely by design return CompletableFuture.supplyAsync(() -> { throw new BusinessException(ex.getCause() != null ? ex.getCause() : ex); }); @@ -239,7 +240,6 @@ CompletableFuture deserialize(@Nonnull Context context, @Nonnull Metho ConcurrentHashMap argMap = new ConcurrentHashMap<>(); return deserializer.>deserialize(data, Typedef.class.getGenericSuperclass()) .thenCompose((rawArgs) -> { - List> argPromises = new ArrayList<>(); for (int i = 1; i < method.getParameterCount(); i++) { Parameter param = method.getParameters()[i]; @@ -247,21 +247,20 @@ CompletableFuture deserialize(@Nonnull Context context, @Nonnull Metho if (rawArgs.containsKey(nameAnnot.value())) { byte[] paramData = (byte[]) rawArgs.remove(nameAnnot.value()); // requirement on deserializer: Serializable -> byte[] if (paramData != null) { - CompletableFuture argPromise = - deserializer.deserialize(paramData, param.getParameterizedType()) - .thenAccept((s) -> argMap.put(nameAnnot.value(), s)); + CompletableFuture argPromise = deserializer + .deserialize(paramData, param.getParameterizedType()) + .thenAccept((s) -> argMap.put(nameAnnot.value(), s)); argPromises.add(argPromise); } } } if (rawArgs.size() > 0) { String message = String.format("Too many arguments (%d instead of %s) to %s.%s", - Integer.valueOf(rawArgs.size() + method.getParameterCount() + 1), Integer - .valueOf(method.getParameterCount()), + Integer.valueOf(rawArgs.size() + method.getParameterCount() + 1), + Integer.valueOf(method.getParameterCount()), method.getDeclaringClass().getSimpleName(), method.getName()); throw new InvocationException(message); } - return CompletableFuture.allOf(argPromises.toArray(new CompletableFuture[]{})); }) .thenApply((vd) -> { diff --git a/rpc/src/main/java/io/teris/rpc/ServiceFactory.java b/kite-rpc/src/main/java/io/teris/kite/rpc/ServiceFactory.java similarity index 79% rename from rpc/src/main/java/io/teris/rpc/ServiceFactory.java rename to kite-rpc/src/main/java/io/teris/kite/rpc/ServiceFactory.java index e410ff2..8d3c577 100644 --- a/rpc/src/main/java/io/teris/rpc/ServiceFactory.java +++ b/kite-rpc/src/main/java/io/teris/kite/rpc/ServiceFactory.java @@ -2,13 +2,20 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc; +package io.teris.kite.rpc; import java.util.Map; import java.util.function.Supplier; import javax.annotation.Nonnull; +import io.teris.kite.Deserializer; +import io.teris.kite.Serializer; + +/** + * Defines a factory to construct service proxy instances that invoke service + * calls remotely via the supplied RemoteRequestor. + */ public interface ServiceFactory { /** @@ -23,27 +30,23 @@ public interface ServiceFactory { /** * @return a new instance of the client service factory builder. */ - static Builder builder() { - return new ServiceFactoryImpl.BuilderImpl(); + static PreBuilder invoker(@Nonnull ServiceInvoker serviceInvoker) { + return new ServiceFactoryImpl.BuilderImpl(serviceInvoker); } - /** - * Defines a step-builder interface for the client service factory. - */ - interface Builder { - - /** - * Binds a remote caller used to perform data transport between the client and the - * server. - */ - @Nonnull - Builder serviceInvoker(@Nonnull ServiceInvoker serviceInvoker); + interface PreBuilder { /** * Binds a serializer for serializing service method arguments before remote invocation. */ @Nonnull Builder serializer(@Nonnull Serializer serializer); + } + + /** + * Defines a step-builder interface for the client service factory. + */ + interface Builder { /** * Binds a deserializer for a specific content type for deserializing data recevied diff --git a/rpc/src/main/java/io/teris/rpc/ServiceFactoryImpl.java b/kite-rpc/src/main/java/io/teris/kite/rpc/ServiceFactoryImpl.java similarity index 85% rename from rpc/src/main/java/io/teris/rpc/ServiceFactoryImpl.java rename to kite-rpc/src/main/java/io/teris/kite/rpc/ServiceFactoryImpl.java index 9481165..94e06d9 100644 --- a/rpc/src/main/java/io/teris/rpc/ServiceFactoryImpl.java +++ b/kite-rpc/src/main/java/io/teris/kite/rpc/ServiceFactoryImpl.java @@ -2,7 +2,7 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc; +package io.teris.kite.rpc; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; @@ -12,15 +12,18 @@ import java.util.function.Supplier; import javax.annotation.Nonnull; +import io.teris.kite.Deserializer; +import io.teris.kite.Serializer; + /** * Implements the ServiceFactory: the client side proxy factory. */ -class ServiceFactoryImpl implements ServiceFactory { +final class ServiceFactoryImpl implements ServiceFactory { - static class BuilderImpl implements ServiceFactory.Builder { + static class BuilderImpl implements ServiceFactory.PreBuilder, ServiceFactory.Builder { - private ServiceInvoker serviceInvoker = null; + private final ServiceInvoker serviceInvoker; private Serializer serializer = null; @@ -28,11 +31,8 @@ static class BuilderImpl implements ServiceFactory.Builder { private final Map deserializerMap = new HashMap<>(); - @Nonnull - @Override - public Builder serviceInvoker(@Nonnull ServiceInvoker serviceInvoker) { + BuilderImpl(ServiceInvoker serviceInvoker) { this.serviceInvoker = serviceInvoker; - return this; } @Nonnull diff --git a/rpc/src/main/java/io/teris/rpc/ServiceInvoker.java b/kite-rpc/src/main/java/io/teris/kite/rpc/ServiceInvoker.java similarity index 58% rename from rpc/src/main/java/io/teris/rpc/ServiceInvoker.java rename to kite-rpc/src/main/java/io/teris/kite/rpc/ServiceInvoker.java index aa850c9..12e9899 100644 --- a/rpc/src/main/java/io/teris/rpc/ServiceInvoker.java +++ b/kite-rpc/src/main/java/io/teris/kite/rpc/ServiceInvoker.java @@ -2,14 +2,21 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc; +package io.teris.kite.rpc; import java.util.Map.Entry; import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import io.teris.kite.Context; + +/** + * The client side transport layer interface. An implementing instance + * is expected to be provided to the ServiceFactory builder, + * so that service proxies get a way of performing a remote call over the wire. + */ public interface ServiceInvoker { @Nonnull diff --git a/rpc/src/main/java/io/teris/rpc/ServiceProxyInvocationHandler.java b/kite-rpc/src/main/java/io/teris/kite/rpc/ServiceProxyInvocationHandler.java similarity index 93% rename from rpc/src/main/java/io/teris/rpc/ServiceProxyInvocationHandler.java rename to kite-rpc/src/main/java/io/teris/kite/rpc/ServiceProxyInvocationHandler.java index 88b25d1..0dfdbce 100644 --- a/rpc/src/main/java/io/teris/rpc/ServiceProxyInvocationHandler.java +++ b/kite-rpc/src/main/java/io/teris/kite/rpc/ServiceProxyInvocationHandler.java @@ -2,7 +2,7 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc; +package io.teris.kite.rpc; import java.io.Serializable; import java.lang.reflect.InvocationHandler; @@ -18,7 +18,9 @@ import java.util.concurrent.Future; import java.util.function.Supplier; -import io.teris.rpc.internal.ProxyMethodUtil; +import io.teris.kite.Context; +import io.teris.kite.Deserializer; +import io.teris.kite.Serializer; /** @@ -34,7 +36,7 @@ * In case the service method returns a future, the execution is wrapped into a * completable future, exceptional if required, and no exceptions will be thrown directly. */ -class ServiceProxyInvocationHandler implements InvocationHandler { +final class ServiceProxyInvocationHandler implements InvocationHandler { private final ServiceInvoker serviceInvoker; @@ -45,7 +47,7 @@ class ServiceProxyInvocationHandler implements InvocationHandler { private final Supplier uidGenerator; ServiceProxyInvocationHandler(ServiceInvoker serviceInvoker, Serializer serializer, Map deserializerMap, Supplier uidGenerator) { - this.serviceInvoker = Objects.requireNonNull(serviceInvoker, "Service invoker is required"); + this.serviceInvoker = Objects.requireNonNull(serviceInvoker, "RemoteRequestor is required"); this.serializer = Objects.requireNonNull(serializer, "Serializer is required"); this.uidGenerator = Objects.requireNonNull(uidGenerator, "Unique Id generator is required"); if (deserializerMap != null) { @@ -75,9 +77,9 @@ CompletableFuture callRemote(Method method, Object String routingKey; Entry> parsedArgs; try { - type = ProxyMethodUtil.returnType(method); - routingKey = ProxyMethodUtil.route(method); - parsedArgs = ProxyMethodUtil.arguments(method, args); + type = ServiceProxyUtil.returnType(method); + routingKey = ServiceProxyUtil.route(method); + parsedArgs = ServiceProxyUtil.arguments(method, args); } catch (RuntimeException ex) { result.completeExceptionally(ex); diff --git a/rpc/src/main/java/io/teris/rpc/internal/ProxyMethodUtil.java b/kite-rpc/src/main/java/io/teris/kite/rpc/ServiceProxyUtil.java similarity index 91% rename from rpc/src/main/java/io/teris/rpc/internal/ProxyMethodUtil.java rename to kite-rpc/src/main/java/io/teris/kite/rpc/ServiceProxyUtil.java index 14fa5e0..bb1b85b 100644 --- a/rpc/src/main/java/io/teris/rpc/internal/ProxyMethodUtil.java +++ b/kite-rpc/src/main/java/io/teris/kite/rpc/ServiceProxyUtil.java @@ -1,8 +1,8 @@ /* - * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved + * Copyright (c) Oleg Sklyar & teris.io, 2018. All rights reserved. */ -package io.teris.rpc.internal; +package io.teris.kite.rpc; import java.io.Serializable; import java.lang.reflect.Method; @@ -18,18 +18,17 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; -import io.teris.rpc.Context; -import io.teris.rpc.InvocationException; -import io.teris.rpc.Name; -import io.teris.rpc.Service; +import io.teris.kite.Context; +import io.teris.kite.Name; +import io.teris.kite.Service; -public final class ProxyMethodUtil { +final class ServiceProxyUtil { - private ProxyMethodUtil() {} + private ServiceProxyUtil() {} @Nonnull - public static String route(@Nonnull Method method) throws InvocationException { + static String route(@Nonnull Method method) throws InvocationException { String serviceRoute = serviceRoute(method); String methodName = methodName(method); String res = sanitizeRoute(serviceRoute + "." + methodName); @@ -92,7 +91,7 @@ private static String sanitizeRoute(String route) { } @Nonnull - public static Entry> arguments(@Nonnull Method method, @Nullable Object[] args) throws InvocationException { + static Entry> arguments(@Nonnull Method method, @Nullable Object[] args) throws InvocationException { validateArgumentTypes(method); if (args == null || args.length < 1 || args[0] == null) { String message = String.format("First argument to %s.%s must be a (non-null) instance of %s", @@ -122,7 +121,7 @@ public static Entry> arguments(@Non return new SimpleEntry<>(context, payload); } - public static void validateArgumentTypes(Method method) throws InvocationException { + static void validateArgumentTypes(Method method) throws InvocationException { if (method.getParameterCount() == 0 || !Context.class.isAssignableFrom(method.getParameters()[0].getType())) { String message = String.format("First parameters to %s.%s must be %s", method.getDeclaringClass().getSimpleName(), method.getName(), Context.class.getSimpleName()); @@ -162,7 +161,7 @@ else if (!Serializable.class.isAssignableFrom(clazz) && !clazz.isPrimitive() && } @Nonnull - public static Type returnType(@Nonnull Method method) throws InvocationException { + static Type returnType(@Nonnull Method method) throws InvocationException { Type returnType = method.getGenericReturnType(); if (returnType instanceof ParameterizedType && CompletableFuture.class.isAssignableFrom(method.getReturnType())) { returnType = ((ParameterizedType) returnType).getActualTypeArguments()[0]; diff --git a/rpc/src/main/java/io/teris/rpc/ServiceValidator.java b/kite-rpc/src/main/java/io/teris/kite/rpc/ServiceValidator.java similarity index 75% rename from rpc/src/main/java/io/teris/rpc/ServiceValidator.java rename to kite-rpc/src/main/java/io/teris/kite/rpc/ServiceValidator.java index 6bf6ccd..16dd562 100644 --- a/rpc/src/main/java/io/teris/rpc/ServiceValidator.java +++ b/kite-rpc/src/main/java/io/teris/kite/rpc/ServiceValidator.java @@ -2,21 +2,18 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc; +package io.teris.kite.rpc; import java.lang.reflect.Method; import java.util.HashSet; import java.util.Set; import javax.annotation.Nonnull; -import io.teris.rpc.InvocationException; -import io.teris.rpc.internal.ProxyMethodUtil; - /** - * Validates a service definition. + * Validates a service definition before invoking on the server side. */ -public final class ServiceValidator { +final class ServiceValidator { private ServiceValidator() {} @@ -26,7 +23,7 @@ private ServiceValidator() {} * @param serviceClass the interface class to validate. * @throws InvocationException on any invalid definition logic. */ - public static void validate(@Nonnull Class serviceClass) throws InvocationException { + static void validate(@Nonnull Class serviceClass) throws InvocationException { if (!serviceClass.isInterface()) { String message = String.format("Service definition %s must be an interface", serviceClass.getSimpleName()); throw new InvocationException(message); @@ -43,9 +40,9 @@ public static void validate(@Nonnull Class serviceClass) throws Invocatio serviceClass.getSimpleName(), method.getName()); throw new InvocationException(message); } - ProxyMethodUtil.route(method); - ProxyMethodUtil.validateArgumentTypes(method); - ProxyMethodUtil.returnType(method); + ServiceProxyUtil.route(method); + ServiceProxyUtil.validateArgumentTypes(method); + ServiceProxyUtil.returnType(method); foundMethodNames.add(method.getName()); } } diff --git a/kite-rpc/src/main/java/io/teris/kite/rpc/TechnicalException.java b/kite-rpc/src/main/java/io/teris/kite/rpc/TechnicalException.java new file mode 100644 index 0000000..1b3923d --- /dev/null +++ b/kite-rpc/src/main/java/io/teris/kite/rpc/TechnicalException.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) Oleg Sklyar & teris.io, 2018. All rights reserved. + */ + +package io.teris.kite.rpc; + +import javax.annotation.Nonnull; + + +/** + * Similar to InvocationException, but public and instead of being transported as is + * it will be converted to the transport specific error notification (thus + * resulting in e.g. 4xx HTTP codes with the HTTP transport etc). + */ +public class TechnicalException extends RuntimeException { + + private static final long serialVersionUID = -6793878671436341755L; + + /** + * Constructs an InvocationException with the provided detail message. + * + * @param message the detailed exception message. + */ + public TechnicalException(@Nonnull String message) { + super(message); + } + + /** + * Constructs an InvocationException with the given detail message and cause. + * + * @param message the detailed exception message. + * @param cause the exception cause. + */ + public TechnicalException(@Nonnull String message, @Nonnull Throwable cause) { + super(String.format("%s [caused by %s%s]", message, cause.getClass().getSimpleName(), + cause.getMessage() != null ? ": " + cause.getMessage() : "")); + setStackTrace(cause.getStackTrace()); + } +} diff --git a/rpc/src/test/java/io/teris/rpc/ServiceDispatcherImplDeserializeTest.java b/kite-rpc/src/test/java/io/teris/kite/rpc/ServiceExporterDeserializeTest.java similarity index 75% rename from rpc/src/test/java/io/teris/rpc/ServiceDispatcherImplDeserializeTest.java rename to kite-rpc/src/test/java/io/teris/kite/rpc/ServiceExporterDeserializeTest.java index 335846e..468709a 100644 --- a/rpc/src/test/java/io/teris/rpc/ServiceDispatcherImplDeserializeTest.java +++ b/kite-rpc/src/test/java/io/teris/kite/rpc/ServiceExporterDeserializeTest.java @@ -2,7 +2,7 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc; +package io.teris.kite.rpc; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; @@ -22,10 +22,13 @@ import org.junit.Test; import org.junit.rules.ExpectedException; -import io.teris.rpc.testfixture.TestSerializer; +import io.teris.kite.Context; +import io.teris.kite.Name; +import io.teris.kite.Service; +import io.teris.kite.rpc.testfixture.TestSerializer; -public class ServiceDispatcherImplDeserializeTest { +public class ServiceExporterDeserializeTest { @Rule public ExpectedException exception = ExpectedException.none(); @@ -51,7 +54,7 @@ public interface AService { @Test public void deserialize_argsWithGenerics_success() throws Exception { - ServiceDispatcherImpl underTest = new ServiceDispatcherImpl(Collections.emptyMap(), Collections.emptyList(), serializer, Collections.emptyMap(), null, () -> "1234"); + ServiceExporterImpl underTest = new ServiceExporterImpl(Collections.emptyMap(), Collections.emptyList(), serializer, Collections.emptyMap(), null, () -> "1234"); HashSet keys = new HashSet<>(Arrays.asList("Ab", "Bc")); HashMap data = new HashMap<>(); @@ -70,7 +73,7 @@ public void deserialize_argsWithGenerics_success() throws Exception { @Test public void deserialize_emptyData_success_nulls() throws Exception { - ServiceDispatcherImpl underTest = new ServiceDispatcherImpl(Collections.emptyMap(), Collections.emptyList(), serializer, Collections.emptyMap(), null, () -> "1234"); + ServiceExporterImpl underTest = new ServiceExporterImpl(Collections.emptyMap(), Collections.emptyList(), serializer, Collections.emptyMap(), null, () -> "1234"); Object[] actual = underTest.deserialize(context, method, new byte[]{}).get(); assertEquals(3, actual.length); @@ -81,7 +84,7 @@ public void deserialize_emptyData_success_nulls() throws Exception { @Test public void deserialize_nullData_success_nulls() throws Exception { - ServiceDispatcherImpl underTest = new ServiceDispatcherImpl(Collections.emptyMap(), Collections.emptyList(), serializer, Collections.emptyMap(), null, () -> "1234"); + ServiceExporterImpl underTest = new ServiceExporterImpl(Collections.emptyMap(), Collections.emptyList(), serializer, Collections.emptyMap(), null, () -> "1234"); Object[] actual = underTest.deserialize(context, method, null).get(); assertEquals(3, actual.length); @@ -92,7 +95,7 @@ public void deserialize_nullData_success_nulls() throws Exception { @Test public void deserialize_noParams_emptyData_success_contextOnly() throws Exception { - ServiceDispatcherImpl underTest = new ServiceDispatcherImpl(Collections.emptyMap(), Collections.emptyList(), serializer, Collections.emptyMap(), null, () -> "1234"); + ServiceExporterImpl underTest = new ServiceExporterImpl(Collections.emptyMap(), Collections.emptyList(), serializer, Collections.emptyMap(), null, () -> "1234"); Method emptyMethod = AService.class.getMethod("empty", Context.class); @@ -103,7 +106,7 @@ public void deserialize_noParams_emptyData_success_contextOnly() throws Exceptio @Test public void deserialize_missingArgs_success_null() throws Exception { - ServiceDispatcherImpl underTest = new ServiceDispatcherImpl(Collections.emptyMap(), Collections.emptyList(), serializer, Collections.emptyMap(), null, () -> "1234"); + ServiceExporterImpl underTest = new ServiceExporterImpl(Collections.emptyMap(), Collections.emptyList(), serializer, Collections.emptyMap(), null, () -> "1234"); HashMap data = new HashMap<>(); data.put("Cd", Integer.valueOf(25)); @@ -120,7 +123,7 @@ public void deserialize_missingArgs_success_null() throws Exception { @Test public void deserialize_extraArgs_throws() throws Exception { - ServiceDispatcherImpl underTest = new ServiceDispatcherImpl(Collections.emptyMap(), Collections.emptyList(), serializer, Collections.emptyMap(), null, () -> "1234"); + ServiceExporterImpl underTest = new ServiceExporterImpl(Collections.emptyMap(), Collections.emptyList(), serializer, Collections.emptyMap(), null, () -> "1234"); HashSet keys = new HashSet<>(Arrays.asList("Ab", "Bc")); LinkedHashMap args = new LinkedHashMap<>(); diff --git a/rpc/src/test/java/io/teris/rpc/ServiceDispatcherTest.java b/kite-rpc/src/test/java/io/teris/kite/rpc/ServiceExporterTest.java similarity index 83% rename from rpc/src/test/java/io/teris/rpc/ServiceDispatcherTest.java rename to kite-rpc/src/test/java/io/teris/kite/rpc/ServiceExporterTest.java index fa0aafc..9ddacdd 100644 --- a/rpc/src/test/java/io/teris/rpc/ServiceDispatcherTest.java +++ b/kite-rpc/src/test/java/io/teris/kite/rpc/ServiceExporterTest.java @@ -2,7 +2,7 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc; +package io.teris.kite.rpc; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; @@ -32,10 +32,15 @@ import org.junit.Test; import org.junit.rules.ExpectedException; -import io.teris.rpc.testfixture.TestSerializer; +import io.teris.kite.Context; +import io.teris.kite.Deserializer; +import io.teris.kite.Name; +import io.teris.kite.Serializer; +import io.teris.kite.Service; +import io.teris.kite.rpc.testfixture.TestSerializer; -public class ServiceDispatcherTest { +public class ServiceExporterTest { private static final Serializer serializer = new TestSerializer(); @@ -53,6 +58,7 @@ public class Response implements Serializable { @Service(value="some") public interface SomeService { + CompletableFuture async(Context context, @Name("value") String value); String sync(Context context, @Name("value") String value) throws IOException; @@ -71,10 +77,9 @@ public interface OtherService { public void builder_acceptsAll_success() { Map deserializerMap = new HashMap<>(); deserializerMap.put("some content", serializer.deserializer()); - ServiceDispatcher.builder() - .serializer(serializer) - .bind(SomeService.class, mock(SomeService.class)) - .bind(OtherService.class, mock(OtherService.class)) + ServiceExporter.serializer(serializer) + .export(SomeService.class, mock(SomeService.class)) + .export(OtherService.class, mock(OtherService.class)) .deserializer("text", serializer.deserializer()) .deserializers(deserializerMap) .executors(Executors.newCachedThreadPool()) @@ -86,17 +91,16 @@ public void builder_acceptsAll_success() { public void constructor_serializerNull_throws() { exception.expect(NullPointerException.class); exception.expectMessage("Serializer is required"); - new ServiceDispatcherImpl(Collections.emptyMap(), Collections.emptyList(), null, Collections.emptyMap(), null, () -> "1234"); + new ServiceExporterImpl(Collections.emptyMap(), Collections.emptyList(), null, Collections.emptyMap(), null, () -> "1234"); } @Test - public void dispatchRoutes() { - ServiceDispatcher dispatcher = ServiceDispatcher.builder() - .serializer(serializer) - .bind(SomeService.class, mock(SomeService.class)) - .bind(OtherService.class, mock(OtherService.class)) + public void routes_bindsOk() { + ServiceExporter dispatcher = ServiceExporter.serializer(serializer) + .export(SomeService.class, mock(SomeService.class)) + .export(OtherService.class, mock(OtherService.class)) .build(); - assertEquals(new TreeSet<>(Arrays.asList("other.sync", "some.async", "some.avoidmethod", "some.sync", "some.voidmethod")), dispatcher.dispatchRoutes()); + assertEquals(new TreeSet<>(Arrays.asList("other.sync", "some.async", "some.avoidmethod", "some.sync", "some.voidmethod")), dispatcher.routes()); } @@ -105,9 +109,8 @@ public void call_async_success() throws Exception { SomeService serviceImpl = mock(SomeService.class); doReturn(CompletableFuture.completedFuture("boo")).when(serviceImpl).async(any(), any()); - ServiceDispatcher dispatcher = ServiceDispatcher.builder() - .serializer(serializer) - .bind(SomeService.class, serviceImpl) + ServiceExporter dispatcher = ServiceExporter.serializer(serializer) + .export(SomeService.class, serviceImpl) .build(); CompletableFuture> promise = dispatcher.call("some.async", new Context(), "{\"value\":\"foo\"}".getBytes()); @@ -123,9 +126,8 @@ public void call_sync_success() throws Exception { SomeService serviceImpl = mock(SomeService.class); doReturn("boo").when(serviceImpl).sync(any(), any()); - ServiceDispatcher dispatcher = ServiceDispatcher.builder() - .serializer(serializer) - .bind(SomeService.class, serviceImpl) + ServiceExporter dispatcher = ServiceExporter.serializer(serializer) + .export(SomeService.class, serviceImpl) .build(); CompletableFuture> promise = dispatcher.call("some.sync", new Context(), "{\"value\":\"foo\"}".getBytes()); @@ -141,10 +143,9 @@ public void call_withNoRequestId_setsId() throws Exception { SomeService serviceImpl = mock(SomeService.class); doReturn("boo").when(serviceImpl).sync(any(), any()); - ServiceDispatcher dispatcher = ServiceDispatcher.builder() - .serializer(serializer) + ServiceExporter dispatcher = ServiceExporter.serializer(serializer) .uidGenerator(() -> "1234") - .bind(SomeService.class, serviceImpl) + .export(SomeService.class, serviceImpl) .build(); Context context = new Context(); @@ -170,11 +171,10 @@ public void call_preprocessorsUpdateContext_success() throws Exception { return CompletableFuture.completedFuture(context); }; - ServiceDispatcher dispatcher = ServiceDispatcher.builder() - .serializer(serializer) + ServiceExporter dispatcher = ServiceExporter.serializer(serializer) .preprocessor(prep1) .preprocessor(prep2) - .bind(SomeService.class, serviceImpl) + .export(SomeService.class, serviceImpl) .build(); CompletableFuture> promise = dispatcher.call("some.sync", new Context(), "{\"value\":\"foo\"}".getBytes()); @@ -201,11 +201,10 @@ public void call_preprocessorException_exceptionWrapped() throws Exception { throw new InvocationException("boom"); }; - ServiceDispatcher dispatcher = ServiceDispatcher.builder() - .serializer(serializer) + ServiceExporter dispatcher = ServiceExporter.serializer(serializer) .preprocessor(prep1) .preprocessor(prep2) - .bind(SomeService.class, serviceImpl) + .export(SomeService.class, serviceImpl) .build(); CompletableFuture> promise = dispatcher.call("some.sync", new Context(), "{\"value\":\"foo\"}".getBytes()); @@ -217,9 +216,8 @@ public void call_preprocessorException_exceptionWrapped() throws Exception { @Test public void call_void_success() throws Exception { - ServiceDispatcher dispatcher = ServiceDispatcher.builder() - .serializer(serializer) - .bind(SomeService.class, mock(SomeService.class)) + ServiceExporter dispatcher = ServiceExporter.serializer(serializer) + .export(SomeService.class, mock(SomeService.class)) .build(); CompletableFuture> promise = dispatcher.call("some.voidmethod", new Context(), "{\"value\":\"foo\"}".getBytes()); @@ -231,9 +229,8 @@ public void call_void_success() throws Exception { @Test public void call_Void_success() throws Exception { - ServiceDispatcher dispatcher = ServiceDispatcher.builder() - .serializer(serializer) - .bind(SomeService.class, mock(SomeService.class)) + ServiceExporter dispatcher = ServiceExporter.serializer(serializer) + .export(SomeService.class, mock(SomeService.class)) .build(); CompletableFuture> promise = dispatcher.call("some.avoidmethod", new Context(), "{\"value\":\"foo\"}".getBytes()); @@ -245,7 +242,7 @@ public void call_Void_success() throws Exception { @Test public void call_noRoute_exceptionWrappedIntoExDataHolder() throws Exception { - ServiceDispatcher dispatcher = new ServiceDispatcherImpl(Collections.emptyMap(), Collections.emptyList(), new TestSerializer(), Collections.emptyMap(), null, () -> "1234"); + ServiceExporter dispatcher = new ServiceExporterImpl(Collections.emptyMap(), Collections.emptyList(), new TestSerializer(), Collections.emptyMap(), null, () -> "1234"); CompletableFuture> promise = dispatcher.call("some.route", new Context(), new byte[]{}); Entry res = promise.get(5, TimeUnit.SECONDS); Response response = deserializer.deserialize(res.getValue(), Response.class).get(5, TimeUnit.SECONDS); @@ -260,9 +257,8 @@ public void call_async_businessLogicCompletesExceptionally_exceptionWrappedIntoE throw new RuntimeException("boom"); })).when(serviceImpl).async(any(), any()); - ServiceDispatcher dispatcher = ServiceDispatcher.builder() - .serializer(serializer) - .bind(SomeService.class, serviceImpl) + ServiceExporter dispatcher = ServiceExporter.serializer(serializer) + .export(SomeService.class, serviceImpl) .build(); CompletableFuture> promise = dispatcher.call("some.async", new Context(), "{\"value\":\"foo\"}".getBytes()); @@ -277,9 +273,8 @@ public void call_async_businessLogicRuntimeException_exceptionWrappedIntoExDataH SomeService serviceImpl = mock(SomeService.class); doThrow(new RuntimeException("boom")).when(serviceImpl).async(any(), any()); - ServiceDispatcher dispatcher = ServiceDispatcher.builder() - .serializer(serializer) - .bind(SomeService.class, serviceImpl) + ServiceExporter dispatcher = ServiceExporter.serializer(serializer) + .export(SomeService.class, serviceImpl) .build(); CompletableFuture> promise = dispatcher.call("some.async", new Context(), "{\"value\":\"foo\"}".getBytes()); @@ -294,9 +289,8 @@ public void call_sync_businessLogicRuntimeException_exceptionWrappedIntoExDataHo SomeService serviceImpl = mock(SomeService.class); doThrow(new RuntimeException("boom")).when(serviceImpl).sync(any(), any()); - ServiceDispatcher dispatcher = ServiceDispatcher.builder() - .serializer(serializer) - .bind(SomeService.class, serviceImpl) + ServiceExporter dispatcher = ServiceExporter.serializer(serializer) + .export(SomeService.class, serviceImpl) .build(); CompletableFuture> promise = dispatcher.call("some.sync", new Context(), "{\"value\":\"foo\"}".getBytes()); @@ -311,9 +305,8 @@ public void call_sync_businessLogicCheckedException_exceptionWrappedIntoExDataHo SomeService serviceImpl = mock(SomeService.class); doThrow(new IOException("boom")).when(serviceImpl).sync(any(), any()); - ServiceDispatcher dispatcher = ServiceDispatcher.builder() - .serializer(serializer) - .bind(SomeService.class, serviceImpl) + ServiceExporter dispatcher = ServiceExporter.serializer(serializer) + .export(SomeService.class, serviceImpl) .build(); CompletableFuture> promise = dispatcher.call("some.sync", new Context(), "{\"value\":\"foo\"}".getBytes()); @@ -332,10 +325,9 @@ public void call_sync_businessViaolatesServiceRestrictions_completesExceptionall doReturn(serializer.contentType()).when(mockedSerializer).contentType(); doThrow(new RuntimeException("boom")).when(mockedSerializer).serialize(any()); - ServiceDispatcher dispatcher = ServiceDispatcher.builder() - .serializer(mockedSerializer) + ServiceExporter dispatcher = ServiceExporter.serializer(mockedSerializer) .deserializer(serializer.contentType(), serializer.deserializer()) - .bind(SomeService.class, serviceImpl) + .export(SomeService.class, serviceImpl) .build(); CompletableFuture> promise = dispatcher.call("some.sync", new Context(), null); @@ -355,9 +347,8 @@ public void call_updates_contextWithSerializerContentType() throws Exception { doAnswer(invocation -> serializer.serialize(invocation.getArgument(0))).when(mockedSerializer).serialize(any()); doReturn(serializer.deserializer()).when(mockedSerializer).deserializer(); - ServiceDispatcher dispatcher = ServiceDispatcher.builder() - .serializer(mockedSerializer) - .bind(SomeService.class, serviceImpl) + ServiceExporter dispatcher = ServiceExporter.serializer(mockedSerializer) + .export(SomeService.class, serviceImpl) .build(); Context context = new Context(); diff --git a/rpc/src/test/java/io/teris/rpc/ServiceFactoryImplVoidReturnTest.java b/kite-rpc/src/test/java/io/teris/kite/rpc/ServiceFactoryImplVoidReturnTest.java similarity index 93% rename from rpc/src/test/java/io/teris/rpc/ServiceFactoryImplVoidReturnTest.java rename to kite-rpc/src/test/java/io/teris/kite/rpc/ServiceFactoryImplVoidReturnTest.java index 4b65352..aa7fadf 100644 --- a/rpc/src/test/java/io/teris/rpc/ServiceFactoryImplVoidReturnTest.java +++ b/kite-rpc/src/test/java/io/teris/kite/rpc/ServiceFactoryImplVoidReturnTest.java @@ -2,7 +2,7 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc; +package io.teris.kite.rpc; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; @@ -22,8 +22,12 @@ import org.junit.Test; import org.junit.rules.ExpectedException; -import io.teris.rpc.testfixture.TestDeserializer; -import io.teris.rpc.testfixture.TestSerializer; +import io.teris.kite.Context; +import io.teris.kite.Deserializer; +import io.teris.kite.Serializer; +import io.teris.kite.Service; +import io.teris.kite.rpc.testfixture.TestDeserializer; +import io.teris.kite.rpc.testfixture.TestSerializer; public class ServiceFactoryImplVoidReturnTest { diff --git a/rpc/src/test/java/io/teris/rpc/ServiceFactoryTest.java b/kite-rpc/src/test/java/io/teris/kite/rpc/ServiceFactoryTest.java similarity index 69% rename from rpc/src/test/java/io/teris/rpc/ServiceFactoryTest.java rename to kite-rpc/src/test/java/io/teris/kite/rpc/ServiceFactoryTest.java index c29e84c..5020f6d 100644 --- a/rpc/src/test/java/io/teris/rpc/ServiceFactoryTest.java +++ b/kite-rpc/src/test/java/io/teris/kite/rpc/ServiceFactoryTest.java @@ -2,7 +2,7 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc; +package io.teris.kite.rpc; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; @@ -21,7 +21,11 @@ import org.junit.Test; import org.junit.rules.ExpectedException; -import io.teris.rpc.testfixture.TestSerializer; +import io.teris.kite.Context; +import io.teris.kite.Deserializer; +import io.teris.kite.Serializer; +import io.teris.kite.Service; +import io.teris.kite.rpc.testfixture.TestSerializer; public class ServiceFactoryTest { @@ -29,38 +33,18 @@ public class ServiceFactoryTest { @Rule public ExpectedException exception = ExpectedException.none(); - @Test - public void builder_missingSerializer_throws() { - exception.expect(NullPointerException.class); - exception.expectMessage("Serializer is required"); - ServiceFactory.builder() - .serviceInvoker(mock(ServiceInvoker.class)) - .build(); - } - - @Test - public void builder_missingServiceInvoker_throws() { - exception.expect(NullPointerException.class); - exception.expectMessage("Service invoker is required"); - ServiceFactory.builder() - .serializer(mock(Serializer.class)) - .build(); - } - @Test public void builder_serializerAndServiceInvoker_success() { - ServiceFactory.builder() + ServiceFactory.invoker(mock(ServiceInvoker.class)) .serializer(mock(Serializer.class)) - .serviceInvoker(mock(ServiceInvoker.class)) .build(); } @Test public void builder_allOptions_success() { Map deserializerMap = new HashMap<>(); - ServiceFactory.builder() + ServiceFactory.invoker(mock(ServiceInvoker.class)) .serializer(mock(Serializer.class)) - .serviceInvoker(mock(ServiceInvoker.class)) .deserializer("text", mock(Deserializer.class)) .deserializers(deserializerMap) .uidGenerator(() -> "myUniqueId") @@ -75,9 +59,8 @@ public interface SomeService { @Test public void newInstance_goodServiceDef_success() { - ServiceFactory factory = ServiceFactory.builder() + ServiceFactory factory = ServiceFactory.invoker(mock(ServiceInvoker.class)) .serializer(new TestSerializer()) - .serviceInvoker(mock(ServiceInvoker.class)) .build(); factory.newInstance(SomeService.class); @@ -89,9 +72,8 @@ public void newInstance_invocation_successAndUidGenerator() { Entry payload = new SimpleEntry<>(context, "{\"payload\":\"foo\"}".getBytes()); ServiceInvoker invoker = mock(ServiceInvoker.class); doReturn(CompletableFuture.completedFuture(payload)).when(invoker).call(anyString(), any(), isNull()); - ServiceFactory factory = ServiceFactory.builder() + ServiceFactory factory = ServiceFactory.invoker(invoker) .serializer(new TestSerializer()) - .serviceInvoker(invoker) .uidGenerator(() -> "someId") .build(); diff --git a/rpc/src/test/java/io/teris/rpc/ServiceProxyInvocationHandlerTest.java b/kite-rpc/src/test/java/io/teris/kite/rpc/ServiceProxyInvocationHandlerTest.java similarity index 98% rename from rpc/src/test/java/io/teris/rpc/ServiceProxyInvocationHandlerTest.java rename to kite-rpc/src/test/java/io/teris/kite/rpc/ServiceProxyInvocationHandlerTest.java index 026f490..7c35b97 100644 --- a/rpc/src/test/java/io/teris/rpc/ServiceProxyInvocationHandlerTest.java +++ b/kite-rpc/src/test/java/io/teris/kite/rpc/ServiceProxyInvocationHandlerTest.java @@ -2,7 +2,7 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc; +package io.teris.kite.rpc; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotSame; @@ -37,7 +37,12 @@ import org.junit.Test; import org.junit.rules.ExpectedException; -import io.teris.rpc.testfixture.TestSerializer; +import io.teris.kite.Context; +import io.teris.kite.Deserializer; +import io.teris.kite.Name; +import io.teris.kite.Serializer; +import io.teris.kite.Service; +import io.teris.kite.rpc.testfixture.TestSerializer; public class ServiceProxyInvocationHandlerTest { @@ -52,7 +57,7 @@ public class ServiceProxyInvocationHandlerTest { @Test public void constructor_missingServiceInvoker_throws() { exception.expect(NullPointerException.class); - exception.expectMessage("Service invoker is required"); + exception.expectMessage("RemoteRequestor is required"); new ServiceProxyInvocationHandler(null, null, Collections.emptyMap(), null); } @@ -82,7 +87,7 @@ public void constructor_withDeserializerMap_success() { new ServiceProxyInvocationHandler(mock(ServiceInvoker.class), serializer, deserializerMap, uidGenerator); } - @Service(replace = "io.teris.rpc.ServiceProxyInvocationHandlerTest.Some") + @Service(replace = "io.teris.kite.rpc.ServiceProxyInvocationHandlerTest.Some") public interface SomeService { CompletableFuture asyncMethod(Context context); diff --git a/rpc/src/test/java/io/teris/rpc/internal/ProxyMethodUtilArgumentsTest.java b/kite-rpc/src/test/java/io/teris/kite/rpc/ServiceProxyUtilArgumentsTest.java similarity index 97% rename from rpc/src/test/java/io/teris/rpc/internal/ProxyMethodUtilArgumentsTest.java rename to kite-rpc/src/test/java/io/teris/kite/rpc/ServiceProxyUtilArgumentsTest.java index 0b5a0f0..a31e1f7 100644 --- a/rpc/src/test/java/io/teris/rpc/internal/ProxyMethodUtilArgumentsTest.java +++ b/kite-rpc/src/test/java/io/teris/kite/rpc/ServiceProxyUtilArgumentsTest.java @@ -1,8 +1,8 @@ /* - * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved + * Copyright (c) Oleg Sklyar & teris.io, 2018. All rights reserved. */ -package io.teris.rpc.internal; +package io.teris.kite.rpc; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; @@ -27,11 +27,11 @@ import org.junit.Test; import org.junit.rules.ExpectedException; -import io.teris.rpc.Context; -import io.teris.rpc.Name; +import io.teris.kite.Context; +import io.teris.kite.Name; -public class ProxyMethodUtilArgumentsTest { +public class ServiceProxyUtilArgumentsTest { private static final Context context = new Context(); @@ -279,7 +279,7 @@ static S get(Class serviceClass, CompletableFuture doInvoke(Method method) { CompletableFuture res = new CompletableFuture<>(); try { - ProxyMethodUtil.returnType(method); + ServiceProxyUtil.returnType(method); res.complete(response); } catch (RuntimeException ex) { diff --git a/rpc/src/test/java/io/teris/rpc/internal/ProxyMethodUtilRouteTest.java b/kite-rpc/src/test/java/io/teris/kite/rpc/ServiceProxyUtilRouteTest.java similarity index 80% rename from rpc/src/test/java/io/teris/rpc/internal/ProxyMethodUtilRouteTest.java rename to kite-rpc/src/test/java/io/teris/kite/rpc/ServiceProxyUtilRouteTest.java index 1f086f5..3e6a6c0 100644 --- a/rpc/src/test/java/io/teris/rpc/internal/ProxyMethodUtilRouteTest.java +++ b/kite-rpc/src/test/java/io/teris/kite/rpc/ServiceProxyUtilRouteTest.java @@ -1,8 +1,8 @@ /* - * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved + * Copyright (c) Oleg Sklyar & teris.io, 2018. All rights reserved. */ -package io.teris.rpc.internal; +package io.teris.kite.rpc; import static org.junit.Assert.assertEquals; @@ -16,16 +16,16 @@ import org.junit.Test; import org.junit.rules.ExpectedException; -import io.teris.rpc.Name; -import io.teris.rpc.Service; +import io.teris.kite.Name; +import io.teris.kite.Service; -public class ProxyMethodUtilRouteTest { +public class ServiceProxyUtilRouteTest { @Rule public ExpectedException exception = ExpectedException.none(); - @Service(replace = "io.teris.rpc.internal.ProxyMethodUtilRouteTest.A") + @Service(replace = "io.teris.kite.rpc.ServiceProxyUtilRouteTest.A") interface AService { @Name("") @@ -50,12 +50,12 @@ interface PathOvewriteService { void foo(); } - @Service(replace = "internal.ProxyMethodUtilRouteTest", value = "some.path") + @Service(replace = "ServiceProxyUtilRouteTest", value = "some.path") interface PartSubstituteService { void foo(); } - @Service(replace = "internal.ProxyMethodUtilRouteTest", value = "some.path") + @Service(replace = "ServiceProxyUtilRouteTest", value = "some.path") interface ThingService { @Name("foo") void nofoo(); @@ -84,7 +84,7 @@ public void route_nonEmptyPath_emptyMethod_success() throws Exception { CompletableFuture done = new CompletableFuture<>(); SingleMethodService s = Proxier.get(SingleMethodService.class, done); s.emptyMethodName(); - assertEquals("io.teris.rpc.internal.proxymethodutilroutetest.singlemethod", done.get()); + assertEquals("io.teris.kite.rpc.serviceproxyutilroutetest.singlemethod", done.get()); } @Test @@ -92,7 +92,7 @@ public void route_nested_success() throws Exception { CompletableFuture done = new CompletableFuture<>(); NestedService s = Proxier.get(NestedService.class, done); s.foo(); - assertEquals("io.teris.rpc.internal.proxymethodutilroutetest.nested.foo", done.get()); + assertEquals("io.teris.kite.rpc.serviceproxyutilroutetest.nested.foo", done.get()); } @Test @@ -108,7 +108,7 @@ public void route_partSubstitute_success() throws Exception { CompletableFuture done = new CompletableFuture<>(); PartSubstituteService s = Proxier.get(PartSubstituteService.class, done); s.foo(); - assertEquals("io.teris.rpc.some.path.partsubstitute.foo", done.get()); + assertEquals("io.teris.kite.rpc.some.path.partsubstitute.foo", done.get()); } @Test @@ -116,7 +116,7 @@ public void route_allSubstitute_success() throws Exception { CompletableFuture done = new CompletableFuture<>(); ThingService s = Proxier.get(ThingService.class, done); s.nofoo(); - assertEquals("io.teris.rpc.some.path.thing.foo", done.get()); + assertEquals("io.teris.kite.rpc.some.path.thing.foo", done.get()); } private static class Proxier implements InvocationHandler { @@ -136,7 +136,7 @@ static S get(Class serviceClass, CompletableFuture done) { @Override public Object invoke(Object proxy, Method method, Object[] args) { try { - done.complete(ProxyMethodUtil.route(method)); + done.complete(ServiceProxyUtil.route(method)); } catch (Exception ex) { done.completeExceptionally(ex); diff --git a/rpc/src/test/java/io/teris/rpc/ServiceValidatorTest.java b/kite-rpc/src/test/java/io/teris/kite/rpc/ServiceValidatorTest.java similarity index 95% rename from rpc/src/test/java/io/teris/rpc/ServiceValidatorTest.java rename to kite-rpc/src/test/java/io/teris/kite/rpc/ServiceValidatorTest.java index 526ac02..63d2d65 100644 --- a/rpc/src/test/java/io/teris/rpc/ServiceValidatorTest.java +++ b/kite-rpc/src/test/java/io/teris/kite/rpc/ServiceValidatorTest.java @@ -2,12 +2,16 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc; +package io.teris.kite.rpc; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import io.teris.kite.Context; +import io.teris.kite.Name; +import io.teris.kite.Service; + public class ServiceValidatorTest { diff --git a/rpc/src/test/java/io/teris/rpc/testfixture/TestDeserializer.java b/kite-rpc/src/test/java/io/teris/kite/rpc/testfixture/TestDeserializer.java similarity index 94% rename from rpc/src/test/java/io/teris/rpc/testfixture/TestDeserializer.java rename to kite-rpc/src/test/java/io/teris/kite/rpc/testfixture/TestDeserializer.java index e5bd139..06ca306 100644 --- a/rpc/src/test/java/io/teris/rpc/testfixture/TestDeserializer.java +++ b/kite-rpc/src/test/java/io/teris/kite/rpc/testfixture/TestDeserializer.java @@ -2,7 +2,7 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc.testfixture; +package io.teris.kite.rpc.testfixture; import java.io.Serializable; import java.lang.reflect.Type; @@ -15,7 +15,7 @@ import com.google.gson.JsonElement; import com.google.gson.JsonParseException; -import io.teris.rpc.Deserializer; +import io.teris.kite.Deserializer; public class TestDeserializer implements Deserializer { diff --git a/rpc/src/test/java/io/teris/rpc/testfixture/TestSerializer.java b/kite-rpc/src/test/java/io/teris/kite/rpc/testfixture/TestSerializer.java similarity index 88% rename from rpc/src/test/java/io/teris/rpc/testfixture/TestSerializer.java rename to kite-rpc/src/test/java/io/teris/kite/rpc/testfixture/TestSerializer.java index 9edf38a..ed0743d 100644 --- a/rpc/src/test/java/io/teris/rpc/testfixture/TestSerializer.java +++ b/kite-rpc/src/test/java/io/teris/kite/rpc/testfixture/TestSerializer.java @@ -2,7 +2,7 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc.testfixture; +package io.teris.kite.rpc.testfixture; import java.io.Serializable; import java.util.concurrent.CompletableFuture; @@ -10,8 +10,8 @@ import com.google.gson.Gson; -import io.teris.rpc.Deserializer; -import io.teris.rpc.Serializer; +import io.teris.kite.Deserializer; +import io.teris.kite.Serializer; public class TestSerializer implements Serializer { diff --git a/kite/build.gradle b/kite/build.gradle new file mode 100644 index 0000000..3e7095b --- /dev/null +++ b/kite/build.gradle @@ -0,0 +1,11 @@ +/* + * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved + */ + +plugins.apply(JavaPlugin) + +dependencies { + compileOnly(findbugsModule) + + testCompile(junitModule) +} diff --git a/rpc/src/main/java/io/teris/rpc/Context.java b/kite/src/main/java/io/teris/kite/Context.java similarity index 98% rename from rpc/src/main/java/io/teris/rpc/Context.java rename to kite/src/main/java/io/teris/kite/Context.java index a039ad0..a1470e6 100644 --- a/rpc/src/main/java/io/teris/rpc/Context.java +++ b/kite/src/main/java/io/teris/kite/Context.java @@ -2,7 +2,7 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc; +package io.teris.kite; import java.util.Collection; import java.util.Map; diff --git a/rpc/src/main/java/io/teris/rpc/Deserializer.java b/kite/src/main/java/io/teris/kite/Deserializer.java similarity index 90% rename from rpc/src/main/java/io/teris/rpc/Deserializer.java rename to kite/src/main/java/io/teris/kite/Deserializer.java index 4f68bcd..872c6a5 100644 --- a/rpc/src/main/java/io/teris/rpc/Deserializer.java +++ b/kite/src/main/java/io/teris/kite/Deserializer.java @@ -1,11 +1,7 @@ /* * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ - -/* - * Copyright Profidata AG. All rights reserved. - */ -package io.teris.rpc; +package io.teris.kite; import java.io.Serializable; import java.lang.reflect.Type; diff --git a/rpc/src/main/java/io/teris/rpc/Name.java b/kite/src/main/java/io/teris/kite/Name.java similarity index 97% rename from rpc/src/main/java/io/teris/rpc/Name.java rename to kite/src/main/java/io/teris/kite/Name.java index 3bb97c6..f6bbcf2 100644 --- a/rpc/src/main/java/io/teris/rpc/Name.java +++ b/kite/src/main/java/io/teris/kite/Name.java @@ -2,7 +2,7 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc; +package io.teris.kite; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -26,4 +26,4 @@ * The name under which to export the element. */ String value(); -} \ No newline at end of file +} diff --git a/rpc/src/main/java/io/teris/rpc/Serializer.java b/kite/src/main/java/io/teris/kite/Serializer.java similarity index 91% rename from rpc/src/main/java/io/teris/rpc/Serializer.java rename to kite/src/main/java/io/teris/kite/Serializer.java index 1d86c7c..b5119d2 100644 --- a/rpc/src/main/java/io/teris/rpc/Serializer.java +++ b/kite/src/main/java/io/teris/kite/Serializer.java @@ -1,16 +1,14 @@ /* * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ - -/* - * Copyright Profidata AG. All rights reserved. - */ -package io.teris.rpc; +package io.teris.kite; import java.io.Serializable; import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; +import io.teris.kite.Deserializer; + /** * Provides support to serialize POJOs into byte array of `contentType`. diff --git a/rpc/src/main/java/io/teris/rpc/Service.java b/kite/src/main/java/io/teris/kite/Service.java similarity index 97% rename from rpc/src/main/java/io/teris/rpc/Service.java rename to kite/src/main/java/io/teris/kite/Service.java index 53534b5..f69d890 100644 --- a/rpc/src/main/java/io/teris/rpc/Service.java +++ b/kite/src/main/java/io/teris/kite/Service.java @@ -2,7 +2,7 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc; +package io.teris.kite; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -33,4 +33,4 @@ * contain it. */ String replace() default ""; -} \ No newline at end of file +} diff --git a/rpc/src/test/java/io/teris/rpc/ContextTest.java b/kite/src/test/java/io/teris/kite/ContextTest.java similarity index 98% rename from rpc/src/test/java/io/teris/rpc/ContextTest.java rename to kite/src/test/java/io/teris/kite/ContextTest.java index 2e3b99f..8cbb4f4 100644 --- a/rpc/src/test/java/io/teris/rpc/ContextTest.java +++ b/kite/src/test/java/io/teris/kite/ContextTest.java @@ -2,7 +2,7 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -package io.teris.rpc; +package io.teris.kite; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; diff --git a/rpc/src/main/java/io/teris/rpc/ResponseFields.java b/rpc/src/main/java/io/teris/rpc/ResponseFields.java deleted file mode 100644 index e007946..0000000 --- a/rpc/src/main/java/io/teris/rpc/ResponseFields.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved - */ - -package io.teris.rpc; - -/** - * Defines field names for composing response objects. - */ -interface ResponseFields { - - String PAYLOAD = "payload"; - - String EXCEPTION = "exception"; - - String ERROR_MESSAGE = "errorMessage"; -} diff --git a/settings.gradle b/settings.gradle index 028f059..e337d25 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,10 +2,13 @@ * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved */ -include(":rpc") -include(":serialization-json") -include(":vertx") -include(":jms") -include(":amqp") +include(":kite") +include(":kite-gson") +include(":kite-fasterxml") + +include(":kite-rpc") +include(":kite-rpc-vertx") +include(":kite-rpc-jms") +include(":kite-rpc-amqp") include(":integration-tests") diff --git a/vertx/src/main/java/io/teris/rpc/http/vertx/VertxServiceInvoker.java b/vertx/src/main/java/io/teris/rpc/http/vertx/VertxServiceInvoker.java deleted file mode 100644 index da01aa4..0000000 --- a/vertx/src/main/java/io/teris/rpc/http/vertx/VertxServiceInvoker.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved - */ - -package io.teris.rpc.http.vertx; - -import java.util.concurrent.CompletableFuture; -import javax.annotation.Nonnull; - -import io.teris.rpc.ServiceInvoker; -import io.vertx.core.http.HttpClient; - - -public interface VertxServiceInvoker extends ServiceInvoker { - - @Nonnull - CompletableFuture close(); - - @Nonnull - static Builder builder(@Nonnull HttpClient httpClient) { - return new VertxServiceInvokerImpl.BuilderImpl(httpClient); - } - - interface Builder { - - @Nonnull - Builder uriPrefix(@Nonnull String uriPrefix); - - @Nonnull - VertxServiceInvoker build(); - } -} diff --git a/vertx/src/main/java/io/teris/rpc/http/vertx/VertxServiceRouter.java b/vertx/src/main/java/io/teris/rpc/http/vertx/VertxServiceRouter.java deleted file mode 100644 index d9dd1a5..0000000 --- a/vertx/src/main/java/io/teris/rpc/http/vertx/VertxServiceRouter.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (c) teris.io & Oleg Sklyar, 2017. All rights reserved - */ - -package io.teris.rpc.http.vertx; - - -import java.util.List; -import javax.annotation.Nonnull; - -import io.teris.rpc.ServiceDispatcher; -import io.vertx.core.Handler; -import io.vertx.ext.web.Router; -import io.vertx.ext.web.RoutingContext; - - -/** - * Provides the mechanism to register HTTP endpoints and relay the incoming requests to - * matching service dispatchers, for which endpoints are registered. - */ -public interface VertxServiceRouter { - - /** - * Registers HTTP endpoints for every service method bound to the dispatcher using - * all the preconditions of the router. - */ - @Nonnull - VertxServiceRouter route(@Nonnull ServiceDispatcher serviceDispatcher); - - @Nonnull - static Builder builder(@Nonnull Router router) { - return new VertxServiceRouterImpl.BuilderImpl(router); - } - - interface Builder { - - /** - * Adds a URI prefix to all routes generated automatically from @Service annotations. - * The prefix may contain slashes, e.g. `api`, `/api` and `/api/v2` are all accepted values. - */ - @Nonnull - Builder uriPrefix(@Nonnull String uriPrefix); - - /** - * Registers an HTTP routing context middleware to be executed for any matching endpoint - * before the actual bound service. Multiple middleware handlers are executed in order - * of registration. Handlers are expected to either complete the request with an error - * (e.g. authentication) or call `next` to pass over to the next handler, and, eventually - * to the service dispatcher. - */ - @Nonnull - Builder preconditioner(@Nonnull Handler preconditioner); - - @Nonnull - Builder preconditioners(@Nonnull List> preconditioners); - - /** - * Should URIs be case sensitive (the library provides routes in lower case) and - * match exactly (default: false). This leads to faster route matching due to lack of - * regex operations at the cost of flexibility. - */ - @Nonnull - Builder caseSensitive(); - - /** - * Builds a router that can take Service Dispatchers and register corresponding - * endpoints. - */ - @Nonnull - VertxServiceRouter build(); - } -}