From 4aaaae765b1cf6281b8f8ac1e8db0be2d1acd316 Mon Sep 17 00:00:00 2001 From: William Cheng Date: Wed, 28 Apr 2021 14:53:51 +0800 Subject: [PATCH] [Scala][Akka] Use Files.createTempFile to address security concerns (#9348) * fix scala akka server temp file issue * update test templates --- .../multipartDirectives.mustache | 3 +- .../2_0/templates/Java/ApiClient.mustache | 318 +++++-- .../Java/libraries/jersey2/ApiClient.mustache | 816 +++++++++++++++--- .../Java/libraries/jersey2/JSON.mustache | 247 +++++- .../Java/libraries/jersey2/api.mustache | 171 +++- .../libraries/jersey2/build.gradle.mustache | 84 +- .../Java/libraries/jersey2/build.sbt.mustache | 45 +- .../Java/libraries/jersey2/pom.mustache | 202 +++-- .../2_0/templates/Java/model.mustache | 39 +- .../.openapi-generator/FILES | 15 + .../.openapi-generator/VERSION | 2 +- .../server/MultipartDirectives.scala | 3 +- 12 files changed, 1608 insertions(+), 337 deletions(-) create mode 100644 samples/server/petstore/scala-akka-http-server/.openapi-generator/FILES diff --git a/modules/openapi-generator/src/main/resources/scala-akka-http-server/multipartDirectives.mustache b/modules/openapi-generator/src/main/resources/scala-akka-http-server/multipartDirectives.mustache index 98a2186fd2e7..6e802204c57d 100644 --- a/modules/openapi-generator/src/main/resources/scala-akka-http-server/multipartDirectives.mustache +++ b/modules/openapi-generator/src/main/resources/scala-akka-http-server/multipartDirectives.mustache @@ -1,6 +1,7 @@ package {{invokerPackage}} import java.io.File +import java.nio.file.Files import akka.annotation.ApiMayChange import akka.http.scaladsl.model.Multipart.FormData @@ -69,7 +70,7 @@ trait MultipartDirectives { object MultipartDirectives extends MultipartDirectives with FileUploadDirectives { val tempFileFromFileInfo: FileInfo => File = { - file: FileInfo => File.createTempFile(file.fileName, ".tmp") + file: FileInfo => Files.createTempFile(file.fileName, ".tmp").toFile() } } diff --git a/modules/openapi-generator/src/test/resources/2_0/templates/Java/ApiClient.mustache b/modules/openapi-generator/src/test/resources/2_0/templates/Java/ApiClient.mustache index 116f14c86503..d75e455961c5 100644 --- a/modules/openapi-generator/src/test/resources/2_0/templates/Java/ApiClient.mustache +++ b/modules/openapi-generator/src/test/resources/2_0/templates/Java/ApiClient.mustache @@ -1,28 +1,38 @@ -//overloaded main template file to add this comment - {{>licenseInfo}} package {{invokerPackage}}; +{{#threetenbp}} +import org.threeten.bp.*; + +{{/threetenbp}} import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.*; +{{#joda}} +import com.fasterxml.jackson.datatype.joda.JodaModule; +{{/joda}} {{#java8}} -import com.fasterxml.jackson.datatype.jsr310.*; -{{/java8}} -{{^java8}} -import com.fasterxml.jackson.datatype.joda.*; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +{{^threetenbp}} +import java.time.OffsetDateTime; +{{/threetenbp}} {{/java8}} +{{#threetenbp}} +import com.fasterxml.jackson.datatype.threetenbp.ThreeTenModule; +{{/threetenbp}} import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider; import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.GenericType; import com.sun.jersey.api.client.config.DefaultClientConfig; +import com.sun.jersey.api.client.filter.GZIPContentEncodingFilter; import com.sun.jersey.api.client.filter.LoggingFilter; import com.sun.jersey.api.client.WebResource.Builder; import com.sun.jersey.multipart.FormDataMultiPart; import com.sun.jersey.multipart.file.FileDataBodyPart; +import javax.ws.rs.core.Cookie; import javax.ws.rs.core.Response.Status.Family; import javax.ws.rs.core.MediaType; @@ -31,7 +41,9 @@ import java.util.Collections; import java.util.Map; import java.util.Map.Entry; import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Arrays; import java.util.ArrayList; import java.util.Date; import java.util.TimeZone; @@ -44,14 +56,51 @@ import java.io.UnsupportedEncodingException; import java.text.DateFormat; import {{invokerPackage}}.auth.Authentication; +{{#hasHttpBasicMethods}} import {{invokerPackage}}.auth.HttpBasicAuth; +{{/hasHttpBasicMethods}} +{{#hasHttpBearerMethods}} +import {{invokerPackage}}.auth.HttpBearerAuth; +{{/hasHttpBearerMethods}} +{{#hasApiKeyMethods}} import {{invokerPackage}}.auth.ApiKeyAuth; +{{/hasApiKeyMethods}} +{{#hasOAuthMethods}} import {{invokerPackage}}.auth.OAuth; +{{/hasOAuthMethods}} {{>generatedAnnotation}} -public class ApiClient { +public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} { private Map defaultHeaderMap = new HashMap(); - private String basePath = "{{basePath}}"; + private Map defaultCookieMap = new HashMap(); + private String basePath = "{{{basePath}}}"; + protected List servers = new ArrayList({{#servers}}{{#-first}}Arrays.asList( +{{/-first}} new ServerConfiguration( + "{{{url}}}", + "{{{description}}}{{^description}}No description provided{{/description}}", + new HashMap(){{#variables}}{{#-first}} {{ +{{/-first}} put("{{{name}}}", new ServerVariable( + "{{{description}}}{{^description}}No description provided{{/description}}", + "{{{defaultValue}}}", + new HashSet( + {{#enumValues}} + {{#-first}} + Arrays.asList( + {{/-first}} + "{{{.}}}"{{^-last}},{{/-last}} + {{#-last}} + ) + {{/-last}} + {{/enumValues}} + ) + )); + {{#-last}} + }}{{/-last}}{{/variables}} + ){{^-last}},{{/-last}} + {{#-last}} + ){{/-last}}{{/servers}}); + protected Integer serverIndex = 0; + protected Map serverVariables = null; private boolean debugging = false; private int connectionTimeout = 0; @@ -69,15 +118,23 @@ public class ApiClient { objectMapper = new ObjectMapper(); objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + objectMapper.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false); objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); objectMapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING); objectMapper.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING); + {{#joda}} + objectMapper.registerModule(new JodaModule()); + {{/joda}} {{#java8}} objectMapper.registerModule(new JavaTimeModule()); {{/java8}} - {{^java8}} - objectMapper.registerModule(new JodaModule()); - {{/java8}} + {{#threetenbp}} + ThreeTenModule module = new ThreeTenModule(); + module.addDeserializer(Instant.class, CustomInstantDeserializer.INSTANT); + module.addDeserializer(OffsetDateTime.class, CustomInstantDeserializer.OFFSET_DATE_TIME); + module.addDeserializer(ZonedDateTime.class, CustomInstantDeserializer.ZONED_DATE_TIME); + objectMapper.registerModule(module); + {{/threetenbp}} objectMapper.setDateFormat(ApiClient.buildDefaultDateFormat()); dateFormat = ApiClient.buildDefaultDateFormat(); @@ -86,8 +143,9 @@ public class ApiClient { setUserAgent("{{#httpUserAgent}}{{{.}}}{{/httpUserAgent}}{{^httpUserAgent}}OpenAPI-Generator/{{{artifactVersion}}}/java{{/httpUserAgent}}"); // Setup authentications (key: authentication name, value: authentication). - authentications = new HashMap();{{#authMethods}}{{#isBasic}} - authentications.put("{{name}}", new HttpBasicAuth());{{/isBasic}}{{#isApiKey}} + authentications = new HashMap();{{#authMethods}}{{#isBasic}}{{#isBasicBasic}} + authentications.put("{{name}}", new HttpBasicAuth());{{/isBasicBasic}}{{^isBasicBasic}} + authentications.put("{{name}}", new HttpBearerAuth("{{scheme}}"));{{/isBasicBasic}}{{/isBasic}}{{#isApiKey}} authentications.put("{{name}}", new ApiKeyAuth({{#isKeyInHeader}}"header"{{/isKeyInHeader}}{{^isKeyInHeader}}"query"{{/isKeyInHeader}}, "{{keyParamName}}"));{{/isApiKey}}{{#isOAuth}} authentications.put("{{name}}", new OAuth());{{/isOAuth}}{{/authMethods}} // Prevent the authentications from being modified. @@ -112,6 +170,7 @@ public class ApiClient { DefaultClientConfig conf = new DefaultClientConfig(); conf.getSingletons().add(jsonProvider); Client client = Client.create(conf); + client.addFilter(new GZIPContentEncodingFilter({{#useGzipFeature}}true{{/useGzipFeature}}{{^useGzipFeature}}false{{/useGzipFeature}})); if (debugging) { client.addFilter(new LoggingFilter()); } @@ -156,6 +215,33 @@ public class ApiClient { return this; } + public List getServers() { + return servers; + } + + public ApiClient setServers(List servers) { + this.servers = servers; + return this; + } + + public Integer getServerIndex() { + return serverIndex; + } + + public ApiClient setServerIndex(Integer serverIndex) { + this.serverIndex = serverIndex; + return this; + } + + public Map getServerVariables() { + return serverVariables; + } + + public ApiClient setServerVariables(Map serverVariables) { + this.serverVariables = serverVariables; + return this; + } + /** * Gets the status code of the previous request * @return Status code @@ -190,6 +276,24 @@ public class ApiClient { return authentications.get(authName); } + {{#hasHttpBearerMethods}} + /** + * Helper method to set access token for the first Bearer authentication. + * @param bearerToken Bearer token + */ + public void setBearerToken(String bearerToken) { + for (Authentication auth : authentications.values()) { + if (auth instanceof HttpBearerAuth) { + ((HttpBearerAuth) auth).setBearerToken(bearerToken); + return; + } + } + throw new RuntimeException("No Bearer authentication configured!"); + } + + {{/hasHttpBearerMethods}} + + {{#hasHttpBasicMethods}} /** * Helper method to set username for the first HTTP basic authentication. * @param username Username @@ -218,9 +322,12 @@ public class ApiClient { throw new RuntimeException("No HTTP basic authentication configured!"); } + {{/hasHttpBasicMethods}} + + {{#hasApiKeyMethods}} /** * Helper method to set API key value for the first API key authentication. - * @param apiKey API key + * @param apiKey the API key */ public void setApiKey(String apiKey) { for (Authentication auth : authentications.values()) { @@ -246,6 +353,9 @@ public class ApiClient { throw new RuntimeException("No API key authentication configured!"); } + {{/hasApiKeyMethods}} + + {{#hasOAuthMethods}} /** * Helper method to set access token for the first OAuth2 authentication. * @param accessToken Access token @@ -260,6 +370,8 @@ public class ApiClient { throw new RuntimeException("No OAuth2 authentication configured!"); } + {{/hasOAuthMethods}} + /** * Set the User-Agent header's value (by adding to the default header map). * @param userAgent User agent @@ -282,6 +394,18 @@ public class ApiClient { return this; } + /** + * Add a default cookie. + * + * @param key The cookie's key + * @param value The cookie's value + * @return API client + */ + public ApiClient addDefaultCookie(String key, String value) { + defaultCookieMap.put(key, value); + return this; + } + /** * Check that whether debugging is enabled for this API client. * @return True if debugging is on @@ -378,7 +502,9 @@ public class ApiClient { return ""; } else if (param instanceof Date) { return formatDate((Date) param); - } else if (param instanceof Collection) { + } {{#jsr310}}else if (param instanceof OffsetDateTime) { + return formatOffsetDateTime((OffsetDateTime) param); + } {{/jsr310}}else if (param instanceof Collection) { StringBuilder b = new StringBuilder(); for(Object o : (Collection)param) { if(b.length() > 0) { @@ -392,62 +518,71 @@ public class ApiClient { } } - /* - * Format to {@code Pair} objects. - * @param collectionFormat Collection format - * @param name Name - * @param value Value - * @return List of pair + /** + * Formats the specified query parameter to a list containing a single {@code Pair} object. + * + * Note that {@code value} must not be a collection. + * + * @param name The name of the parameter. + * @param value The value of the parameter. + * @return A list containing a single {@code Pair} object. */ - public List parameterToPairs(String collectionFormat, String name, Object value){ + public List parameterToPair(String name, Object value) { List params = new ArrayList(); // preconditions - if (name == null || name.isEmpty() || value == null) return params; + if (name == null || name.isEmpty() || value == null || value instanceof Collection) return params; - Collection valueCollection; - if (value instanceof Collection) { - valueCollection = (Collection) value; - } else { - params.add(new Pair(name, parameterToString(value))); + params.add(new Pair(name, parameterToString(value))); + return params; + } + + /** + * Formats the specified collection query parameters to a list of {@code Pair} objects. + * + * Note that the values of each of the returned Pair objects are percent-encoded. + * + * @param collectionFormat The collection format of the parameter. + * @param name The name of the parameter. + * @param value The value of the parameter. + * @return A list of {@code Pair} objects. + */ + public List parameterToPairs(String collectionFormat, String name, Collection value) { + List params = new ArrayList(); + + // preconditions + if (name == null || name.isEmpty() || value == null) { return params; } - if (valueCollection.isEmpty()){ - return params; - } - - // get the collection format - String format = (collectionFormat == null || collectionFormat.isEmpty() ? "csv" : collectionFormat); // default: csv - // create the params based on the collection format - if ("multi".equals(format)) { - for (Object item : valueCollection) { - params.add(new Pair(name, parameterToString(item))); + if ("multi".equals(collectionFormat)) { + for (Object item : value) { + params.add(new Pair(name, escapeString(parameterToString(item)))); } - return params; } + // collectionFormat is assumed to be "csv" by default String delimiter = ","; - if ("csv".equals(format)) { - delimiter = ","; - } else if ("ssv".equals(format)) { - delimiter = " "; - } else if ("tsv".equals(format)) { - delimiter = "\t"; - } else if ("pipes".equals(format)) { - delimiter = "|"; + // escape all delimiters except commas, which are URI reserved + // characters + if ("ssv".equals(collectionFormat)) { + delimiter = escapeString(" "); + } else if ("tsv".equals(collectionFormat)) { + delimiter = escapeString("\t"); + } else if ("pipes".equals(collectionFormat)) { + delimiter = escapeString("|"); } StringBuilder sb = new StringBuilder() ; - for (Object item : valueCollection) { + for (Object item : value) { sb.append(delimiter); - sb.append(parameterToString(item)); + sb.append(escapeString(parameterToString(item))); } - params.add(new Pair(name, sb.substring(1))); + params.add(new Pair(name, sb.substring(delimiter.length()))); return params; } @@ -458,11 +593,13 @@ public class ApiClient { * application/json * application/json; charset=UTF8 * APPLICATION/JSON + * application/vnd.company+json * @param mime MIME * @return True if MIME type is boolean */ public boolean isJsonMime(String mime) { - return mime != null && mime.matches("(?i)application\\/json(;.*)?"); + String jsonMime = "(?i)^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$"; + return mime != null && (mime.matches(jsonMime) || mime.equals("*/*")); } /** @@ -493,10 +630,10 @@ public class ApiClient { * * @param contentTypes The Content-Type array to select from * @return The Content-Type header to use. If the given array is empty, - * JSON will be used. + * or matches "any", JSON will be used. */ public String selectHeaderContentType(String[] contentTypes) { - if (contentTypes.length == 0) { + if (contentTypes.length == 0 || contentTypes[0].equals("*/*")) { return "application/json"; } for (String contentType : contentTypes) { @@ -561,11 +698,24 @@ public class ApiClient { * * @param path The sub path * @param queryParams The query parameters + * @param collectionQueryParams The collection query parameters * @return The full URL */ - private String buildUrl(String path, List queryParams) { + private String buildUrl(String path, List queryParams, List collectionQueryParams) { + String baseURL; + if (serverIndex != null) { + if (serverIndex < 0 || serverIndex >= servers.size()) { + throw new ArrayIndexOutOfBoundsException(String.format( + "Invalid index %d when selecting the host settings. Must be less than %d", serverIndex, servers.size() + )); + } + baseURL = servers.get(serverIndex).URL(serverVariables); + } else { + baseURL = basePath; + } + final StringBuilder url = new StringBuilder(); - url.append(basePath).append(path); + url.append(baseURL).append(path); if (queryParams != null && !queryParams.isEmpty()) { // support (constant) query string in `path`, e.g. "/posts?draft=1" @@ -584,17 +734,34 @@ public class ApiClient { } } + if (collectionQueryParams != null && !collectionQueryParams.isEmpty()) { + String prefix = url.toString().contains("?") ? "&" : "?"; + for (Pair param : collectionQueryParams) { + if (param.getValue() != null) { + if (prefix != null) { + url.append(prefix); + prefix = null; + } else { + url.append("&"); + } + String value = parameterToString(param.getValue()); + // collection query parameter value already escaped as part of parameterToPairs + url.append(escapeString(param.getName())).append("=").append(value); + } + } + } + return url.toString(); } - private ClientResponse getAPIResponse(String path, String method, List queryParams, Object body, Map headerParams, Map formParams, String accept, String contentType, String[] authNames) throws ApiException { + private ClientResponse getAPIResponse(String path, String method, List queryParams, List collectionQueryParams, Object body, Map headerParams, Map cookieParams, Map formParams, String accept, String contentType, String[] authNames) throws ApiException { if (body != null && !formParams.isEmpty()) { throw new ApiException(500, "Cannot have body and form params"); } - updateParamsForAuth(authNames, queryParams, headerParams); + updateParamsForAuth(authNames, queryParams, headerParams, cookieParams); - final String url = buildUrl(path, queryParams); + final String url = buildUrl(path, queryParams, collectionQueryParams); Builder builder; if (accept == null) { builder = httpClient.resource(url).getRequestBuilder(); @@ -602,12 +769,21 @@ public class ApiClient { builder = httpClient.resource(url).accept(accept); } - for (String key : headerParams.keySet()) { - builder = builder.header(key, headerParams.get(key)); + for (Entry keyValue : headerParams.entrySet()) { + builder = builder.header(keyValue.getKey(), keyValue.getValue()); } - for (String key : defaultHeaderMap.keySet()) { - if (!headerParams.containsKey(key)) { - builder = builder.header(key, defaultHeaderMap.get(key)); + for (Map.Entry keyValue : defaultHeaderMap.entrySet()) { + if (!headerParams.containsKey(keyValue.getKey())) { + builder = builder.header(keyValue.getKey(), keyValue.getValue()); + } + } + + for (Entry keyValue : cookieParams.entrySet()) { + builder = builder.cookie(new Cookie(keyValue.getKey(), keyValue.getValue())); + } + for (Map.Entry keyValue : defaultCookieMap.entrySet()) { + if (!cookieParams.containsKey(keyValue.getKey())) { + builder = builder.cookie(new Cookie(keyValue.getKey(), keyValue.getValue())); } } @@ -623,8 +799,9 @@ public class ApiClient { response = builder.type(contentType).delete(ClientResponse.class, serialize(body, contentType, formParams)); } else if ("PATCH".equals(method)) { response = builder.type(contentType).header("X-HTTP-Method-Override", "PATCH").post(ClientResponse.class, serialize(body, contentType, formParams)); - } - else { + } else if ("HEAD".equals(method)) { + response = builder.head(); + } else { throw new ApiException(500, "unknown method type " + method); } return response; @@ -637,8 +814,10 @@ public class ApiClient { * @param path The sub-path of the HTTP URL * @param method The request method, one of "GET", "POST", "PUT", and "DELETE" * @param queryParams The query parameters + * @param collectionQueryParams The collection query parameters * @param body The request body object - if it is not binary, otherwise null * @param headerParams The header parameters + * @param cookieParams The cookie parameters * @param formParams The form parameters * @param accept The request's Accept header * @param contentType The request's Content-Type header @@ -647,9 +826,9 @@ public class ApiClient { * @return The response body in type of string * @throws ApiException API exception */ - public T invokeAPI(String path, String method, List queryParams, Object body, Map headerParams, Map formParams, String accept, String contentType, String[] authNames, GenericType returnType) throws ApiException { + public T invokeAPI(String path, String method, List queryParams, List collectionQueryParams, Object body, Map headerParams, Map cookieParams, Map formParams, String accept, String contentType, String[] authNames, GenericType returnType) throws ApiException { - ClientResponse response = getAPIResponse(path, method, queryParams, body, headerParams, formParams, accept, contentType, authNames); + ClientResponse response = getAPIResponse(path, method, queryParams, collectionQueryParams, body, headerParams, cookieParams, formParams, accept, contentType, authNames); statusCode = response.getStatusInfo().getStatusCode(); responseHeaders = response.getHeaders(); @@ -686,12 +865,13 @@ public class ApiClient { * @param authNames The authentications to apply * @param queryParams Query parameters * @param headerParams Header parameters + * @param cookieParams Cookie parameters */ - private void updateParamsForAuth(String[] authNames, List queryParams, Map headerParams) { + private void updateParamsForAuth(String[] authNames, List queryParams, Map headerParams, Map cookieParams) { for (String authName : authNames) { Authentication auth = authentications.get(authName); if (auth == null) throw new RuntimeException("Authentication undefined: " + authName); - auth.applyToParams(queryParams, headerParams); + auth.applyToParams(queryParams, headerParams, cookieParams); } } diff --git a/modules/openapi-generator/src/test/resources/2_0/templates/Java/libraries/jersey2/ApiClient.mustache b/modules/openapi-generator/src/test/resources/2_0/templates/Java/libraries/jersey2/ApiClient.mustache index 52c3c06e24cc..65494e8ea04a 100644 --- a/modules/openapi-generator/src/test/resources/2_0/templates/Java/libraries/jersey2/ApiClient.mustache +++ b/modules/openapi-generator/src/test/resources/2_0/templates/Java/libraries/jersey2/ApiClient.mustache @@ -1,5 +1,3 @@ -//overloaded template file within library folder to add this comment - package {{invokerPackage}}; import javax.ws.rs.client.Client; @@ -13,9 +11,12 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; +{{#hasOAuthMethods}} +import com.github.scribejava.core.model.OAuth2AccessToken; +{{/hasOAuthMethods}} import org.glassfish.jersey.client.ClientConfig; import org.glassfish.jersey.client.ClientProperties; -import org.glassfish.jersey.filter.LoggingFilter; +import org.glassfish.jersey.client.HttpUrlConnectorProvider; import org.glassfish.jersey.jackson.JacksonFeature; import org.glassfish.jersey.media.multipart.FormDataBodyPart; import org.glassfish.jersey.media.multipart.FormDataContentDisposition; @@ -25,22 +26,38 @@ import org.glassfish.jersey.media.multipart.MultiPartFeature; import java.io.IOException; import java.io.InputStream; -{{^supportJava6}} +import java.net.URI; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.security.cert.X509Certificate; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; import java.nio.file.Files; +import java.nio.file.Paths; import java.nio.file.StandardCopyOption; -{{/supportJava6}} -{{#supportJava6}} -import org.apache.commons.io.FileUtils; -{{/supportJava6}} +import org.glassfish.jersey.logging.LoggingFeature; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.Collection; import java.util.Collections; import java.util.Map; import java.util.Map.Entry; import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Arrays; import java.util.ArrayList; import java.util.Date; -import java.util.TimeZone; +{{#jsr310}} +{{#threetenbp}} +import org.threeten.bp.OffsetDateTime; +{{/threetenbp}} +{{^threetenbp}} +import java.time.OffsetDateTime; +{{/threetenbp}} +{{/jsr310}} import java.net.URLEncoder; @@ -53,30 +70,120 @@ import java.util.regex.Pattern; import {{invokerPackage}}.auth.Authentication; import {{invokerPackage}}.auth.HttpBasicAuth; +import {{invokerPackage}}.auth.HttpBearerAuth; +{{#hasHttpSignatureMethods}} +import {{invokerPackage}}.auth.HttpSignatureAuth; +{{/hasHttpSignatureMethods}} import {{invokerPackage}}.auth.ApiKeyAuth; +{{#hasOAuthMethods}} import {{invokerPackage}}.auth.OAuth; +{{/hasOAuthMethods}} {{>generatedAnnotation}} -public class ApiClient { - private Map defaultHeaderMap = new HashMap(); - private String basePath = "{{{basePath}}}"; - private boolean debugging = false; - private int connectionTimeout = 0; +public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} { + protected Map defaultHeaderMap = new HashMap(); + protected Map defaultCookieMap = new HashMap(); + protected String basePath = "{{{basePath}}}"; + protected String userAgent; + private static final Logger log = Logger.getLogger(ApiClient.class.getName()); - private Client httpClient; - private JSON json; - private String tempFolderPath = null; + protected List servers = new ArrayList({{#servers}}{{#-first}}Arrays.asList( +{{/-first}} new ServerConfiguration( + "{{{url}}}", + "{{{description}}}{{^description}}No description provided{{/description}}", + new HashMap(){{#variables}}{{#-first}} {{ +{{/-first}} put("{{{name}}}", new ServerVariable( + "{{{description}}}{{^description}}No description provided{{/description}}", + "{{{defaultValue}}}", + new HashSet( + {{#enumValues}} + {{#-first}} + Arrays.asList( + {{/-first}} + "{{{.}}}"{{^-last}},{{/-last}} + {{#-last}} + ) + {{/-last}} + {{/enumValues}} + ) + )); + {{#-last}} + }}{{/-last}}{{/variables}} + ){{^-last}},{{/-last}} + {{#-last}} + ){{/-last}}{{/servers}}); + protected Integer serverIndex = 0; + protected Map serverVariables = null; + protected Map> operationServers = new HashMap>() {{ + {{#apiInfo}} + {{#apis}} + {{#operations}} + {{#operation}} + {{#servers}} + {{#-first}} + put("{{{classname}}}.{{{operationId}}}", new ArrayList(Arrays.asList( + {{/-first}} + new ServerConfiguration( + "{{{url}}}", + "{{{description}}}{{^description}}No description provided{{/description}}", + new HashMap(){{#variables}}{{#-first}} {{ +{{/-first}} put("{{{name}}}", new ServerVariable( + "{{{description}}}{{^description}}No description provided{{/description}}", + "{{{defaultValue}}}", + new HashSet( + {{#enumValues}} + {{#-first}} + Arrays.asList( + {{/-first}} + "{{{.}}}"{{^-last}},{{/-last}} + {{#-last}} + ) + {{/-last}} + {{/enumValues}} + ) + )); + {{#-last}} + }}{{/-last}}{{/variables}} + ){{^-last}},{{/-last}} + {{#-last}} + )));{{/-last}} + {{/servers}} + {{/operation}} + {{/operations}} + {{/apis}} + {{/apiInfo}} + }}; + protected Map operationServerIndex = new HashMap(); + protected Map> operationServerVariables = new HashMap>(); + protected boolean debugging = false; + protected ClientConfig clientConfig; + protected int connectionTimeout = 0; + private int readTimeout = 0; - private Map authentications; + protected Client httpClient; + protected JSON json; + protected String tempFolderPath = null; - private int statusCode; - private Map> responseHeaders; + protected Map authentications; + protected Map authenticationLookup; - private DateFormat dateFormat; + protected DateFormat dateFormat; + /** + * Constructs a new ApiClient with default parameters. + */ public ApiClient() { + this(null); + } + + /** + * Constructs a new ApiClient with the specified authentication parameters. + * + * @param authMap A hash map containing authentication parameters. + */ + public ApiClient(Map authMap) { json = new JSON(); - httpClient = buildHttpClient(debugging); + httpClient = buildHttpClient(); this.dateFormat = new RFC3339DateFormat(); @@ -84,16 +191,59 @@ public class ApiClient { setUserAgent("{{#httpUserAgent}}{{{.}}}{{/httpUserAgent}}{{^httpUserAgent}}OpenAPI-Generator/{{{artifactVersion}}}/java{{/httpUserAgent}}"); // Setup authentications (key: authentication name, value: authentication). - authentications = new HashMap();{{#authMethods}}{{#isBasic}} - authentications.put("{{name}}", new HttpBasicAuth());{{/isBasic}}{{#isApiKey}} - authentications.put("{{name}}", new ApiKeyAuth({{#isKeyInHeader}}"header"{{/isKeyInHeader}}{{^isKeyInHeader}}"query"{{/isKeyInHeader}}, "{{keyParamName}}"));{{/isApiKey}}{{#isOAuth}} - authentications.put("{{name}}", new OAuth());{{/isOAuth}}{{/authMethods}} + authentications = new HashMap(); + Authentication auth = null; + {{#authMethods}} + if (authMap != null) { + auth = authMap.get("{{name}}"); + } + {{#isBasic}} + {{#isBasicBasic}} + if (auth instanceof HttpBasicAuth) { + authentications.put("{{name}}", auth); + } else { + authentications.put("{{name}}", new HttpBasicAuth()); + } + {{/isBasicBasic}} + {{#isBasicBearer}} + if (auth instanceof HttpBearerAuth) { + authentications.put("{{name}}", auth); + } else { + authentications.put("{{name}}", new HttpBearerAuth("{{scheme}}")); + } + {{/isBasicBearer}} + {{#isHttpSignature}} + if (auth instanceof HttpSignatureAuth) { + authentications.put("{{name}}", auth); + } + {{/isHttpSignature}} + {{/isBasic}} + {{#isApiKey}} + if (auth instanceof ApiKeyAuth) { + authentications.put("{{name}}", auth); + } else { + authentications.put("{{name}}", new ApiKeyAuth({{#isKeyInHeader}}"header"{{/isKeyInHeader}}{{^isKeyInHeader}}"query"{{/isKeyInHeader}}, "{{keyParamName}}")); + } + {{/isApiKey}} + {{#isOAuth}} + if (auth instanceof OAuth) { + authentications.put("{{name}}", auth); + } else { + authentications.put("{{name}}", new OAuth(basePath, "{{tokenUrl}}")); + } + {{/isOAuth}} + {{/authMethods}} // Prevent the authentications from being modified. authentications = Collections.unmodifiableMap(authentications); + + // Setup authentication lookup (key: authentication alias, value: authentication name) + authenticationLookup = new HashMap();{{#authMethods}}{{#vendorExtensions.x-auth-id-alias}} + authenticationLookup.put("{{name}}", "{{.}}");{{/vendorExtensions.x-auth-id-alias}}{{/authMethods}} } /** * Gets the JSON instance to do JSON serialization and deserialization. + * * @return JSON */ public JSON getJSON() { @@ -109,33 +259,77 @@ public class ApiClient { return this; } + /** + * Returns the base URL to the location where the OpenAPI document is being served. + * + * @return The base URL to the target host. + */ public String getBasePath() { return basePath; } + /** + * Sets the base URL to the location where the OpenAPI document is being served. + * + * @param basePath The base URL to the target host. + */ public ApiClient setBasePath(String basePath) { this.basePath = basePath; + {{#hasOAuthMethods}} + setOauthBasePath(basePath); + {{/hasOAuthMethods}} return this; } - /** - * Gets the status code of the previous request - * @return Status code - */ - public int getStatusCode() { - return statusCode; + public List getServers() { + return servers; } - /** - * Gets the response headers of the previous request - * @return Response headers - */ - public Map> getResponseHeaders() { - return responseHeaders; + public ApiClient setServers(List servers) { + this.servers = servers; + updateBasePath(); + return this; } + public Integer getServerIndex() { + return serverIndex; + } + + public ApiClient setServerIndex(Integer serverIndex) { + this.serverIndex = serverIndex; + updateBasePath(); + return this; + } + + public Map getServerVariables() { + return serverVariables; + } + + public ApiClient setServerVariables(Map serverVariables) { + this.serverVariables = serverVariables; + updateBasePath(); + return this; + } + + private void updateBasePath() { + if (serverIndex != null) { + setBasePath(servers.get(serverIndex).URL(serverVariables)); + } + } + + {{#hasOAuthMethods}} + private void setOauthBasePath(String basePath) { + for(Authentication auth : authentications.values()) { + if (auth instanceof OAuth) { + ((OAuth) auth).setBasePath(basePath); + } + } + } + + {{/hasOAuthMethods}} /** * Get authentications (key: authentication name, value: authentication). + * * @return Map of authentication object */ public Map getAuthentications() { @@ -154,13 +348,14 @@ public class ApiClient { /** * Helper method to set username for the first HTTP basic authentication. + * * @param username Username */ - public void setUsername(String username) { + public ApiClient setUsername(String username) { for (Authentication auth : authentications.values()) { if (auth instanceof HttpBasicAuth) { ((HttpBasicAuth) auth).setUsername(username); - return; + return this; } } throw new RuntimeException("No HTTP basic authentication configured!"); @@ -168,13 +363,14 @@ public class ApiClient { /** * Helper method to set password for the first HTTP basic authentication. + * * @param password Password */ - public void setPassword(String password) { + public ApiClient setPassword(String password) { for (Authentication auth : authentications.values()) { if (auth instanceof HttpBasicAuth) { ((HttpBasicAuth) auth).setPassword(password); - return; + return this; } } throw new RuntimeException("No HTTP basic authentication configured!"); @@ -182,56 +378,167 @@ public class ApiClient { /** * Helper method to set API key value for the first API key authentication. + * * @param apiKey API key */ - public void setApiKey(String apiKey) { + public ApiClient setApiKey(String apiKey) { for (Authentication auth : authentications.values()) { if (auth instanceof ApiKeyAuth) { ((ApiKeyAuth) auth).setApiKey(apiKey); - return; + return this; } } throw new RuntimeException("No API key authentication configured!"); } + /** + * Helper method to configure authentications which respects aliases of API keys. + * + * @param secrets Hash map from authentication name to its secret. + */ + public ApiClient configureApiKeys(Map secrets) { + for (Map.Entry authEntry : authentications.entrySet()) { + Authentication auth = authEntry.getValue(); + if (auth instanceof ApiKeyAuth) { + String name = authEntry.getKey(); + // respect x-auth-id-alias property + name = authenticationLookup.containsKey(name) ? authenticationLookup.get(name) : name; + if (secrets.containsKey(name)) { + ((ApiKeyAuth) auth).setApiKey(secrets.get(name)); + } + } + } + return this; + } + /** * Helper method to set API key prefix for the first API key authentication. + * * @param apiKeyPrefix API key prefix */ - public void setApiKeyPrefix(String apiKeyPrefix) { + public ApiClient setApiKeyPrefix(String apiKeyPrefix) { for (Authentication auth : authentications.values()) { if (auth instanceof ApiKeyAuth) { ((ApiKeyAuth) auth).setApiKeyPrefix(apiKeyPrefix); - return; + return this; } } throw new RuntimeException("No API key authentication configured!"); } + /** + * Helper method to set bearer token for the first Bearer authentication. + * + * @param bearerToken Bearer token + */ + public ApiClient setBearerToken(String bearerToken) { + for (Authentication auth : authentications.values()) { + if (auth instanceof HttpBearerAuth) { + ((HttpBearerAuth) auth).setBearerToken(bearerToken); + return this; + } + } + throw new RuntimeException("No Bearer authentication configured!"); + } + + + {{#hasOAuthMethods}} /** * Helper method to set access token for the first OAuth2 authentication. * @param accessToken Access token */ - public void setAccessToken(String accessToken) { + public ApiClient setAccessToken(String accessToken) { for (Authentication auth : authentications.values()) { if (auth instanceof OAuth) { ((OAuth) auth).setAccessToken(accessToken); - return; + return this; } } throw new RuntimeException("No OAuth2 authentication configured!"); } + /** + * Helper method to set the credentials for the first OAuth2 authentication. + * + * @param clientId the client ID + * @param clientSecret the client secret + */ + public ApiClient setOauthCredentials(String clientId, String clientSecret) { + for (Authentication auth : authentications.values()) { + if (auth instanceof OAuth) { + ((OAuth) auth).setCredentials(clientId, clientSecret, isDebugging()); + return this; + } + } + throw new RuntimeException("No OAuth2 authentication configured!"); + } + + /** + * Helper method to set the password flow for the first OAuth2 authentication. + * + * @param username the user name + * @param password the user password + */ + public ApiClient setOauthPasswordFlow(String username, String password) { + for (Authentication auth : authentications.values()) { + if (auth instanceof OAuth) { + ((OAuth) auth).usePasswordFlow(username, password); + return this; + } + } + throw new RuntimeException("No OAuth2 authentication configured!"); + } + + /** + * Helper method to set the authorization code flow for the first OAuth2 authentication. + * + * @param code the authorization code + */ + public ApiClient setOauthAuthorizationCodeFlow(String code) { + for (Authentication auth : authentications.values()) { + if (auth instanceof OAuth) { + ((OAuth) auth).useAuthorizationCodeFlow(code); + return this; + } + } + throw new RuntimeException("No OAuth2 authentication configured!"); + } + + /** + * Helper method to set the scopes for the first OAuth2 authentication. + * + * @param scope the oauth scope + */ + public ApiClient setOauthScope(String scope) { + for (Authentication auth : authentications.values()) { + if (auth instanceof OAuth) { + ((OAuth) auth).setScope(scope); + return this; + } + } + throw new RuntimeException("No OAuth2 authentication configured!"); + } + + {{/hasOAuthMethods}} /** * Set the User-Agent header's value (by adding to the default header map). * @param userAgent Http user agent * @return API client */ public ApiClient setUserAgent(String userAgent) { + userAgent = userAgent; addDefaultHeader("User-Agent", userAgent); return this; } + /** + * Get the User-Agent header's value. + * @return User-Agent string + */ + public String getUserAgent(){ + return userAgent; + } + /** * Add a default header. * @@ -244,6 +551,39 @@ public class ApiClient { return this; } + /** + * Add a default cookie. + * + * @param key The cookie's key + * @param value The cookie's value + * @return API client + */ + public ApiClient addDefaultCookie(String key, String value) { + defaultCookieMap.put(key, value); + return this; + } + + /** + * Gets the client config. + * @return Client config + */ + public ClientConfig getClientConfig() { + return clientConfig; + } + + /** + * Set the client config. + * + * @param clientConfig Set the client config + * @return API client + */ + public ApiClient setClientConfig(ClientConfig clientConfig) { + this.clientConfig = clientConfig; + // Rebuild HTTP Client according to the new "clientConfig" value. + this.httpClient = buildHttpClient(); + return this; + } + /** * Check that whether debugging is enabled for this API client. * @return True if debugging is switched on @@ -261,7 +601,7 @@ public class ApiClient { public ApiClient setDebugging(boolean debugging) { this.debugging = debugging; // Rebuild HTTP Client according to the new "debugging" value. - this.httpClient = buildHttpClient(debugging); + this.httpClient = buildHttpClient(); return this; } @@ -307,6 +647,27 @@ public class ApiClient { return this; } + /** + * read timeout (in milliseconds). + * @return Read timeout + */ + public int getReadTimeout() { + return readTimeout; + } + + /** + * Set the read timeout (in milliseconds). + * A value of 0 means no timeout, otherwise values must be between 1 and + * {@link Integer#MAX_VALUE}. + * @param readTimeout Read timeout in milliseconds + * @return API client + */ + public ApiClient setReadTimeout(int readTimeout) { + this.readTimeout = readTimeout; + httpClient.property(ClientProperties.READ_TIMEOUT, readTimeout); + return this; + } + /** * Get the date format used to parse/format date parameters. * @return Date format @@ -359,7 +720,9 @@ public class ApiClient { return ""; } else if (param instanceof Date) { return formatDate((Date) param); - } else if (param instanceof Collection) { + } {{#jsr310}}else if (param instanceof OffsetDateTime) { + return formatOffsetDateTime((OffsetDateTime) param); + } {{/jsr310}}else if (param instanceof Collection) { StringBuilder b = new StringBuilder(); for(Object o : (Collection)param) { if(b.length() > 0) { @@ -439,11 +802,14 @@ public class ApiClient { * application/json * application/json; charset=UTF8 * APPLICATION/JSON + * application/vnd.company+json + * "* / *" is also default to JSON * @param mime MIME * @return True if the MIME type is JSON */ public boolean isJsonMime(String mime) { - return mime != null && mime.matches("(?i)application\\/json(;.*)?"); + String jsonMime = "(?i)^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$"; + return mime != null && (mime.matches(jsonMime) || mime.equals("*/*")); } /** @@ -510,7 +876,7 @@ public class ApiClient { * @return Entity * @throws ApiException API exception */ - public Entity serialize(Object obj, Map formParams, String contentType) throws ApiException { + public Entity serialize(Object obj, Map formParams, String contentType, boolean isBodyNullable) throws ApiException { Entity entity; if (contentType.startsWith("multipart/form-data")) { MultiPart multiPart = new MultiPart(); @@ -534,11 +900,60 @@ public class ApiClient { entity = Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE); } else { // We let jersey handle the serialization - entity = Entity.entity(obj, contentType); + if (isBodyNullable) { // payload is nullable + if (obj instanceof String) { + entity = Entity.entity(obj == null ? "null" : "\"" + ((String)obj).replaceAll("\"", Matcher.quoteReplacement("\\\"")) + "\"", contentType); + } else { + entity = Entity.entity(obj == null ? "null" : obj, contentType); + } + } else { + if (obj instanceof String) { + entity = Entity.entity(obj == null ? "" : "\"" + ((String)obj).replaceAll("\"", Matcher.quoteReplacement("\\\"")) + "\"", contentType); + } else { + entity = Entity.entity(obj == null ? "" : obj, contentType); + } + } } return entity; } + /** + * Serialize the given Java object into string according the given + * Content-Type (only JSON, HTTP form is supported for now). + * @param obj Object + * @param formParams Form parameters + * @param contentType Context type + * @param isBodyNullable True if the body is nullable + * @return String + * @throws ApiException API exception + */ + public String serializeToString(Object obj, Map formParams, String contentType, boolean isBodyNullable) throws ApiException { + try { + if (contentType.startsWith("multipart/form-data")) { + throw new ApiException("multipart/form-data not yet supported for serializeToString (http signature authentication)"); + } else if (contentType.startsWith("application/x-www-form-urlencoded")) { + String formString = ""; + for (Entry param : formParams.entrySet()) { + formString = param.getKey() + "=" + URLEncoder.encode(parameterToString(param.getValue()), "UTF-8") + "&"; + } + + if (formString.length() == 0) { // empty string + return formString; + } else { + return formString.substring(0, formString.length() - 1); + } + } else { + if (isBodyNullable) { + return obj == null ? "null" : json.getMapper().writeValueAsString(obj); + } else { + return obj == null ? "" : json.getMapper().writeValueAsString(obj); + } + } + } catch (Exception ex) { + throw new ApiException("Failed to perform serializeToString: " + ex.toString()); + } + } + /** * Deserialize response body to Java object according to the Content-Type. * @param Type @@ -566,8 +981,9 @@ public class ApiClient { List contentTypes = response.getHeaders().get("Content-Type"); if (contentTypes != null && !contentTypes.isEmpty()) contentType = String.valueOf(contentTypes.get(0)); - if (contentType == null) - throw new ApiException(500, "missing Content-Type in response"); + + // read the entity stream multiple times + response.bufferEntity(); return response.readEntity(returnType); } @@ -581,13 +997,7 @@ public class ApiClient { public File downloadFileFromResponse(Response response) throws ApiException { try { File file = prepareDownloadFile(response); -{{^supportJava6}} Files.copy(response.readEntity(InputStream.class), file.toPath(), StandardCopyOption.REPLACE_EXISTING); -{{/supportJava6}} -{{#supportJava6}} - // Java6 falls back to commons.io for file copying - FileUtils.copyToFile(response.readEntity(InputStream.class), file); -{{/supportJava6}} return file; } catch (IOException e) { throw new ApiException(e); @@ -618,132 +1028,286 @@ public class ApiClient { prefix = filename.substring(0, pos) + "-"; suffix = filename.substring(pos); } - // File.createTempFile requires the prefix to be at least three characters long + // Files.createTempFile requires the prefix to be at least three characters long if (prefix.length() < 3) prefix = "download-"; } if (tempFolderPath == null) - return File.createTempFile(prefix, suffix); + return Files.createTempFile(prefix, suffix).toFile(); else - return File.createTempFile(prefix, suffix, new File(tempFolderPath)); + return Files.createTempFile(Paths.get(tempFolderPath), prefix, suffix).toFile(); } /** * Invoke API by sending HTTP request with the given options. * * @param Type + * @param operation The qualified name of the operation * @param path The sub-path of the HTTP URL - * @param method The request method, one of "GET", "POST", "PUT", and "DELETE" + * @param method The request method, one of "GET", "POST", "PUT", "HEAD" and "DELETE" * @param queryParams The query parameters * @param body The request body object * @param headerParams The header parameters + * @param cookieParams The cookie parameters * @param formParams The form parameters * @param accept The request's Accept header * @param contentType The request's Content-Type header * @param authNames The authentications to apply * @param returnType The return type into which to deserialize the response + * @param isBodyNullable True if the body is nullable * @return The response body in type of string * @throws ApiException API exception */ - public T invokeAPI(String path, String method, List queryParams, Object body, Map headerParams, Map formParams, String accept, String contentType, String[] authNames, GenericType returnType) throws ApiException { - updateParamsForAuth(authNames, queryParams, headerParams); + public ApiResponse invokeAPI( + String operation, + String path, + String method, + List queryParams, + Object body, + Map headerParams, + Map cookieParams, + Map formParams, + String accept, + String contentType, + String[] authNames, + GenericType returnType, + boolean isBodyNullable) + throws ApiException { - // Not using `.target(this.basePath).path(path)` below, + // Not using `.target(targetURL).path(path)` below, // to support (constant) query string in `path`, e.g. "/posts?draft=1" - WebTarget target = httpClient.target(this.basePath + path); + String targetURL; + if (serverIndex != null && operationServers.containsKey(operation)) { + Integer index = operationServerIndex.containsKey(operation) ? operationServerIndex.get(operation) : serverIndex; + Map variables = operationServerVariables.containsKey(operation) ? + operationServerVariables.get(operation) : serverVariables; + List serverConfigurations = operationServers.get(operation); + if (index < 0 || index >= serverConfigurations.size()) { + throw new ArrayIndexOutOfBoundsException( + String.format( + "Invalid index %d when selecting the host settings. Must be less than %d", + index, serverConfigurations.size())); + } + targetURL = serverConfigurations.get(index).URL(variables) + path; + } else { + targetURL = this.basePath + path; + } + WebTarget target = httpClient.target(targetURL); if (queryParams != null) { for (Pair queryParam : queryParams) { if (queryParam.getValue() != null) { - target = target.queryParam(queryParam.getName(), queryParam.getValue()); + target = target.queryParam(queryParam.getName(), escapeString(queryParam.getValue())); } } } Invocation.Builder invocationBuilder = target.request().accept(accept); - for (Entry entry : headerParams.entrySet()) { + for (Entry entry : cookieParams.entrySet()) { + String value = entry.getValue(); + if (value != null) { + invocationBuilder = invocationBuilder.cookie(entry.getKey(), value); + } + } + + for (Entry entry : defaultCookieMap.entrySet()) { + String value = entry.getValue(); + if (value != null) { + invocationBuilder = invocationBuilder.cookie(entry.getKey(), value); + } + } + + Entity entity = serialize(body, formParams, contentType, isBodyNullable); + + // put all headers in one place + Map allHeaderParams = new HashMap<>(defaultHeaderMap); + allHeaderParams.putAll(headerParams); + + // update different parameters (e.g. headers) for authentication + updateParamsForAuth( + authNames, + queryParams, + allHeaderParams, + cookieParams, + serializeToString(body, formParams, contentType, isBodyNullable), + method, + target.getUri()); + + for (Entry entry : allHeaderParams.entrySet()) { String value = entry.getValue(); if (value != null) { invocationBuilder = invocationBuilder.header(entry.getKey(), value); } } - for (Entry entry : defaultHeaderMap.entrySet()) { - String key = entry.getKey(); - if (!headerParams.containsKey(key)) { - String value = entry.getValue(); - if (value != null) { - invocationBuilder = invocationBuilder.header(key, value); + Response response = null; + + try { + response = sendRequest(method, invocationBuilder, entity); + + {{#hasOAuthMethods}} + // If OAuth is used and a status 401 is received, renew the access token and retry the request + if (response.getStatusInfo() == Status.UNAUTHORIZED) { + for (String authName : authNames) { + Authentication authentication = authentications.get(authName); + if (authentication instanceof OAuth) { + OAuth2AccessToken accessToken = ((OAuth) authentication).renewAccessToken(); + if (accessToken != null) { + invocationBuilder.header("Authorization", null); + invocationBuilder.header("Authorization", "Bearer " + accessToken.getAccessToken()); + response = sendRequest(method, invocationBuilder, entity); + } + break; + } } } + + {{/hasOAuthMethods}} + int statusCode = response.getStatusInfo().getStatusCode(); + Map> responseHeaders = buildResponseHeaders(response); + + if (response.getStatusInfo() == Status.NO_CONTENT) { + return new ApiResponse(statusCode, responseHeaders); + } else if (response.getStatusInfo().getFamily() == Status.Family.SUCCESSFUL) { + if (returnType == null) { + return new ApiResponse(statusCode, responseHeaders); + } else { + return new ApiResponse(statusCode, responseHeaders, deserialize(response, returnType)); + } + } else { + String message = "error"; + String respBody = null; + if (response.hasEntity()) { + try { + respBody = String.valueOf(response.readEntity(String.class)); + message = respBody; + } catch (RuntimeException e) { + // e.printStackTrace(); + } + } + throw new ApiException( + response.getStatus(), message, buildResponseHeaders(response), respBody); + } + } finally { + try { + response.close(); + } catch (Exception e) { + // it's not critical, since the response object is local in method invokeAPI; that's fine, + // just continue + } } + } - Entity entity = serialize(body, formParams, contentType); - + private Response sendRequest(String method, Invocation.Builder invocationBuilder, Entity entity) { Response response; - - if ("GET".equals(method)) { - response = invocationBuilder.get(); - } else if ("POST".equals(method)) { + if ("POST".equals(method)) { response = invocationBuilder.post(entity); } else if ("PUT".equals(method)) { response = invocationBuilder.put(entity); } else if ("DELETE".equals(method)) { - response = invocationBuilder.delete(); + response = invocationBuilder.method("DELETE", entity); } else if ("PATCH".equals(method)) { - response = invocationBuilder.header("X-HTTP-Method-Override", "PATCH").post(entity); + response = invocationBuilder.method("PATCH", entity); } else { - throw new ApiException(500, "unknown method type " + method); + response = invocationBuilder.method(method); } + return response; + } - statusCode = response.getStatusInfo().getStatusCode(); - responseHeaders = buildResponseHeaders(response); - - if (response.getStatus() == Status.NO_CONTENT.getStatusCode()) { - return null; - } else if (response.getStatusInfo().getFamily() == Status.Family.SUCCESSFUL) { - if (returnType == null) - return null; - else - return deserialize(response, returnType); - } else { - String message = "error"; - String respBody = null; - if (response.hasEntity()) { - try { - respBody = String.valueOf(response.readEntity(String.class)); - message = respBody; - } catch (RuntimeException e) { - // e.printStackTrace(); - } - } - throw new ApiException( - response.getStatus(), - message, - buildResponseHeaders(response), - respBody); - } + /** + * @deprecated Add qualified name of the operation as a first parameter. + */ + @Deprecated + public ApiResponse invokeAPI(String path, String method, List queryParams, Object body, Map headerParams, Map cookieParams, Map formParams, String accept, String contentType, String[] authNames, GenericType returnType, boolean isBodyNullable) throws ApiException { + return invokeAPI(null, path, method, queryParams, body, headerParams, cookieParams, formParams, accept, contentType, authNames, returnType, isBodyNullable); } /** * Build the Client used to make HTTP requests. - * @param debugging Debug setting * @return Client */ - private Client buildHttpClient(boolean debugging) { - final ClientConfig clientConfig = new ClientConfig(); + protected Client buildHttpClient() { + // recreate the client config to pickup changes + clientConfig = getDefaultClientConfig(); + + ClientBuilder clientBuilder = ClientBuilder.newBuilder(); + customizeClientBuilder(clientBuilder); + clientBuilder = clientBuilder.withConfig(clientConfig); + return clientBuilder.build(); + } + + /** + * Get the default client config. + * @return Client config + */ + public ClientConfig getDefaultClientConfig() { + ClientConfig clientConfig = new ClientConfig(); clientConfig.register(MultiPartFeature.class); clientConfig.register(json); clientConfig.register(JacksonFeature.class); + clientConfig.property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true); + // turn off compliance validation to be able to send payloads with DELETE calls + clientConfig.property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true); if (debugging) { - clientConfig.register(LoggingFilter.class); + clientConfig.register(new LoggingFeature(java.util.logging.Logger.getLogger(LoggingFeature.DEFAULT_LOGGER_NAME), java.util.logging.Level.INFO, LoggingFeature.Verbosity.PAYLOAD_ANY, 1024*50 /* Log payloads up to 50K */)); + clientConfig.property(LoggingFeature.LOGGING_FEATURE_VERBOSITY, LoggingFeature.Verbosity.PAYLOAD_ANY); + // Set logger to ALL + java.util.logging.Logger.getLogger(LoggingFeature.DEFAULT_LOGGER_NAME).setLevel(java.util.logging.Level.ALL); + } else { + // suppress warnings for payloads with DELETE calls: + java.util.logging.Logger.getLogger("org.glassfish.jersey.client").setLevel(java.util.logging.Level.SEVERE); } - return ClientBuilder.newClient(clientConfig); + + return clientConfig; } - private Map> buildResponseHeaders(Response response) { + /** + * Customize the client builder. + * + * This method can be overriden to customize the API client. For example, this can be used to: + * 1. Set the hostname verifier to be used by the client to verify the endpoint's hostname + * against its identification information. + * 2. Set the client-side key store. + * 3. Set the SSL context that will be used when creating secured transport connections to + * server endpoints from web targets created by the client instance that is using this SSL context. + * 4. Set the client-side trust store. + * + * To completely disable certificate validation (at your own risk), you can + * override this method and invoke disableCertificateValidation(clientBuilder). + */ + protected void customizeClientBuilder(ClientBuilder clientBuilder) { + // No-op extension point + } + + /** + * Disable X.509 certificate validation in TLS connections. + * + * Please note that trusting all certificates is extremely risky. + * This may be useful in a development environment with self-signed certificates. + */ + protected void disableCertificateValidation(ClientBuilder clientBuilder) throws KeyManagementException, NoSuchAlgorithmException { + TrustManager[] trustAllCerts = new X509TrustManager[] { + new X509TrustManager() { + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + @Override + public void checkClientTrusted(X509Certificate[] certs, String authType) { + } + @Override + public void checkServerTrusted(X509Certificate[] certs, String authType) { + } + } + }; + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, trustAllCerts, new SecureRandom()); + clientBuilder.sslContext(sslContext); + } + + protected Map> buildResponseHeaders(Response response) { Map> responseHeaders = new HashMap>(); for (Entry> entry: response.getHeaders().entrySet()) { List values = entry.getValue(); @@ -760,12 +1324,20 @@ public class ApiClient { * Update query and header parameters based on authentication settings. * * @param authNames The authentications to apply + * @param queryParams List of query parameters + * @param headerParams Map of header parameters + * @param cookieParams Map of cookie parameters + * @param method HTTP method (e.g. POST) + * @param uri HTTP URI */ - private void updateParamsForAuth(String[] authNames, List queryParams, Map headerParams) { + protected void updateParamsForAuth(String[] authNames, List queryParams, Map headerParams, + Map cookieParams, String payload, String method, URI uri) throws ApiException { for (String authName : authNames) { Authentication auth = authentications.get(authName); - if (auth == null) throw new RuntimeException("Authentication undefined: " + authName); - auth.applyToParams(queryParams, headerParams); + if (auth == null) { + continue; + } + auth.applyToParams(queryParams, headerParams, cookieParams, payload, method, uri); } } } diff --git a/modules/openapi-generator/src/test/resources/2_0/templates/Java/libraries/jersey2/JSON.mustache b/modules/openapi-generator/src/test/resources/2_0/templates/Java/libraries/jersey2/JSON.mustache index 52e94ec9326a..3297c77e49b2 100644 --- a/modules/openapi-generator/src/test/resources/2_0/templates/Java/libraries/jersey2/JSON.mustache +++ b/modules/openapi-generator/src/test/resources/2_0/templates/Java/libraries/jersey2/JSON.mustache @@ -1,18 +1,32 @@ -//overloaded template file within library folder to add this comment - package {{invokerPackage}}; +{{#threetenbp}} +import org.threeten.bp.*; +{{/threetenbp}} import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.databind.*; +{{#openApiNullable}} +import org.openapitools.jackson.nullable.JsonNullableModule; +{{/openApiNullable}} {{#java8}} -import com.fasterxml.jackson.datatype.jsr310.*; -{{/java8}} -{{^java8}} -import com.fasterxml.jackson.datatype.joda.*; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; {{/java8}} +{{#joda}} +import com.fasterxml.jackson.datatype.joda.JodaModule; +{{/joda}} +{{#threetenbp}} +import com.fasterxml.jackson.datatype.threetenbp.ThreeTenModule; +{{/threetenbp}} +{{#models.0}} +import {{modelPackage}}.*; +{{/models.0}} import java.text.DateFormat; - +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import javax.ws.rs.core.GenericType; import javax.ws.rs.ext.ContextResolver; {{>generatedAnnotation}} @@ -22,7 +36,9 @@ public class JSON implements ContextResolver { public JSON() { mapper = new ObjectMapper(); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.configure(MapperFeature.ALLOW_COERCION_OF_SCALARS, false); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true); + mapper.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, true); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); mapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING); mapper.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING); @@ -30,9 +46,20 @@ public class JSON implements ContextResolver { {{#java8}} mapper.registerModule(new JavaTimeModule()); {{/java8}} - {{^java8}} + {{#joda}} mapper.registerModule(new JodaModule()); - {{/java8}} + {{/joda}} + {{#threetenbp}} + ThreeTenModule module = new ThreeTenModule(); + module.addDeserializer(Instant.class, CustomInstantDeserializer.INSTANT); + module.addDeserializer(OffsetDateTime.class, CustomInstantDeserializer.OFFSET_DATE_TIME); + module.addDeserializer(ZonedDateTime.class, CustomInstantDeserializer.ZONED_DATE_TIME); + mapper.registerModule(module); + {{/threetenbp}} + {{#openApiNullable}} + JsonNullableModule jnm = new JsonNullableModule(); + mapper.registerModule(jnm); + {{/openApiNullable}} } /** @@ -47,4 +74,204 @@ public class JSON implements ContextResolver { public ObjectMapper getContext(Class type) { return mapper; } + + /** + * Get the object mapper + * + * @return object mapper + */ + public ObjectMapper getMapper() { return mapper; } + + /** + * Returns the target model class that should be used to deserialize the input data. + * The discriminator mappings are used to determine the target model class. + * + * @param node The input data. + * @param modelClass The class that contains the discriminator mappings. + */ + public static Class getClassForElement(JsonNode node, Class modelClass) { + ClassDiscriminatorMapping cdm = modelDiscriminators.get(modelClass); + if (cdm != null) { + return cdm.getClassForElement(node, new HashSet>()); + } + return null; + } + + /** + * Helper class to register the discriminator mappings. + */ + private static class ClassDiscriminatorMapping { + // The model class name. + Class modelClass; + // The name of the discriminator property. + String discriminatorName; + // The discriminator mappings for a model class. + Map> discriminatorMappings; + + // Constructs a new class discriminator. + ClassDiscriminatorMapping(Class cls, String propertyName, Map> mappings) { + modelClass = cls; + discriminatorName = propertyName; + discriminatorMappings = new HashMap>(); + if (mappings != null) { + discriminatorMappings.putAll(mappings); + } + } + + // Return the name of the discriminator property for this model class. + String getDiscriminatorPropertyName() { + return discriminatorName; + } + + // Return the discriminator value or null if the discriminator is not + // present in the payload. + String getDiscriminatorValue(JsonNode node) { + // Determine the value of the discriminator property in the input data. + if (discriminatorName != null) { + // Get the value of the discriminator property, if present in the input payload. + node = node.get(discriminatorName); + if (node != null && node.isValueNode()) { + String discrValue = node.asText(); + if (discrValue != null) { + return discrValue; + } + } + } + return null; + } + + /** + * Returns the target model class that should be used to deserialize the input data. + * This function can be invoked for anyOf/oneOf composed models with discriminator mappings. + * The discriminator mappings are used to determine the target model class. + * + * @param node The input data. + * @param visitedClasses The set of classes that have already been visited. + */ + Class getClassForElement(JsonNode node, Set> visitedClasses) { + if (visitedClasses.contains(modelClass)) { + // Class has already been visited. + return null; + } + // Determine the value of the discriminator property in the input data. + String discrValue = getDiscriminatorValue(node); + if (discrValue == null) { + return null; + } + Class cls = discriminatorMappings.get(discrValue); + // It may not be sufficient to return this cls directly because that target class + // may itself be a composed schema, possibly with its own discriminator. + visitedClasses.add(modelClass); + for (Class childClass : discriminatorMappings.values()) { + ClassDiscriminatorMapping childCdm = modelDiscriminators.get(childClass); + if (childCdm == null) { + continue; + } + if (!discriminatorName.equals(childCdm.discriminatorName)) { + discrValue = getDiscriminatorValue(node); + if (discrValue == null) { + continue; + } + } + if (childCdm != null) { + // Recursively traverse the discriminator mappings. + Class childDiscr = childCdm.getClassForElement(node, visitedClasses); + if (childDiscr != null) { + return childDiscr; + } + } + } + return cls; + } + } + + /** + * Returns true if inst is an instance of modelClass in the OpenAPI model hierarchy. + * + * The Java class hierarchy is not implemented the same way as the OpenAPI model hierarchy, + * so it's not possible to use the instanceof keyword. + * + * @param modelClass A OpenAPI model class. + * @param inst The instance object. + */ + public static boolean isInstanceOf(Class modelClass, Object inst, Set> visitedClasses) { + if (modelClass.isInstance(inst)) { + // This handles the 'allOf' use case with single parent inheritance. + return true; + } + if (visitedClasses.contains(modelClass)) { + // This is to prevent infinite recursion when the composed schemas have + // a circular dependency. + return false; + } + visitedClasses.add(modelClass); + + // Traverse the oneOf/anyOf composed schemas. + Map descendants = modelDescendants.get(modelClass); + if (descendants != null) { + for (GenericType childType : descendants.values()) { + if (isInstanceOf(childType.getRawType(), inst, visitedClasses)) { + return true; + } + } + } + return false; + } + + /** + * A map of discriminators for all model classes. + */ + private static Map, ClassDiscriminatorMapping> modelDiscriminators = new HashMap, ClassDiscriminatorMapping>(); + + /** + * A map of oneOf/anyOf descendants for each model class. + */ + private static Map, Map> modelDescendants = new HashMap, Map>(); + + /** + * Register a model class discriminator. + * + * @param modelClass the model class + * @param discriminatorPropertyName the name of the discriminator property + * @param mappings a map with the discriminator mappings. + */ + public static void registerDiscriminator(Class modelClass, String discriminatorPropertyName, Map> mappings) { + ClassDiscriminatorMapping m = new ClassDiscriminatorMapping(modelClass, discriminatorPropertyName, mappings); + modelDiscriminators.put(modelClass, m); + } + + /** + * Register the oneOf/anyOf descendants of the modelClass. + * + * @param modelClass the model class + * @param descendants a map of oneOf/anyOf descendants. + */ + public static void registerDescendants(Class modelClass, Map descendants) { + modelDescendants.put(modelClass, descendants); + } + + private static JSON json; + + static + { + json = new JSON(); + } + + /** + * Get the default JSON instance. + * + * @return the default JSON instance + */ + public static JSON getDefault() { + return json; + } + + /** + * Set the default JSON instance. + * + * @param json JSON instance to be used + */ + public static void setDefault(JSON json) { + JSON.json = json; + } } diff --git a/modules/openapi-generator/src/test/resources/2_0/templates/Java/libraries/jersey2/api.mustache b/modules/openapi-generator/src/test/resources/2_0/templates/Java/libraries/jersey2/api.mustache index e6e83d3550b8..666430c834e1 100644 --- a/modules/openapi-generator/src/test/resources/2_0/templates/Java/libraries/jersey2/api.mustache +++ b/modules/openapi-generator/src/test/resources/2_0/templates/Java/libraries/jersey2/api.mustache @@ -1,9 +1,8 @@ -//overloaded template file within library folder to add this comment - package {{package}}; import {{invokerPackage}}.ApiException; import {{invokerPackage}}.ApiClient; +import {{invokerPackage}}.ApiResponse; import {{invokerPackage}}.Configuration; import {{invokerPackage}}.Pair; @@ -17,8 +16,8 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -{{/fullJavaUtil}} +{{/fullJavaUtil}} {{>generatedAnnotation}} {{#operations}} public class {{classname}} { @@ -32,15 +31,26 @@ public class {{classname}} { this.apiClient = apiClient; } + /** + * Get the API cilent + * + * @return API client + */ public ApiClient getApiClient() { return apiClient; } + /** + * Set the API cilent + * + * @param apiClient an instance of API client + */ public void setApiClient(ApiClient apiClient) { this.apiClient = apiClient; } {{#operation}} + {{^vendorExtensions.x-group-parameters}} /** * {{summary}} * {{notes}} @@ -51,8 +61,61 @@ public class {{classname}} { * @return {{returnType}} {{/returnType}} * @throws ApiException if fails to make API call + {{#responses.0}} + * @http.response.details + + + {{#responses}} + + {{/responses}} +
Status Code Description Response Headers
{{code}} {{message}} {{#headers}} * {{baseName}} - {{description}}
{{/headers}}{{^headers.0}} - {{/headers.0}}
+ {{/responses.0}} + {{#isDeprecated}} + * @deprecated + {{/isDeprecated}} + {{#externalDocs}} + * {{description}} + * @see {{summary}} Documentation + {{/externalDocs}} */ + {{#isDeprecated}} + @Deprecated + {{/isDeprecated}} public {{#returnType}}{{{returnType}}} {{/returnType}}{{^returnType}}void {{/returnType}}{{operationId}}({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) throws ApiException { + {{#returnType}}return {{/returnType}}{{operationId}}WithHttpInfo({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}){{#returnType}}.getData(){{/returnType}}; + } + {{/vendorExtensions.x-group-parameters}} + + {{^vendorExtensions.x-group-parameters}} + /** + * {{summary}} + * {{notes}} + {{#allParams}} + * @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}} + {{/allParams}} + * @return ApiResponse<{{#returnType}}{{returnType}}{{/returnType}}{{^returnType}}Void{{/returnType}}> + * @throws ApiException if fails to make API call + {{#responses.0}} + * @http.response.details + + + {{#responses}} + + {{/responses}} +
Status Code Description Response Headers
{{code}} {{message}} {{#headers}} * {{baseName}} - {{description}}
{{/headers}}{{^headers.0}} - {{/headers.0}}
+ {{/responses.0}} + {{#isDeprecated}} + * @deprecated + {{/isDeprecated}} + {{#externalDocs}} + * {{description}} + * @see {{summary}} Documentation + {{/externalDocs}} + */ + {{#isDeprecated}} + @Deprecated + {{/isDeprecated}} + public{{/vendorExtensions.x-group-parameters}}{{#vendorExtensions.x-group-parameters}}private{{/vendorExtensions.x-group-parameters}} ApiResponse<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Void{{/returnType}}> {{operationId}}WithHttpInfo({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) throws ApiException { Object localVarPostBody = {{#bodyParam}}{{paramName}}{{/bodyParam}}{{^bodyParam}}null{{/bodyParam}}; {{#allParams}}{{#required}} // verify the required parameter '{{paramName}}' is set @@ -61,12 +124,13 @@ public class {{classname}} { } {{/required}}{{/allParams}} // create path and map variables - String localVarPath = "{{{path}}}".replaceAll("\\{format\\}","json"){{#pathParams}} + String localVarPath = "{{{path}}}"{{#pathParams}} .replaceAll("\\{" + "{{baseName}}" + "\\}", apiClient.escapeString({{{paramName}}}.toString())){{/pathParams}}; // query params {{javaUtilPrefix}}List localVarQueryParams = new {{javaUtilPrefix}}ArrayList(); {{javaUtilPrefix}}Map localVarHeaderParams = new {{javaUtilPrefix}}HashMap(); + {{javaUtilPrefix}}Map localVarCookieParams = new {{javaUtilPrefix}}HashMap(); {{javaUtilPrefix}}Map localVarFormParams = new {{javaUtilPrefix}}HashMap(); {{#queryParams}} @@ -77,6 +141,10 @@ public class {{classname}} { localVarHeaderParams.put("{{baseName}}", apiClient.parameterToString({{paramName}})); {{/headerParams}} + {{#cookieParams}}if ({{paramName}} != null) + localVarCookieParams.put("{{baseName}}", apiClient.parameterToString({{paramName}})); + {{/cookieParams}} + {{#formParams}}if ({{paramName}} != null) localVarFormParams.put("{{baseName}}", {{paramName}}); {{/formParams}} @@ -95,11 +163,100 @@ public class {{classname}} { {{#returnType}} GenericType<{{{returnType}}}> localVarReturnType = new GenericType<{{{returnType}}}>() {}; - return apiClient.invokeAPI(localVarPath, "{{httpMethod}}", localVarQueryParams, localVarPostBody, localVarHeaderParams, localVarFormParams, localVarAccept, localVarContentType, localVarAuthNames, localVarReturnType); - {{/returnType}}{{^returnType}} - apiClient.invokeAPI(localVarPath, "{{httpMethod}}", localVarQueryParams, localVarPostBody, localVarHeaderParams, localVarFormParams, localVarAccept, localVarContentType, localVarAuthNames, null); + {{/returnType}} + return apiClient.invokeAPI("{{classname}}.{{operationId}}", localVarPath, "{{httpMethod}}", localVarQueryParams, localVarPostBody, + localVarHeaderParams, localVarCookieParams, localVarFormParams, localVarAccept, localVarContentType, + localVarAuthNames, {{#returnType}}localVarReturnType{{/returnType}}{{^returnType}}null{{/returnType}}, {{#bodyParam}}{{#isNullable}}true{{/isNullable}}{{^isNullable}}false{{/isNullable}}{{/bodyParam}}{{^bodyParam}}false{{/bodyParam}}); } + {{#vendorExtensions.x-group-parameters}} + + public class API{{operationId}}Request { + {{#allParams}} + private {{#isRequired}}final {{/isRequired}}{{{dataType}}} {{paramName}}; + {{/allParams}} + + private API{{operationId}}Request({{#pathParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/pathParams}}) { + {{#pathParams}} + this.{{paramName}} = {{paramName}}; + {{/pathParams}} + } + {{#allParams}} + {{^isPathParam}} + + /** + * Set {{paramName}} + * @param {{paramName}} {{description}} ({{^required}}optional{{^isContainer}}{{#defaultValue}}, default to {{.}}{{/defaultValue}}{{/isContainer}}{{/required}}{{#required}}required{{/required}}) + * @return API{{operationId}}Request + */ + public API{{operationId}}Request {{paramName}}({{{dataType}}} {{paramName}}) { + this.{{paramName}} = {{paramName}}; + return this; + } + {{/isPathParam}} + {{/allParams}} + + /** + * Execute {{operationId}} request + {{#returnType}}* @return {{.}}{{/returnType}} + * @throws ApiException if fails to make API call + {{#responses.0}} + * @http.response.details + + + {{#responses}} + + {{/responses}} +
Status Code Description Response Headers
{{code}} {{message}} {{#headers}} * {{baseName}} - {{description}}
{{/headers}}{{^headers.0}} - {{/headers.0}}
+ {{/responses.0}} + {{#isDeprecated}}* @deprecated{{/isDeprecated}} + */ + {{#isDeprecated}}@Deprecated{{/isDeprecated}} + public {{#returnType}}{{{.}}}{{/returnType}}{{^returnType}}void{{/returnType}} execute() throws ApiException { + {{#returnType}}return {{/returnType}}this.executeWithHttpInfo().getData(); + } + + /** + * Execute {{operationId}} request with HTTP info returned + * @return ApiResponse<{{#returnType}}{{.}}{{/returnType}}{{^returnType}}Void{{/returnType}}> + * @throws ApiException if fails to make API call + {{#responses.0}} + * @http.response.details + + + {{#responses}} + + {{/responses}} +
Status Code Description Response Headers
{{code}} {{message}} {{#headers}} * {{baseName}} - {{description}}
{{/headers}}{{^headers.0}} - {{/headers.0}}
+ {{/responses.0}} + {{#isDeprecated}} + * @deprecated{{/isDeprecated}} + */ + {{#isDeprecated}} + @Deprecated + {{/isDeprecated}} + public ApiResponse<{{#returnType}}{{{.}}}{{/returnType}}{{^returnType}}Void{{/returnType}}> executeWithHttpInfo() throws ApiException { + return {{operationId}}WithHttpInfo({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}); + } + } + + /** + * {{summary}} + * {{notes}}{{#pathParams}} + * @param {{paramName}} {{description}} (required){{/pathParams}} + * @return {{operationId}}Request + * @throws ApiException if fails to make API call + {{#isDeprecated}}* @deprecated{{/isDeprecated}} + {{#externalDocs}}* {{description}} + * @see {{summary}} Documentation{{/externalDocs}} + */ + {{#isDeprecated}} + @Deprecated + {{/isDeprecated}} + public API{{operationId}}Request {{operationId}}({{#pathParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/pathParams}}) throws ApiException { + return new API{{operationId}}Request({{#pathParams}}{{paramName}}{{^-last}}, {{/-last}}{{/pathParams}}); + } + {{/vendorExtensions.x-group-parameters}} {{/operation}} } {{/operations}} diff --git a/modules/openapi-generator/src/test/resources/2_0/templates/Java/libraries/jersey2/build.gradle.mustache b/modules/openapi-generator/src/test/resources/2_0/templates/Java/libraries/jersey2/build.gradle.mustache index c76ec3218088..378de9fb1e1f 100644 --- a/modules/openapi-generator/src/test/resources/2_0/templates/Java/libraries/jersey2/build.gradle.mustache +++ b/modules/openapi-generator/src/test/resources/2_0/templates/Java/libraries/jersey2/build.gradle.mustache @@ -1,5 +1,3 @@ -//overloaded template file within library folder to add this comment - apply plugin: 'idea' apply plugin: 'eclipse' @@ -8,11 +6,12 @@ version = '{{artifactVersion}}' buildscript { repositories { + maven { url "https://repo1.maven.org/maven2" } jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.5.+' - classpath 'com.github.dcendents:android-maven-gradle-plugin:1.3' + classpath 'com.android.tools.build:gradle:2.3.+' + classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' } } @@ -27,18 +26,13 @@ if(hasProperty('target') && target == 'android') { apply plugin: 'com.github.dcendents.android-maven' android { - compileSdkVersion 23 - buildToolsVersion '23.0.2' + compileSdkVersion 25 + buildToolsVersion '25.0.2' defaultConfig { minSdkVersion 14 - targetSdkVersion 23 + targetSdkVersion 25 } compileOptions { - {{#supportJava6}} - sourceCompatibility JavaVersion.VERSION_1_6 - targetCompatibility JavaVersion.VERSION_1_6 - {{/supportJava6}} - {{^supportJava6}} {{#java8}} sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -47,7 +41,6 @@ if(hasProperty('target') && target == 'android') { sourceCompatibility JavaVersion.VERSION_1_7 targetCompatibility JavaVersion.VERSION_1_7 {{/java8}} - {{/supportJava6}} } // Rename the aar correctly @@ -91,11 +84,6 @@ if(hasProperty('target') && target == 'android') { apply plugin: 'java' apply plugin: 'maven' - {{#supportJava6}} - sourceCompatibility = JavaVersion.VERSION_1_6 - targetCompatibility = JavaVersion.VERSION_1_6 - {{/supportJava6}} - {{^supportJava6}} {{#java8}} sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 @@ -104,7 +92,6 @@ if(hasProperty('target') && target == 'android') { sourceCompatibility = JavaVersion.VERSION_1_7 targetCompatibility = JavaVersion.VERSION_1_7 {{/java8}} - {{/supportJava6}} install { repositories.mavenInstaller { @@ -119,38 +106,61 @@ if(hasProperty('target') && target == 'android') { } ext { - swagger_annotations_version = "1.5.8" - jackson_version = "2.7.5" - jersey_version = "2.22.2" - {{^java8}} - jodatime_version = "2.9.4" - {{/java8}} - {{#supportJava6}} - commons_io_version=2.5 - commons_lang3_version=3.5 - {{/supportJava6}} - junit_version = "4.13" + swagger_annotations_version = "1.5.22" + jackson_version = "2.10.5" + jackson_databind_version = "2.10.5.1" + {{#openApiNullable}} + jackson_databind_nullable_version = "0.2.1" + {{/openApiNullable}} + jersey_version = "2.27" + junit_version = "4.13.1" + {{#threetenbp}} + threetenbp_version = "2.9.10" + {{/threetenbp}} + {{#hasOAuthMethods}} + scribejava_apis_version = "6.9.0" + {{/hasOAuthMethods}} + {{#hasHttpSignatureMethods}} + tomitribe_http_signatures_version = "1.5" + {{/hasHttpSignatureMethods}} } dependencies { implementation "io.swagger:swagger-annotations:$swagger_annotations_version" + implementation "com.google.code.findbugs:jsr305:3.0.2" implementation "org.glassfish.jersey.core:jersey-client:$jersey_version" + implementation "org.glassfish.jersey.inject:jersey-hk2:$jersey_version" implementation "org.glassfish.jersey.media:jersey-media-multipart:$jersey_version" implementation "org.glassfish.jersey.media:jersey-media-json-jackson:$jersey_version" + implementation "org.glassfish.jersey.connectors:jersey-apache-connector:$jersey_version" implementation "com.fasterxml.jackson.core:jackson-core:$jackson_version" implementation "com.fasterxml.jackson.core:jackson-annotations:$jackson_version" - implementation "com.fasterxml.jackson.core:jackson-databind:$jackson_version" + implementation "com.fasterxml.jackson.core:jackson-databind:$jackson_databind_version" + {{#openApiNullable}} + implementation "org.openapitools:jackson-databind-nullable:$jackson_databind_nullable_version" + {{/openApiNullable}} + {{#joda}} + implementation "com.fasterxml.jackson.datatype:jackson-datatype-joda:$jackson_version" + {{/joda}} {{#java8}} implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version" {{/java8}} + {{#hasOAuthMethods}} + implementation "com.github.scribejava:scribejava-apis:$scribejava_apis_version" + {{/hasOAuthMethods}} + {{#hasHttpSignatureMethods}} + implementation "org.tomitribe:tomitribe-http-signatures:$tomitribe_http_signatures_version" + {{/hasHttpSignatureMethods}} + {{#threetenbp}} + implementation "com.github.joschi.jackson:jackson-datatype-threetenbp:$threetenbp_version" + {{/threetenbp}} {{^java8}} - implementation "com.fasterxml.jackson.datatype:jackson-datatype-joda:$jackson_version" - implementation "joda-time:joda-time:$jodatime_version" implementation "com.brsanthu:migbase64:2.2" {{/java8}} - {{#supportJava6}} - implementation "commons-io:commons-io:$commons_io_version" - implementation "org.apache.commons:commons-lang3:$commons_lang3_version" - {{/supportJava6}} + implementation 'javax.annotation:javax.annotation-api:1.3.2' testImplementation "junit:junit:$junit_version" } + +javadoc { + options.tags = [ "http.response.details:a:Http Response Details" ] +} diff --git a/modules/openapi-generator/src/test/resources/2_0/templates/Java/libraries/jersey2/build.sbt.mustache b/modules/openapi-generator/src/test/resources/2_0/templates/Java/libraries/jersey2/build.sbt.mustache index 1049751cf3a4..9823c3c657e9 100644 --- a/modules/openapi-generator/src/test/resources/2_0/templates/Java/libraries/jersey2/build.sbt.mustache +++ b/modules/openapi-generator/src/test/resources/2_0/templates/Java/libraries/jersey2/build.sbt.mustache @@ -1,5 +1,3 @@ -//overloaded template file within library folder to add this comment - lazy val root = (project in file(".")). settings( organization := "{{groupId}}", @@ -11,26 +9,35 @@ lazy val root = (project in file(".")). publishArtifact in (Compile, packageDoc) := false, resolvers += Resolver.mavenLocal, libraryDependencies ++= Seq( - "io.swagger" % "swagger-annotations" % "1.5.8", - "org.glassfish.jersey.core" % "jersey-client" % "2.22.2", - "org.glassfish.jersey.media" % "jersey-media-multipart" % "2.22.2", - "org.glassfish.jersey.media" % "jersey-media-json-jackson" % "2.22.2", - "com.fasterxml.jackson.core" % "jackson-core" % "2.7.5", - "com.fasterxml.jackson.core" % "jackson-annotations" % "2.7.5", - "com.fasterxml.jackson.core" % "jackson-databind" % "2.7.5", + "io.swagger" % "swagger-annotations" % "1.5.22", + "org.glassfish.jersey.core" % "jersey-client" % "2.27", + "org.glassfish.jersey.inject" % "jersey-hk2" % "2.27", + "org.glassfish.jersey.media" % "jersey-media-multipart" % "2.27", + "org.glassfish.jersey.media" % "jersey-media-json-jackson" % "2.27", + "org.glassfish.jersey.connectors" % "jersey-apache-connector" % "2.27", + "com.fasterxml.jackson.core" % "jackson-core" % "2.10.5" % "compile", + "com.fasterxml.jackson.core" % "jackson-annotations" % "2.10.5.1" % "compile", + "com.fasterxml.jackson.core" % "jackson-databind" % "2.10.5.1" % "compile", + {{#joda}} + "com.fasterxml.jackson.datatype" % "jackson-datatype-joda" % "2.9.10" % "compile", + {{/joda}} {{#java8}} - "com.fasterxml.jackson.datatype" % "jackson-datatype-jsr310" % "2.7.5", - {{/java8}} - {{^java8}} - "com.fasterxml.jackson.datatype" % "jackson-datatype-joda" % "2.7.5", - "joda-time" % "joda-time" % "2.9.4", + "com.fasterxml.jackson.datatype" % "jackson-datatype-jsr310" % "2.9.10" % "compile", + {{/java8}} + {{#threetenbp}} + "com.github.joschi.jackson" % "jackson-datatype-threetenbp" % "2.9.10" % "compile", + {{/threetenbp}} + {{#hasOAuthMethods}} + "com.github.scribejava" % "scribejava-apis" % "6.9.0" % "compile", + {{/hasOAuthMethods}} + {{#hasHttpSignatureMethods}} + "org.tomitribe" % "tomitribe-http-signatures" % "1.5" % "compile", + {{/hasHttpSignatureMethods}} + {{^java8}} "com.brsanthu" % "migbase64" % "2.2", {{/java8}} - {{#supportJava6}} - "org.apache.commons" % "commons-lang3" % "3.5", - "commons-io" % "commons-io" % "2.5", - {{/supportJava6}} - "junit" % "junit" % "4.13" % "test", + "javax.annotation" % "javax.annotation-api" % "1.3.2" % "compile", + "junit" % "junit" % "4.13.1" % "test", "com.novocode" % "junit-interface" % "0.10" % "test" ) ) diff --git a/modules/openapi-generator/src/test/resources/2_0/templates/Java/libraries/jersey2/pom.mustache b/modules/openapi-generator/src/test/resources/2_0/templates/Java/libraries/jersey2/pom.mustache index 3e1a02651469..f36f0cd88178 100644 --- a/modules/openapi-generator/src/test/resources/2_0/templates/Java/libraries/jersey2/pom.mustache +++ b/modules/openapi-generator/src/test/resources/2_0/templates/Java/libraries/jersey2/pom.mustache @@ -1,5 +1,3 @@ -//overloaded template file within library folder to add this comment - 4.0.0 @@ -15,6 +13,13 @@ {{scmDeveloperConnection}} {{scmUrl}} +{{#parentOverridden}} + + {{{parentGroupId}}} + {{{parentArtifactId}}} + {{{parentVersion}}} + +{{/parentOverridden}} @@ -58,7 +63,7 @@ org.apache.maven.plugins maven-surefire-plugin - 2.12 + 3.0.0-M4 @@ -68,7 +73,8 @@ -Xms512m -Xmx1500m methods - pertest + 10 + false @@ -106,6 +112,7 @@ org.codehaus.mojo build-helper-maven-plugin + 1.10 add_sources @@ -115,8 +122,7 @@ - - src/main/java + src/main/java @@ -128,8 +134,7 @@ - - src/test/java + src/test/java @@ -138,7 +143,7 @@ org.apache.maven.plugins maven-compiler-plugin - 2.5.1 + 3.8.1 {{#java8}} 1.8 @@ -148,15 +153,19 @@ 1.7 1.7 {{/java8}} + true + 128m + 512m + + -Xlint:all + -J-Xss4m + org.apache.maven.plugins maven-javadoc-plugin 3.1.1 - - none - attach-javadocs @@ -165,6 +174,22 @@ + + none + {{#java8}} + 1.8 + {{/java8}} + {{^java8}} + 1.7 + {{/java8}} + + + http.response.details + a + Http Response Details: + + + org.apache.maven.plugins @@ -210,7 +235,14 @@ io.swagger swagger-annotations - ${swagger-core-version} + ${swagger-annotations-version} + + + + + com.google.code.findbugs + jsr305 + 3.0.2 @@ -219,6 +251,11 @@ jersey-client ${jersey-version} + + org.glassfish.jersey.inject + jersey-hk2 + ${jersey-version} + org.glassfish.jersey.media jersey-media-multipart @@ -244,49 +281,86 @@ com.fasterxml.jackson.core jackson-databind + ${jackson-databind-version} + + {{#openApiNullable}} + + org.openapitools + jackson-databind-nullable + ${jackson-databind-nullable-version} + + {{/openApiNullable}} + {{#withXml}} + + + org.glassfish.jersey.media + jersey-media-jaxb + ${jersey-version} + + {{/withXml}} + {{#joda}} + + com.fasterxml.jackson.datatype + jackson-datatype-joda ${jackson-version} + {{/joda}} {{#java8}} - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - ${jackson-version} - + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson-version} + {{/java8}} + {{#threetenbp}} + + com.github.joschi.jackson + jackson-datatype-threetenbp + ${threetenbp-version} + + {{/threetenbp}} {{^java8}} - - com.fasterxml.jackson.datatype - jackson-datatype-joda - ${jackson-version} - - - joda-time - joda-time - ${jodatime-version} - - - - - com.brsanthu - migbase64 - 2.2 - + + + com.brsanthu + migbase64 + 2.2 + {{/java8}} - - {{#supportJava6}} - - org.apache.commons - commons-lang3 - ${commons_lang3_version} - - - - commons-io - commons-io - ${commons_io_version} - - {{/supportJava6}} - + {{#hasHttpSignatureMethods}} + + org.tomitribe + tomitribe-http-signatures + ${http-signature-version} + + {{/hasHttpSignatureMethods}} + {{#hasOAuthMethods}} + + com.github.scribejava + scribejava-apis + ${scribejava-apis-version} + + {{/hasOAuthMethods}} + {{#useBeanValidation}} + + + javax.validation + validation-api + 1.1.0.Final + provided + + {{/useBeanValidation}} + + javax.annotation + javax.annotation-api + ${javax-annotation-version} + provided + + + org.glassfish.jersey.connectors + jersey-apache-connector + ${jersey-version} + junit @@ -297,17 +371,21 @@ UTF-8 - 1.5.18 - 2.22.2 - 2.8.9 - {{^java8}} - 2.9.4 - {{/java8}} - {{#supportJava6}} - 2.5 - 3.5 - {{/supportJava6}} - 1.0.0 - 4.13 + 1.6.1 + 2.30.1 + 2.10.5 + 2.10.5.1 + 0.2.1 + {{#threetenbp}} + 2.9.10 + {{/threetenbp}} + 1.3.2 + 4.13.1 + {{#hasHttpSignatureMethods}} + 1.5 + {{/hasHttpSignatureMethods}} + {{#hasOAuthMethods}} + 6.9.0 + {{/hasOAuthMethods}} diff --git a/modules/openapi-generator/src/test/resources/2_0/templates/Java/model.mustache b/modules/openapi-generator/src/test/resources/2_0/templates/Java/model.mustache index 07adf19139f6..8fd078eb3d62 100644 --- a/modules/openapi-generator/src/test/resources/2_0/templates/Java/model.mustache +++ b/modules/openapi-generator/src/test/resources/2_0/templates/Java/model.mustache @@ -1,31 +1,54 @@ -//overloaded main template file to add this comment - {{>licenseInfo}} package {{package}}; -{{^supportJava6}} +{{#useReflectionEqualsHashCode}} +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +{{/useReflectionEqualsHashCode}} import java.util.Objects; -{{/supportJava6}} -{{#supportJava6}} -import org.apache.commons.lang3.ObjectUtils; -{{/supportJava6}} +import java.util.Arrays; {{#imports}} import {{import}}; {{/imports}} {{#serializableModel}} import java.io.Serializable; {{/serializableModel}} +{{#jackson}} +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +{{#withXml}} +import com.fasterxml.jackson.dataformat.xml.annotation.*; +{{/withXml}} +{{/jackson}} +{{#withXml}} +import javax.xml.bind.annotation.*; +{{/withXml}} +{{#jsonb}} +import java.lang.reflect.Type; +import javax.json.bind.annotation.JsonbTypeDeserializer; +import javax.json.bind.annotation.JsonbTypeSerializer; +import javax.json.bind.serializer.DeserializationContext; +import javax.json.bind.serializer.JsonbDeserializer; +import javax.json.bind.serializer.JsonbSerializer; +import javax.json.bind.serializer.SerializationContext; +import javax.json.stream.JsonGenerator; +import javax.json.stream.JsonParser; +import javax.json.bind.annotation.JsonbProperty; +{{/jsonb}} {{#parcelableModel}} import android.os.Parcelable; import android.os.Parcel; {{/parcelableModel}} {{#useBeanValidation}} import javax.validation.constraints.*; +import javax.validation.Valid; {{/useBeanValidation}} +{{#performBeanValidation}} +import org.hibernate.validator.constraints.*; +{{/performBeanValidation}} {{#models}} {{#model}} -{{#isEnum}}{{>modelEnum}}{{/isEnum}}{{^isEnum}}{{>pojo}}{{/isEnum}} +{{#isEnum}}{{>modelEnum}}{{/isEnum}}{{^isEnum}}{{#vendorExtensions.x-is-one-of-interface}}{{>oneof_interface}}{{/vendorExtensions.x-is-one-of-interface}}{{^vendorExtensions.x-is-one-of-interface}}{{>pojo}}{{/vendorExtensions.x-is-one-of-interface}}{{/isEnum}} {{/model}} {{/models}} diff --git a/samples/server/petstore/scala-akka-http-server/.openapi-generator/FILES b/samples/server/petstore/scala-akka-http-server/.openapi-generator/FILES new file mode 100644 index 000000000000..cfcbab175e5d --- /dev/null +++ b/samples/server/petstore/scala-akka-http-server/.openapi-generator/FILES @@ -0,0 +1,15 @@ +README.md +build.sbt +src/main/scala/org/openapitools/server/AkkaHttpHelper.scala +src/main/scala/org/openapitools/server/Controller.scala +src/main/scala/org/openapitools/server/MultipartDirectives.scala +src/main/scala/org/openapitools/server/StringDirectives.scala +src/main/scala/org/openapitools/server/api/PetApi.scala +src/main/scala/org/openapitools/server/api/StoreApi.scala +src/main/scala/org/openapitools/server/api/UserApi.scala +src/main/scala/org/openapitools/server/model/ApiResponse.scala +src/main/scala/org/openapitools/server/model/Category.scala +src/main/scala/org/openapitools/server/model/Order.scala +src/main/scala/org/openapitools/server/model/Pet.scala +src/main/scala/org/openapitools/server/model/Tag.scala +src/main/scala/org/openapitools/server/model/User.scala diff --git a/samples/server/petstore/scala-akka-http-server/.openapi-generator/VERSION b/samples/server/petstore/scala-akka-http-server/.openapi-generator/VERSION index d99e7162d01f..d509cc92aa80 100644 --- a/samples/server/petstore/scala-akka-http-server/.openapi-generator/VERSION +++ b/samples/server/petstore/scala-akka-http-server/.openapi-generator/VERSION @@ -1 +1 @@ -5.0.0-SNAPSHOT \ No newline at end of file +5.1.1-SNAPSHOT \ No newline at end of file diff --git a/samples/server/petstore/scala-akka-http-server/src/main/scala/org/openapitools/server/MultipartDirectives.scala b/samples/server/petstore/scala-akka-http-server/src/main/scala/org/openapitools/server/MultipartDirectives.scala index 79891d7095a8..2ebbd943db2f 100644 --- a/samples/server/petstore/scala-akka-http-server/src/main/scala/org/openapitools/server/MultipartDirectives.scala +++ b/samples/server/petstore/scala-akka-http-server/src/main/scala/org/openapitools/server/MultipartDirectives.scala @@ -1,6 +1,7 @@ package org.openapitools.server import java.io.File +import java.nio.file.Files import akka.annotation.ApiMayChange import akka.http.scaladsl.model.Multipart.FormData @@ -69,7 +70,7 @@ trait MultipartDirectives { object MultipartDirectives extends MultipartDirectives with FileUploadDirectives { val tempFileFromFileInfo: FileInfo => File = { - file: FileInfo => File.createTempFile(file.fileName, ".tmp") + file: FileInfo => Files.createTempFile(file.fileName, ".tmp").toFile() } }