Skip to content

Provides the necessary data model for REST-specific communication between channels

License

Notifications You must be signed in to change notification settings

MikhailSterkhov/jrest2

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

62 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

drawing

Dependency

To use the data library in your project, you need to prescribe a dependency. Below is an example of how to use the dependency for different build systems:

Maven

Dependency block for Maven structure project:

<repositories>
    <repository>
        <id>jitpack.io</id>
        <url>https://jitpack.io</url>
    </repository>
</repositories>
<dependency>
    <groupId>com.github.MikhailSterkhov</groupId>
    <artifactId>jrest2</artifactId>
    <version>${jrest.version}</version>
</dependency>

Gradle

Dependency block for Gradle structure project:

repositories {
	mavenCentral()
	maven { url 'https://jitpack.io' }
}
compileOnly 'com.github.MikhailSterkhov:jrest2:${jrest.version}'

What is this ?

This library implements a complete HTTP/HTTPS protocol from scratch.
It currently supports the following versions of the HTTP protocol:

VERSION SUPPORTED
HTTP/1.0 âś… Supported
HTTP/1.1 âś… Supported
HTTP/2 â›” Not Supported
HTTP/3 â›” Not Supported

On top of a completely self-contained protocol implementation is built
a layered API structure with different configurations and ways of
initializing and applying data to the connection flow.

Also, this library implements the ability to apply and read SSL certificates
to both the client part of the connection (read/write)
and the server part of the connection


How to use?

Here are some examples of how to use the functionality of the top-level API to interact with the HTTP protocol

CLIENTS

Let's start with the client part of the connection.

First of all, it is necessary to determine what type of channel we will work with and what we need. For this purpose, the client factory is implemented com.jrest.http.client.HttpClients:

// variants of Sockets implementation:
HttpClients.createSocketClient(ExecutorService);
HttpClients.createSocketClient(ExecutorService, boolean keepAlive);
HttpClients.createSocketClient(ExecutorService, int connectTimeout);
HttpClients.createSocketClient(ExecutorService, int connectTimeout, boolean keepAlive);
HttpClients.createSocketClient();
HttpClients.createSocketClient(boolean keepAlive);
HttpClients.createSocketClient(int connectTimeout);
HttpClients.createSocketClient(int connectTimeout, boolean keepAlive);

// variants of HttpURLConnection implementation:
HttpClients.createClient(ExecutorService);
HttpClients.createClient(ExecutorService, int connectTimeout);
HttpClients.createClient(ExecutorService, int connectTimeout, int readTimeout);
HttpClients.createClient();

// variants of binary http-client wrappers implementation:
HttpClients.binary(HttpClient httpClient, Reader reader);
HttpClients.binary(HttpClient httpClient, InputStream inputStream);
HttpClients.binary(HttpClient httpClient, File file) throws IOException;
HttpClients.binary(HttpClient httpClient, Path path) throws IOException;

Suppose we decide to implement a Socket connection with the ability
to make requests asynchronously, and we set its connect-timeout = 3000ms,
and keep-alive = false to automatically close the socket after the request is executed.

Example:

HttpClient httpClient = HttpClients.createSocketClient(
        Executors.newCachedThreadPool(), connectTimeout, keepAlive);

Next, we can already call any of more than a hundred functions
to fulfill the request and get an instant response.

For example, send a GET request to the public web page https://catfact.ninja/fact,
from where we will get the result as JSON with a random fact about cats :D

Example:

httpClient.executeGet("https://catfact.ninja/fact")
        .ifPresent(response -> {
            
            HttpProtocol protocol = response.getProtocol(); // HTTP/1.1
            String statusLine = response.getHeaders().getFirst(null); // HTTP/1.1 200 OK

            ResponseCode responseCode = response.getCode();
            
            if (!responseCode.isSuccessful()) {
                throw new RuntimeException("Content not found - " + responseCode);
            }

            System.out.println(httpResponse.getContent().getText());
            // {"fact":"A cat usually has about 12 whiskers on each side of its face.","length":61}
        });

The client API also implements one cool thing, thanks to which
you can simplify the implementation of HTTP requests as much
as possible by writing just a few words in the code to do it!

