Skip to content

Commit 5d318c5

Browse files
committed
update documentation
1 parent c165826 commit 5d318c5

File tree

1 file changed

+317
-3
lines changed

1 file changed

+317
-3
lines changed

README.md

Lines changed: 317 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,334 @@
66

77
# java-http-server
88

9-
A wrapper around [Javalin](https://javalin.io/) enabling you to craft CRUD REST-services in no-time.
9+
A wrapper around [Javalin](https://javalin.io/) enabling you to craft database-backed CRUD REST-services in no-time.
1010

1111
This library reduces boilerplate code when setting up a REST-server connected to a relational database (MariaDB adapter exists) providing extensive builders to generate standard CRUD REST-endpoints.
1212

13-
Further it has interceptors and extensions allowing you to interact with the process of generating a response for a specific request at any level.
13+
Further it has interceptors and extensions allowing you to interact with the process of generating a response for a specific request at any level. The sync-extensions may abort the standard-process at any point or simply alter the DTOs passed.
14+
15+
16+
17+
## Prerequisites
18+
19+
A relational database (MariaDB) that holds your entities (we use Liquibase for git-supported, structured, versioned DDL-manipulation) and a JPA persistenceUnit for that database.
20+
(You should use our [java-rdb-utils](https://github.com/UnterrainerInformatik/java-rdb-utils) project for this, since it deals with max-accuracy timestamps and LocalDateTime vs. UTC as well as reducing boilerplate code on Liquibase-startup and shutdown).
21+
22+
### Minimal Example
23+
24+
```java
25+
// Get configuration containing necessary environment variables.
26+
configuration = MyProgramConfiguration.read();
27+
// Create an EntityManagerFactory using java-rdb-utils.
28+
// Registers shutdownhook to close emf as well.
29+
EntityManagerFactory emf =
30+
dbUtils.createAutoclosingEntityManagerFactory(MyProgram.class, "my-server");
31+
32+
// Create a JpqlTransactionManager which will be used to maintain transactions
33+
// throughout the server.
34+
JpqlTransactionManager jpqlTransactionManager = new JpqlTransactionManager(emf);
35+
36+
// Create the server.
37+
HttpServer server = HttpServer.builder()
38+
.applicationName("my-rest-server")
39+
.jsonMapper(jsonMapper)
40+
.orikaFactory(orikaFactory)
41+
.build();
42+
43+
// All handlers are added and considered in order.
44+
// After you're done adding handlers, start the server.
45+
server.start();
46+
```
47+
48+
When starting this server, you'll be able to access the endpoints using Postman or a similar REST client.
49+
50+
### Standard endpoints available
51+
52+
* AppNameHandler
53+
Path: GET "/"
54+
Returns: The name of the server
55+
* AppVersionHandler
56+
Path: GET "/version"
57+
Returns: The version of the registered version-provider, or the http-server if none given
58+
* DateTimeHandler
59+
Path: GET "/datetime"
60+
Returns: The current date and time on the server in UTC
61+
* HealthHandler
62+
Path: GET "/health"
63+
Returns: "healthy" if the server is up and running
64+
* PostmanCollectionHandler
65+
Path: GET "/postman"
66+
Returns: The content of the file "src/main/resources/postman_collection.json", if any
67+
68+
69+
70+
## Custom Handlers
71+
72+
The reason why you're doing REST services is that you have some data to expose to your client.
73+
The next example takes such data (a user) and exposes it.
74+
75+
First, let's create the JPA used to read and write to and from the database.
76+
It's linked to the table using JPA annotations.
77+
78+
### User.jpa
79+
80+
```java
81+
@Data
82+
@NoArgsConstructor
83+
@EqualsAndHashCode(callSuper = true)
84+
@SuperBuilder(toBuilder = true)
85+
@Entity
86+
@Table(name = "user")
87+
public class UserJpa extends BasicJpa {
88+
89+
private String name;
90+
}
91+
```
92+
93+
Then let's create the JSON object. That's the Data-Transfer-Object being sent to and from the server via HTTP. The server does all of the mapping by itself.
94+
95+
### User.json
96+
97+
```java
98+
@Data
99+
@NoArgsConstructor
100+
@SuperBuilder(toBuilder = true)
101+
@EqualsAndHashCode(callSuper = true)
102+
public class UserJson extends BasicJson {
103+
104+
private String name;
105+
}
106+
```
107+
108+
And lastly, update the server-code so that we expose the endpoint.
109+
110+
### Server
111+
112+
```java
113+
// omitted for brevity...
114+
// (see first, minimal example)
115+
// Last line here is the creation of the server ending with ".build()"
116+
117+
// Register a custom handler for the resource 'user'.
118+
server.handlerGroupFor(UserJpa.class, UserJson.class, jpqlTransactionManager)
119+
.path("users")
120+
.dao(new JpqlDao<UserJpa>(emf, UserJpa.class))
121+
.endpoints(Endpoint.ALL)
122+
.addRoleFor(Endpoint.ALL, RoleBuilder.open())
123+
.getListInterceptor()
124+
.query("userName = :userName[string]")
125+
.build()
126+
.add();
127+
128+
server.start();
129+
```
130+
131+
Now you can use the resource reachable via '/users'.
132+
133+
134+
135+
## URL Schema
136+
137+
The server uses an all-plural schema, meaning that the resource name is the plural of the word for it.
138+
Additionally all resource-names are lower-case only due to restrictions within Javalin as of this version.
139+
140+
`GET local.myserver.com/users/12`
141+
to get the user with the ID 12.
142+
Referred to as 'get-by-ID'.
143+
144+
`GET local.myserver.com/users?size=10&offset=0`
145+
to get the list of the next 10 users starting with offset 0.
146+
Referred to as 'get-list'. The result has a global count and prev, next, first, last links.
147+
148+
`POST local.myserver.com/users` with payload `{ "name": "testName" }`
149+
to persist the new user with name `testName`.
150+
Referred to as 'create'.
151+
152+
`PUT local.myserver.com/users/12` with payload `{ "name": "testName1" }`
153+
to update the name of the user with the ID 12 to `testName1`.
154+
Referred to as 'full-update'.
155+
156+
`DEL local.myserver.com/users/12`
157+
to delete the user with the ID 12.
158+
Referred to as 'delete'.
14159

15160

16161

17162
## Standard Request-Response Process
18163

164+
When sending requests to the server, it will do the following in the following order to get to returning a response object.
165+
19166
![standard-request-response-process-simple](https://github.com/UnterrainerInformatik/java-http-server/raw/master/docs/standard-request-response-process-simple.png)
20167

21168

22169

23170
## Request-Response Process with Extensions and Interceptors
24171

25-
![standard-request-response-process](https://github.com/UnterrainerInformatik/java-http-server/raw/master/docs/standard-request-response-process.png)
172+
In addition to the standard process, you may register extensions (sync and async) or any number of get-list-interceptors at your leasure.
173+
174+
![standard-request-response-process](https://github.com/UnterrainerInformatik/java-http-server/raw/master/docs/standard-request-response-process.png)
175+
176+
### Sync-Extensions
177+
178+
Synchronous extensions run in the context of the request-response-process and therefore may alter or stop it. The backdraw is that they stall the request-response-process for as long as it takes executing them of course.
179+
180+
If you have long-running operations, you better choose an async-extension point.
181+
182+
#### Example
183+
184+
```java
185+
server.handlerGroupFor(SomeSingletonJpa.class, SomeSingletonJson.class, jpqlTransactionManager)
186+
.path("cmd/somesingleton")
187+
.dao(new JpqlAsyncDao<SomeSingletonJpa>(emf, SomeSingletonJpa.class))
188+
.endpoints(Endpoint.ALL)
189+
.addRoleFor(Endpoint.ALL, RoleBuilder.open())
190+
.extension()
191+
.preInsertSync((ctx, em, receivedJson, resultJpa) -> {
192+
if (someSingletonDao.lockedGetNextWith(em, AsyncState.PROCESSING, AsyncState.PROCESSING) != null)
193+
throw new ConflictException(
194+
"A singleton-run is already in progress. Only a single singleton-run is allowed to be running at any given time.");
195+
resultJpa.setState(AsyncState.PROCESSING);
196+
resultJpa.setStartedOn(LocalDateTime.now(ZoneOffset.UTC));
197+
return resultJpa;
198+
})
199+
.extension()
200+
.add();
201+
server.start();
202+
```
203+
204+
The throwing of an HttpException here stops the request-response-process returning the error-message for that exception including the correct status-code.
205+
206+
207+
208+
### Async-Extensions
209+
210+
Run in their own context, detached from the request-response-process and therefore cannot alter or stop it.
211+
212+
#### Example
213+
214+
```java
215+
server.handlerGroupFor(SubscriptionJpa.class, SubscriptionJson.class, jpqlTransactionManager)
216+
.path("/subscriptions")
217+
.dao(subscriptionDao)
218+
.endpoints(Endpoint.ALL)
219+
.addRoleFor(Endpoint.ALL, RoleBuilder.open())
220+
.extension()
221+
.postDeleteAsync(id -> {
222+
subscriptionHandler.updateSubscriptions();
223+
})
224+
.extension()
225+
.postInsertAsync((receivedJson, mappedJpa, createdJpa, response) -> {
226+
subscriptionHandler.updateSubscriptions();
227+
})
228+
.extension()
229+
.postModifyAsync((receivedId, receivedJson, readJpa, mappedJpa, persistedJpa, response) -> {
230+
subscriptionHandler.updateSubscriptions();
231+
})
232+
.add();
233+
server.start();
234+
```
235+
236+
Here we're running `subscriptionHandler.updateSubscriptions()` every time a subscription is changed using our CRUD endpoints.
237+
238+
### Get-List-Interceptors
239+
240+
These are called in order of registration BEFORE calling the standard get-list code.
241+
If any single one of those completes without an exception or without returning false, then all other interceptors will be omitted as well as the standard get-list code. The result of the interceptor will be taken and the response will be built using that data.
242+
243+
This allows you to customize ordering, path-parameters and so on, without you having to write all the necessary code to allow for paging all by yourself over and over again.
244+
245+
They come in two flavors.
246+
247+
#### Query-Based Get-List-Interceptors
248+
249+
The server has an integrated language we called RQL (like in REST query language) that allows you to specify and combine several additional query-parameters and the way those are mapped to the database.
250+
251+
Be cautious when using those and be sure to have the right indexes on your database to support the queries your users are then able to build using your query parameters.
252+
253+
##### Example 1
254+
255+
```java
256+
server.handlerGroupFor(SubscriptionJpa.class, SubscriptionJson.class, jpqlTransactionManager)
257+
.path("/subscriptions")
258+
.dao(subscriptionDao)
259+
.endpoints(Endpoint.ALL)
260+
.addRoleFor(Endpoint.ALL, RoleBuilder.open())
261+
.getListInterceptor()
262+
.query("idString = :stringId[string]")
263+
.build()
264+
.add();
265+
server.start();
266+
```
267+
268+
This interceptor is used if the (mandatory) path-parameter `stringId` is set to a value. It is treated as a string internally and is matched using the `=` operator on a database-level to the database-field `idString`. So if you'd pass it the value 'test' the resulting JPQL query would look like this:
269+
270+
```sql
271+
SELECT o from <yourObject> WHERE idString=:stringId
272+
''' with the following parameters being set for that query...
273+
setParam(stringId, "test")
274+
```
275+
276+
You may specify a parameter as optional by pre-fixing the database-field name with a question-mark like so:
277+
278+
##### Example 2
279+
280+
```java
281+
.getListInterceptor()
282+
.query("scanId = :scanId[long] AND (?name LIKE :sn[string] OR ?idString LIKE :sn[string] OR ?description LIKE :sn[string])")
283+
.build()
284+
```
285+
286+
Where `scanId` is a numeric mandatory parameter and the rest is checked using the `LIKE` operator but since the parameter `sn` is optional, the usages are as well.
287+
288+
289+
290+
#### Explicit Get-List-Interceptors
291+
292+
Are registered as anonymous methods returning an `InterceptorData` object or null, if to be omitted.
293+
294+
Here you can do everything the integrated RQL language doesn't make up for.
295+
296+
##### InterceptorData
297+
298+
```java
299+
@Data
300+
@AllArgsConstructor
301+
@Builder
302+
public class InterceptorData {
303+
304+
private String selectClause;
305+
private String whereClause;
306+
private String joinClause;
307+
private String orderByClause;
308+
private ParamMap params;
309+
private String partOfQueryString;
310+
}
311+
```
312+
313+
##### Example
314+
315+
```java
316+
server.handlerGroupFor(SubscriptionJpa.class, SubscriptionJson.class, jpqlTransactionManager)
317+
.path("/subscriptions")
318+
.dao(subscriptionDao)
319+
.endpoints(Endpoint.ALL)
320+
.addRoleFor(Endpoint.ALL, RoleBuilder.open())
321+
.getListInterceptor(subscriptionInterceptor::select)
322+
.add();
323+
server.start();
324+
```
325+
326+
Where the method `subscriptionInterceptor.select` is some longer method resulting in an `InterceptorData` object being returned like along the lines of this:
327+
328+
```java
329+
public InterceptorData select(final Context ctx, final HandlerUtils hu) {
330+
// (locNameLike=:string AND locale=:string) AND hasTags=:[long] AND
331+
// anyTags=:[long] AND
332+
// state=:string AND quality=:string
333+
String locNameLike = hu.getQueryParamAsString(ctx, "locNameLike", null);
334+
String hasTags = hu.getQueryParamAsString(ctx, "hasTags", null);
335+
...
336+
```
337+
338+
339+

0 commit comments

Comments
 (0)