Enterprise Java

Project Student: Webservice Client with Jersey

This is part of Project Student. Other posts are Webservice Client with Jersey, Business Layer and Persistence with Spring Data.

The first layer of the RESTful webapp onion is the webservice client. It can be used to mimic web pages containing AJAX content or by programmatic users of the webapp. N.B., the latter might include other webapps, e.g., if you have an internal RESTful server surrounded by a number of presentation servers that create conventional web pages.

Design Decisions

Jersey – I use the Jersey library for the REST calls. I considered several choices but decided to go with Jersey since it’s lightweight and won’t impose many restrictions on the developer. In contrast using a Spring library, for instance, could cause problems in an EJB3 environment in addition to pulling in additional libraries.

UUIDs – the database will use integer primary keys but the web service will use UUIDs to identify values. This is for security – if an attacker knows that account ID 1008 exists then it’s a fairly safe bet that account ID 1007 exists. More importantly user ID 0 probably exists and has additional privileges than the typical user.

This isn’t true with UUIDs – for the most part knowing one UUID gives no insight into other UUIDs. That’s not 100% accurate – some UUIDs are composed with IP addresses or timestamps so the universe of possible values can be dramatically reduced by a knowledgeable attacker – but a random UUID is “good enough” for now.

Limitations

I am taking a “implement the least functionality required” approach so the initial implementation has a number of limitations.

Authentication – there is no attempt made to provide authentication information.

Encryption – there is no attempt made to encrypt the webservice call.

Only CRUD methods – only basic CRUD methods are supported.

Remember – limitations are fine but they must be clearly documented. In the best of worlds they’ll be added to an agile backlog.

Client API

The client API is basic CRUD. We can add functionality later.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public interface CourseRestClient {
 
    /**
     * Get list of all courses.
     */
    Course[] getAllCourses();
 
    /**
     * Get details for specific course.
     * @param uuid
     */
    Course getCourse(String uuid);
 
    /**
     * Create specific course.
     * @param name
     */
    Course createCourse(String name);
 
    /**
     * Update specific course.
     * @param uuid
     * @param name
     */
    Course updateCourse(String uuid, String name);
 
    /**
     * Delete course.
     * @param uuid
     */
    void deleteCourse(String uuid);
}

Exceptions

The API includes three runtime exceptions. The first, RestClientException, is an abstract runtime exception that is the base class for all other exceptions.

An ObjectNotFoundException is thrown when an expected value is missing. (Implementation note: this is triggered by a 404 status code.) This exception contains enough information to uniquely identify the expected object.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class ObjectNotFoundException extends RestClientException {
    private static final long serialVersionUID = 1L;
 
    private final String resource;
    private final Class<? extends PersistentObject> objectClass;
    private final String uuid;
 
    public ObjectNotFoundException(final String resource,
            final Class<? extends PersistentObject> objectClass,
            final String uuid) {
        super("object not found: " + resource + "[" + uuid + "]");
        this.resource = resource;
        this.objectClass = objectClass;
        this.uuid = uuid;
    }
 
    public String getResource() {
        return resource;
    }
 
    public Class<? extends PersistentObject> getObjectClass() {
        return objectClass;
    }
 
    public String getUuid() {
        return uuid;
    }
}

A RestClientFailureException is a generic handler for unexpected or unhandled status codes.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class RestClientFailureException extends RestClientException {
    private static final long serialVersionUID = 1L;
 
    private final String resource;
    private final Class<? extends PersistentObject> objectClass;
    private final String uuid;
    private final int statusCode;
 
    /**
     * Constructor
     *
     * @param resource
     * @param objectClass
     * @param uuid
     * @param response
     */
    public RestClientFailureException(final String resource,
            final Class<? extends PersistentObject> objectClass,
            final String uuid, final ClientResponse response) {
        super("rest client received error: " + resource + "[" + uuid + "]");
        this.resource = resource;
        this.objectClass = objectClass;
        this.uuid = uuid;
        this.statusCode = response.getStatus();
    }
 
    public String getResource() {
        return resource;
    }
 
    public Class<? extends PersistentObject> getObjectClass() {
        return objectClass;
    }
 
    /**
     * Get UUID, "<none>" (during listAllX()) or "(name)" (during createX())
     *
     * @return
     */
    public String getUuid() {
        return uuid;
    }
 
    /**
     * Get standard HTTP status code.
     *
     * @return
     */
    public int getStatusCode() {
        return statusCode;
    }
}