BINARY FILES

Basic information you need to know when writing a binary:

The first lines are general Properties that can be applied in the queries themselves. The most important among them is the host = ... line. It is mandatory in application, and indicates the main address part of the URL that will be accessed.

Next after Properties are the functions. Their structure is described by the following signature:

<name>: <METHOD> /<URI> {
  ...
}

The content of the function is divided into several keywords
that can be used within the body of the function:

  • head: One of the headings of the query
  • attr: URI attributes that will be appended to the URL with a '?' (e.g. /employee?id=1, where 'id' is an attribute)
  • body: Request body
    • length: The size of the body to be sent under the guise of the 'Content-Length' header
    • type: The body type that will be sent under the 'Content-Type' header appearance
    • text: Header content as Hyper text

The values that come after the keyword are mostly
in the Properties format.

Example binary (/catfacts.restbin):

host = https://catfact.ninja/
randomCatFact = A cat usually has about 12 whiskers on each side of its face.
userAgent = JRest-Binary/1.1
contentType = application/json

getFact: GET /fact {
    head User-Agent = ${userAgent}
    head Accept = text/plain
    attr length = 50
}

createFact: POST /fact {
    head User-Agent = ${userAgent}
    body {
        type = ${contentType}
        text = {"fact": "${randomCatFact}", "length": 61}
    }
}

After successfully writing our binary, we can start executing it by first creating a BinaryHttpClient
via the factory: HttpClients.createBinaryClient(HttpClient, <path-to-binary>)

BinaryHttpClient has 2 additional methods that distinguish
it from other HTTP clients: executeBinary(name) and executeBinaryAsync(name).

Example (Java Client):

BinaryHttpClient httpClient = HttpClients.binary(
        HttpClients.createClient(),
        getClass().getResourceAsStream("/catfacts.restbin"));

httpClient.executeBinary("getFact")
        .ifPresent(httpResponse -> {

                HttpProtocol protocol = response.getProtocol(); // HTTP/1.1
                String statusLine = response.getHeaders().getFirst(null); // HTTP/1.1 200 OK

                ResponseCode responseCode = response.getCode();
            
                if (!responseCode.isSuccessful()) {
                    throw new RuntimeException("Content not found - " + responseCode);
                }

                System.out.println(httpResponse.getContent().getText());
                // {"fact":"A cat usually has about 12 whiskers on each side of its face.","length":61}
        });

And also for executing binary functions you can use input properties to
customize the request from the outside.

Here is an example.

Example (binary with inputs):

host = http://localhost:8080/

get_employee: GET /employee {
    attr id = ${input.employee_id}
}

post_employee: POST /employee {
    body {
        text = ${input.employee}
    }
}

Here we can notice the ${input.employee_id} property, we expect
to get it from the client.
Below I will give an example of applying it to an executable file.

Example (Java Client):

BinaryHttpClient httpClient = HttpClients.binary(
        HttpClients.createClient(),
        HttpClientBinaryUrlTest.class.getResourceAsStream("/employee.restbin"));

httpClient.executeBinary("get_employee",
                Attributes.newAttributes().with("employee_id", 567))
        .ifPresent(httpResponse -> {
            
            System.out.println(httpResponse.getContent().getText());
            // {"id":567,"firstName":"Piter","lastName":"Harrison","jobInfo":{"company":"Microsoft Corporation","website":"https://www.microsoft.com/","profession":"Developer C#","salary":3500}}
        });

SERVERS

To create a server and initialize it, things are a bit more complicated,
but only because it is a server, and it needs full business logic.

Let's start with the simplest creation of the server as an object,
form it from the parameters we need:

Example:

HttpServer httpServer = HttpServer.builder()
        .build();

Several components are required to properly initialize the server,
each of which affects a specific part of the software part:

