View Javadoc
1   /*
2    * The coLAB project
3    * Copyright (C) 2021-2023 AlbaSim, MEI, HEIG-VD, HES-SO
4    *
5    * Licensed under the MIT License
6    */
7   package ch.colabproject.colab.generator.plugin.rest;
8   
9   import ch.colabproject.colab.generator.model.exceptions.HttpException;
10  import jakarta.json.bind.Jsonb;
11  import jakarta.json.bind.JsonbBuilder;
12  import jakarta.json.bind.JsonbException;
13  import jakarta.ws.rs.client.*;
14  import jakarta.ws.rs.core.*;
15  import jakarta.ws.rs.core.Response.Status;
16  import org.glassfish.jersey.media.multipart.FormDataMultiPart;
17  import org.glassfish.jersey.media.multipart.MultiPartFeature;
18  import org.glassfish.jersey.media.multipart.file.FileDataBodyPart;
19  
20  import java.io.File;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.nio.charset.StandardCharsets;
24  import java.util.ArrayList;
25  import java.util.List;
26  import java.util.Map;
27  
28  /**
29   * JakartaEE-based rest client.
30   *
31   * @author maxence
32   */
33  public class RestClient {
34  
35      /**
36       * Base URL.
37       */
38      private WebTarget webTarget;
39  
40      /**
41       * internal HTTP client.
42       */
43      private Client client;
44  
45      /**
46       * Jsonb mapper
47       */
48      private final Jsonb jsonb;
49  
50      /**
51       * Create a REST client.
52       *
53       * @param baseUri        base URL
54       * @param cookieName     session cookie name
55       * @param jsonb          jsonb mapper to use, if null a default mapper will be used
56       * @param clientFeatures REST client feature
57       */
58      public RestClient(String baseUri, String cookieName, Jsonb jsonb, Object... clientFeatures) {
59          ClientBuilder builder = ClientBuilder.newBuilder();
60  
61          builder.register(MultiPartFeature.class);
62  
63          for (Object feature : clientFeatures) {
64              builder.register(feature);
65          }
66  
67          if (cookieName != null && !cookieName.isBlank()) {
68              builder.register(new CookieFilter(cookieName));
69          }
70  
71          this.client = builder.build();
72          this.webTarget = client.target(baseUri);
73  
74          if (jsonb != null) {
75              this.jsonb = jsonb;
76          } else {
77              this.jsonb = JsonbBuilder.create();
78          }
79      }
80  
81      /**
82       * Close the client.
83       */
84      public void close() {
85          client.close();
86      }
87  
88      /**
89       * Try to read entity
90       *
91       * @param <T>    entity type
92       * @param stream input stream to read from
93       * @param type   entity type
94       *
95       * @return instance of T or null
96       */
97      private <T> T readEntity(InputStream stream, GenericType<T> type) {
98          try {
99              if (stream != null && stream.available() > 0) {
100                 return jsonb.fromJson(stream, type.getType());
101             } else {
102                 return null;
103             }
104         } catch (JsonbException | IOException ex) {
105             // silent ex
106             return null;
107         }
108     }
109 
110     /**
111      * Convert InputStream to string
112      *
113      * @param stream the stream to convert
114      *
115      * @return the content of the stream as string
116      */
117     private String readTextualEntity(InputStream stream) {
118         try {
119             return new String(stream.readAllBytes(), StandardCharsets.UTF_8);
120         } catch (IOException ex) {
121             // silent ex
122             return null;
123         }
124     }
125 
126     private <T> T processResponse(Response response, GenericType<T> type) {
127         Status.Family family = Status.Family.familyOf(response.getStatus());
128         if (family == Status.Family.SUCCESSFUL) {
129             if (response.getStatus() == 204) {
130                 return null;
131             } else {
132                 if (Response.class.isAssignableFrom(type.getRawType())) {
133                     return (T) response;
134                 }
135                 Object entity = response.getEntity();
136                 if (entity instanceof InputStream) {
137                     InputStream stream = (InputStream) entity;
138                     String contentType = response.getHeaderString("Content-Type");
139                     if (contentType.startsWith("application/json")
140                         || contentType.startsWith("text/json")) {
141                         return readEntity(stream, type);
142                     } else if (contentType.startsWith("text/plain")
143                         || contentType.startsWith("text/html")) {
144                         if (type.getRawType() == String.class) {
145                             return (T) readTextualEntity(stream);
146                         } else {
147                             return null;
148                         }
149                     } else {
150                         return null;
151                     }
152                 } else {
153                     return null;
154                 }
155             }
156         } else if (family == Status.Family.CLIENT_ERROR) {
157             HttpException error = readEntity((InputStream) response.getEntity(),
158                 new GenericType<>(HttpException.class));
159             if (error != null) {
160                 throw error;
161             } else {
162                 throw new ClientException(Status.fromStatusCode(response.getStatus()));
163             }
164         } else if (family == Status.Family.SERVER_ERROR) {
165             throw new ServerException(Status.fromStatusCode(response.getStatus()));
166         } else {
167             throw new ServerException(Status.fromStatusCode(response.getStatus()));
168         }
169     }
170 
171     /**
172      * Create FormData to send
173      *
174      * @param fields form content
175      *
176      * @return the formdata to send
177      */
178     private FormDataMultiPart getFormData(Map<String, FormField> fields) {
179         FormDataMultiPart multipart = new FormDataMultiPart();
180 
181         fields.forEach((fieldName, data) -> {
182             MediaType mimeType = data.getMimeType();
183             if (data.getData() instanceof File) {
184                 FileDataBodyPart filePart;
185                 if (mimeType != null) {
186                     filePart = new FileDataBodyPart(fieldName, (File) data.getData(), mimeType);
187                 } else {
188                     filePart = new FileDataBodyPart(fieldName, (File) data.getData());
189                 }
190                 multipart.bodyPart(filePart);
191             } else {
192                 if (mimeType != null) {
193                     multipart.field(fieldName, data.getData(), mimeType);
194                 } else {
195                     multipart.field(fieldName, data.getData().toString());
196                 }
197             }
198         });
199         return multipart;
200     }
201 
202     /**
203      * send GET request.
204      *
205      * @param <T>    expected return type
206      * @param path   rest path
207      * @param type   expected return generic type
208      * @param accept list of accepted MIME types
209      *
210      * @return instance of T
211      */
212     public <T> T get(String path, GenericType<T> type, String... accept) {
213         return processResponse(webTarget.path(path).request()
214             .accept(accept).get(),
215             type);
216     }
217 
218     /**
219      * send DELETE request.
220      *
221      * @param <T>    expected return type
222      * @param path   rest path
223      * @param type   expected return type
224      * @param accept list of accepted MIME types
225      *
226      * @return instance of T
227      */
228     public <T> T delete(String path, GenericType<T> type, String... accept) {
229         return processResponse(webTarget.path(path).request()
230             .accept(accept).delete(),
231             type);
232     }
233 
234     /**
235      * send JSON POST request.
236      *
237      * @param <T>    expected return type
238      * @param path   rest path
239      * @param body   POST body
240      * @param type   expected return type
241      * @param accept list of accepted MIME types
242      *
243      * @return instance of T
244      */
245     public <T> T post(String path, Object body, GenericType<T> type, String... accept) {
246         return processResponse(webTarget.path(path).request()
247             .accept(accept)
248             .post(Entity.entity(body, MediaType.APPLICATION_JSON_TYPE)),
249             type);
250     }
251 
252     /**
253      * send multipart form POST request.
254      *
255      * @param <T>    expected return type
256      * @param path   rest path
257      * @param fields form data
258      * @param type   expected return type
259      * @param accept list of accepted MIME types
260      *
261      * @return instance of T
262      */
263     public <T> T post(
264         String path,
265         Map<String, FormField> fields,
266         GenericType<T> type,
267         String... accept
268     ) {
269         FormDataMultiPart multipart = getFormData(fields);
270 
271         return processResponse(webTarget.path(path).request()
272             .accept(accept)
273             .post(Entity.entity(multipart, multipart.getMediaType())),
274             type);
275     }
276 
277     /**
278      * send POST request with an empty body.
279      *
280      * @param <T>    expected return type
281      * @param path   rest path
282      * @param type   expected return type
283      * @param accept list of accepted MIME types
284      *
285      * @return instance of T
286      */
287     public <T> T post(String path, GenericType<T> type, String... accept) {
288         return this.post(path, "", type, accept);
289     }
290 
291     /**
292      * send PUT request with JSON body.
293      *
294      * @param <T>    expected return type
295      * @param path   rest path
296      * @param body   POST body
297      * @param type   expected return type
298      * @param accept list of accepted MIME types
299      *
300      * @return instance of T
301      */
302     public <T> T put(String path, Object body, GenericType<T> type, String... accept) {
303         return processResponse(webTarget.path(path).request()
304             .accept(accept)
305             .put(Entity.entity(body, MediaType.APPLICATION_JSON_TYPE)),
306             type);
307     }
308 
309     /**
310      * send multipart form PUT request.
311      *
312      * @param <T>    expected return type
313      * @param path   rest path
314      * @param fields form data
315      * @param type   expected return type
316      * @param accept list of accepted MIME types
317      *
318      * @return instance of T
319      */
320     public <T> T put(String path, Map<String, FormField> fields, GenericType<T> type,
321         String... accept) {
322         FormDataMultiPart multipart = getFormData(fields);
323 
324         return processResponse(webTarget.path(path).request()
325             .accept(accept)
326             .put(Entity.entity(multipart, multipart.getMediaType())),
327             type);
328     }
329 
330     /**
331      * send PUT request with an empty body.
332      *
333      * @param <T>    expected return type
334      * @param path   rest path
335      * @param type   expected return type
336      * @param accept list of accepted MIME types
337      *
338      * @return instance of T
339      */
340     public <T> T put(String path, GenericType<T> type, String... accept) {
341         return this.put(path, "", type, accept);
342 
343     }
344 
345     /**
346      * Cookie filter make sure the session cookie is set. The sessionId is set by server response.
347      * After the first request, the previously saved sessionId is injected in all requests.
348      */
349     public static class CookieFilter implements ClientRequestFilter, ClientResponseFilter {
350 
351         /**
352          * sessionId
353          */
354         private String sessionId;
355 
356         /**
357          * Name of the cookie
358          */
359         private final String cookieName;
360 
361         /**
362          * New filter
363          *
364          * @param cookieName name of the cookie to manage
365          */
366         public CookieFilter(String cookieName) {
367             this.cookieName = cookieName;
368             this.sessionId = null;
369         }
370 
371         /**
372          * If set, add Cookie header to the request.
373          * <p>
374          * {@inheritDoc }
375          */
376         @Override
377         public void filter(ClientRequestContext requestContext) throws IOException {
378             if (this.sessionId != null) {
379                 List<Object> cookies = new ArrayList<>();
380                 Cookie cookie = new Cookie.Builder(cookieName)
381                         .value(sessionId)
382                         .build();
383                 cookies.add(cookie);
384                 requestContext.getHeaders().put("Cookie", cookies);
385             }
386         }
387 
388         /**
389          * On request response, fetch set-cookie value
390          * <p>
391          * {@inheritDoc }
392          */
393         @Override
394         public void filter(ClientRequestContext requestContext,
395             ClientResponseContext responseContext) throws IOException {
396             NewCookie get = responseContext.getCookies().get(cookieName);
397             if (get != null) {
398                 if (get.getMaxAge() == 0) {
399                     this.sessionId = null;
400                 } else {
401                     this.sessionId = get.getValue();
402                 }
403             }
404         }
405 
406     }
407 }