We’ll want to add an UnauthorizedOperationException after we add client authentication.

Client Implementation

Basic CRUD implementations are typically boilerplate so we can use an abstract class to do most of the heavy lifting. More advanced functionality will probably require this class to make Jersey calls directly.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/**
 * This is the Course-specific implementation.
 */
public class CourseRestClientImpl extends AbstractRestClientImpl<Course>
        implements CourseRestClient {
    private static final Course[] EMPTY_COURSE_ARRAY = new Course[0];
 
    /**
     * Constructor.
     *
     * @param courseResource
     */
    public CourseRestClientImpl(final String resource) {
        super(resource, Course.class, Course[].class);
    }
 
    /**
     * Create JSON string.
     *
     * @param name
     * @return
     */
    String createJson(final String name) {
        return String.format("{ \"name\": \"%s\" }", name);
    }
 
    /**
     * @see com.invariantproperties.sandbox.student.webservice.client.CourseRestClient#getAllCourses()
     */
    public Course[] getAllCourses() {
        return super.getAllObjects(EMPTY_COURSE_ARRAY);
    }
 
    /**
     * @see com.invariantproperties.sandbox.student.webservice.client.CourseRestClient#getCourse(java.lang.String)
     */
    public Course getCourse(final String uuid) {
        return super.getObject(uuid);
    }
 
    /**
     * @see com.invariantproperties.sandbox.student.webservice.client.CourseRestClient#createCourse(java.lang.String)
     */
    public Course createCourse(final String name) {
        if (name == null || name.isEmpty()) {
            throw new IllegalArgumentException("'name' is required");
        }
 
        return createObject(createJson(name));
    }
 
    /**
     * @see com.invariantproperties.sandbox.student.webservice.client.CourseRestClient#updateCourse(java.lang.String,
     *      java.lang.String)
     */
    public Course updateCourse(final String uuid, final String name) {
        if (name == null || name.isEmpty()) {
            throw new IllegalArgumentException("'name' is required");
        }
 
        return super.updateObject(createJson(name), uuid);
    }
 
    /**
     * @see com.invariantproperties.sandbox.student.webservice.client.CourseRestClient#deleteCourse(java.lang.String)
     */
    public void deleteCourse(final String uuid) {
        super.deleteObject(uuid);
    }
}

The abstract base class does the heavy lifting.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
public class AbstractRestClientImpl<T extends PersistentObject> {
    private final String resource;
    private final Class<T> objectClass;
    private final Class<T[]> objectArrayClass;
 
    /**
     * Constructor.
     *
     * @param resource
     */
    public AbstractRestClientImpl(final String resource,
            final Class<T> objectClass, final Class<T[]> objectArrayClass) {
        this.resource = resource;
        this.objectClass = objectClass;
        this.objectArrayClass = objectArrayClass;
    }
 
    /**
     * Helper method for testing.
     *
     * @return
     */
    Client createClient() {
        return Client.create();
    }
 
    /**
     * List all objects. This is a risky method since there's no attempt at
     * pagination.
     */
    public T[] getAllObjects(final T[] emptyListClass) {
        final Client client = createClient();
 
        try {
            final WebResource webResource = client.resource(resource);
            final ClientResponse response = webResource.accept(
                    MediaType.APPLICATION_JSON).get(ClientResponse.class);
 
            if (response.getStatus() == Response.Status.OK.getStatusCode()) {
                T[] entities = response.getEntity(objectArrayClass);
                return entities;
            } else {
                throw new RestClientFailureException(resource, objectClass,
                        "<none>", response);
            }
        } finally {
            client.destroy();
        }
    }
 
    /**
     * Get a specific object.
     */
    public T getObject(String uuid) {
        final Client client = createClient();
 
        try {
            final WebResource webResource = client.resource(resource + uuid);
            final ClientResponse response = webResource.accept(
                    MediaType.APPLICATION_JSON).get(ClientResponse.class);
 
            if (response.getStatus() == Response.Status.OK.getStatusCode()) {
                final T entity = response.getEntity(objectClass);
                return entity;
            } else if (response.getStatus() == Response.Status.NOT_FOUND
                    .getStatusCode()) {
                throw new ObjectNotFoundException(resource, objectClass, uuid);
            } else {
                throw new RestClientFailureException(resource, objectClass,
                        uuid, response);
            }
        } finally {
            client.destroy();
        }
    }
 
