Distributed Systems¶

2021/22¶

Lab 2

Nuno Preguiça, Sérgio Duarte, Dina Borrego, João Vilalonga

Goals¶

In the end of this lab you should be able to:

  • Understand what a WebService REST is
  • Know how to develop a WS REST and Server in Java (using JAX-RS)
  • Know how to develop a REST Client in Java (using JAX-RX)
  • Use Docker to test your service using your clients

Understanding REST WebServices¶

REST : REpresentational State Transfer¶

Architectural pattern to access information

Fundamental approach: an application is perceived as a collection of resources.

Key implications:

  • A resource is identified by a URI/URL;
  • The URL returns a document with a representation of the resource;
  • A URL can refer to a collection of resources;
  • It is possible to refer to other resources (from a resource) using links.

REST : REpresentational State Transfer¶

Consider an application that is used to manage contact cards.

  • Each contact card is a resource and has an URL;
  • The card's URL will return its representation;
    • ex: a textual representation of the fields of the card: name of the person, phone, e-mail, postal address – but it could also be a binary representation.
  • An URL can point to the whole collection of existing contact cards.
  • A contact card can refer to another card, by including the URL of ther other card;
    • ex: for instance to refer to the spouse of that person.

REST Protocol¶

A client-server protocol that is stateless.

- each request contains all the information that is necessary to process the request.

Implications:

  • The server does not need to keep track of relations among different requests;
  • Simple interaction patterns in systems using REST simple;
  • Allows transparent caching.

REST Protocol¶

The REST interface is uniform: all resources are accessed by a set of well-defined HTTP operations:

  • POST: Creates a new resource

  • GET: Obtains (a representation of) an existing resource

  • PUT: Updates or Replaces an existing resource

  • DELETE: Eliminates an existing resource

WebServices REST in Java (using JAX-RS)¶

Development of a WebServices REST in Java¶

Jersey (JAX-RS) is a framework that simplifies the development of REST services in Java.

  • Java code is instrumented through annotations (e.g., @PATH, @GET, @POST, @DELETE, …)

  • Java Reflection is used by the Jersey runtime to generate code automatically based on those annotations.

Want to know more? Jersey 3.x

Development of a WebServices REST in java¶

Jersey is split into multiple libraries.

Maven can handle Jersey dependencies via the pom.xml:

<dependencies>
    <dependency>
        <groupId>org.glassfish.jersey.media</groupId>
        <artifactId>jersey-media-json-jackson</artifactId>
        <version>3.0.4</version>
    </dependency>
    <dependency>
        <groupId>org.glassfish.jersey.containers</groupId>
        <artifactId>jersey-container-jdk-http</artifactId>
        <version>3.0.4</version>
    </dependency>
    ...
</dependencies>

Development of a WebServices REST in Java¶

Consider an User management service

  • Users are defined by their e-mail, unique userId, full name, and password;
    • Users are modelled by a User class.
  • A server is responsible for keeping information for all users in the system, it allows:

    • Create a new user (given the information above) if the users does not exist already;
    • Obtain the information of a user given its username and password;
    • Update the information of a user given its username and password;
    • Delete an user given its username and password;
    • Search users by specifying a regular expression.

User resource¶

Resources are modelled as Java classes, with some requirements.

public class User {
    private String email;
    private String userId;
    private String fullName;
    private String password;

    public User(){  
    }

    public User(String userId, String fullName, String email, String password) {
        this.email = email;
        this.userId = userId;
        this.fullName = fullName;
        this.password = password;
    }
    ...
}

Notes:

  • Required: public constructor without arguments;
  • Good practice: state as private fields
    • Getter/setter methods for private fields; needed for serialization and deserialization to/from network.

Defining the REST Service Interface¶

Java interface enriched with Jersey's annotations.

@Path(RestUsers.PATH)
public interface RestUsers {
    public static final String PATH = "/users";
    public static final String USER_ID = "userId";
    public static final String PASSWORD = "password";

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    String createUser(User user);

    @GET
    @Path("/{" + USER_ID + "}")
    @Produces(MediaType.APPLICATION_JSON)
    User getUser(@PathParam(USER_ID) String userId, @QueryParam(PASSWORD) String password);
    ...
}

