View Javadoc
1   package fr.tiogars.chat.client;
2   
3   import com.fasterxml.jackson.core.type.TypeReference;
4   import com.fasterxml.jackson.databind.ObjectMapper;
5   import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
6   
7   import java.io.IOException;
8   import java.net.URI;
9   import java.net.http.HttpClient;
10  import java.net.http.HttpRequest;
11  import java.net.http.HttpResponse;
12  import java.util.List;
13  
14  /**
15   * Provides HTTP access to the Task API.
16   *
17   * @since 0.1.0
18   */
19  public class TaskClient {
20  
21      private static final String API_PATH = "/api/v1/chat";
22      private static final String APPLICATION_JSON = "application/json";
23      private static final String TEXT_CSV = "text/csv";
24      private static final TypeReference<List<Task>> TEMPLATE_LIST = new TypeReference<>() {
25      };
26  
27      private final URI baseUri;
28      private final HttpClient httpClient;
29      private final ObjectMapper objectMapper;
30  
31      /**
32       * Creates a client backed by the default Java HTTP client.
33       *
34       * @param baseUri base URI of the server
35       */
36      public TaskClient(URI baseUri) {
37          this(baseUri, HttpClient.newHttpClient(), new ObjectMapper().registerModule(new JavaTimeModule())
38                  .disable(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS));
39      }
40  
41      TaskClient(URI baseUri, HttpClient httpClient, ObjectMapper objectMapper) {
42          this.baseUri = baseUri;
43          this.httpClient = httpClient;
44          this.objectMapper = objectMapper;
45      }
46  
47      /**
48       * Creates a Task.
49       *
50       * @param request template data to create
51       * @return template created by the server
52       * @throws IOException if request serialization or response deserialization fails
53       * @throws InterruptedException if the thread is interrupted while awaiting the response
54       * @throws TaskException if the server returns an unexpected HTTP status
55       */
56      public Task create(CreateTaskRequest request) throws IOException, InterruptedException {
57          return sendJson(jsonRequest(uri(""), "POST", request), Task.class, 201);
58      }
59  
60      /**
61       * Lists all Tasks.
62       *
63       * @return templates returned by the server
64       * @throws IOException if response deserialization fails
65       * @throws InterruptedException if the thread is interrupted while awaiting the response
66       * @throws TaskException if the server returns an unexpected HTTP status
67       */
68      public List<Task> findAll() throws IOException, InterruptedException {
69          return sendJson(getRequest(uri(""), APPLICATION_JSON), TEMPLATE_LIST, 200);
70      }
71  
72      /**
73       * Finds a Task by identifier.
74       *
75       * @param id persistent template identifier
76       * @return matching template
77       * @throws IOException if response deserialization fails
78       * @throws InterruptedException if the thread is interrupted while awaiting the response
79       * @throws TaskException if the server returns an unexpected HTTP status
80       */
81      public Task findById(long id) throws IOException, InterruptedException {
82          return sendJson(getRequest(uri("/" + id), APPLICATION_JSON), Task.class, 200);
83      }
84  
85      /**
86       * Updates a Task.
87       *
88       * @param id persistent template identifier
89       * @param request replacement label and description
90       * @return template updated by the server
91       * @throws IOException if request serialization or response deserialization fails
92       * @throws InterruptedException if the thread is interrupted while awaiting the response
93       * @throws TaskException if the server returns an unexpected HTTP status
94       */
95      public Task update(long id, UpdateTaskRequest request) throws IOException, InterruptedException {
96          return sendJson(jsonRequest(uri("/" + id), "PUT", request), Task.class, 200);
97      }
98  
99      /**
100      * Deletes a Task.
101      *
102      * @param id persistent template identifier
103      * @throws IOException if the request fails
104      * @throws InterruptedException if the thread is interrupted while awaiting the response
105      * @throws TaskException if the server returns an unexpected HTTP status
106      */
107     public void delete(long id) throws IOException, InterruptedException {
108         HttpRequest request = HttpRequest.newBuilder(uri("/" + id)).DELETE().build();
109         send(request, 204);
110     }
111 
112     /**
113      * Seeds Tasks from structured requests.
114      *
115      * @param requests templates to seed
116      * @return templates stored by the server
117      * @throws IOException if request serialization or response deserialization fails
118      * @throws InterruptedException if the thread is interrupted while awaiting the response
119      * @throws TaskException if the server returns an unexpected HTTP status
120      */
121     public List<Task> seed(List<CreateTaskRequest> requests) throws IOException, InterruptedException {
122         return sendJson(jsonRequest(uri("/seed"), "POST", requests), TEMPLATE_LIST, 201);
123     }
124 
125     /**
126      * Imports Tasks from JSON-compatible requests.
127      *
128      * @param requests templates to import
129      * @return templates imported by the server
130      * @throws IOException if request serialization or response deserialization fails
131      * @throws InterruptedException if the thread is interrupted while awaiting the response
132      * @throws TaskException if the server returns an unexpected HTTP status
133      */
134     public List<Task> importJson(List<CreateTaskRequest> requests)
135             throws IOException, InterruptedException {
136         return sendJson(jsonRequest(uri("/import.json"), "POST", requests), TEMPLATE_LIST, 201);
137     }
138 
139     /**
140      * Imports Tasks from a CSV document.
141      *
142      * @param csv CSV document to import
143      * @return templates imported by the server
144      * @throws IOException if the request or response deserialization fails
145      * @throws InterruptedException if the thread is interrupted while awaiting the response
146      * @throws TaskException if the server returns an unexpected HTTP status
147      */
148     public List<Task> importCsv(String csv) throws IOException, InterruptedException {
149         HttpRequest request = HttpRequest.newBuilder(uri("/import.csv"))
150                 .header("Content-Type", TEXT_CSV)
151                 .header("Accept", APPLICATION_JSON)
152                 .POST(HttpRequest.BodyPublishers.ofString(csv))
153                 .build();
154         return sendJson(request, TEMPLATE_LIST, 201);
155     }
156 
157     /**
158      * Exports all Tasks as JSON-compatible objects.
159      *
160      * @return templates exported by the server
161      * @throws IOException if response deserialization fails
162      * @throws InterruptedException if the thread is interrupted while awaiting the response
163      * @throws TaskException if the server returns an unexpected HTTP status
164      */
165     public List<Task> exportJson() throws IOException, InterruptedException {
166         return sendJson(getRequest(uri("/export.json"), APPLICATION_JSON), TEMPLATE_LIST, 200);
167     }
168 
169     /**
170      * Exports all Tasks as CSV.
171      *
172      * @return CSV document returned by the server
173      * @throws IOException if the request fails
174      * @throws InterruptedException if the thread is interrupted while awaiting the response
175      * @throws TaskException if the server returns an unexpected HTTP status
176      */
177     public String exportCsv() throws IOException, InterruptedException {
178         return send(getRequest(uri("/export.csv"), TEXT_CSV), 200).body();
179     }
180 
181     private HttpRequest jsonRequest(URI uri, String method, Object body) throws IOException {
182         return HttpRequest.newBuilder(uri)
183                 .header("Content-Type", APPLICATION_JSON)
184                 .header("Accept", APPLICATION_JSON)
185                 .method(method, HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)))
186                 .build();
187     }
188 
189     private HttpRequest getRequest(URI uri, String accept) {
190         return HttpRequest.newBuilder(uri).header("Accept", accept).GET().build();
191     }
192 
193     private <T> T sendJson(HttpRequest request, Class<T> responseType, int expectedStatus)
194             throws IOException, InterruptedException {
195         return objectMapper.readValue(send(request, expectedStatus).body(), responseType);
196     }
197 
198     private <T> T sendJson(HttpRequest request, TypeReference<T> responseType, int expectedStatus)
199             throws IOException, InterruptedException {
200         return objectMapper.readValue(send(request, expectedStatus).body(), responseType);
201     }
202 
203     private HttpResponse<String> send(HttpRequest request, int expectedStatus) throws IOException, InterruptedException {
204         HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
205         if (response.statusCode() != expectedStatus) {
206             throw new TaskException(
207                     "Expected HTTP " + expectedStatus + " but received " + response.statusCode());
208         }
209         return response;
210     }
211 
212     private URI uri(String suffix) {
213         return baseUri.resolve(API_PATH + suffix);
214     }
215 }