diff --git a/README.md b/README.md index acf41c2a1..536956580 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,35 @@ -# Symphony SDK for Java +# Symphony Java BDK [![CircleCI](https://circleci.com/gh/SymphonyPlatformSolutions/symphony-api-client-java.svg?style=shield)](https://circleci.com/gh/SymphonyPlatformSolutions/symphony-api-client-java) [![License: MIT](https://img.shields.io/badge/License-MIT-purple.svg)](https://opensource.org/licenses/MIT) [![Email](https://img.shields.io/static/v1?label=contact&message=email&color=darkgoldenrod)](mailto:platformsolutions@symphony.com?subject=Java%20SDK) -[![License: MIT](https://img.shields.io/badge/License-MIT-purple.svg)](https://opensource.org/licenses/MIT) [![Email](https://img.shields.io/static/v1?label=contact&message=email&color=darkgoldenrod)](mailto:platformsolutions@symphony.com?subject=Java%20SDK) +The Symphony Java BDK helps you to create Bots and Applications on top of the [Symphony REST APIs](https://developers.symphony.com/restapi/reference). + +Documentation about BDK features and usage is available under [docs](./docs/index.md) folder. + +## How to Build + +As this project contains modules for the legacy SDK/BDK as well as for the 2.0 ones, some +Maven are defined to make the build faster depending on which version you are working on. + +The BDK can be built and published to your local Maven cache using the [Maven Wrapper](https://github.com/takari/maven-wrapper). + +**Build Legacy Modules** + +The `legacy` profile is activated by default so there is no specific argument to define to have +it part of the build. However, it is also possible to skip legacy modules to be built using argument `-P -legacy`: + +```shell script +# build the legacy modules +./mvnw clean install + +# skip building legacy modules +./mvnw clean install -P -legacy +``` + +**Build BDK 2.0 Modules** + +Still in construction, the 2.0 modules are deactivated by default but can be activated through the Maven profile `2.0` : + +```shell script +# build the 2.0 modules only, skip the legacy ones +./mvnw clean install -P2.0,-legacy +``` -This repository aggregates these java libraries: -* [legacy](legacy/README.md) \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 000000000..414c3f21b --- /dev/null +++ b/docs/index.md @@ -0,0 +1 @@ +# Symphony BDK Reference Documentation \ No newline at end of file diff --git a/docs/internal/bdk-architecture.md b/docs/internal/bdk-architecture.md new file mode 100644 index 000000000..87a98818c --- /dev/null +++ b/docs/internal/bdk-architecture.md @@ -0,0 +1,18 @@ +# BDK Project Architecture + +The BDK project is composed in a set of different Maven modules. The approach consists in having one module per BDK +"layer". + +## The layers + +The BDK is divided in 3 different layers: +- `core` that contains the minimal set of classes to configure, authenticate and use +the main APIs +- `advanced` that contains additional features on top of the core module such as the +command API, the template API or even an NLP integration +- `framework` that provides connectors (or starters) for the main Java frameworks +such as [SpringBoot](https://spring.io/projects/spring-boot), +[MicroProfile](https://projects.eclipse.org/projects/technology.microprofile), +[Micronaut](https://micronaut.io/) or [Quarkus](https://quarkus.io/) + +### Core diff --git a/pom.xml b/pom.xml index 8fa7a53c6..e733bd711 100644 --- a/pom.xml +++ b/pom.xml @@ -7,17 +7,31 @@ symphony-api-client-java-parent 1.2.0-SNAPSHOT Symphony Java BDK Project - https://github.com/SymphonyPlatformSolutions/symphony-api-client-java Symphony Java BDK Parent + https://github.com/SymphonyPlatformSolutions/symphony-api-client-java pom - - symphony-bdk-legacy - - UTF-8 UTF-8 + + + 1.1.0 + 2.6 + 3.9 + 1.18.12 + 2.2 + 1.7.30 + + + RELEASE + 1.2.3 + 3.4.6 + 5.11.1 + + + false + @@ -42,6 +56,77 @@ https://github.com/SymphonyPlatformSolutions/symphony-api-client-java + + + + + org.projectlombok + lombok + ${lombok.version} + + + + + org.apiguardian + apiguardian-api + ${apiguardian-api.version} + + + + + commons-io + commons-io + ${commons-io.version} + + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + + + + + com.brsanthu + migbase64 + ${migbase64.version} + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + ch.qos.logback + logback-classic + ${logback.version} + runtime + + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + org.mock-server + mockserver-netty + ${mockserver.version} + test + + + + + @@ -54,39 +139,15 @@ 1.8 - - org.apache.maven.plugins - maven-javadoc-plugin - 3.0.1 - - - attach-javadocs - - jar - - - - - - org.apache.maven.plugins - maven-source-plugin - 3.0.1 - - - attach-sources - - jar - - - - org.apache.maven.plugins maven-surefire-plugin - 2.12.4 + 2.22.2 - false + ${skipTests} false + false + true @@ -110,7 +171,7 @@ org.jacoco jacoco-maven-plugin - 0.8.4 + 0.8.5 @@ -136,13 +197,33 @@ - + + + legacy + + true + + + symphony-bdk-legacy + + + + + 2.0 + + false + + + symphony-bdk-core + symphony-bdk-core-invokers + symphony-bdk-examples + + + - skipStepsOnWindows + release - - !windows - + false @@ -160,19 +241,46 @@ + + org.apache.maven.plugins + maven-javadoc-plugin + 3.0.1 + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.0.1 + + + attach-sources + + jar + + + + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + - - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - diff --git a/symphony-bdk-core-invokers/pom.xml b/symphony-bdk-core-invokers/pom.xml new file mode 100644 index 000000000..0805fe432 --- /dev/null +++ b/symphony-bdk-core-invokers/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + + symphony-api-client-java-parent + com.symphony.platformsolutions + 1.2.0-SNAPSHOT + + + symphony-bdk-core-invokers + Symphony Java BDK Core Invokers + Symphony Java BDK Core Invokers Module + pom + + + symphony-bdk-core-invoker-api + symphony-bdk-core-invoker-jersey2 + + + diff --git a/symphony-bdk-core-invokers/symphony-bdk-core-invoker-api/pom.xml b/symphony-bdk-core-invokers/symphony-bdk-core-invoker-api/pom.xml new file mode 100644 index 000000000..0ff8a0423 --- /dev/null +++ b/symphony-bdk-core-invokers/symphony-bdk-core-invoker-api/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + symphony-bdk-core-invokers + com.symphony.platformsolutions + 1.2.0-SNAPSHOT + + + symphony-bdk-core-invoker-api + Symphony Java BDK Core Invoker API + Symphony Java BDK Core Invoker API Module + + + 2.29.1 + + + + + + org.glassfish.jersey.core + jersey-client + ${jersey.version} + + + + org.projectlombok + lombok + provided + + + + org.apiguardian + apiguardian-api + + + + + diff --git a/symphony-bdk-core-invokers/symphony-bdk-core-invoker-api/src/main/java/com/symphony/bdk/core/api/invoker/ApiClient.java b/symphony-bdk-core-invokers/symphony-bdk-core-invoker-api/src/main/java/com/symphony/bdk/core/api/invoker/ApiClient.java new file mode 100644 index 000000000..2880ce6cb --- /dev/null +++ b/symphony-bdk-core-invokers/symphony-bdk-core-invoker-api/src/main/java/com/symphony/bdk/core/api/invoker/ApiClient.java @@ -0,0 +1,162 @@ +package com.symphony.bdk.core.api.invoker; + +import org.apiguardian.api.API; + +import java.util.List; +import java.util.Map; + +import javax.ws.rs.core.GenericType; + +/** + * Interface used to perform HTTP requests performed by the generated Swagger code. + */ +@API(status = API.Status.STABLE) +public interface ApiClient { + + /** + * Invoke API by sending HTTP request with the given options. + * + * @param Type + * @param path The sub-path of the HTTP URL + * @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 + * @return The response body in type of string + * @throws ApiException API exception + */ + ApiResponse invokeAPI( + String path, + String method, + List queryParams, + Object body, + Map headerParams, + Map cookieParams, + Map formParams, + String accept, + String contentType, + String[] authNames, + GenericType returnType + ) throws ApiException; + + /** + * Returns the API base path + * @return API base path + */ + String getBasePath(); + + /** + * Set the API base path + * @param basePath Base path + * @return API client + */ + ApiClient setBasePath(String basePath); + + /** + * Set the User-Agent header's value (by adding to the default header map). + * @param userAgent Http user agent + * @return API client + */ + ApiClient setUserAgent(String userAgent); + + /** + * Add a default header. + * @param key The header's key + * @param value The header's value + * @return API client + */ + ApiClient addDefaultHeader(String key, String value); + + /** + * The path of temporary folder used to store downloaded files from endpoints + * with file response. The default value is null, i.e. using + * the system's default tempopary folder. + * @return Temp folder path + */ + String getTempFolderPath(); + + /** + * Set temp folder path + * @param tempFolderPath Temp folder path + * @return API client + */ + ApiClient setTempFolderPath(String tempFolderPath); + + /** + * Connect timeout (in milliseconds). + * @return Connection timeout + */ + int getConnectTimeout(); + + /** + * Set the connect timeout (in milliseconds). + * A value of 0 means no timeout, otherwise values must be between 1 and {@link Integer#MAX_VALUE}. + * @param connectionTimeout Connection timeout in milliseconds + * @return API client + */ + ApiClient setConnectTimeout(int connectionTimeout); + + /** + * read timeout (in milliseconds). + * @return Read timeout + */ + int getReadTimeout(); + + /** + * 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 + */ + ApiClient setReadTimeout(int readTimeout); + + /** + * Format the given parameter object into string. + * @param param Object + * @return Object in string format + */ + String parameterToString(Object param); + + /** + * Format to {@code Pair} objects. + * @param collectionFormat Collection format + * @param name Name + * @param value Value + * @return List of pairs + */ + List parameterToPairs(String collectionFormat, String name, Object value); + + /** + * Select the Accept header's value from the given accepts array: + * if JSON exists in the given array, use it; + * otherwise use all of them (joining into a string) + * @param accepts The accepts array to select from + * @return The Accept header to use. If the given array is empty, + * null will be returned (not to set the Accept header explicitly). + */ + String selectHeaderAccept(String[] accepts); + + /** + * Select the Content-Type header's value from the given array: + * if JSON exists in the given array, use it; + * otherwise use the first one of the array. + * @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. + */ + String selectHeaderContentType(String[] contentTypes); + + /** + * Escape the given string to be used as URL query value. + * @param str String + * @return Escaped string + */ + String escapeString(String str); +} diff --git a/symphony-bdk-core-invokers/symphony-bdk-core-invoker-api/src/main/java/com/symphony/bdk/core/api/invoker/ApiException.java b/symphony-bdk-core-invokers/symphony-bdk-core-invoker-api/src/main/java/com/symphony/bdk/core/api/invoker/ApiException.java new file mode 100644 index 000000000..8061886d0 --- /dev/null +++ b/symphony-bdk-core-invokers/symphony-bdk-core-invoker-api/src/main/java/com/symphony/bdk/core/api/invoker/ApiException.java @@ -0,0 +1,58 @@ +package com.symphony.bdk.core.api.invoker; + +import lombok.Getter; +import org.apiguardian.api.API; + +import java.util.List; +import java.util.Map; + +import javax.ws.rs.core.GenericType; + +/** + * Main exception raised when invoking {@link ApiClient#invokeAPI(String, String, List, Object, Map, Map, Map, String, String, String[], GenericType)}. + * + * Initially generated by the OpenAPI Maven Generator + */ +@Getter +@API(status = API.Status.STABLE) +public class ApiException extends Exception { + + private int code = 0; + private Map> responseHeaders = null; + private String responseBody = null; + + /** + * Creates new {@link ApiException} instance. + * + * @param message the detail message. + * @param throwable the cause. + */ + public ApiException(String message, Throwable throwable) { + super(message, throwable); + } + + /** + * Creates new {@link ApiException} instance. + * + * @param code the HTTP response status code. + * @param message the detail message. + */ + public ApiException(int code, String message) { + super(message); + this.code = code; + } + + /** + * Creates new {@link ApiException} instance. + * + * @param code the HTTP response status code. + * @param message the detail message. + * @param responseHeaders list of headers returned by the server. + * @param responseBody content of the response sent back by the server. + */ + public ApiException(int code, String message, Map> responseHeaders, String responseBody) { + this(code, message); + this.responseHeaders = responseHeaders; + this.responseBody = responseBody; + } +} diff --git a/symphony-bdk-core-invokers/symphony-bdk-core-invoker-api/src/main/java/com/symphony/bdk/core/api/invoker/ApiResponse.java b/symphony-bdk-core-invokers/symphony-bdk-core-invoker-api/src/main/java/com/symphony/bdk/core/api/invoker/ApiResponse.java new file mode 100644 index 000000000..84753b988 --- /dev/null +++ b/symphony-bdk-core-invokers/symphony-bdk-core-invoker-api/src/main/java/com/symphony/bdk/core/api/invoker/ApiResponse.java @@ -0,0 +1,44 @@ +package com.symphony.bdk.core.api.invoker; + +import lombok.Getter; +import org.apiguardian.api.API; + +import java.util.List; +import java.util.Map; + +/** + * API response returned by API call. + * + * @param The type of data that is deserialized from response body + */ +@Getter +@API(status = API.Status.STABLE) +public class ApiResponse { + + private final int statusCode; + private final Map> headers; + private final T data; + + /** + * Creates new {@link ApiResponse} instance. + * + * @param statusCode The status code of HTTP response + * @param headers The headers of HTTP response + */ + public ApiResponse(int statusCode, Map> headers) { + this(statusCode, headers, null); + } + + /** + * Creates new {@link ApiResponse} instance. + * + * @param statusCode The status code of HTTP response + * @param headers The headers of HTTP response + * @param data The object deserialized from response bod + */ + public ApiResponse(int statusCode, Map> headers, T data) { + this.statusCode = statusCode; + this.headers = headers; + this.data = data; + } +} diff --git a/symphony-bdk-core-invokers/symphony-bdk-core-invoker-api/src/main/java/com/symphony/bdk/core/api/invoker/Pair.java b/symphony-bdk-core-invokers/symphony-bdk-core-invoker-api/src/main/java/com/symphony/bdk/core/api/invoker/Pair.java new file mode 100644 index 000000000..31bf94b4d --- /dev/null +++ b/symphony-bdk-core-invokers/symphony-bdk-core-invoker-api/src/main/java/com/symphony/bdk/core/api/invoker/Pair.java @@ -0,0 +1,40 @@ +package com.symphony.bdk.core.api.invoker; + +import lombok.Getter; +import org.apiguardian.api.API; + +/** + * Pair of string values. Used by generated code only. + */ +@Getter +@API(status = API.Status.INTERNAL) +public class Pair { + + private String name = ""; + private String value = ""; + + public Pair(String name, String value) { + this.setName(name); + this.setValue(value); + } + + private void setName(String name) { + if (isInvalidString(name)) { + return; + } + + this.name = name; + } + + private void setValue(String value) { + if (isInvalidString(value)) { + return; + } + + this.value = value; + } + + private static boolean isInvalidString(String arg) { + return arg == null || arg.trim().isEmpty(); + } +} diff --git a/symphony-bdk-core-invokers/symphony-bdk-core-invoker-jersey2/pom.xml b/symphony-bdk-core-invokers/symphony-bdk-core-invoker-jersey2/pom.xml new file mode 100644 index 000000000..35ba257ff --- /dev/null +++ b/symphony-bdk-core-invokers/symphony-bdk-core-invoker-jersey2/pom.xml @@ -0,0 +1,148 @@ + + + 4.0.0 + + symphony-bdk-core-invokers + com.symphony.platformsolutions + 1.2.0-SNAPSHOT + + + symphony-bdk-core-invoker-jersey2 + Symphony Java BDK Core Invoker Jersey2 + Symphony Java BDK Core Invoker Jersey2 Module + + + + + 0.2.1 + 2.10.1 + 2.29.1 + 3.0.2 + 1.6.0 + + + + + + + com.symphony.platformsolutions + symphony-bdk-core-invoker-api + 1.2.0-SNAPSHOT + + + + io.jsonwebtoken + jjwt + 0.9.1 + + + + org.bouncycastle + bcpkix-jdk15on + 1.64 + + + + commons-io + commons-io + + + + org.apache.commons + commons-lang3 + + + + org.glassfish.jersey.connectors + jersey-apache-connector + 2.30.1 + + + + + + + + io.swagger + swagger-annotations + ${swagger-annotations.version} + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + ${jackson.version} + + + org.openapitools + jackson-databind-nullable + ${jackson-databind-nullable.version} + + + + org.glassfish.jersey.media + jersey-media-json-jackson + ${jersey.version} + + + org.glassfish.jersey.core + jersey-client + ${jersey.version} + + + org.glassfish.jersey.inject + jersey-hk2 + ${jersey.version} + + + org.glassfish.jersey.media + jersey-media-multipart + ${jersey.version} + + + + com.google.code.findbugs + jsr305 + ${jsr305.version} + + + + com.brsanthu + migbase64 + + + + + + + org.junit.jupiter + junit-jupiter + test + + + ch.qos.logback + logback-classic + ${logback.version} + test + + + org.mock-server + mockserver-netty + test + + + + + diff --git a/symphony-bdk-core-invokers/symphony-bdk-core-invoker-jersey2/src/main/java/com/symphony/bdk/core/api/invoker/jersey2/ApiClientJersey2.java b/symphony-bdk-core-invokers/symphony-bdk-core-invoker-jersey2/src/main/java/com/symphony/bdk/core/api/invoker/jersey2/ApiClientJersey2.java new file mode 100644 index 000000000..9ab94c058 --- /dev/null +++ b/symphony-bdk-core-invokers/symphony-bdk-core-invoker-jersey2/src/main/java/com/symphony/bdk/core/api/invoker/jersey2/ApiClientJersey2.java @@ -0,0 +1,566 @@ +package com.symphony.bdk.core.api.invoker.jersey2; + +import com.symphony.bdk.core.api.invoker.ApiClient; +import com.symphony.bdk.core.api.invoker.ApiException; +import com.symphony.bdk.core.api.invoker.ApiResponse; +import com.symphony.bdk.core.api.invoker.Pair; + +import org.apiguardian.api.API; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +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; +import org.glassfish.jersey.media.multipart.MultiPart; +import org.glassfish.jersey.media.multipart.MultiPartFeature; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Form; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +/** + * Jersey2 implementation for the {@link ApiClient} interface called by generated code. + */ +@API(status = API.Status.STABLE) +public class ApiClientJersey2 implements ApiClient { + + protected final Map defaultHeaderMap = new HashMap<>(); + + protected final Client httpClient; + protected final JSON json; + + protected String basePath = "https://acme.symphony.com"; + protected int connectionTimeout = 0; + protected int readTimeout = 0; + protected String tempFolderPath = null; + + public ApiClientJersey2() { + + this.json = new JSON(); + this.httpClient = this.buildHttpClient(); + + // Set default User-Agent. + this.setUserAgent("Symphony BDK/2.0/java"); // TODO configure version from pom.xml + } + + public ApiClientJersey2(final String basePath) { + this(); + this.setBasePath(basePath); + } + + /** + * {@inheritDoc} + */ + @Override + public ApiResponse invokeAPI( + final String path, + final String method, + final List queryParams, + final Object body, + final Map headerParams, + final Map cookieParams, + final Map formParams, + final String accept, + final String contentType, + final String[] authNames, + final GenericType returnType + ) throws ApiException { + + // Not using `.target(this.basePath).path(path)` below, + // to support (constant) query string in `path`, e.g. "/posts?draft=1" + WebTarget target = httpClient.target(this.basePath + path); + + if (queryParams != null) { + for (Pair queryParam : queryParams) { + if (queryParam.getValue() != null) { + target = target.queryParam(queryParam.getName(), escapeString(queryParam.getValue())); + } + } + } + + Invocation.Builder invocationBuilder = target.request().accept(accept); + + for (Entry entry : headerParams.entrySet()) { + String value = entry.getValue(); + if (value != null) { + invocationBuilder = invocationBuilder.header(entry.getKey(), value); + } + } + + for (Entry entry : cookieParams.entrySet()) { + String value = entry.getValue(); + if (value != null) { + invocationBuilder = invocationBuilder.cookie(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); + } + } + } + + Entity entity = (body == null && formParams == null) ? Entity.json("") : this.serialize(body, formParams, contentType); + + Response response = null; + + try { + if ("GET".equals(method)) { + response = invocationBuilder.get(); + } else if ("POST".equals(method)) { + response = invocationBuilder.post(entity); + } else if ("PUT".equals(method)) { + response = invocationBuilder.put(entity); + } else if ("DELETE".equals(method)) { + response = invocationBuilder.method("DELETE", entity); + } else if ("PATCH".equals(method)) { + response = invocationBuilder.method("PATCH", entity); + } else if ("HEAD".equals(method)) { + response = invocationBuilder.head(); + } else if ("OPTIONS".equals(method)) { + response = invocationBuilder.options(); + } else if ("TRACE".equals(method)) { + response = invocationBuilder.trace(); + } else { + throw new ApiException(500, "unknown method type " + method); + } + + int statusCode = response.getStatusInfo().getStatusCode(); + Map> responseHeaders = buildResponseHeaders(response); + + if (response.getStatus() == Status.NO_CONTENT.getStatusCode()) { + 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 + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public String getBasePath() { + return basePath; + } + + /** + * {@inheritDoc} + */ + @Override + public ApiClient setBasePath(String basePath) { + this.basePath = basePath; + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public ApiClient setUserAgent(String userAgent) { + addDefaultHeader("User-Agent", userAgent); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public ApiClient addDefaultHeader(String key, String value) { + defaultHeaderMap.put(key, value); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public String getTempFolderPath() { + return tempFolderPath; + } + + /** + * {@inheritDoc} + */ + @Override + public ApiClient setTempFolderPath(String tempFolderPath) { + this.tempFolderPath = tempFolderPath; + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public int getConnectTimeout() { + return connectionTimeout; + } + + /** + * {@inheritDoc} + */ + @Override + public ApiClient setConnectTimeout(int connectionTimeout) { + this.connectionTimeout = connectionTimeout; + httpClient.property(ClientProperties.CONNECT_TIMEOUT, connectionTimeout); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public int getReadTimeout() { + return readTimeout; + } + + /** + * {@inheritDoc} + */ + @Override + public ApiClient setReadTimeout(int readTimeout) { + this.readTimeout = readTimeout; + httpClient.property(ClientProperties.READ_TIMEOUT, readTimeout); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public String parameterToString(Object param) { + if (param == null) { + return ""; + } else if (param instanceof Collection) { + StringBuilder b = new StringBuilder(); + for (Object o : (Collection) param) { + if (b.length() > 0) { + b.append(','); + } + b.append(o); + } + return b.toString(); + } else { + return String.valueOf(param); + } + } + + /** + * {@inheritDoc} + */ + @Override + public List parameterToPairs(String collectionFormat, String name, Object value) { + List params = new ArrayList(); + + // preconditions + if (name == null || name.isEmpty() || value == null) { return params; } + + Collection valueCollection; + if (value instanceof Collection) { + valueCollection = (Collection) value; + } else { + params.add(new Pair(name, parameterToString(value))); + return params; + } + + if (valueCollection.isEmpty()) { + return params; + } + + // get the collection format (default: csv) + String format = (collectionFormat == null || collectionFormat.isEmpty() ? "csv" : collectionFormat); + + // create the params based on the collection format + if ("multi".equals(format)) { + for (Object item : valueCollection) { + params.add(new Pair(name, parameterToString(item))); + } + + return params; + } + + String delimiter = ","; + + switch (format) { + case "csv": + delimiter = ","; + break; + case "ssv": + delimiter = " "; + break; + case "tsv": + delimiter = "\t"; + break; + case "pipes": + delimiter = "|"; + break; + } + + StringBuilder sb = new StringBuilder(); + for (Object item : valueCollection) { + sb.append(delimiter); + sb.append(parameterToString(item)); + } + + params.add(new Pair(name, sb.substring(1))); + + return params; + } + + /** + * {@inheritDoc} + */ + @Override + public String selectHeaderAccept(String[] accepts) { + if (accepts.length == 0) { + return null; + } + for (String accept : accepts) { + if (isJsonMime(accept)) { + return accept; + } + } + return String.join(",", accepts); + } + + /** + * {@inheritDoc} + */ + @Override + public String selectHeaderContentType(String[] contentTypes) { + if (contentTypes.length == 0) { + return "application/json"; + } + for (String contentType : contentTypes) { + if (isJsonMime(contentType)) { + return contentType; + } + } + return contentTypes[0]; + } + + /** + * {@inheritDoc} + */ + @Override + public String escapeString(String str) { + try { + return URLEncoder.encode(str, "utf8").replaceAll("\\+", "%20"); + } catch (UnsupportedEncodingException e) { + return str; + } + } + + /** + * Check if the given MIME is a JSON MIME. + * JSON MIME examples: + * 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 + */ + protected boolean isJsonMime(String mime) { + String jsonMime = "(?i)^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$"; + return mime != null && (mime.matches(jsonMime) || mime.equals("*/*")); + } + + /** + * Serialize the given Java object into string entity according the given + * Content-Type (only JSON is supported for now). + * @param obj Object + * @param formParams Form parameters + * @param contentType Context type + * @return Entity + * @throws ApiException API exception + */ + protected Entity serialize(Object obj, Map formParams, String contentType) { + Entity entity; + if (contentType.startsWith("multipart/form-data")) { + MultiPart multiPart = new MultiPart(); + for (Entry param : formParams.entrySet()) { + if (param.getValue() instanceof File) { + File file = (File) param.getValue(); + FormDataContentDisposition contentDisp = FormDataContentDisposition.name(param.getKey()) + .fileName(file.getName()).size(file.length()).build(); + multiPart.bodyPart(new FormDataBodyPart(contentDisp, file, MediaType.APPLICATION_OCTET_STREAM_TYPE)); + } else { + FormDataContentDisposition contentDisp = FormDataContentDisposition.name(param.getKey()).build(); + multiPart.bodyPart(new FormDataBodyPart(contentDisp, parameterToString(param.getValue()))); + } + } + entity = Entity.entity(multiPart, MediaType.MULTIPART_FORM_DATA_TYPE); + } else if (contentType.startsWith("application/x-www-form-urlencoded")) { + Form form = new Form(); + for (Entry param : formParams.entrySet()) { + form.param(param.getKey(), parameterToString(param.getValue())); + } + entity = Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE); + } else { + // We let jersey handle the serialization + entity = Entity.entity(obj, contentType); + } + return entity; + } + + /** + * Deserialize response body to Java object according to the Content-Type. + * @param Type + * @param response Response + * @param returnType Return type + * @return Deserialize object + * @throws ApiException API exception + */ + @SuppressWarnings("unchecked") + protected T deserialize(Response response, GenericType returnType) throws ApiException { + if (response == null || returnType == null) { + return null; + } + + if ("byte[]".equals(returnType.toString())) { + // Handle binary response (byte array). + return (T) response.readEntity(byte[].class); + } else if (returnType.getRawType() == File.class) { + // Handle file downloading. + return (T) downloadFileFromResponse(response); + } + + return response.readEntity(returnType); + } + + /** + * Download file from the given response. + * @param response Response + * @return File + * @throws ApiException If fail to read file content from response and write to disk + */ + protected File downloadFileFromResponse(final Response response) throws ApiException { + try { + final File file = this.prepareDownloadFile(response); + Files.copy(response.readEntity(InputStream.class), file.toPath(), StandardCopyOption.REPLACE_EXISTING); + return file; + } catch (IOException e) { + throw new ApiException("Unable to download file from response", e); + } + } + + protected File prepareDownloadFile(Response response) throws IOException { + String filename = null; + String contentDisposition = (String) response.getHeaders().getFirst("Content-Disposition"); + if (contentDisposition != null && !"".equals(contentDisposition)) { + // Get filename from the Content-Disposition header. + Pattern pattern = Pattern.compile("filename=['\"]?([^'\"\\s]+)['\"]?"); + Matcher matcher = pattern.matcher(contentDisposition); + if (matcher.find()) { filename = matcher.group(1); } + } + + String prefix; + String suffix = null; + if (filename == null) { + prefix = "download-"; + suffix = ""; + } else { + int pos = filename.lastIndexOf('.'); + if (pos == -1) { + prefix = filename + "-"; + } else { + prefix = filename.substring(0, pos) + "-"; + suffix = filename.substring(pos); + } + // File.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); } else { + return File.createTempFile(prefix, suffix, new File(tempFolderPath)); + } + } + + /** + * Build the Client used to make HTTP requests. + * @return Client + */ + protected Client buildHttpClient() { + final ClientConfig clientConfig = new ClientConfig(); + clientConfig.register(MultiPartFeature.class); + clientConfig.register(this.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); + java.util.logging.Logger.getLogger("org.glassfish.jersey.client").setLevel(java.util.logging.Level.SEVERE); + performAdditionalClientConfiguration(clientConfig); + return ClientBuilder.newClient(clientConfig); + } + + protected void performAdditionalClientConfiguration(ClientConfig clientConfig) { + // No-op extension point + } + + protected Map> buildResponseHeaders(Response response) { + Map> responseHeaders = new HashMap>(); + for (Entry> entry : response.getHeaders().entrySet()) { + List values = entry.getValue(); + List headers = new ArrayList(); + for (Object o : values) { + headers.add(String.valueOf(o)); + } + responseHeaders.put(entry.getKey(), headers); + } + return responseHeaders; + } +} diff --git a/symphony-bdk-core-invokers/symphony-bdk-core-invoker-jersey2/src/main/java/com/symphony/bdk/core/api/invoker/jersey2/JSON.java b/symphony-bdk-core-invokers/symphony-bdk-core-invoker-jersey2/src/main/java/com/symphony/bdk/core/api/invoker/jersey2/JSON.java new file mode 100644 index 000000000..3f02a1306 --- /dev/null +++ b/symphony-bdk-core-invokers/symphony-bdk-core-invoker-jersey2/src/main/java/com/symphony/bdk/core/api/invoker/jersey2/JSON.java @@ -0,0 +1,45 @@ +package com.symphony.bdk.core.api.invoker.jersey2; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.apiguardian.api.API; +import org.openapitools.jackson.nullable.JsonNullableModule; + +import java.text.DateFormat; + +import javax.ws.rs.ext.ContextResolver; + +@API(status = API.Status.INTERNAL) +public class JSON implements ContextResolver { + + private final ObjectMapper mapper; + + public JSON() { + this.mapper = new ObjectMapper(); + this.mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + this.mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + this.mapper.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false); + this.mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + this.mapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING); + this.mapper.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING); + this.mapper.setDateFormat(new RFC3339DateFormat()); + this.mapper.registerModule(new JavaTimeModule()); + this.mapper.registerModule(new JsonNullableModule()); + } + + /** + * Set the date format for JSON (de)serialization with Date properties. + * @param dateFormat Date format + */ + public void setDateFormat(DateFormat dateFormat) { + mapper.setDateFormat(dateFormat); + } + + @Override + public ObjectMapper getContext(Class type) { + return this.mapper; + } +} diff --git a/symphony-bdk-core-invokers/symphony-bdk-core-invoker-jersey2/src/main/java/com/symphony/bdk/core/api/invoker/jersey2/RFC3339DateFormat.java b/symphony-bdk-core-invokers/symphony-bdk-core-invoker-jersey2/src/main/java/com/symphony/bdk/core/api/invoker/jersey2/RFC3339DateFormat.java new file mode 100644 index 000000000..501c4b91f --- /dev/null +++ b/symphony-bdk-core-invokers/symphony-bdk-core-invoker-jersey2/src/main/java/com/symphony/bdk/core/api/invoker/jersey2/RFC3339DateFormat.java @@ -0,0 +1,20 @@ +package com.symphony.bdk.core.api.invoker.jersey2; + +import com.fasterxml.jackson.databind.util.ISO8601DateFormat; +import com.fasterxml.jackson.databind.util.ISO8601Utils; +import org.apiguardian.api.API; + +import java.text.FieldPosition; +import java.util.Date; + +@API(status = API.Status.INTERNAL) +public class RFC3339DateFormat extends ISO8601DateFormat { + + // Same as ISO8601DateFormat but serializing milliseconds. + @Override + public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) { + String value = ISO8601Utils.format(date, true); + toAppendTo.append(value); + return toAppendTo; + } +} \ No newline at end of file diff --git a/symphony-bdk-core/pom.xml b/symphony-bdk-core/pom.xml new file mode 100644 index 000000000..127691614 --- /dev/null +++ b/symphony-bdk-core/pom.xml @@ -0,0 +1,194 @@ + + + 4.0.0 + + symphony-api-client-java-parent + com.symphony.platformsolutions + 1.2.0-SNAPSHOT + + + symphony-bdk-core + Symphony Java BDK Core + Symphony Java BDK Core Module + + + + + 1.64 + 0.9.1 + 3.0.2 + 1.6.0 + + + 4.3.0 + java + jersey2 + com.symphony.bdk.gen.api + https://raw.githubusercontent.com/symphonyoss/symphony-api-spec/master + + + 2.22.2 + + + + + + + com.symphony.platformsolutions + symphony-bdk-core-invoker-api + 1.2.0-SNAPSHOT + + + + org.projectlombok + lombok + provided + + + + org.apiguardian + apiguardian-api + + + + org.slf4j + slf4j-api + + + + commons-io + commons-io + + + + org.apache.commons + commons-lang3 + + + + com.brsanthu + migbase64 + + + + io.jsonwebtoken + jjwt + ${jjwt.version} + + + + org.bouncycastle + bcpkix-jdk15on + ${bcpkix-jdk15on.version} + + + + + + + io.swagger + swagger-annotations + ${swagger-annotations.version} + + + com.google.code.findbugs + jsr305 + ${jsr305.version} + + + + + + + org.junit.jupiter + junit-jupiter + test + + + ch.qos.logback + logback-classic + test + + + org.mock-server + mockserver-netty + test + + + + + + + + + org.openapitools + openapi-generator-maven-plugin + ${openapi-generator-maven-plugin.version} + + + ${codegen.generatorName} + ${codegen.invoker.library} + + false + false + false + ${codegen.base.package} + ${codegen.base.package}.model + com.symphony.bdk.core.api.invoker + /Users/thibault.pensec/local/dx/symphony-api-client-java/templates + + true + src/main/java + java8 + false + + + + + agent-api + process-sources + + generate + + + ${codegen.spec.base}/agent/agent-api-public.yaml + + + + pod-api + process-sources + + generate + + + ${codegen.spec.base}/pod/pod-api-public.yaml + + + + authenticator-api + process-sources + + generate + + + ${codegen.spec.base}/authenticator/authenticator-api-public.yaml + + + + login-api + process-sources + + generate + + + ${codegen.spec.base}/login/login-api-public.yaml + + + + + + + + \ No newline at end of file diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/BdkCore.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/BdkCore.java new file mode 100644 index 000000000..3eb2e2374 --- /dev/null +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/BdkCore.java @@ -0,0 +1,5 @@ +package com.symphony.bdk.core; + +public class BdkCore { + // dummy class +} diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/JwtHelper.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/JwtHelper.java new file mode 100644 index 000000000..075c35d8d --- /dev/null +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/JwtHelper.java @@ -0,0 +1,117 @@ +package com.symphony.bdk.core.auth; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.apiguardian.api.API; +import org.bouncycastle.asn1.pkcs.RSAPrivateKey; +import org.bouncycastle.crypto.params.RSAPrivateCrtKeyParameters; +import org.bouncycastle.crypto.util.PrivateKeyInfoFactory; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemReader; + +import java.io.IOException; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; +import java.util.Date; + +/** + * JWT helper class, used to : + *
    + *
  • load a private key
  • + *
  • generated a signed JWT for a given user
  • + *