@Path(STRING)¶

Used to define the base path of the URL used for accessing the service.

@Path(RestUsers.PATH)
public interface RestUsers {

    public static final String PATH = "/users";
    ...
}

The URL of the service is defined by appending the annotation's value to the base URL of the server.

For example, if the server URL is http://myserver:8080/rest, the service will be accessible at http://myserver:8080/rest/users

@POST¶

Used to create a new resource.

@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
String createUser(User user);

A HTTP POST request is used when the service is accessed.

@Consumes¶

Indicates the method will receive an argument through the body of the HTTP request

@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
String createUser(User user);

We typically encode Java objects sent in the body of an HTTP request in JavaScript Object Notation (JSON)

@Produces¶

Indicates the method will return a value encoded in the body of the HTTP response.

@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
String createUser(User user);

@Consumes / @Produces¶

Jersey supports various types of encodings, including:

  • JSON (MediaType.APPLICATION_JSON), for simplicity
  • Text (MediaType.TEXT_PLAIN)
  • XML (MediaType.APPLICATION_XML)

and

  • Octet-stream (MediaType.APPLICATION_OCTET_STREAM),
    for transferring binary data (byte[]).

@GET¶

Used to get the representation of an existing resource.

public static final String USER_ID = "userId";
public static final String PASSWORD = "password";

@GET
@Path("/{" + USER_ID + "}")
@Produces(MediaType.APPLICATION_JSON)
User getUser(@PathParam(USER_ID) String userId, @QueryParam(PASSWORD) String password);

@QueryParam¶

Used to retrieve optional (query parameter) values from the request.

  • @QueryParam(key)
  • @DefaultValue(value),
    can used to supply a default value when key is missing in the request

@Path¶

In a method refers to what follows in addition to the service base path.

public static final String USER_ID = "userId";

@GET
@Path("/{" + USER_ID + "}")
@Produces(MediaType.APPLICATION_JSON)
User getUser(@PathParam(USER_ID) String userId, @QueryParam(PASSWORD) String password);

Example: http://myserver:8080/rest/users/preguiça?password=12345

The method will be invoked with "preguiça" as the value of userId, and password will be 12345.

@Path + @PathParam¶

Used in combination to encode and retrieve values as part of request path.

  • @Path("/.../{var1}/.../{var2}/...")
  • @PathParam(var1)
  • @PathParam(var2)

Variables obtained from the path have to be associated with a matching parameter.

Only simple primitive types (and strings) can be passed this way.

@PUT¶

Used to update an existing resource.

public static final String USER_ID = "userId";
public static final String PASSWORD = "password";