    /**
     * Create an object with the specified values.
     */
    public T createObject(final String json) {
        final Client client = createClient();
 
        try {
            final WebResource webResource = client.resource(resource);
            final ClientResponse response = webResource
                    .type(MediaType.APPLICATION_JSON)
                    .accept(MediaType.APPLICATION_JSON)
                    .post(ClientResponse.class, json);
 
            if (response.getStatus() == Response.Status.CREATED.getStatusCode()) {
                final T entity = response.getEntity(objectClass);
                return entity;
            } else {
                throw new RestClientFailureException(resource, objectClass, "("
                        + json + ")", response);
            }
        } finally {
            client.destroy();
        }
    }
 
    /**
     * Update an object with the specified json.
     */
    public T updateObject(final String json, final String uuid) {
        final Client client = createClient();
 
        try {
            final WebResource webResource = client.resource(resource + uuid);
            final ClientResponse response = webResource
                    .type(MediaType.APPLICATION_JSON)
                    .accept(MediaType.APPLICATION_JSON)
                    .post(ClientResponse.class, json);
 
            if (response.getStatus() == Response.Status.OK.getStatusCode()) {
                final T entity = response.getEntity(objectClass);
                return entity;
            } else if (response.getStatus() == Response.Status.NOT_FOUND
                    .getStatusCode()) {
                throw new ObjectNotFoundException(resource, objectClass, uuid);
            } else {
                throw new RestClientFailureException(resource, objectClass,
                        uuid, response);
            }
        } finally {
            client.destroy();
        }
    }
 
    /**
     * Delete specified object.
     */
    public void deleteObject(String uuid) {
        final Client client = createClient();
 
        try {
            final WebResource webResource = client.resource(resource + uuid);
            final ClientResponse response = webResource.accept(
                    MediaType.APPLICATION_JSON).delete(ClientResponse.class);
 
            if (response.getStatus() == Response.Status.GONE.getStatusCode()) {
                // do nothing
            } else if (response.getStatus() == Response.Status.NOT_FOUND
                    .getStatusCode()) {
                // do nothing - delete is idempotent
            } else {
                throw new RestClientFailureException(resource, objectClass,
                        uuid, response);
            }
        } finally {
            client.destroy();
        }
    }
}

Unit Testing

We now come to our test code. Important: we want to test our code’s behavior and not its implementation.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
public class CourseRestClientImplTest {
    private static final String UUID = "uuid";
    private static final String NAME = "name";
 
    @Test
    public void testGetAllCoursesEmpty() {
        CourseRestClient client = new CourseRestClientMock(200, new Course[0]);
        Course[] results = client.getAllCourses();
        assertEquals(0, results.length);
    }
 
    @Test
    public void testGetAllCoursesNonEmpty() {
        Course course = new Course();
        course.setUuid(UUID);
        CourseRestClient client = new CourseRestClientMock(200,
                new Course[] { course });
        Course[] results = client.getAllCourses();
        assertEquals(1, results.length);
    }
 
    @Test(expected = RestClientFailureException.class)
    public void testGetAllCoursesError() {
        CourseRestClient client = new CourseRestClientMock(500, null);
        client.getAllCourses();
    }
 
    @Test
    public void testGetCourse() {
        Course course = new Course();
        course.setUuid(UUID);
        CourseRestClient client = new CourseRestClientMock(200, course);
        Course results = client.getCourse(course.getUuid());
        assertEquals(course.getUuid(), results.getUuid());
    }
 
    @Test(expected = ObjectNotFoundException.class)
    public void testGetCourseMissing() {
        CourseRestClient client = new CourseRestClientMock(404, null);
        client.getCourse(UUID);
    }
 
    @Test(expected = RestClientFailureException.class)
    public void testGetCourseError() {
        CourseRestClient client = new CourseRestClientMock(500, null);
        client.getCourse(UUID);
    }
 