PARAMETER TYPE USAGE EXAMPLE DESCRIPTION
InetSocketAddress .socketAddress(new InetSocketAddress(80)) Server bindings address and port.
ExecutorService .executorService(Executors.newCachedThreadPool()) Service to execute threads, if not specified, a cached thread pool is used. (CachedThreadPool is used by default if null is specified)
HttpProtocol .protocol(HttpProtocol.HTTP_1_0) HTTP protocol, by default HTTP/1.1.
SslContent .ssl(SslContent.builder()...) SSL settings for HTTPS, if null, HTTP is used.
HttpListener .notFoundListener(httpRequest -> ...) Listener to handle requests that have not found an appropriate handler. (404 Not Found)

Now based on this information let's try to implement a server
that supports HTTP/1.1 protocol without SSL certificates

Example;

HttpServer httpServer = HttpServer.builder()
        .socketAddress(new InetSocketAddress(8080))
        .build();

httpServer.bind();

We can now intercept requests that come to us by skipping or sending
back some kind of response. Request listeners can be either asynchronous
or synchronous.

Examples:

httpServer.registerListener(httpRequest -> {
    
    System.out.println(httpRequest);
    return HttpResponse.ok();
});
httpServer.registerAsyncListener("/employee", httpRequest -> 
        HttpResponse.ok(Content.fromEntity(
                Employee.builder()
                        .id(567))
                        .jobInfo(EmployeeJob.builder()
                                .company("Microsoft Corporation")
                                .website("https://www.microsoft.com/")
                                .profession("Developer C#")
                                .salary(3500)
                                .build())
                        .firstName("Piter")
                        .lastName("Harrison")
                        .build())));

Realizing perfectly well that handling each such request in the form of
registering them through listeners would not be entirely convenient,
especially in the case where there may be quite a few endpoints.

Therefore, the MVC module was implemented, providing a more flexible
and readable implementation of HTTP requests interception.

For the example, let's create an instance that will be a repository
of HTTP requests for our server and register it:

@HttpServer
public class EmployeesHttpRepository {
}
HttpServer httpServer = HttpServer.builder()
        .socketAddress(new InetSocketAddress(8080))
        .build();

httpServer.registerRepository(new EmployeesHttpRepository()); // <----
        
httpServer.bind();

Now we can proceed to the nuances of its further construction,
because this is where the most interesting things begin!

To implement some endpoint, we have several annotations that
allow us to do so:

  • @HttpRequestMapping
  • @HttpGet
  • @HttpPost
  • @HttpDelete
  • @HttpPut
  • @HttpPatch
  • @HttpConnect
  • @HttpHead
  • @HttpOptions
  • @HttpTrace

Let's start with the simplest one and implement the processing
of GET request to the path /employee with the possibility of
specifying the identifier of the Employee we need
through attributes (for example, /employee?id=567)

Example:

@HttpServer
public class EmployeesHttpRepository {

    @HttpGet("/employee")
    public HttpResponse getEmployee(HttpRequest request) {
        Attributes attributes = request.getAttributes();
        Optional<Integer> attributeIdOptional = attributes.getInteger("id");

        if (!attributeIdOptional.isPresent()) {
            return ...;
        }
        return HttpResponse.ok(Content.fromEntity(
                Employee.builder()
                        .id(attributeIdOptional.get())
                        .jobInfo(EmployeeJob.builder()
                                .company("Microsoft Corporation")
                                .website("https://www.microsoft.com/")
                                .profession("Developer C#")
                                .salary(3500)
                                .build())
                        .firstName("Piter")
                        .lastName("Harrison")
                        .build()));
    }
}

Now, suppose in the line where we check for the passed attribute !attributeIdOptional.isPresent() we need to pass the processing of this request to the NotFoundListener that was specified when HttpServer was initialized.

To do this, we need to return the HttpListener.SKIP_ACTION constant:

if (!attributeIdOptional.isPresent()) {
    return HttpListener.SKIP_ACTION;
}

But in this case it would be more correct to return
a 400 Bad Request error, and for this we can call
the function from HttpResponse in one of two ways:

if (!attributeIdOptional.isPresent()) {
    return HttpResponse.builder()
                .code(ResponseCode.BAD_REQUEST)
                .build();
}

or just:

if (!attributeIdOptional.isPresent()) {
    return HttpResponse.badRequest();
}

Now when we query the http://localhost:8080/employee?id=567
page, we get the following result:

drawing


ADDITIONAL HTTP-SERVER FEATURES

But that's not all!

The HTTP server repository has several other features that add some flexibility and convenience in exceptional cases of library use.

Let's go through some of them!


Annotation @HttpBeforeExecution:

Annotation allows you to pre-validate an incoming request,
change some parameters, or perform additional processes before
processing:

Example:

@HttpBeforeExecution
public void before(HttpRequest httpRequest) {
    httpRequest.setHeaders(
            httpRequest.getHeaders()
                    .set(Headers.Def.USER_AGENT, "Mikhail Sterkhov")
    );
}

Annotation @HttpAsync:

You can hang this annotation on literally any method that handles queries.

It implements some kind of wrapper of the handler in separate threads,
if it is really necessary for the implementation.

Example:

@HttpAsync
@HttpPatch("/employee")
public HttpResponse patchEmployee(HttpRequest request) {
    Employee employee = request.getContent().toEntity(Employee.class);
    try {
        employeesService.patch(employee);
        return HttpResponse.ok();
    } 
    catch (EmployeeException exception) {
        return HttpResponse.internalError();
    }
}

Annotation @HttpAuthenticator:

To verify requests via authorization, you can use a fully dedicated
functionality for this purpose, which provides you with an entire
model API to implement HTTP request authentication.

For examples:

Basic

private static final Token.UsernameAndPassword APPROVAL_TOKEN =
            Token.UsernameAndPassword.of("jrest_admin", "password");

@HttpAuthenticator
public ApprovalResult approveAuth(UnapprovedRequest request) {
    return request.basicAuthenticate(APPROVAL_TOKEN);
}

Bearer

private static final String GENERATED_API_TOKEN = TokenGenerator.defaults(30).generate();

@HttpAuthenticator
public ApprovalResult approveAuth(UnapprovedRequest request) {
    return request.bearerAuthenticate(GENERATED_API_TOKEN);
}

Bearer (custom)

private static final String GENERATED_API_TOKEN = TokenGenerator.defaults(30).generate();

@HttpAuthenticator
public ApprovalResult approveAuth(UnapprovedRequest request) {
    if (request.getAuthentication() != Authentication.BEARER) {
        return ApprovalResult.forbidden();
    }

    HttpCredentials credentials = request.getRequestCredentials();
    Token token = credentials.getToken();

    if (Objects.equals(token.getValue(), GENERATED_API_TOKEN)) {
        return ApprovalResult.approve();
    }
    
    return ApprovalResult.forbidden();
}

In case you want to apply authorization
without using classes with the @HttpServer annotation,
there are ways to do it too, let's look at a few of them:

HttpServer httpServer = ...;
Token.UsernameAndPassword credentials = Token.UsernameAndPassword.of("jrest_admin", "password")
httpServer.addAuthenticator(HttpBasicAuthenticator.of(credentials));
HttpServer httpServer = ...;
httpServer.addAuthenticator(HttpBearerAuthenticator.of(
        Arrays.asList(
                "c9636ffe984e41d7b03c1b42d72402210aa9e64f2bedd6064a70416ba5e",
                "f9af04c492c35e468100f9eead215903a67cdc3168fd95d78ca9bd4f9173",
                "fe332dc685090ddbbf1a7569f22ac2bbe0f13644dbcd3f77cbeaf8f86c47"));
HttpServer httpServer = ...;
httpServer.addAuthenticator(HttpBearerAuthenticator.single(
        "c9636ffe984e41d7b03c1b42d72402210aa9e64f2bedd6064a70416ba5e"));
HttpServer httpServer = ...;
httpServer.addAuthenticator(Authentication.DIGEST, (unapprovedRequest) -> { return ApprovalResult.forbidden(); });

Annotation @HttpNotAuthorized:

With this annotation, you can mark methods with HTTP requests
to be excluded from the request authentication process.

Example:

@HttpNotAuthorized
@HttpGet("/employee")
public HttpResponse doGet(HttpRequest request) {
    // request handle logic...
}

Support a Developments

Development by @MikhailSterkhov
We can support me here:

Buy Me A Coffee