github.com/phrase/openapi@v0.0.0-20240514140800-49e8a106740e/openapi-generator/templates/java/libraries/vertx/ApiClient.mustache (about) 1 package {{invokerPackage}}; 2 3 import {{invokerPackage}}.auth.Authentication; 4 import {{invokerPackage}}.auth.HttpBasicAuth; 5 import {{invokerPackage}}.auth.HttpBearerAuth; 6 import {{invokerPackage}}.auth.ApiKeyAuth; 7 {{#hasOAuthMethods}} 8 import {{invokerPackage}}.auth.OAuth; 9 {{/hasOAuthMethods}} 10 11 import com.fasterxml.jackson.annotation.JsonInclude; 12 import com.fasterxml.jackson.core.type.TypeReference; 13 import com.fasterxml.jackson.databind.DeserializationFeature; 14 import com.fasterxml.jackson.databind.ObjectMapper; 15 import com.fasterxml.jackson.databind.SerializationFeature; 16 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 17 import org.openapitools.jackson.nullable.JsonNullableModule; 18 import io.vertx.core.*; 19 import io.vertx.core.buffer.Buffer; 20 import io.vertx.core.file.AsyncFile; 21 import io.vertx.core.file.FileSystem; 22 import io.vertx.core.file.OpenOptions; 23 import io.vertx.core.http.HttpHeaders; 24 import io.vertx.core.http.HttpMethod; 25 import io.vertx.core.json.DecodeException; 26 import io.vertx.core.json.Json; 27 import io.vertx.core.json.JsonObject; 28 import io.vertx.ext.web.client.HttpRequest; 29 import io.vertx.ext.web.client.HttpResponse; 30 import io.vertx.ext.web.client.WebClient; 31 import io.vertx.ext.web.client.WebClientOptions; 32 33 import java.text.DateFormat; 34 import java.util.*; 35 import java.util.function.Consumer; 36 import java.util.regex.Matcher; 37 import java.util.regex.Pattern; 38 39 import static java.util.stream.Collectors.toMap; 40 41 {{>generatedAnnotation}} 42 public class ApiClient { 43 44 private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern.compile("filename=['\"]?([^'\"\\s]+)['\"]?"); 45 private static final OpenOptions FILE_DOWNLOAD_OPTIONS = new OpenOptions().setCreate(true).setTruncateExisting(true); 46 47 private final Vertx vertx; 48 private final JsonObject config; 49 private final String identifier; 50 51 private MultiMap defaultHeaders = MultiMap.caseInsensitiveMultiMap(); 52 private MultiMap defaultCookies = MultiMap.caseInsensitiveMultiMap(); 53 private Map<String, Authentication> authentications; 54 private String basePath = "{{{basePath}}}"; 55 private DateFormat dateFormat; 56 private ObjectMapper objectMapper; 57 private String downloadsDir = ""; 58 59 public ApiClient(Vertx vertx, JsonObject config) { 60 Objects.requireNonNull(vertx, "Vertx must not be null"); 61 Objects.requireNonNull(config, "Config must not be null"); 62 63 this.vertx = vertx; 64 65 // Use RFC3339 format for date and datetime. 66 // See http://xml2rfc.ietf.org/public/rfc/html/rfc3339.html#anchor14 67 this.dateFormat = new RFC3339DateFormat(); 68 69 // Use UTC as the default time zone. 70 this.dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); 71 72 // Build object mapper 73 this.objectMapper = new ObjectMapper(); 74 this.objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); 75 this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 76 this.objectMapper.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false); 77 this.objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); 78 this.objectMapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING); 79 this.objectMapper.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING); 80 this.objectMapper.registerModule(new JavaTimeModule()); 81 this.objectMapper.setDateFormat(dateFormat); 82 JsonNullableModule jnm = new JsonNullableModule(); 83 this.objectMapper.registerModule(jnm); 84 85 // Setup authentications (key: authentication name, value: authentication). 86 this.authentications = new HashMap<>();{{#authMethods}}{{#isBasic}}{{#isBasicBasic}} 87 authentications.put("{{name}}", new HttpBasicAuth());{{/isBasicBasic}}{{^isBasicBasic}} 88 authentications.put("{{name}}", new HttpBearerAuth("{{scheme}}"));{{/isBasicBasic}}{{/isBasic}}{{#isApiKey}} 89 authentications.put("{{name}}", new ApiKeyAuth({{#isKeyInHeader}}"header"{{/isKeyInHeader}}{{#isKeyInQuery}}"query"{{/isKeyInQuery}}{{#isKeyInCookie}}"cookie"{{/isKeyInCookie}}, "{{keyParamName}}"));{{/isApiKey}}{{#isOAuth}} 90 authentications.put("{{name}}", new OAuth());{{/isOAuth}}{{/authMethods}} 91 // Prevent the authentications from being modified. 92 this.authentications = Collections.unmodifiableMap(authentications); 93 94 // Configurations 95 this.basePath = config.getString("basePath", this.basePath); 96 this.downloadsDir = config.getString("downloadsDir", this.downloadsDir); 97 this.config = config; 98 this.identifier = UUID.randomUUID().toString(); 99 } 100 101 public Vertx getVertx() { 102 return vertx; 103 } 104 105 public ObjectMapper getObjectMapper() { 106 return objectMapper; 107 } 108 109 public ApiClient setObjectMapper(ObjectMapper objectMapper) { 110 this.objectMapper = objectMapper; 111 return this; 112 } 113 114 public synchronized WebClient getWebClient() { 115 String webClientIdentifier = "web-client-" + identifier; 116 WebClient webClient = Vertx.currentContext().get(webClientIdentifier); 117 if (webClient == null) { 118 webClient = buildWebClient(vertx, config); 119 Vertx.currentContext().put(webClientIdentifier, webClient); 120 } 121 return webClient; 122 } 123 124 public String getBasePath() { 125 return basePath; 126 } 127 128 public ApiClient setBasePath(String basePath) { 129 this.basePath = basePath; 130 return this; 131 } 132 133 public String getDownloadsDir() { 134 return downloadsDir; 135 } 136 137 public ApiClient setDownloadsDir(String downloadsDir) { 138 this.downloadsDir = downloadsDir; 139 return this; 140 } 141 142 public MultiMap getDefaultHeaders() { 143 return defaultHeaders; 144 } 145 146 public ApiClient addDefaultHeader(String key, String value) { 147 defaultHeaders.add(key, value); 148 return this; 149 } 150 151 public MultiMap getDefaultCookies() { 152 return defaultHeaders; 153 } 154 155 public ApiClient addDefaultCookie(String key, String value) { 156 defaultCookies.add(key, value); 157 return this; 158 } 159 160 /** 161 * Get authentications (key: authentication name, value: authentication). 162 * 163 * @return Map of authentication object 164 */ 165 public Map<String, Authentication> getAuthentications() { 166 return authentications; 167 } 168 169 /** 170 * Get authentication for the given name. 171 * 172 * @param authName The authentication name 173 * @return The authentication, null if not found 174 */ 175 public Authentication getAuthentication(String authName) { 176 return authentications.get(authName); 177 } 178 179 /** 180 * Helper method to set access token for the first Bearer authentication. 181 * @param bearerToken Bearer token 182 */ 183 public ApiClient setBearerToken(String bearerToken) { 184 for (Authentication auth : authentications.values()) { 185 if (auth instanceof HttpBearerAuth) { 186 ((HttpBearerAuth) auth).setBearerToken(bearerToken); 187 return this; 188 } 189 } 190 throw new RuntimeException("No Bearer authentication configured!"); 191 } 192 193 /** 194 * Helper method to set username for the first HTTP basic authentication. 195 * 196 * @param username Username 197 */ 198 public ApiClient setUsername(String username) { 199 for (Authentication auth : authentications.values()) { 200 if (auth instanceof HttpBasicAuth) { 201 ((HttpBasicAuth) auth).setUsername(username); 202 return this; 203 } 204 } 205 throw new RuntimeException("No HTTP basic authentication configured!"); 206 } 207 208 /** 209 * Helper method to set password for the first HTTP basic authentication. 210 * 211 * @param password Password 212 */ 213 public ApiClient setPassword(String password) { 214 for (Authentication auth : authentications.values()) { 215 if (auth instanceof HttpBasicAuth) { 216 ((HttpBasicAuth) auth).setPassword(password); 217 return this; 218 } 219 } 220 throw new RuntimeException("No HTTP basic authentication configured!"); 221 } 222 223 /** 224 * Helper method to set API key value for the first API key authentication. 225 * 226 * @param apiKey API key 227 */ 228 public ApiClient setApiKey(String apiKey) { 229 for (Authentication auth : authentications.values()) { 230 if (auth instanceof ApiKeyAuth) { 231 ((ApiKeyAuth) auth).setApiKey(apiKey); 232 return this; 233 } 234 } 235 throw new RuntimeException("No API key authentication configured!"); 236 } 237 238 /** 239 * Helper method to set API key prefix for the first API key authentication. 240 * 241 * @param apiKeyPrefix API key prefix 242 */ 243 public ApiClient setApiKeyPrefix(String apiKeyPrefix) { 244 for (Authentication auth : authentications.values()) { 245 if (auth instanceof ApiKeyAuth) { 246 ((ApiKeyAuth) auth).setApiKeyPrefix(apiKeyPrefix); 247 return this; 248 } 249 } 250 throw new RuntimeException("No API key authentication configured!"); 251 } 252 253 {{#hasOAuthMethods}} 254 /** 255 * Helper method to set access token for the first OAuth2 authentication. 256 * 257 * @param accessToken Access token 258 */ 259 public ApiClient setAccessToken(String accessToken) { 260 for (Authentication auth : authentications.values()) { 261 if (auth instanceof OAuth) { 262 ((OAuth) auth).setAccessToken(accessToken); 263 return this; 264 } 265 } 266 throw new RuntimeException("No OAuth2 authentication configured!"); 267 } 268 269 {{/hasOAuthMethods}} 270 /** 271 * Format the given Date object into string. 272 * 273 * @param date Date 274 * @return Date in string format 275 */ 276 public String formatDate(Date date) { 277 return dateFormat.format(date); 278 } 279 280 /** 281 * Format the given parameter object into string. 282 * 283 * @param param Object 284 * @return Object in string format 285 */ 286 public String parameterToString(Object param) { 287 if (param == null) { 288 return ""; 289 } else if (param instanceof Date) { 290 return formatDate((Date) param); 291 } else if (param instanceof Collection) { 292 StringBuilder b = new StringBuilder(); 293 for (Object o : (Collection) param) { 294 if (b.length() > 0) { 295 b.append(','); 296 } 297 b.append(String.valueOf(o)); 298 } 299 return b.toString(); 300 } else { 301 return String.valueOf(param); 302 } 303 } 304 305 /* 306 * Format to {@code Pair} objects. 307 * @param collectionFormat Collection format 308 * @param name Name 309 * @param value Value 310 * @return List of pairs 311 */ 312 public List<Pair> parameterToPairs(String collectionFormat, String name, Object value) { 313 List<Pair> params = new ArrayList<Pair>(); 314 315 // preconditions 316 if (name == null || name.isEmpty() || value == null) return params; 317 318 Collection valueCollection; 319 if (value instanceof Collection) { 320 valueCollection = (Collection) value; 321 } else { 322 params.add(new Pair(name, parameterToString(value))); 323 return params; 324 } 325 326 if (valueCollection.isEmpty()) { 327 return params; 328 } 329 330 // get the collection format (default: csv) 331 String format = (collectionFormat == null || collectionFormat.isEmpty() ? "csv" : collectionFormat); 332 333 // create the params based on the collection format 334 if ("multi".equals(format)) { 335 for (Object item : valueCollection) { 336 params.add(new Pair(name, parameterToString(item))); 337 } 338 return params; 339 } 340 341 String delimiter = ","; 342 if ("csv".equals(format)) { 343 delimiter = ","; 344 } else if ("ssv".equals(format)) { 345 delimiter = " "; 346 } else if ("tsv".equals(format)) { 347 delimiter = "\t"; 348 } else if ("pipes".equals(format)) { 349 delimiter = "|"; 350 } 351 352 StringBuilder sb = new StringBuilder(); 353 for (Object item : valueCollection) { 354 sb.append(delimiter); 355 sb.append(parameterToString(item)); 356 } 357 358 params.add(new Pair(name, sb.substring(1))); 359 360 return params; 361 } 362 363 /** 364 * Check if the given MIME is a JSON MIME. 365 * JSON MIME examples: 366 * application/json 367 * application/json; charset=UTF8 368 * APPLICATION/JSON 369 * application/vnd.company+json 370 * 371 * @param mime MIME 372 * @return True if the MIME type is JSON 373 */ 374 private boolean isJsonMime(String mime) { 375 String jsonMime = "(?i)^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$"; 376 return mime != null && (mime.matches(jsonMime) || mime.equalsIgnoreCase("application/json-patch+json")); 377 } 378 379 /** 380 * Select the Accept header's value from the given accepts array: 381 * if JSON exists in the given array, use it; 382 * otherwise use all of them (joining into a string) 383 * 384 * @param accepts The accepts array to select from 385 * @return The Accept header to use. If the given array is empty, 386 * null will be returned (not to set the Accept header explicitly). 387 */ 388 protected String selectHeaderAccept(String[] accepts) { 389 if (accepts.length == 0) { 390 return null; 391 } 392 for (String accept : accepts) { 393 if (isJsonMime(accept)) { 394 return accept; 395 } 396 } 397 return StringUtil.join(accepts, ","); 398 } 399 400 /** 401 * Select the Content-Type header's value from the given array: 402 * if JSON exists in the given array, use it; 403 * otherwise use the first one of the array. 404 * 405 * @param contentTypes The Content-Type array to select from 406 * @return The Content-Type header to use. If the given array is empty, 407 * JSON will be used. 408 */ 409 protected String selectHeaderContentType(String[] contentTypes) { 410 if (contentTypes.length == 0) { 411 return "application/json"; 412 } 413 for (String contentType : contentTypes) { 414 if (isJsonMime(contentType)) { 415 return contentType; 416 } 417 } 418 return contentTypes[0]; 419 } 420 421 public void sendBody(HttpRequest<Buffer> request, 422 Handler<AsyncResult<HttpResponse<Buffer>>> responseHandler, 423 Object body) { 424 if (body instanceof byte[]) { 425 Buffer buffer = Buffer.buffer((byte[]) body); 426 request.sendBuffer(buffer, responseHandler); 427 } else if (body instanceof AsyncFile) { 428 AsyncFile file = (AsyncFile) body; 429 request.sendStream(file, responseHandler); 430 } else { 431 request.sendJson(body, responseHandler); 432 } 433 } 434 435 /** 436 * Invoke API by sending HTTP request with the given options. 437 * 438 * @param <T> Type 439 * @param path The sub-path of the HTTP URL 440 * @param method The request method, one of "GET", "POST", "PUT", "HEAD" and "DELETE" 441 * @param queryParams The query parameters 442 * @param body The request body object 443 * @param headerParams The header parameters 444 * @param cookieParams The cookie parameters 445 * @param formParams The form parameters 446 * @param accepts The request's Accept headers 447 * @param contentTypes The request's Content-Type headers 448 * @param authNames The authentications to apply 449 * @param returnType The return type into which to deserialize the response 450 * @param resultHandler The asynchronous response handler 451 */ 452 public <T> void invokeAPI(String path, String method, List<Pair> queryParams, Object body, MultiMap headerParams, 453 MultiMap cookieParams, Map<String, Object> formParams, String[] accepts, String[] contentTypes, String[] authNames, 454 TypeReference<T> returnType, Handler<AsyncResult<T>> resultHandler) { 455 456 updateParamsForAuth(authNames, queryParams, headerParams, cookieParams); 457 458 if (accepts != null && accepts.length > 0) { 459 headerParams.add(HttpHeaders.ACCEPT, selectHeaderAccept(accepts)); 460 } 461 462 if (contentTypes != null) { 463 headerParams.add(HttpHeaders.CONTENT_TYPE, selectHeaderContentType(contentTypes)); 464 } 465 466 HttpMethod httpMethod = HttpMethod.valueOf(method); 467 HttpRequest<Buffer> request = getWebClient().requestAbs(httpMethod, basePath + path); 468 469 if (httpMethod == HttpMethod.PATCH) { 470 request.putHeader("X-HTTP-Method-Override", "PATCH"); 471 } 472 473 queryParams.forEach(entry -> { 474 if (entry.getValue() != null) { 475 request.addQueryParam(entry.getName(), entry.getValue()); 476 } 477 }); 478 479 headerParams.forEach(entry -> { 480 if (entry.getValue() != null) { 481 request.putHeader(entry.getKey(), entry.getValue()); 482 } 483 }); 484 485 defaultHeaders.forEach(entry -> { 486 if (entry.getValue() != null) { 487 request.putHeader(entry.getKey(), entry.getValue()); 488 } 489 }); 490 491 final MultiMap cookies = MultiMap.caseInsensitiveMultiMap().addAll(cookieParams).addAll(defaultCookies); 492 request.putHeader("Cookie", buildCookieHeader(cookies)); 493 494 Handler<AsyncResult<HttpResponse<Buffer>>> responseHandler = buildResponseHandler(returnType, resultHandler); 495 if (body != null) { 496 sendBody(request, responseHandler, body); 497 } else if (formParams != null && !formParams.isEmpty()) { 498 Map<String, String> formMap = formParams.entrySet().stream().collect(toMap(Map.Entry::getKey, entry -> parameterToString(entry.getValue()))); 499 MultiMap form = MultiMap.caseInsensitiveMultiMap().addAll(formMap); 500 request.sendForm(form, responseHandler); 501 } else { 502 request.send(responseHandler); 503 } 504 } 505 506 private String buildCookieHeader(MultiMap cookies) { 507 final StringBuilder cookieValue = new StringBuilder(); 508 String delimiter = ""; 509 for (final Map.Entry<String, String> entry : cookies.entries()) { 510 if (entry.getValue() != null) { 511 cookieValue.append(String.format("%s%s=%s", delimiter, entry.getKey(), entry.getValue())); 512 delimiter = "; "; 513 } 514 } 515 return cookieValue.toString(); 516 } 517 518 /** 519 * Sanitize filename by removing path. 520 * e.g. ../../sun.gif becomes sun.gif 521 * 522 * @param filename The filename to be sanitized 523 * @return The sanitized filename 524 */ 525 protected String sanitizeFilename(String filename) { 526 return filename.replaceAll(".*[/\\\\]", ""); 527 } 528 529 /** 530 * Create a filename from the given headers. 531 * When the headers have no "Content-Disposition" information, a random UUID name is generated. 532 * 533 * @param headers The HTTP response headers 534 * @return The filename 535 */ 536 protected String generateFilename(MultiMap headers) { 537 String filename = UUID.randomUUID().toString(); 538 String contentDisposition = headers.get("Content-Disposition"); 539 if (contentDisposition != null && !contentDisposition.isEmpty()) { 540 Matcher matcher = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition); 541 if (matcher.find()) { 542 filename = sanitizeFilename(matcher.group(1)); 543 } 544 } 545 return filename; 546 } 547 548 /** 549 * File Download handling. 550 * 551 * @param response The HTTP response 552 * @param handler The response handler 553 */ 554 protected <T> void handleFileDownload(HttpResponse<Buffer> response, Handler<AsyncResult<T>> handler) { 555 FileSystem fs = getVertx().fileSystem(); 556 557 String filename = generateFilename(response.headers()); 558 Consumer<String> fileHandler = directory -> { 559 fs.open(directory + filename, FILE_DOWNLOAD_OPTIONS, asyncFileResult -> { 560 if (asyncFileResult.succeeded()) { 561 AsyncFile asyncFile = asyncFileResult.result(); 562 asyncFile.write(response.bodyAsBuffer()); 563 //noinspection unchecked 564 handler.handle(Future.succeededFuture((T) asyncFile)); 565 } else { 566 handler.handle(ApiException.fail(asyncFileResult.cause())); 567 } 568 }); 569 }; 570 571 String dir = getDownloadsDir(); 572 if (dir != null && !dir.isEmpty()) { 573 fs.mkdirs(dir, mkdirResult -> { 574 String sanitizedFolder = dir.endsWith("/") ? dir : dir + "/"; 575 fileHandler.accept(sanitizedFolder); 576 }); 577 } else { 578 fileHandler.accept(""); 579 } 580 } 581 582 /** 583 * Build a response handler for the HttpResponse. 584 * 585 * @param returnType The return type 586 * @param handler The response handler 587 * @return The HTTP response handler 588 */ 589 protected <T> Handler<AsyncResult<HttpResponse<Buffer>>> buildResponseHandler(TypeReference<T> returnType, 590 Handler<AsyncResult<T>> handler) { 591 return response -> { 592 AsyncResult<T> result; 593 if (response.succeeded()) { 594 HttpResponse<Buffer> httpResponse = response.result(); 595 if (httpResponse.statusCode() / 100 == 2) { 596 if (httpResponse.statusCode() == 204 || returnType == null) { 597 result = Future.succeededFuture(null); 598 } else { 599 T resultContent = null; 600 if ("byte[]".equals(returnType.getType().toString())) { 601 resultContent = (T) httpResponse.body().getBytes(); 602 result = Future.succeededFuture(resultContent); 603 } else if (AsyncFile.class.equals(returnType.getType())) { 604 handleFileDownload(httpResponse, handler); 605 return; 606 } else { 607 try { 608 resultContent = this.objectMapper.readValue(httpResponse.bodyAsString(), returnType); 609 result = Future.succeededFuture(resultContent); 610 } catch (Exception e) { 611 result = ApiException.fail(new DecodeException("Failed to decode:" + e.getMessage(), e)); 612 } 613 } 614 } 615 } else { 616 result = ApiException.fail(httpResponse.statusMessage(), httpResponse.statusCode(), httpResponse.headers(), httpResponse.bodyAsString()); 617 } 618 } else if (response.cause() instanceof ApiException) { 619 result = Future.failedFuture(response.cause()); 620 } else { 621 result = ApiException.fail(500, response.cause() != null ? response.cause().getMessage() : null); 622 } 623 handler.handle(result); 624 }; 625 } 626 627 /** 628 * Build the WebClient used to make HTTP requests. 629 * 630 * @param vertx Vertx 631 * @return WebClient 632 */ 633 protected WebClient buildWebClient(Vertx vertx, JsonObject config) { 634 635 if (!config.containsKey("userAgent")) { 636 config.put("userAgent", "{{#httpUserAgent}}{{{.}}}{{/httpUserAgent}}{{^httpUserAgent}}OpenAPI-Generator/{{{artifactVersion}}}/java{{/httpUserAgent}}"); 637 } 638 639 return WebClient.create(vertx, new WebClientOptions(config)); 640 } 641 642 643 /** 644 * Update query and header parameters based on authentication settings. 645 * 646 * @param authNames The authentications to apply 647 */ 648 protected void updateParamsForAuth(String[] authNames, List<Pair> queryParams, MultiMap headerParams, MultiMap cookieParams) { 649 for (String authName : authNames) { 650 Authentication auth = authentications.get(authName); 651 if (auth == null) throw new RuntimeException("Authentication undefined: " + authName); 652 auth.applyToParams(queryParams, headerParams, cookieParams); 653 } 654 } 655 }