    @Test
    public void testCreateCourse() {
        Course course = new Course();
        course.setName(NAME);
        CourseRestClient client = new CourseRestClientMock(
                Response.Status.CREATED.getStatusCode(), course);
        Course results = client.createCourse(course.getName());
        assertEquals(course.getName(), results.getName());
    }
 
    @Test(expected = RestClientFailureException.class)
    public void testCreateCourseError() {
        CourseRestClient client = new CourseRestClientMock(500, null);
        client.createCourse(UUID);
    }
 
    @Test
    public void testUpdateCourse() {
        Course course = new Course();
        course.setUuid(UUID);
        course.setName(NAME);
        CourseRestClient client = new CourseRestClientMock(200, course);
        Course results = client
                .updateCourse(course.getUuid(), course.getName());
        assertEquals(course.getUuid(), results.getUuid());
        assertEquals(course.getName(), results.getName());
    }
 
    @Test(expected = ObjectNotFoundException.class)
    public void testUpdateCourseMissing() {
        CourseRestClient client = new CourseRestClientMock(404, null);
        client.updateCourse(UUID, NAME);
    }
 
    @Test(expected = RestClientFailureException.class)
    public void testUpdateCourseError() {
        CourseRestClient client = new CourseRestClientMock(500, null);
        client.updateCourse(UUID, NAME);
    }
 
    @Test
    public void testDeleteCourse() {
        Course course = new Course();
        course.setUuid(UUID);
        CourseRestClient client = new CourseRestClientMock(
                Response.Status.GONE.getStatusCode(), null);
        client.deleteCourse(course.getUuid());
    }
 
    @Test
    public void testDeleteCourseMissing() {
        CourseRestClient client = new CourseRestClientMock(404, null);
        client.deleteCourse(UUID);
    }
 
    @Test(expected = RestClientFailureException.class)
    public void testDeleteCourseError() {
        CourseRestClient client = new CourseRestClientMock(500, null);
        client.deleteCourse(UUID);
    }
}

Finally we need to create a to-be-tested object with a mocked REST client. We can’t use dependency injection since Client.createClient is a static method but we’ve wrapped that call in a package-private method that we can override. That method creates a mocked Client that provides the rest of the values required from Jersey library.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class CourseRestClientMock extends CourseRestClientImpl {
    static final String RESOURCE = "test://rest/course/";
    private Client client;
    private WebResource webResource;
    private WebResource.Builder webResourceBuilder;
    private ClientResponse response;
    private final int status;
    private final Object results;
 
    CourseRestClientMock(int status, Object results) {
        super(RESOURCE);
        this.status = status;
        this.results = results;
    }
 
    /**
     * Override createClient() so it returns mocked object. These expectations
     * will handle basic CRUD operations, more advanced functionality will
     * require inspecting JSON payload of POST call.
     */
    Client createClient() {
        client = Mockito.mock(Client.class);
        webResource = Mockito.mock(WebResource.class);
        webResourceBuilder = Mockito.mock(WebResource.Builder.class);
        response = Mockito.mock(ClientResponse.class);
        when(client.resource(any(String.class))).thenReturn(webResource);
        when(webResource.accept(any(String.class))).thenReturn(
                webResourceBuilder);
        when(webResource.type(any(String.class)))
                .thenReturn(webResourceBuilder);
        when(webResourceBuilder.accept(any(String.class))).thenReturn(
                webResourceBuilder);
        when(webResourceBuilder.type(any(String.class))).thenReturn(
                webResourceBuilder);
        when(webResourceBuilder.get(eq(ClientResponse.class))).thenReturn(
                response);
        when(
                webResourceBuilder.post(eq(ClientResponse.class),
                        any(String.class))).thenReturn(response);
        when(
                webResourceBuilder.put(eq(ClientResponse.class),
                        any(String.class))).thenReturn(response);
        when(webResourceBuilder.delete(eq(ClientResponse.class))).thenReturn(
                response);
        when(response.getStatus()).thenReturn(status);
        when(response.getEntity(any(Class.class))).thenReturn(results);
        return client;
    }
}

Integration Testing

This is the outermost layer of the onion so there’s no meaningful integration tests.

Source Code

 

Subscribe
Notify of
guest


This site uses Akismet to reduce spam. Learn how your comment data is processed.

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
blabla
blabla
11 years ago

nice tuto

Back to top button