-
Notifications
You must be signed in to change notification settings - Fork 4
library service
Make sure Library schema module is installed.
First step is to run Java code generator to get service implementation stubs:
mvn -pl library-service generate-sources
All the manually written classes will be created in
src/main/java/ws/epigraph/examples/library
folder.
Lets start by creating a mock backend to store authors information. It is very simple:
package ws.epigraph.examples.library;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
public class AuthorsBackend {
private static final AtomicLong nextId = new AtomicLong();
private static final Map<AuthorId, AuthorData> authors = new HashMap<>();
public static AuthorId ALLAN_POE = addAuthor("Allan", null, "Poe");
public static AuthorId CONAN_DOYLE = addAuthor("Arthur", "Conan", "Doyle");
public static AuthorId MARK_TWAIN = addAuthor("Mark", null, "Twain");
private static AuthorId addAuthor(String firstName, String middleName, String lastName) {
AuthorId id = AuthorId.create(nextId.incrementAndGet());
authors.put(id, new AuthorData(firstName, middleName, lastName));
return id;
}
public static AuthorData get(AuthorId id) {
return authors.get(id);
}
public static class AuthorData {
public final String firstName;
public final String middleName;
public final String lastName;
public AuthorData(String firstName, String middleName, String lastName) {
this.firstName = firstName;
this.middleName = middleName;
this.lastName = lastName;
}
}
}
AuthorData
represents backend view of the author information.
AuthorBacked
creates a few hard-coded entries and provides a single
get
method to access them by id values.
Books backend is very similar. BookData
is a backend view of a book
and there are a few hard-coded entries:
package ws.epigraph.examples.library;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
public class BooksBackend {
private static final AtomicLong nextId = new AtomicLong();
private static final Map<BookId, BookData> books = new HashMap<>();
public static BookId addBook(String title, AuthorId authorId, String text) {
BookId id = BookId.create(nextId.incrementAndGet());
books.put(id, new BookData(id, title, authorId, text));
return id;
}
public static BookData get(BookId id) {
return books.get(id);
}
public static class BookData {
public final BookId id;
public final String title;
public final AuthorId authorId;
public final String text;
public BookData(BookId id, String title, AuthorId authorId, String text) {
this.id = id;
this.title = title;
this.authorId = authorId;
this.text = text;
}
}
static {
addBook(
"The Gold Bug",
AuthorsBackend.ALLAN_POE,
"book text here, removed for clarity"
);
addBook(
"A Study In Scarlet",
AuthorsBackend.CONAN_DOYLE,
"book text here, removed for clarity"
);
addBook(
"The Adventures of Tom Sawyer",
AuthorsBackend.MARK_TWAIN,
"book text here, removed for clarity"
);
}
}
BooksReadOperation
is where all the heavy-lifting is happening. It
extends generated
ws.epigraph.examples.library.resources.books.operations.read.AbstractReadOperation
class and provides process
method implementation:
@Override
protected @NotNull CompletableFuture<BookId_BookRecord_Map.Data> process(
@NotNull BookId_BookRecord_Map.Builder.Data booksDataBuilder,
@NotNull ReqOutputBooksFieldProjection booksFieldProjection) {
BookId_BookRecord_Map.Builder booksMap = BookId_BookRecord_Map.create();
ReqOutputBookId_BookRecord_MapProjection booksMapProjection = booksFieldProjection.dataProjection();
ReqOutputBookRecordProjection bookRecordProjection = booksMapProjection.itemsProjection();
for (ReqOutputBookId_BookRecord_MapKeyProjection keyProjection : booksMapProjection.keys()) {
final BookId.Imm bookId = keyProjection.value();
booksMap.put_(bookId, BookBuilder.buildBook(bookId, bookRecordProjection));
}
booksDataBuilder.set(booksMap);
return CompletableFuture.completedFuture(booksDataBuilder);
}
process
method takes two parameters:
- empty data builder instance for the books map. This builder must be filled in based on the request
- request projection which is a full description of the request: which keys and fields are requested together with any parameters
Output is a CompletableFuture
of the map data, which allows to run
heavy computations or long I/O calls asynchronously. In our case
we do everything in the same thread and wrap result in
completedFuture
.
Map data builder can contain one of two things: either map instance
or an ErrorValue
if the whole map can't be built for some reason. In
our case we can never have such a global failure, so first thing we
do is create map builder by calling BookId_BookRecord_Map.create()
.
We set it as a value for the map data builder at the very end. The rest
of the code fills map builder with entries.
Next step is to extract books map projection from the /books
field
projection using dataProjection
call. Resulting booksMapProjection
contains two pieces of information: keys()
with a list of requested
map keys and itemsProjection()
for map values. We extract both of
them and iterate over the keys calling getBook
for each of them.
BookBuilder.buildBook
receives book ID, book record projection and returns back a
BookRecord.Value
object which contains either a BookRecord
instance
or an ErrorValue
in case of an error.
booksMap.put_
puts BookRecord.Value
instance in the map. We could
also use put
for putting BookRecord
instances and putError
for
putting errors.
buildBook
receives book ID, book record projection and must build
a BookRecord.Value
. Implementation is straight-forward:
public static BookRecord.Value buildBook(BookId bookId, ReqOutputBookRecordProjection bookRecordProjection) {
BooksBackend.BookData bookData = BooksBackend.get(bookId);
if (bookData == null) {
return BookRecord.type.createValue(
new ErrorValue(404, "No book with id " + bookId.getVal())
);
} else {
BookRecord.Builder book = BookRecord.create();
book.setTitle(bookData.title);
// only get author if requested
ReqOutputAuthorProjection authorProjection = bookRecordProjection.author();
if (authorProjection != null)
book.setAuthor(buildAuthor(bookData.authorId, authorProjection));
// only get text if requested
ReqOutputTextProjection textProjection = bookRecordProjection.text();
if (textProjection != null)
book.setText(buildText(bookData, textProjection));
return book.asValue();
}
}
If book is not found then a 404 error is reported back. Otherwise a record builder is created and filled according to the request projection: author and text fields are only built if requested.
It is not strictly necessary to consult with the projection about what to build, operation can always build everything and framework will remove whatever was not requested from the output. If, however, certain parts are expensive to build, it's better to only build them if requested by the client.
getAuthor
is also quite simple, with the only difference that
Author
is a entity type, so there is an extra-step:
private static Author buildAuthor(AuthorId authorId, ReqOutputAuthorProjection authorProjection) {
Author.Builder author = Author.create();
author.setId(authorId);
ReqOutputAuthorRecordProjection authorRecordProjection = authorProjection.record();
if (authorRecordProjection !=null)
author.setRecord_(buildAuthorRecord(authorId, authorRecordProjection));
return author;
}
id
tag is always set, record
is only populated if requested,
deletaging to getAuthorRecord
:
private static AuthorRecord.Value buildAuthorRecord(AuthorId authorId, ReqOutputAuthorRecordProjection authorRecordProjection) {
AuthorsBackend.AuthorData authorData = AuthorsBackend.get(authorId);
if (authorData == null) {
return AuthorRecord.type.createValue(
new ErrorValue(404, "No author with id " + authorId)
);
} else {
AuthorRecord.Builder author = AuthorRecord.create();
if (authorData.firstName != null || authorRecordProjection.firstName() != null)
author.setFirstName(authorData.firstName);
if (authorData.middleName != null || authorRecordProjection.middleName() != null)
author.setMiddleName(authorData.middleName);
if (authorData.lastName != null || authorRecordProjection.lastName() != null)
author.setLastName(authorData.lastName);
return author.asValue();
}
}
Notice how name fields are only populated if non-null
in the backend or requested by the projection.
Setting them to null
is different from not setting them: they will
contain null
values in the former and not exist at all in the latter
case.
This method is more interesting as it shows how to deal with input
parameters and meta-data. It's a bit long because we want to carefully
treat offset
and count
and change them to the closest valid value
if they get out of bounds.
private static Text buildText(BooksBackend.BookData bookData, ReqOutputTextProjection textProjection) {
Text.Builder text = Text.create();
ReqOutputPlainTextProjection plainTextProjection = textProjection.plain();
if (plainTextProjection != null) {
String bookText = bookData.text;
long textLength = bookText.length();
// handle parameters
Long offset = plainTextProjection.getOffsetParameter();
if (offset == null) offset = 0L;
if (offset < 0 || offset >= bookText.length())
offset = textLength - 1;
Long count = plainTextProjection.getCountParameter();
if (count == null || count < 0 || offset + count > textLength - 1)
count = textLength - offset;
int beginIndex = Math.toIntExact(offset);
int endIndex = Math.toIntExact(beginIndex + count);
PlainText.Builder plainText = PlainText.create(bookText.substring(beginIndex, endIndex));
// provide meta if requested
if (plainTextProjection.meta() != null) {
plainText.setMeta(
PlainTextRange.create()
.setOffset(offset)
.setCount(count)
);
}
text.setPlain(plainText);
}
return text;
}
BooksResourceFactory
extends generated AbstractBooksResourceFactory
and provides a single method to construct read operation implementation:
public class BooksResourceFactory extends AbstractBooksResourceFactory {
@Override
protected ReadOperation<BookId_BookRecord_Map.Data> constructReadOperation(
ReadOperationDeclaration operationDeclaration) throws ServiceInitializationException {
return new BooksReadOperation(operationDeclaration);
}
}
As you can see, it just creates BooksReadOperation
described above.
This is the main class. It creates Epigraph Service
instance
with a single resource and then uses it to initialize UndertowHandler
:
public class LibraryServer {
public static final int PORT = 8888;
public static final String HOST = "localhost";
public static final int TIMEOUT = 100; // response timeout in ms
private static Service buildLibraryService() throws ServiceInitializationException {
return new Service(
BooksResourceDeclaration.INSTANCE.fieldName(), // root field name
Collections.singleton( // collection of resources
new BooksResourceFactory().getBooksResource()
)
);
}
public static void main(String[] args) throws ServiceInitializationException {
Undertow server = Undertow.builder()
.addHttpListener(PORT, HOST)
.setServerOption(UndertowOptions.DECODE_URL, false) // don't decode URLs
.setHandler(new UndertowHandler(buildLibraryService(), TIMEOUT))
.build();
server.start();
}
}
Next section: running the example