Skip to content
This repository has been archived by the owner on Apr 13, 2019. It is now read-only.

library service

Konstantin Sobolev edited this page Jun 7, 2017 · 13 revisions

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.

Authors backend

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

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"
    );
  }
}

Read operation implementation

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.

BookBuilder.buildBook

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.

buildAuthor

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.

buildText

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;
  }

Resource factory

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.

Server

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