Protocol
The protocol provides support for mapping a request to a method declared on a handler. It also features role-based access control through an AuthorizationHandler.
Javadoc can be found here.
The dynamic invocation when using annotations uses Java reflection and is pretty fast. Previous
versions of chili-core used reflectasm but as of J11 Java reflection is 10% faster, or 50% faster
if setAccessible(true)
is called.
If the programmatic API is used instead, reflection will not be used at all.
Registering a protocol
There are two ways of creating a protocol mapping, lets create the protocol instance first.
private Protocol<Request> protocol = new Protocol<>();
Then we need to set up some mappings and the default role for the API.
Annotations
Annotations can be used to simplify the mapping.
Setting the default Role
required for the api to USER
and mapping a method to a route called list
.
@Roles(USER)
public class MyHandler implements CoreHandler {
@Api
public void list(Request request) {
request.write(someList);
}
}
Programmatically
Programmatic registration is more dynamic and a tiny bit faster at runtime.
protocol.setRoles(Role.USER)
.use("ROUTE", this::list)
.use("ROUTE", this::listAsAdmin, Role.ADMIN);
Authentication
Authentication is required by default for all routes as Role.USER.
Strategy
The @Authenticator
annotation can be applied to a function with the signature
Function<Request, Future<RoleType>>
, which maps a request into a role. If the user is not
authorized, then the RoleType of PUBLIC
is used. It’s also possible to define custom role types.
@Authenticator
public Future<RoleType> authenticator(Request request) {
Future<RoleType> future = Future.future();
tokenFactory.verify(request.token()).setHandler(verify -> {
if (verify.succeeded()) {
future.complete(Role.USER);
} else {
future.complete(Role.PUBLIC);
}
});
return future;
}
For the authenticator to be registered in the protocol the handler needs to be annotated and the handler
passed to the Protocol
constructor or registered with the Protocol.annotated(handler)
method.
If annotations are not being used, the authenticator can be set with the following,
protocol.authenticator(this::authenticator);
Levels
When resolving the access level for a route, the max level of all the current principals roles is used by the default authorizer. This does not allow for granular permission schemes but it’s very simple.
Custom roles can be defined, they need to implement the RoleType
interface and be added to
the RoleMap
.
Example of creating a custom role
public SuperRootRole implements RoleType {
public String getName() {
return "super root role";
}
public int getLevel() {
return Integer.MAX_VALUE;
}
}
Registering the role in the RoleMap
,
RoleMap.put("super_root", new SuperRootRole());
In order to provide more advanced authorization models, the authorization handler can be replaced in the protocol.
This is done by calling
protocol.setAuthorizationHandler(customHandler);
The interface to implement, AuthorizationHandler.
Processing
Processing is the invocation of a mapped method, it’s initiated from the CoreHandler::handle
method.
public void handle(Request request) {
protocol.process(request);
}
This handles authorization and error handling internally within the protocol.
Documenting the API
Programmatically documenting
protocol
.model(DefaultModel.class)
.description("this is the API")
.use("wowza", this::wowza)
.description("does the wowza.")
.model(Wowza.class);
Alternatively, with annotations
@Description("this is the API")
@DataModel(Wowza.class)
public class MyHandler implements CoreHandler {
@Api
@Description("does the wowza.")
@DataModel(Wowza.class)
public void wowza(Request request) {
request.accept();
}
}
The annotation based setup requires that either the handler is passed to the protocol
constructor or that
protocol.annotated(handler)
is called.
When a protocol is documented the protocol will automatically add a route for retrieving a the specification in a custom
format generated by the Serializer.describe(class)
method.
Note that it is possible to mix the annotated and programmatic setup on the same protocol instance. It is also possible to register multiple classes onto the same protocol.
- It’s currently not possible to generate any OpenAPI definitions from an API. (wink wink new PR?)
Example
The current format looks like this, from the highscore sample
description: "A simple highscore API."
target: "api"
routes:
update:
description: "Updates the highscore entries with the given entry."
model:
score: "java.lang.Integer"
player: "java.lang.String"
roles:
- "PUBLIC"
list:
description: "Returns a list of the current highscore selection."
roles:
- "PUBLIC"
It resembles OpenAPI 3, which means that adding support for it should be easy. Consider implementing codegen from API descriptions.
The protocol description API can also be used programmatically
class Main {
public static void main(String[] args){
ProtocolDescription<TestPayload> protocol = new ProtocolDescription<>();
protocol.setDescription("a testing api")
.setTarget("master")
.setTemplate(Authentication.class);
Route<TestPayload> route = new Route<TestPayload>("info")
.setDescription("retrieves the info")
.setRoles(Role.ADMIN)
.setTemplate(TestPayload.class);
protocol.addRoute(route);
Serializer.yaml(protocol);
}
private class Authentication {
public String token;
}
private class TestPayload {
public String details;
}
}
outputs;
description: "a testing api"
target: "master"
model:
token: "java.lang.String"
routes:
info:
description: "retrieves the info"
model:
details: "java.lang.String"
roles:
- "ADMIN"
Deserializing a protocol description
ProtocolDescription<Request> description = Serializer.unyaml(description, ProtocolDescription.class);
The protocol description can also be retrieved directly from any Protocol.
Protocol<Request> protocol = new Protocol<>();
ProtocolDescription description = protocol.getDescription();
When using #getDescription the generation of API metadata is not cached as it is when the /api/document route is invoked during request processing. The Protocol instance internally keeps track of when the protocol definition changes and flushes the cache. This means that changes to the protocol during runtime will always be reflected in the documentation.