diff --git a/build.gradle b/build.gradle index 75d8c790..dd0c74b0 100644 --- a/build.gradle +++ b/build.gradle @@ -10,9 +10,8 @@ group 'io.github.jopenlibs' archivesBaseName = 'vault-java-driver' version '5.4.0' -// This project is actually limited to Java 8 compatibility. See below. -sourceCompatibility = 9 -targetCompatibility = 9 +sourceCompatibility = 11 +targetCompatibility = 11 repositories { mavenCentral() @@ -31,46 +30,14 @@ dependencies { testRuntimeOnly('org.slf4j:slf4j-simple:2.0.3') } -// Beginning of Java 9 compatibility config -// -// Allowing a library to support Java 9+ modularity, while also maintaining backwards-compatibility for Java 8 users, is WAY more -// tricky than expected! The lines below are adapted from a blog article (https://dzone.com/articles/building-java-6-8-libraries-for-jpms-in-gradle), -// and cause the built classes to support a Java 8 JRE, while also including a module definition suitable for use with Java 9. There are a -// few considerations that come with this: -// -// * You now need JDK 9 or higher to BUILD this library. You can still USE the built artifact as a dependency in a Java 8 project. -// * Although "sourceCompatibility" and "targetCompatability" above are set for Java 9, the "compileJava" settings below will not -// allow you to build with any code changes that are not Java 8 compatible. -// * Unfortunately, IntelliJ (and perhaps other IDE's?) will show syntax highlighting, code completion tips, etc for Java 9. Sorry for -// the inconvenience. In any case, you should not commit changes to source control without confirming that the project builds. compileJava { - exclude 'module-info.java' - - options.compilerArgs = ['--release', '8'] + options.compilerArgs = ['--release', targetCompatibility.toString()] } tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' } -tasks.register('compileModuleInfoJava', JavaCompile) { - classpath = files() - source = 'src/main/java/module-info.java' - destinationDir = compileJava.destinationDir - options.encoding = compileJava.options.encoding - - doFirst { - options.compilerArgs = [ - '--release', '9', - '--module-path', compileJava.classpath.asPath, - ] - } -} - -compileModuleInfoJava.dependsOn compileJava -classes.dependsOn compileModuleInfoJava -// End of Java 9 compatibility config - tasks.register('javadocJar', Jar) { dependsOn tasks.named("javadoc") archiveClassifier.set('javadoc') @@ -115,8 +82,14 @@ tasks.named('test') { events "passed", "skipped", "failed" } - reports.html.enabled = false - reports.junitXml.enabled = true + reports { + html { + required = false + } + junitXml { + required = true + } + } } def integrationTestTask = tasks.register('integrationTest', Test) { @@ -127,8 +100,14 @@ def integrationTestTask = tasks.register('integrationTest', Test) { events "passed", "skipped", "failed" } - reports.html.enabled = false - reports.junitXml.enabled = true + reports { + html { + required = false + } + junitXml { + required = true + } + } } // diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3301c79d..4d52a502 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https://services.gradle.org/distributions/gradle-7.6.1-all.zip +distributionUrl=https://services.gradle.org/distributions/gradle-8.2-all.zip diff --git a/src/main/java/io/github/jopenlibs/vault/api/Auth.java b/src/main/java/io/github/jopenlibs/vault/api/Auth.java index c25a20bb..f05e7cf8 100644 --- a/src/main/java/io/github/jopenlibs/vault/api/Auth.java +++ b/src/main/java/io/github/jopenlibs/vault/api/Auth.java @@ -439,7 +439,7 @@ public AuthResponse loginByAppID(final String path, final String appId, final St .toString(); final RestResponse restResponse = new Rest()//NOPMD .url(config.getAddress() + "/v1/auth/" + path) - .optionalHeader("X-Vault-Namespace", this.nameSpace) + .header("X-Vault-Namespace", this.nameSpace) .body(requestJson.getBytes(StandardCharsets.UTF_8)) .connectTimeoutSeconds(config.getOpenTimeout()) .readTimeoutSeconds(config.getReadTimeout()) diff --git a/src/main/java/io/github/jopenlibs/vault/rest/Rest.java b/src/main/java/io/github/jopenlibs/vault/rest/Rest.java index 70ec257c..efa9fc33 100644 --- a/src/main/java/io/github/jopenlibs/vault/rest/Rest.java +++ b/src/main/java/io/github/jopenlibs/vault/rest/Rest.java @@ -2,26 +2,30 @@ import io.github.jopenlibs.vault.SslConfig; import io.github.jopenlibs.vault.VaultConfig; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLConnection; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublisher; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpRequest.Builder; +import java.net.http.HttpResponse.BodyHandlers; import java.nio.charset.StandardCharsets; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; -import java.util.ArrayList; +import java.time.Duration; +import java.time.temporal.ChronoUnit; import java.util.Arrays; -import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.StringJoiner; import java.util.TreeMap; -import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; @@ -188,26 +192,6 @@ public Rest header(final String name, final String value) { return this; } - /** - *