+ */ +@API(status = API.Status.INTERNAL) +public class JwtHelper { + + // PKCS#8 format + private static final String PEM_PRIVATE_START = "-----BEGIN PRIVATE KEY-----"; + private static final String PEM_PRIVATE_END = "-----END PRIVATE KEY-----"; + + // PKCS#1 format + private static final String PEM_RSA_PRIVATE_START = "-----BEGIN RSA PRIVATE KEY-----"; + + /** + * Creates a JWT with the provided user name and expiration date, signed with the provided private key. + * @param user the username to authenticate; will be verified by the pod + * @param expiration of the authentication request in milliseconds; cannot be longer than the value defined on the + * pod + * @param privateKey the private RSA key to be used to sign the authentication request; will be checked on the pod + * against + * the public key stored for the user + * @return a signed JWT for a specific user (or subject) + */ + public static String createSignedJwt(String user, long expiration, Key privateKey) { + return Jwts.builder() + .setSubject(user) + .setExpiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(SignatureAlgorithm.RS512, privateKey) + .compact(); + } + + /** + * Creates a RSA Private Key from a PEM String. It supports PKCS#1 and PKCS#8 string formats. + * + * @param pemPrivateKey RSA Private Key content + * @return a {@link PrivateKey} instance + * @throws GeneralSecurityException On invalid Private Key + */ + public static PrivateKey parseRSAPrivateKey(final String pemPrivateKey) throws GeneralSecurityException { + + // PKCS#8 format + if (pemPrivateKey.contains(PEM_PRIVATE_START)) { + return parsePKCS8PrivateKey(pemPrivateKey); + } + // PKCS#1 format + else if (pemPrivateKey.contains(PEM_RSA_PRIVATE_START)) { + return parsePKCS1PrivateKey(pemPrivateKey); + } + // format not detected + else { + throw new GeneralSecurityException("Invalid private key. Header not recognized."); + } + } + + private static PrivateKey parsePKCS1PrivateKey(String pemPrivateKey) throws GeneralSecurityException { + try (final PemReader pemReader = new PemReader(new StringReader(pemPrivateKey))) { + final PemObject privateKeyObject = pemReader.readPemObject(); + final RSAPrivateKey rsa = RSAPrivateKey.getInstance(privateKeyObject.getContent()); + final RSAPrivateCrtKeyParameters privateKeyParameter = new RSAPrivateCrtKeyParameters( + rsa.getModulus(), + rsa.getPublicExponent(), + rsa.getPrivateExponent(), + rsa.getPrime1(), + rsa.getPrime2(), + rsa.getExponent1(), + rsa.getExponent2(), + rsa.getCoefficient() + ); + + return new JcaPEMKeyConverter().getPrivateKey(PrivateKeyInfoFactory.createPrivateKeyInfo(privateKeyParameter)); + } catch (IOException e) { + throw new GeneralSecurityException("Invalid private key.", e); + } + } + + private static PrivateKey parsePKCS8PrivateKey(String pemPrivateKey) throws InvalidKeySpecException, NoSuchAlgorithmException { + + final String privateKeyString = pemPrivateKey + .replace(PEM_PRIVATE_START, "") + .replace(PEM_PRIVATE_END, "") + .replace("\\n", "\n") + .replaceAll("\\s", ""); + + final byte[] keyBytes = Base64.getDecoder().decode(privateKeyString.getBytes(StandardCharsets.UTF_8)); + + return KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(keyBytes)); + } +} diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/JwtHelperTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/JwtHelperTest.java new file mode 100644 index 000000000..2ac13089a --- /dev/null +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/JwtHelperTest.java @@ -0,0 +1,38 @@ +package com.symphony.bdk.core.auth; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.migcomponents.migbase64.Base64; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; + +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; + +/** + * Test class for the {@link JwtHelper}. + * @author Thibault Pensec + * @since 29/02/2020 + */ +@Slf4j +class JwtHelperTest { + + @Test + void loadRSAPrivateKey() throws GeneralSecurityException { + final PrivateKey privateKey = JwtHelper.parseRSAPrivateKey(generateRSAPrivateKey()); + assertNotNull(privateKey); + } + + @SneakyThrows + private static String generateRSAPrivateKey() { + final KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(4096); + final KeyPair kp = kpg.generateKeyPair(); + return "-----BEGIN PRIVATE KEY-----\n" + + Base64.encodeToString(kp.getPrivate().getEncoded(), true) + + "\n-----END PRIVATE KEY-----"; + } +} diff --git a/symphony-bdk-examples/pom.xml b/symphony-bdk-examples/pom.xml new file mode 100644 index 000000000..03b141068 --- /dev/null +++ b/symphony-bdk-examples/pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + + symphony-api-client-java-parent + com.symphony.platformsolutions + 1.2.0-SNAPSHOT + + + symphony-bdk-examples + Symphony Java BDK Examples Parent + Symphony Java BDK Parent Module + pom + + + simple-hello-world + + + diff --git a/symphony-bdk-examples/simple-hello-world/pom.xml b/symphony-bdk-examples/simple-hello-world/pom.xml new file mode 100644 index 000000000..f66ca5339 --- /dev/null +++ b/symphony-bdk-examples/simple-hello-world/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + symphony-bdk-examples + com.symphony.platformsolutions + 1.2.0-SNAPSHOT + + + symphony-bdk-examples-hello-world + Symphony Java BDK Examples - Hello World + Symphony Java BDK Parent - Hello World + + + + + com.symphony.platformsolutions + symphony-bdk-core + 1.2.0-SNAPSHOT + + + + com.symphony.platformsolutions + symphony-bdk-core-invoker-jersey2 + 1.2.0-SNAPSHOT + + + + org.projectlombok + lombok + provided + + + + org.slf4j + slf4j-api + + + + ch.qos.logback + logback-classic + + + + + \ No newline at end of file diff --git a/symphony-bdk-examples/simple-hello-world/src/main/java/com/symphony/bdk/examples/HelloWorldMain.java b/symphony-bdk-examples/simple-hello-world/src/main/java/com/symphony/bdk/examples/HelloWorldMain.java new file mode 100644 index 000000000..c4893bfab --- /dev/null +++ b/symphony-bdk-examples/simple-hello-world/src/main/java/com/symphony/bdk/examples/HelloWorldMain.java @@ -0,0 +1,64 @@ +package com.symphony.bdk.examples; + +import com.symphony.bdk.core.api.invoker.ApiClient; +import com.symphony.bdk.core.api.invoker.jersey2.ApiClientJersey2; +import com.symphony.bdk.core.auth.JwtHelper; +import com.symphony.bdk.gen.api.AuthenticationApi; +import com.symphony.bdk.gen.api.MessageApi; +import com.symphony.bdk.gen.api.MessagesApi; +import com.symphony.bdk.gen.api.model.AuthenticateRequest; +import com.symphony.bdk.gen.api.model.V4Message; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.IOUtils; + +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.PrivateKey; + +@Slf4j +public class HelloWorldMain { + + private static final String POD_BASE_URL = "https://devx1.symphony.com"; + + public static void main(String[] args) throws Exception { + + final AuthenticationApi loginAuthApi = new AuthenticationApi(new ApiClientJersey2(POD_BASE_URL + "/login")); + final AuthenticationApi kmAuthApi = new AuthenticationApi(new ApiClientJersey2(POD_BASE_URL + "/relay")); + + final AuthenticateRequest request = new AuthenticateRequest(); + request.setToken(generateJwt()); + + final String sessionToken = loginAuthApi.pubkeyAuthenticatePost(request).getToken(); + final String keyManagerToken = kmAuthApi.pubkeyAuthenticatePost(request).getToken(); + + log.info("Successfully Authenticated !"); + log.info("#### sessionToken ####\n{}", sessionToken); + log.info("#### keyManagerToken ####\n{}", keyManagerToken); + + final MessagesApi messagesApi = new MessagesApi(new ApiClientJersey2(POD_BASE_URL + "/agent")); + final V4Message message = messagesApi.v4StreamSidMessageCreatePost( + "2IFEMquh3pOHAxcgLF8jU3___ozwgwIVdA", + sessionToken, + keyManagerToken, + "Hello, World!", + null, null, null, null + ); + + log.info("Message {} successfully sent", message.getMessageId()); + } + + private static String generateJwt() throws GeneralSecurityException, IOException { + + final String privateKeyPath = System.getProperty("privateKeyPath"); + final String username = System.getProperty("username"); + + log.info("privateKeyPath={}", privateKeyPath); + log.info("username={}", username); + + final PrivateKey privateKey = JwtHelper.parseRSAPrivateKey(IOUtils.toString(new FileInputStream(privateKeyPath), StandardCharsets.UTF_8)); + return JwtHelper.createSignedJwt(username, 30_000, privateKey); + } +} diff --git a/symphony-bdk-legacy/pom.xml b/symphony-bdk-legacy/pom.xml index 8ddc6d690..cfc6b92ee 100644 --- a/symphony-bdk-legacy/pom.xml +++ b/symphony-bdk-legacy/pom.xml @@ -9,8 +9,7 @@ symphony-bdk-legacy - 1.2.0-SNAPSHOT - Symphony Java BDK Project Legacy + [legacy] Symphony Java BDK Legacy Parent Symphony Java BDK Legacy pom diff --git a/symphony-bdk-legacy/symphony-api-client-java/pom.xml b/symphony-bdk-legacy/symphony-api-client-java/pom.xml index 24d444ad2..f1ebdeaf9 100644 --- a/symphony-bdk-legacy/symphony-api-client-java/pom.xml +++ b/symphony-bdk-legacy/symphony-api-client-java/pom.xml @@ -14,7 +14,6 @@ - 1.1.0 1.9.4 1.14 2.6 @@ -42,7 +41,6 @@ org.apiguardian apiguardian-api - ${apiguardian-api.version} com.fasterxml.jackson.core diff --git a/templates/api.mustache b/templates/api.mustache new file mode 100644 index 000000000..2c303bd86 --- /dev/null +++ b/templates/api.mustache @@ -0,0 +1,250 @@ +package {{package}}; + +import {{invokerPackage}}.ApiException; +import {{invokerPackage}}.ApiClient; +import {{invokerPackage}}.ApiResponse; +import {{invokerPackage}}.Pair; + +import javax.ws.rs.core.GenericType; // TODO remove this + +{{#imports}} +import {{import}}; +{{/imports}} + +{{^fullJavaUtil}} +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +{{/fullJavaUtil}} + +{{>generatedAnnotation}} +{{#operations}} +public class {{classname}} { + + private final ApiClient apiClient; + + public {{classname}}(ApiClient apiClient) { + this.apiClient = apiClient; + } + + /** + * Get the API cilent + * + * @return API client + */ + public ApiClient getApiClient() { + return apiClient; + } + + {{#operation}} + {{^vendorExtensions.x-group-parameters}} + /** + * {{summary}} + * {{notes}} + {{#allParams}} + * @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}} + {{/allParams}} + {{#returnType}} + * @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}}{{#hasMore}}, {{/hasMore}}{{/allParams}}) throws ApiException { + {{#returnType}}return {{/returnType}}{{operationId}}WithHttpInfo({{#allParams}}{{paramName}}{{#hasMore}}, {{/hasMore}}{{/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}}{{#hasMore}}, {{/hasMore}}{{/allParams}}) throws ApiException { + Object localVarPostBody = {{#bodyParam}}{{paramName}}{{/bodyParam}}{{^bodyParam}}null{{/bodyParam}}; + {{#allParams}}{{#required}} + // verify the required parameter '{{paramName}}' is set + if ({{paramName}} == null) { + throw new ApiException(400, "Missing the required parameter '{{paramName}}' when calling {{operationId}}"); + } + {{/required}}{{/allParams}} + // create path and map variables + 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}} + localVarQueryParams.addAll(apiClient.parameterToPairs("{{#collectionFormat}}{{{collectionFormat}}}{{/collectionFormat}}", "{{baseName}}", {{paramName}})); + {{/queryParams}} + + {{#headerParams}}if ({{paramName}} != null) + 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}} + + final String[] localVarAccepts = { + {{#produces}}"{{{mediaType}}}"{{#hasMore}}, {{/hasMore}}{{/produces}} + }; + final String localVarAccept = apiClient.selectHeaderAccept(localVarAccepts); + + final String[] localVarContentTypes = { + {{#consumes}}"{{{mediaType}}}"{{#hasMore}}, {{/hasMore}}{{/consumes}} + }; + final String localVarContentType = apiClient.selectHeaderContentType(localVarContentTypes); + + String[] localVarAuthNames = new String[] { {{#authMethods}}"{{name}}"{{#hasMore}}, {{/hasMore}}{{/authMethods}} }; + + {{#returnType}} + GenericType<{{{returnType}}}> localVarReturnType = new GenericType<{{{returnType}}}>() {}; + + {{/returnType}} + return apiClient.invokeAPI(localVarPath, "{{httpMethod}}", localVarQueryParams, localVarPostBody, + localVarHeaderParams, localVarCookieParams, localVarFormParams, localVarAccept, localVarContentType, + localVarAuthNames, {{#returnType}}localVarReturnType{{/returnType}}{{^returnType}}null{{/returnType}}); + } + {{#vendorExtensions.x-group-parameters}} + + public class API{{operationId}}Request { + {{#allParams}} + private {{#isRequired}}final {{/isRequired}}{{{dataType}}} {{paramName}}; + {{/allParams}} + + private API{{operationId}}Request({{#pathParams}}{{{dataType}}} {{paramName}}{{#hasMore}}, {{/hasMore}}{{/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}}{{#hasMore}}, {{/hasMore}}{{/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}}{{#hasMore}}, {{/hasMore}}{{/pathParams}}) throws ApiException { + return new API{{operationId}}Request({{#pathParams}}{{paramName}}{{#hasMore}}, {{/hasMore}}{{/pathParams}}); + } + {{/vendorExtensions.x-group-parameters}} + {{/operation}} +} +{{/operations}} \ No newline at end of file