@PUT
@Path("/{" + USER_ID + "}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
User updateUser(@PathParam(USER_ID) String userId, @QueryParam(PASSWORD) String password, User user);

The example includes the three ways for passing arguments already shown:

  • as part of the path;
  • as a query parameter;
  • encoded in the request HTTP body (as JSON).

Repeated @Path Annotations¶

@GET
@Path("/{" + USER_ID + "}")
@Produces(MediaType.APPLICATION_JSON)
User getUser(@PathParam(USER_ID) String userId, @QueryParam(PASSWORD) String password);

@PUT
@Path("/{" + USER_ID + "}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
User updateUser(@PathParam(USER_ID) String userId, @QueryParam(PASSWORD) String password, User user);

@DELETE
@Path("/{" + USER_ID + "}")
@Produces(MediaType.APPLICATION_JSON)
User deleteUser(@PathParam(USER_ID) String userId, @QueryParam(PASSWORD) String password);

Not ambiguous! Endpoints are distinguished by HTTP method (GET, PUT and DELETE, in this example).

Note: @DELETE is used to delete an existing resource...

More on annotations and methods¶

GET and DELETE are similar.

  • Should avoid sending information in the request body;<br>@Consumes usually absent;

POST and PUT are similar.

  • Should always send a representation of the resource in the body of the HTTP request;<br>@Consumes usually expected;

GET should always return a representation of the resource.

  • @Produces usually expected.

Implementing the Service¶

Service resources¶

A Java class that implements the service API

@Singleton
public class UsersResource implements RestUsers {
    ...
    public UsersResource() {
    }

    public String createUser(User user) {
     ...
    }
    ...
}

@Singleton¶

Used on resources that keep internal state.

  • Jersey engine will use the same single instance across requests.

Omitted when a stateless server is desired.

  • Jersey runtime will create a new instance per request.

Requires a no-args construtor.

Service implementation methods¶

public String createUser(User user) {
    // Check if user data is valid
    if(user.getUserId() == null || user.getPassword() == null || user.getEmail() == null) {
        throw new WebApplicationException( Status.BAD_REQUEST );
    }
    // Check if userId already exists
    if( users.containsKey(user.getUserId())) {
        throw new WebApplicationException( Status.CONFLICT );
    }
    users.put(user.getUserId(), user);
    return user.getUserId();
}

Annotations can be omitted in the service implementation methods.

Reporting service errors¶

WebApplicationException is used to report application errors to the client as HTTP status codes.

if( users.containsKey(user.getUserId())) {
    throw new WebApplicationException( Status.CONFLICT );
}

users.put(user.getUserId(), user);
return user.getUserId();

HTTP 200/OK is implicit when the method returns a value normally.

Important HTTP Response Codes¶

Range 100 – 199: Information (rarely seen)

Range 200 – 299: Success

  • 200 OK (the operation was successful, and the reply contains information)
  • 204 No Content (the operation was successful but there is no information returned).

Range 300 – 399: Redirection: additional action is required

  • 301 Moved Permanently (the resource is now represented by a new URL, which is provided in this answer)

Range 400 – 499: Client Error (e.g., preparing request)

  • 400 Bad Request
  • 403 Forbidden
  • 404 Not Found - Page/Resource not found
  • 409 Conflict – executing the request violates logic rules

Range 500 – 599: Server Error

  • 500 Internal Server Error – usually means an unhandled exception was thrown while executing request

REST Server¶

REST resources are exposed as endpoints of a HTTP server.

public class UsersServer {
    ...
    public static final int PORT = 8080;
    public static final String SERVICE = "UsersService";
    private static final String SERVER_URI_FMT = "http://%s:%s/rest";

    public static void main(String[] args) {
        try {
            ResourceConfig config = new ResourceConfig();
            config.register(UsersResource.class);

            String ip = InetAddress.getLocalHost().getHostAddress();
            String serverURI = String.format(SERVER_URI_FMT, ip, PORT);

            JdkHttpServerFactory.createHttpServer( URI.create(serverURI), config);

            //More code can be executed here...
        } catch( Exception e) {
            ...
        }
    }
}

Registering resources¶

ResourceConfig config = new ResourceConfig();
config.register(UsersResource.class);

ResourceConfig is used to register resources in a server.

  • Multiple resources (i.e., services) with different @Path annotations allowed.

Instancing the HTTP server¶

The server is instanced using its URI.

String ip = InetAddress.getLocalHost().getHostAddress();
String serverURI = String.format(SERVER_URI_FMT, ip, PORT);           
JdkHttpServerFactory.createHttpServer( URI.create(serverURI), config);

JdkHttpServerFactory launches a HTTP server in a separate thread.

Implementing the Client¶

CreateUserClient¶

ClientConfig config = new ClientConfig();
Client client = ClientBuilder.newClient(config);

WebTarget target = client.target( serverUrl ).path( RestUsers.PATH );

User u = new User( userId, fullName, email, password);
Response r = target.request()
                   .accept(MediaType.APPLICATION_JSON)
                   .post(Entity.entity(u, MediaType.APPLICATION_JSON));

if( r.getStatus() == Status.OK.getStatusCode() && r.hasEntity() )
    System.out.println("Success, created user with id: " + r.readEntity(String.class) );
else
    System.out.println("Error, HTTP error status: " + r.getStatus() );

The example above shows the typical blocks of code for a Jersey client.

ClientConfig + Client¶

ClientConfig config = new ClientConfig();
Client client = ClientBuilder.newClient(config);

ClientConfig is used for controlling client behavior, such as connection timeouts.

Client represents a Jersey client.

WebTarget¶

ClientConfig config = new ClientConfig();
Client client = ClientBuilder.newClient(config);

WebTarget target = client.target( serverUri ).path( RestUsers.PATH );

WebTarget is used to point to a service instance, given its URI;

Can be assembled by concatenation of any number of other elements to the URI;

In the example, the path corresponding to the top-level @Path annotation of the RestUsers interface is appended to the base server URI to yield, something like: http://myserver:8080/rest/users

Parametrization and Invocation¶

Response r = target.request()
                   .accept(MediaType.APPLICATION_JSON)
                   .post(Entity.entity(u, MediaType.APPLICATION_JSON));

.request()

  • to build the request incrementally;

.accept(MediaType.APPLICATION_JSON)

  • indicates the format of the return value;

.post(Entity.entity(u, MediaType.APPLICATION_JSON));

  • issues the actual request, using the POST HTTP method.

Notes: Encoding formats must match the formats specified in the service API.

Entity class is used to encode Java objects in the specified format;

Processing the Response¶

Response r = ... ;

if( r.getStatus() == Status.OK.getStatusCode() && r.hasEntity() )
    System.out.println("Success, created user with id: " + r.readEntity(String.class) );
else
    System.out.println("Error, HTTP error status: " + r.getStatus() );

Response contains both the return status and the value if available.

Note: Response.Status corresponds to the HTTP reply status code and provides a simple explanation of what happened.

Decoding the return value¶

With simple class types, use code like:

r.readEntity( String.class );
    r.readEntity( String[].class );
    r.readEntity( User.class );

With generic class types, use instead:

r.readEntity(new GenericType<List<User>>() {});
    r.readEntity(new GenericType<Map<String, User>>() {});

GetUserClient¶

...
ClientConfig config = new ClientConfig();
Client client = ClientBuilder.newClient(config);

WebTarget target = client.target( serverUrl ).path( RestUsers.PATH );

Response r = target.path( userId )
                   .queryParam(RestUsers.PASSWORD, password)
                   .request()
                   .accept(MediaType.APPLICATION_JSON)
                   .get();
...

Take notice on:

.path( userId ) concatenates the userId argument to the request path already present in the target;
.queryParam(RestUsers.PASSWORD, password) is used to supply the password argument by appending ?password=<value> request target URI.

Testing with Docker¶

  1. Build the image (run in your project folder):

    mvn clean compile assembly:single docker:build

  2. Create the docker network sdnet

    docker network create -d bridge sdnet

  3. Run the server in a named container (with port forwarding)

    docker run -h users-1 --name users-1 --network sdnet -p 8080:8080 sd2122-aula2-xxxxx-yyyyy

Create a user¶

  1. Run another container in interactive mode (to execute clients) in a second terminal window

    docker run -it --network sdnet sd2122-aula2-xxxxx-yyyyy /bin/bash

  1. Type in the container shell:

    java -cp /home/sd/sd2122.jar sd2122.aula2.clients.CreateUserClient http://users-1:8080/rest nmp "Nuno Preguica" nmp@nova.unl.pt 12345

Get the user¶

  1. Type in the container shell:

    java -cp /home/sd/sd2122.jar sd2122.aula2.clients.GetUserClient http://users-1:8080/rest nmp 12345

Get the user (using the browser)¶

GET requests are compatible with normal HTTP requests, like those issued by a browser.

  1. Click on: http://localhost:8080/rest/users/nmp?password=12345

The service is accessible by the host, because:

docker run ... -p 8080:8080 ... exposes port 8080 of the container as port 8080 of the host.

Exercises¶

  • Complete the server;

  • Complete the clients;

  • Test your implementations using docker;

  • Integrate the Discovery class from last week to enable all clients to obtain the server URL automatically.

Notes:¶

In the server, implement failure semantics described in the service API, namely:

  • When operation arguments are invalid (e.g. NULL), fail with 400 (Bad Request);
  • Updating or deleting a user that does not exist fails with 404 (Not Found);
  • Updating or deleting a user fails if password is incorrent with 403 (Forbidden).

Moreover, for the searchUsers method, if no pattern is provided you return all users, if there are no users, you return an empty list, and if a pattern is provided you should check each user individually and add it to the return list only if his name contains the exact sequence of chars in the pattern variable (String class has a method that can help you).