Adds an optional header to be sent with the HTTP request.

- * * - *

The value, if null, will skip adding this header to the request.

- * - *

This method may be chained together repeatedly, to pass multiple headers with a request. - * When the request is ultimately sent, the headers will be sorted by their names.

- * - * @param name The raw header name - * @param value The raw header value - * @return This object, with a header added, ready for other builder-pattern config methods or - * an HTTP verb method - * @deprecated use {@link #header(String, String)} instead. - */ - @Deprecated - public Rest optionalHeader(final String name, final String value) { - header(name, value); - return this; - } - /** *

The number of seconds to wait before giving up on establishing an HTTP(S) connection.

* @@ -284,30 +268,10 @@ public Rest sslContext(final SSLContext sslContext) { * @throws RestException If an error occurs, or an unexpected response received */ public RestResponse get() throws RestException { - if (urlString == null) { - throw new RestException("No URL is set"); - } try { - if (!parameters.isEmpty()) { - // Append parameters to existing query string, or create one - if (urlString.indexOf('?') == -1) { - urlString = urlString + "?" + parametersToQueryString(); - } else { - urlString = urlString + "&" + parametersToQueryString(); - } - } - // Initialize HTTP(S) connection, and set any header values - final URLConnection connection = initURLConnection(urlString, "GET"); - for (final Map.Entry header : headers.entrySet()) { - connection.setRequestProperty(header.getKey(), header.getValue()); - } + var request = buildRequest(true); - // Get the resulting status code - final int statusCode = connectionStatus(connection); - // Download and parse response - final String mimeType = connection.getContentType(); - final byte[] body = responseBodyBytes(connection); - return new RestResponse(statusCode, mimeType, body); + return send(request.GET().build()); } catch (Exception e) { throw new RestException(e); } @@ -361,30 +325,9 @@ public RestResponse put() throws RestException { * @throws RestException If an error occurs, or an unexpected response received */ public RestResponse delete() throws RestException { - if (urlString == null) { - throw new RestException("No URL is set"); - } try { - if (!parameters.isEmpty()) { - // Append parameters to existing query string, or create one - if (urlString.indexOf('?') == -1) { - urlString = urlString + "?" + parametersToQueryString(); - } else { - urlString = urlString + "&" + parametersToQueryString(); - } - } - // Initialize HTTP(S) connection, and set any header values - final URLConnection connection = initURLConnection(urlString, "DELETE"); - for (final Map.Entry header : headers.entrySet()) { - connection.setRequestProperty(header.getKey(), header.getValue()); - } - - // Get the resulting status code - final int statusCode = connectionStatus(connection); - // Download and parse response - final String mimeType = connection.getContentType(); - final byte[] body = responseBodyBytes(connection); - return new RestResponse(statusCode, mimeType, body); + var request = this.buildRequest(true); + return send(request.DELETE().build()); } catch (Exception e) { throw new RestException(e); } @@ -401,104 +344,31 @@ public RestResponse delete() throws RestException { * @return The result of the HTTP operation */ private RestResponse postOrPutImpl(final boolean doPost) throws RestException { - if (urlString == null) { - throw new RestException("No URL is set"); - } try { - // Initialize HTTP connection, and set any header values - URLConnection connection; - if (doPost) { - connection = initURLConnection(urlString, "POST"); - } else { - connection = initURLConnection(urlString, "PUT"); - } - for (final Map.Entry header : headers.entrySet()) { - connection.setRequestProperty(header.getKey(), header.getValue()); - } - - connection.setDoOutput(true); - connection.setRequestProperty("Accept-Charset", "UTF-8"); + // Initialize HTTP(S) connection, and set any header values + var request = this.buildRequest(false); + request.header("Accept-Charset", "UTF-8"); - // If a body payload has been provided, then it takes precedence. Otherwise, look for any additional - // parameters to send as form field values. Parameters sent via the base URL query string are left - // as-is regardless. + BodyPublisher payload; if (body != null) { - final OutputStream outputStream = connection.getOutputStream(); - outputStream.write(body); - outputStream.close(); + payload = BodyPublishers.ofByteArray(body); } else if (!parameters.isEmpty()) { - connection.setRequestProperty("Content-Type", + request.header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"); - final OutputStream outputStream = connection.getOutputStream(); - outputStream.write(parametersToQueryString().getBytes(StandardCharsets.UTF_8)); - outputStream.close(); - } - - // Get the resulting status code - final int statusCode = connectionStatus(connection); - // Download and parse response - final String mimeType = connection.getContentType(); - final byte[] body = responseBodyBytes(connection); - return new RestResponse(statusCode, mimeType, body); - } catch (IOException e) { - throw new RestException(e); - } - } - - /** - *

This helper method constructs a new HttpURLConnection or - * HttpsURLConnection, configured with all of the settings that were passed in - * when first initializing this Rest instance (e.g. timeout thresholds, SSL - * verification, SSL certificate data).

- * - * @param urlString The URL to which this connection will be made - * @param method The applicable request method (e.g. "GET", "POST", etc) - * @throws RestException If the URL cannot be successfully parsed, or if there are errors - * processing an SSL cert, etc. - */ - private URLConnection initURLConnection(final String urlString, final String method) - throws RestException { - URLConnection connection = null; - try { - final URL url = new URL(urlString); - connection = url.openConnection(); - - // Timeout settings, if applicable - if (connectTimeoutSeconds != null) { - connection.setConnectTimeout(connectTimeoutSeconds * 1000); - } - if (readTimeoutSeconds != null) { - connection.setReadTimeout(readTimeoutSeconds * 1000); + payload = BodyPublishers.ofByteArray( + parametersToQueryString().getBytes(StandardCharsets.UTF_8)); + } else { + payload = BodyPublishers.noBody(); } - // SSL settings, if applicable - if (connection instanceof HttpsURLConnection) { - final HttpsURLConnection httpsURLConnection = (HttpsURLConnection) connection; - if (sslVerification != null && !sslVerification) { - // SSL verification disabled - httpsURLConnection.setSSLSocketFactory(DISABLED_SSL_CONTEXT.getSocketFactory()); - httpsURLConnection.setHostnameVerifier((s, sslSession) -> true); - } else if (sslContext != null) { - // Cert file supplied - httpsURLConnection.setSSLSocketFactory(sslContext.getSocketFactory()); - } - httpsURLConnection.setRequestMethod(method); - } else if (connection instanceof HttpURLConnection) { - final HttpURLConnection httpURLConnection = (HttpURLConnection) connection; - httpURLConnection.setRequestMethod(method); + if (doPost) { + return send(request.POST(payload).build()); } else { - final String message = "URL string " + urlString - + " cannot be parsed as an instance of HttpURLConnection or HttpsURLConnection"; - throw new RestException(message); + return send(request.PUT(payload).build()); } - return connection; - } catch (Exception e) { + } catch (IOException | InterruptedException | URISyntaxException e) { throw new RestException(e); - } finally { - if (connection instanceof HttpURLConnection) { - ((HttpURLConnection) connection).disconnect(); - } } } @@ -510,100 +380,83 @@ private URLConnection initURLConnection(final String urlString, final String met * @return A url-encoded URL query string */ private String parametersToQueryString() { - final StringBuilder queryString = new StringBuilder(); - final List> params = new ArrayList<>(parameters.entrySet()); - for (int index = 0; index < params.size(); index++) { - if (index > 0) { - queryString.append('&'); - } - final String name = params.get(index).getKey(); - final String value = params.get(index).getValue(); - queryString.append(name).append('=').append(value); - } - return queryString.toString(); + final var sj = new StringJoiner("&"); + parameters.forEach((name, value) -> sj.add(name + "=" + value)); + + return sj.toString(); } /** - *

This helper method downloads the body of an HTTP response (e.g. a clob of JSON text) as - * binary data.

+ * This helper method initialize {@link HttpClient} and send {@link HttpRequest} to remote + * resource * - * @param connection An active HTTP(S) connection - * @return The body payload, downloaded from the HTTP connection response + * @param req an {@link HttpRequest} request + * @return A {@link RestResponse} instance + * @throws IOException if connection fails + * @throws InterruptedException if connection is interrupted */ - private byte[] responseBodyBytes(final URLConnection connection) throws RestException { - try { - final InputStream inputStream; - final int responseCode = this.connectionStatus(connection); - if (200 <= responseCode && responseCode <= 299) { - inputStream = connection.getInputStream(); - } else { - if (connection instanceof HttpsURLConnection) { - final HttpsURLConnection httpsURLConnection = (HttpsURLConnection) connection; - inputStream = httpsURLConnection.getErrorStream(); - } else { - final HttpURLConnection httpURLConnection = (HttpURLConnection) connection; - inputStream = httpURLConnection.getErrorStream(); - } - } - return handleResponseInputStream(inputStream); - } catch (IOException e) { - return new byte[0]; + private RestResponse send(HttpRequest req) throws IOException, InterruptedException { + final var client = HttpClient.newBuilder(); + if (connectTimeoutSeconds != null) { + client.connectTimeout(Duration.of(connectTimeoutSeconds, ChronoUnit.SECONDS)); } - } - - /** - *

This helper method extracts the HTTP(S) status code from a URLConnection, - * provided that it is an HttpURLConnection or a - * HttpsUrlConnection.

- * - * @param connection An active HTTP(S) connection - */ - private int connectionStatus(final URLConnection connection) throws IOException, RestException { - int statusCode; - if (connection instanceof HttpsURLConnection) { - final HttpsURLConnection httpsURLConnection = (HttpsURLConnection) connection; - statusCode = httpsURLConnection.getResponseCode(); - } else if (connection instanceof HttpURLConnection) { - final HttpURLConnection httpURLConnection = (HttpURLConnection) connection; - statusCode = httpURLConnection.getResponseCode(); - } else { - final String className = connection != null ? connection.getClass().getName() : "null"; - throw new RestException("Expecting a URLConnection of type " - + HttpURLConnection.class.getName() - + " or " - + HttpsURLConnection.class.getName() - + ", found " - + className); + if (sslVerification != null && !sslVerification) { + client.sslContext(DISABLED_SSL_CONTEXT); + } else if (sslContext != null) { + client.sslContext(sslContext); } - return statusCode; + + var response = client.build().send(req, BodyHandlers.ofString()); + + // Get the resulting status code + final var statusCode = response.statusCode(); + + // Download and parse response + final var mimeType = response.headers().firstValue("Content-Type").orElse(""); + final var body = response.body().getBytes(); + + return new RestResponse(statusCode, mimeType, body); } + /** - *

This method handles the response stream from the connection.

+ * This helper method build an {@link HttpRequest.Builder} object used to send requests to + * remote resource * - * @param inputStream The input stream from the connection. - * @return The body payload, downloaded from the HTTP connection response + * @param isGetOrDelete sets if request is called for a GET or DELETE request instead of POST or + * PUT request + * @return a {@link HttpRequest.Builder} bojnect + * @throws URISyntaxException if passed URL isn't valid + * @throws RestException if isn't passed an URL */ - protected byte[] handleResponseInputStream(final InputStream inputStream) { - try { - // getErrorStream() can return null so handle it. - if (inputStream != null) { - final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - - int bytesRead; - final byte[] bytes = new byte[16384]; - while ((bytesRead = inputStream.read(bytes, 0, bytes.length)) != -1) { - byteArrayOutputStream.write(bytes, 0, bytesRead); - } + private Builder buildRequest(Boolean isGetOrDelete) throws URISyntaxException, RestException { + Optional.ofNullable(urlString).orElseThrow(() -> new RestException("No URL is set")); - byteArrayOutputStream.flush(); - return byteArrayOutputStream.toByteArray(); - } else { - return new byte[0]; + var uri = new URI(urlString); + var params = isGetOrDelete ? parametersToQueryString() : ""; + var query = params; + + if (uri.getQuery() != null) { + query = uri.getQuery(); + if (!params.isEmpty()) { + query = uri.getQuery() + "&" + params; } - } catch (IOException e) { - return new byte[0]; } + uri = new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(), + uri.getPath(), query, uri.getFragment()); + + // Initialize HTTP(S) connection, and set any header values + var request = HttpRequest.newBuilder() + .version(Version.HTTP_1_1) + .uri(uri); + + headers.forEach(request::header); + + if (readTimeoutSeconds != null) { + request.timeout(Duration.of(readTimeoutSeconds, ChronoUnit.SECONDS)); + } + + return request; } } diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 512656e5..478d5b5d 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,5 +1,6 @@ module vault.java.driver { requires java.logging; + requires java.net.http; exports io.github.jopenlibs.vault; exports io.github.jopenlibs.vault.api; exports io.github.jopenlibs.vault.api.database; diff --git a/src/test/java/io/github/jopenlibs/vault/rest/GetTests.java b/src/test/java/io/github/jopenlibs/vault/rest/GetTests.java index cc95f61d..8605e857 100644 --- a/src/test/java/io/github/jopenlibs/vault/rest/GetTests.java +++ b/src/test/java/io/github/jopenlibs/vault/rest/GetTests.java @@ -191,16 +191,4 @@ public void testGet_RetrievesResponseBodyWhenStatusIs418() throws RestException assertTrue("Response body doesn't contain word User-Agent", responseBody.contains("User-Agent")); } - - - /** - *

Verify that response body does not cause NPE when input stream is null.

- */ - @Test - public void test_handleResponseInputStream() { - final Rest rest = new Rest(); - final byte[] result = rest.handleResponseInputStream(null); - assertEquals(0, result.length); - } - }