You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by gr...@apache.org on 2022/08/11 21:26:02 UTC
[nifi] branch main updated: NIFI-10244 Added nifi-web-client-api and implementation
This is an automated email from the ASF dual-hosted git repository.
greyp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new 864036674e NIFI-10244 Added nifi-web-client-api and implementation
864036674e is described below
commit 864036674ec101c81701bbd8fbb7f96ad25dd3be
Author: exceptionfactory <ex...@apache.org>
AuthorDate: Tue Jul 19 23:15:57 2022 -0500
NIFI-10244 Added nifi-web-client-api and implementation
- Added nifi-web-client implementation based on OkHttp
- Added WebClientServiceProvider Controller Service interface and implementation
- Corrected comments and added unmodifiableMap wrapper
- Added getHeaderNames() and corrected ProxyContext comments
This closes #6268
Signed-off-by: Paul Grey <gr...@apache.org>
---
nifi-assembly/pom.xml | 6 +
nifi-commons/nifi-web-client-api/pom.xml | 25 ++
.../nifi/web/client/api/HttpEntityHeaders.java | 49 +++
.../nifi/web/client/api/HttpRequestBodySpec.java | 34 ++
.../web/client/api/HttpRequestHeadersSpec.java | 38 ++
.../nifi/web/client/api/HttpRequestMethod.java | 29 ++
.../nifi/web/client/api/HttpRequestUriSpec.java | 32 ++
.../nifi/web/client/api/HttpResponseEntity.java | 46 +++
.../nifi/web/client/api/HttpResponseStatus.java | 58 +++
.../apache/nifi/web/client/api/HttpUriBuilder.java | 80 ++++
.../web/client/api/StandardHttpRequestMethod.java | 37 ++
.../nifi/web/client/api/WebClientService.java | 65 +++
.../web/client/api/WebClientServiceException.java | 45 +++
nifi-commons/nifi-web-client/pom.xml | 42 ++
.../nifi/web/client/BasicProxyAuthenticator.java | 43 ++
.../nifi/web/client/InputStreamRequestBody.java | 56 +++
.../nifi/web/client/StandardHttpEntityHeaders.java | 55 +++
.../web/client/StandardHttpResponseEntity.java | 64 +++
.../nifi/web/client/StandardHttpUriBuilder.java | 80 ++++
.../nifi/web/client/StandardWebClientService.java | 303 ++++++++++++++
.../apache/nifi/web/client/proxy/ProxyContext.java | 46 +++
.../nifi/web/client/redirect/RedirectHandling.java | 28 ++
.../web/client/ssl/SSLSocketFactoryProvider.java | 32 ++
.../ssl/StandardSSLSocketFactoryProvider.java | 70 ++++
.../org/apache/nifi/web/client/ssl/TlsContext.java | 47 +++
.../web/client/StandardHttpUriBuilderTest.java | 134 +++++++
.../web/client/StandardWebClientServiceTest.java | 434 +++++++++++++++++++++
nifi-commons/pom.xml | 2 +
.../nifi-standard-services-api-nar/pom.xml | 6 +
.../nifi-web-client-provider-api/pom.xml | 35 ++
.../provider/api/WebClientServiceProvider.java | 40 ++
.../nifi-web-client-provider-service-nar/pom.xml | 38 ++
.../nifi-web-client-provider-service/pom.xml | 72 ++++
.../provider/service/KeyManagerProvider.java | 35 ++
.../service/StandardKeyManagerProvider.java | 109 ++++++
.../service/StandardWebClientServiceProvider.java | 206 ++++++++++
.../org.apache.nifi.controller.ControllerService | 15 +
.../service/StandardKeyManagerProviderTest.java | 76 ++++
.../StandardWebClientServiceProviderTest.java | 245 ++++++++++++
.../nifi-web-client-provider-bundle/pom.xml | 30 ++
nifi-nar-bundles/nifi-standard-services/pom.xml | 1 +
41 files changed, 2888 insertions(+)
diff --git a/nifi-assembly/pom.xml b/nifi-assembly/pom.xml
index caf87d0a18..c50e2ca7fe 100644
--- a/nifi-assembly/pom.xml
+++ b/nifi-assembly/pom.xml
@@ -844,6 +844,12 @@ language governing permissions and limitations under the License. -->
<version>1.18.0-SNAPSHOT</version>
<type>nar</type>
</dependency>
+ <dependency>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-web-client-provider-service-nar</artifactId>
+ <version>1.18.0-SNAPSHOT</version>
+ <type>nar</type>
+ </dependency>
<!-- dependencies for jaxb/activation/annotation for running NiFi on Java 11 -->
<!-- TODO: remove these once minimum Java version is 11 -->
<dependency>
diff --git a/nifi-commons/nifi-web-client-api/pom.xml b/nifi-commons/nifi-web-client-api/pom.xml
new file mode 100644
index 0000000000..b1d67b506c
--- /dev/null
+++ b/nifi-commons/nifi-web-client-api/pom.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-commons</artifactId>
+ <version>1.18.0-SNAPSHOT</version>
+ </parent>
+ <artifactId>nifi-web-client-api</artifactId>
+ <description>Abstracts standard HTTP client operations without depending on a specific HTTP library</description>
+</project>
diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpEntityHeaders.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpEntityHeaders.java
new file mode 100644
index 0000000000..a402de153e
--- /dev/null
+++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpEntityHeaders.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client.api;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * HTTP Entity Headers supporting retrieval of single or multiple header values
+ */
+public interface HttpEntityHeaders {
+ /**
+ * Get First Header using specified Header Name
+ *
+ * @param headerName Header Name to be retrieved
+ * @return First Header Value or empty when not found
+ */
+ Optional<String> getFirstHeader(String headerName);
+
+ /**
+ * Get Header Values using specified Header Name
+ *
+ * @param headerName Header Name to be retrieved
+ * @return List of Header Values or empty when not found
+ */
+ List<String> getHeader(String headerName);
+
+ /**
+ * Get Header Names
+ *
+ * @return Collection of Header Names or empty when not found
+ */
+ Collection<String> getHeaderNames();
+}
diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestBodySpec.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestBodySpec.java
new file mode 100644
index 0000000000..9ea80bab4d
--- /dev/null
+++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestBodySpec.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client.api;
+
+import java.io.InputStream;
+import java.util.OptionalLong;
+
+/**
+ * HTTP Request Body Specification builder
+ */
+public interface HttpRequestBodySpec extends HttpRequestHeadersSpec {
+ /**
+ * Set Request Body as stream
+ *
+ * @param inputStream Request Body stream is required
+ * @param contentLength Content Length or empty when not known
+ * @return HTTP Request Headers Specification builder
+ */
+ HttpRequestHeadersSpec body(InputStream inputStream, OptionalLong contentLength);
+}
diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestHeadersSpec.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestHeadersSpec.java
new file mode 100644
index 0000000000..8ed965cff5
--- /dev/null
+++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestHeadersSpec.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client.api;
+
+/**
+ * HTTP Request Headers Specification builder
+ */
+public interface HttpRequestHeadersSpec {
+ /**
+ * Add HTTP Request Header using specified name and value
+ *
+ * @param headerName HTTP Header Name
+ * @param headerValue HTTP Header Value
+ * @return HTTP Request Body Specification builder
+ */
+ HttpRequestBodySpec header(String headerName, String headerValue);
+
+ /**
+ * Execute HTTP Request and retrieve HTTP Response
+ *
+ * @return HTTP Response Entity
+ */
+ HttpResponseEntity retrieve();
+}
diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestMethod.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestMethod.java
new file mode 100644
index 0000000000..80f94864d7
--- /dev/null
+++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestMethod.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client.api;
+
+/**
+ * HTTP Request Method
+ */
+public interface HttpRequestMethod {
+ /**
+ * Get HTTP Method
+ *
+ * @return HTTP Method
+ */
+ String getMethod();
+}
diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestUriSpec.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestUriSpec.java
new file mode 100644
index 0000000000..cad4cd74d3
--- /dev/null
+++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpRequestUriSpec.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client.api;
+
+import java.net.URI;
+
+/**
+ * HTTP Request URI Specification builder
+ */
+public interface HttpRequestUriSpec {
+ /**
+ * Create HTTP Request Body builder using specified Request URI
+ *
+ * @param uri Request URI
+ * @return HTTP Request Body Specification builder
+ */
+ HttpRequestBodySpec uri(URI uri);
+}
diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpResponseEntity.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpResponseEntity.java
new file mode 100644
index 0000000000..f0386aece1
--- /dev/null
+++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpResponseEntity.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client.api;
+
+import java.io.Closeable;
+import java.io.InputStream;
+
+/**
+ * HTTP Response Entity extends Closeable to handle closing Response Body
+ */
+public interface HttpResponseEntity extends Closeable {
+ /**
+ * Get HTTP Response Status Code
+ *
+ * @return HTTP Response Status Code
+ */
+ int statusCode();
+
+ /**
+ * Get HTTP Response Headers
+ *
+ * @return HTTP Response Headers
+ */
+ HttpEntityHeaders headers();
+
+ /**
+ * Get HTTP Response Body stream
+ *
+ * @return HTTP Response Body stream can be empty
+ */
+ InputStream body();
+}
diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpResponseStatus.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpResponseStatus.java
new file mode 100644
index 0000000000..915cf3cb63
--- /dev/null
+++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpResponseStatus.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client.api;
+
+/**
+ * Enumeration of Standard HTTP Response Status Codes
+ */
+public enum HttpResponseStatus {
+ OK(200),
+
+ CREATED(201),
+
+ ACCEPTED(202),
+
+ NO_CONTENT(204),
+
+ MOVED_PERMANENTLY(301),
+
+ BAD_REQUEST(400),
+
+ UNAUTHORIZED(401),
+
+ FORBIDDEN(403),
+
+ NOT_FOUND(404),
+
+ METHOD_NOT_ALLOWED(405),
+
+ PROXY_AUTHENTICATION_REQUIRED(407),
+
+ INTERNAL_SERVER_ERROR(500),
+
+ SERVICE_UNAVAILABLE(503);
+
+ private final int code;
+
+ HttpResponseStatus(final int code) {
+ this.code = code;
+ }
+
+ public int getCode() {
+ return code;
+ }
+}
diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpUriBuilder.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpUriBuilder.java
new file mode 100644
index 0000000000..95db442ec9
--- /dev/null
+++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/HttpUriBuilder.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client.api;
+
+import java.net.URI;
+
+/**
+ * HTTP URI Builder supports construction of a URI using component elements
+ */
+public interface HttpUriBuilder {
+ /**
+ * Build URI based on current component elements
+ *
+ * @return URI
+ */
+ URI build();
+
+ /**
+ * Set URI scheme as http or https
+ *
+ * @param scheme URI scheme
+ * @return Builder
+ */
+ HttpUriBuilder scheme(String scheme);
+
+ /**
+ * Set URI host address
+ *
+ * @param host Host address
+ * @return Builder
+ */
+ HttpUriBuilder host(String host);
+
+ /**
+ * Set URI port number
+ *
+ * @param port Port number
+ * @return Builder
+ */
+ HttpUriBuilder port(int port);
+
+ /**
+ * Set path with segments encoded according to URL standard requirements
+ *
+ * @param encodedPath URL-encoded path
+ * @return Builder
+ */
+ HttpUriBuilder encodedPath(String encodedPath);
+
+ /**
+ * Add path segment appending to current path
+ *
+ * @param pathSegment Path segment
+ * @return Builder
+ */
+ HttpUriBuilder addPathSegment(String pathSegment);
+
+ /**
+ * Add query parameter using specified name and value
+ *
+ * @param name Query parameter name
+ * @param value Query parameter value can be null
+ * @return Builder
+ */
+ HttpUriBuilder addQueryParameter(String name, String value);
+}
diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/StandardHttpRequestMethod.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/StandardHttpRequestMethod.java
new file mode 100644
index 0000000000..feb24af2b6
--- /dev/null
+++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/StandardHttpRequestMethod.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client.api;
+
+/**
+ * Enumeration of standard HTTP Request Methods
+ */
+public enum StandardHttpRequestMethod implements HttpRequestMethod {
+ DELETE,
+
+ GET,
+
+ PATCH,
+
+ POST,
+
+ PUT;
+
+ @Override
+ public String getMethod() {
+ return name();
+ }
+}
diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/WebClientService.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/WebClientService.java
new file mode 100644
index 0000000000..bc33a72da4
--- /dev/null
+++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/WebClientService.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client.api;
+
+/**
+ * Service abstraction for HTTP client operations
+ */
+public interface WebClientService {
+ /**
+ * Create HTTP Request builder starting with specified HTTP Request Method
+ *
+ * @param requestMethod HTTP Request Method
+ * @return HTTP Request URI Specification builder
+ */
+ HttpRequestUriSpec method(HttpRequestMethod requestMethod);
+
+ /**
+ * Create HTTP Request builder starting with HTTP DELETE
+ *
+ * @return HTTP Request URI Specification builder
+ */
+ HttpRequestUriSpec delete();
+
+ /**
+ * Create HTTP Request builder starting with HTTP GET
+ *
+ * @return HTTP Request URI Specification builder
+ */
+ HttpRequestUriSpec get();
+
+ /**
+ * Create HTTP Request builder starting with HTTP PATCH
+ *
+ * @return HTTP Request URI Specification builder
+ */
+ HttpRequestUriSpec patch();
+
+ /**
+ * Create HTTP Request builder starting with HTTP POST
+ *
+ * @return HTTP Request URI Specification builder
+ */
+ HttpRequestUriSpec post();
+
+ /**
+ * Create HTTP Request builder starting with HTTP PUT
+ *
+ * @return HTTP Request URI Specification builder
+ */
+ HttpRequestUriSpec put();
+}
diff --git a/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/WebClientServiceException.java b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/WebClientServiceException.java
new file mode 100644
index 0000000000..95f7966cb4
--- /dev/null
+++ b/nifi-commons/nifi-web-client-api/src/main/java/org/apache/nifi/web/client/api/WebClientServiceException.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client.api;
+
+import java.net.URI;
+
+/**
+ * Web Client Service Exception provides a generalized wrapper for HTTP communication failures
+ */
+public class WebClientServiceException extends RuntimeException {
+ /**
+ * Web Service Client Exception with standard HTTP request properties
+ *
+ * @param message Failure message
+ * @param cause Failure cause
+ * @param uri HTTP Request URI
+ * @param httpRequestMethod HTTP Request Method
+ */
+ public WebClientServiceException(
+ final String message,
+ final Throwable cause,
+ final URI uri,
+ final HttpRequestMethod httpRequestMethod
+ ) {
+ super(getMessage(message, uri, httpRequestMethod), cause);
+ }
+
+ private static String getMessage(final String message, final URI uri, final HttpRequestMethod httpRequestMethod) {
+ return String.format("%s HTTP Method [%s] URI [%s]", message, httpRequestMethod, uri);
+ }
+}
diff --git a/nifi-commons/nifi-web-client/pom.xml b/nifi-commons/nifi-web-client/pom.xml
new file mode 100644
index 0000000000..ac8563aed6
--- /dev/null
+++ b/nifi-commons/nifi-web-client/pom.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-commons</artifactId>
+ <version>1.18.0-SNAPSHOT</version>
+ </parent>
+ <artifactId>nifi-web-client</artifactId>
+ <description>Standard implementation of nifi-web-client-api using OkHttp</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-web-client-api</artifactId>
+ <version>1.18.0-SNAPSHOT</version>
+ </dependency>
+ <dependency>
+ <groupId>com.squareup.okhttp3</groupId>
+ <artifactId>okhttp</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.squareup.okhttp3</groupId>
+ <artifactId>mockwebserver</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/BasicProxyAuthenticator.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/BasicProxyAuthenticator.java
new file mode 100644
index 0000000000..2377812cbb
--- /dev/null
+++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/BasicProxyAuthenticator.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client;
+
+import okhttp3.Authenticator;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.Route;
+
+/**
+ * OkHttp Authenticator supporting Proxy Authentication using HTTP Basic credentials
+ */
+class BasicProxyAuthenticator implements Authenticator {
+ private static final String PROXY_AUTHORIZATION_HEADER = "Proxy-Authorization";
+
+ private final String credentials;
+
+ BasicProxyAuthenticator(final String credentials) {
+ this.credentials = credentials;
+ }
+
+ @Override
+ public Request authenticate(final Route route, final Response response) {
+ return response.request()
+ .newBuilder()
+ .header(PROXY_AUTHORIZATION_HEADER, credentials)
+ .build();
+ }
+}
diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/InputStreamRequestBody.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/InputStreamRequestBody.java
new file mode 100644
index 0000000000..68ab5183da
--- /dev/null
+++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/InputStreamRequestBody.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client;
+
+import okhttp3.MediaType;
+import okhttp3.RequestBody;
+import okio.BufferedSink;
+import okio.Okio;
+import okio.Source;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * OkHttp Request Body implementation based on an InputStream
+ */
+class InputStreamRequestBody extends RequestBody {
+ private final InputStream inputStream;
+
+ private final long contentLength;
+
+ InputStreamRequestBody(final InputStream inputStream, final long contentLength) {
+ this.inputStream = inputStream;
+ this.contentLength = contentLength;
+ }
+
+ @Override
+ public long contentLength() {
+ return contentLength;
+ }
+
+ @Override
+ public MediaType contentType() {
+ return null;
+ }
+
+ @Override
+ public void writeTo(final BufferedSink bufferedSink) throws IOException {
+ final Source source = Okio.source(inputStream);
+ bufferedSink.writeAll(source);
+ }
+}
diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpEntityHeaders.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpEntityHeaders.java
new file mode 100644
index 0000000000..26f84e0319
--- /dev/null
+++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpEntityHeaders.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client;
+
+import org.apache.nifi.web.client.api.HttpEntityHeaders;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Standard implementation of HTTP Entity Headers for Standard Web Client Service
+ */
+class StandardHttpEntityHeaders implements HttpEntityHeaders {
+ private final Map<String, List<String>> headers;
+
+ StandardHttpEntityHeaders(final Map<String, List<String>> headers) {
+ this.headers = Collections.unmodifiableMap(headers);
+ }
+
+ @Override
+ public Optional<String> getFirstHeader(final String headerName) {
+ final List<String> values = getHeader(headerName);
+ return values.stream().findFirst();
+ }
+
+ @Override
+ public List<String> getHeader(final String headerName) {
+ Objects.requireNonNull(headerName, "Header Name required");
+ final List<String> values = headers.get(headerName);
+ return values == null ? Collections.emptyList() : Collections.unmodifiableList(values);
+ }
+
+ @Override
+ public Collection<String> getHeaderNames() {
+ return Collections.unmodifiableSet(headers.keySet());
+ }
+}
diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpResponseEntity.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpResponseEntity.java
new file mode 100644
index 0000000000..c1e8bd1825
--- /dev/null
+++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpResponseEntity.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client;
+
+import org.apache.nifi.web.client.api.HttpEntityHeaders;
+import org.apache.nifi.web.client.api.HttpResponseEntity;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Standard implementation of HTTP Response Entity for Standard Web Client Service
+ */
+class StandardHttpResponseEntity implements HttpResponseEntity {
+ private final int statusCode;
+
+ private final HttpEntityHeaders headers;
+
+ private final InputStream body;
+
+ StandardHttpResponseEntity(
+ final int statusCode,
+ final HttpEntityHeaders headers,
+ final InputStream body
+ ) {
+ this.statusCode = statusCode;
+ this.headers = headers;
+ this.body = body;
+ }
+
+ @Override
+ public int statusCode() {
+ return statusCode;
+ }
+
+ @Override
+ public HttpEntityHeaders headers() {
+ return headers;
+ }
+
+ @Override
+ public InputStream body() {
+ return body;
+ }
+
+ @Override
+ public void close() throws IOException {
+ body.close();
+ }
+}
diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpUriBuilder.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpUriBuilder.java
new file mode 100644
index 0000000000..33ecfa3f46
--- /dev/null
+++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardHttpUriBuilder.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client;
+
+import okhttp3.HttpUrl;
+import org.apache.nifi.web.client.api.HttpUriBuilder;
+
+import java.net.URI;
+import java.util.Objects;
+
+/**
+ * Standard HTTP URI Builder based on OkHttp HttpUrl
+ */
+public class StandardHttpUriBuilder implements HttpUriBuilder {
+ private final HttpUrl.Builder builder;
+
+ public StandardHttpUriBuilder() {
+ this.builder = new HttpUrl.Builder();
+ }
+
+ @Override
+ public URI build() {
+ final HttpUrl httpUrl = builder.build();
+ return httpUrl.uri();
+ }
+
+ @Override
+ public HttpUriBuilder scheme(final String scheme) {
+ Objects.requireNonNull(scheme, "Scheme required");
+ builder.scheme(scheme);
+ return this;
+ }
+
+ @Override
+ public HttpUriBuilder host(final String host) {
+ Objects.requireNonNull(host, "Host required");
+ builder.host(host);
+ return this;
+ }
+
+ @Override
+ public HttpUriBuilder port(int port) {
+ builder.port(port);
+ return this;
+ }
+
+ @Override
+ public HttpUriBuilder encodedPath(final String encodedPath) {
+ builder.encodedPath(encodedPath);
+ return this;
+ }
+
+ @Override
+ public HttpUriBuilder addPathSegment(final String pathSegment) {
+ Objects.requireNonNull(pathSegment, "Path segment required");
+ builder.addPathSegment(pathSegment);
+ return this;
+ }
+
+ @Override
+ public HttpUriBuilder addQueryParameter(final String name, final String value) {
+ Objects.requireNonNull(name, "Parameter name required");
+ builder.addQueryParameter(name, value);
+ return this;
+ }
+}
diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardWebClientService.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardWebClientService.java
new file mode 100644
index 0000000000..c0da84130b
--- /dev/null
+++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/StandardWebClientService.java
@@ -0,0 +1,303 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client;
+
+import okhttp3.Call;
+import okhttp3.Credentials;
+import okhttp3.Headers;
+import okhttp3.HttpUrl;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+
+import org.apache.nifi.web.client.api.HttpEntityHeaders;
+import org.apache.nifi.web.client.api.HttpRequestBodySpec;
+import org.apache.nifi.web.client.api.HttpRequestHeadersSpec;
+import org.apache.nifi.web.client.api.HttpRequestMethod;
+import org.apache.nifi.web.client.api.HttpRequestUriSpec;
+import org.apache.nifi.web.client.api.HttpResponseEntity;
+import org.apache.nifi.web.client.api.StandardHttpRequestMethod;
+import org.apache.nifi.web.client.api.WebClientService;
+import org.apache.nifi.web.client.api.WebClientServiceException;
+import org.apache.nifi.web.client.proxy.ProxyContext;
+import org.apache.nifi.web.client.redirect.RedirectHandling;
+import org.apache.nifi.web.client.ssl.SSLSocketFactoryProvider;
+import org.apache.nifi.web.client.ssl.StandardSSLSocketFactoryProvider;
+import org.apache.nifi.web.client.ssl.TlsContext;
+
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509TrustManager;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.Proxy;
+import java.net.URI;
+import java.time.Duration;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.OptionalLong;
+
+/**
+ * Standard implementation of Web Client Service using OkHttp
+ */
+public class StandardWebClientService implements WebClientService {
+ private static final byte[] EMPTY_BYTES = new byte[0];
+
+ private static final SSLSocketFactoryProvider sslSocketFactoryProvider = new StandardSSLSocketFactoryProvider();
+
+ private OkHttpClient okHttpClient;
+
+ /**
+ * Standard Web Client Service constructor creates OkHttpClient using default settings
+ */
+ public StandardWebClientService() {
+ okHttpClient = new OkHttpClient.Builder().build();
+ }
+
+ /**
+ * Set timeout for initial socket connection
+ *
+ * @param connectTimeout Connect Timeout
+ */
+ public void setConnectTimeout(final Duration connectTimeout) {
+ Objects.requireNonNull(connectTimeout, "Connect Timeout required");
+ okHttpClient = okHttpClient.newBuilder().connectTimeout(connectTimeout).build();
+ }
+
+ /**
+ * Set timeout for reading responses from socket connection
+ *
+ * @param readTimeout Read Timeout
+ */
+ public void setReadTimeout(final Duration readTimeout) {
+ Objects.requireNonNull(readTimeout, "Read Timeout required");
+ okHttpClient = okHttpClient.newBuilder().readTimeout(readTimeout).build();
+ }
+
+ /**
+ * Set timeout for writing requests to socket connection
+ *
+ * @param writeTimeout Write Timeout
+ */
+ public void setWriteTimeout(final Duration writeTimeout) {
+ Objects.requireNonNull(writeTimeout, "Write Timeout required");
+ okHttpClient = okHttpClient.newBuilder().writeTimeout(writeTimeout).build();
+ }
+
+ /**
+ * Set Proxy Context configuration for socket communication
+ *
+ * @param proxyContext Proxy Context configuration
+ */
+ public void setProxyContext(final ProxyContext proxyContext) {
+ Objects.requireNonNull(proxyContext, "Proxy Context required");
+ final Proxy proxy = Objects.requireNonNull(proxyContext.getProxy(), "Proxy required");
+ okHttpClient = okHttpClient.newBuilder().proxy(proxy).build();
+
+ final Optional<String> proxyUsername = proxyContext.getUsername();
+ if (proxyUsername.isPresent()) {
+ final String username = proxyUsername.get();
+ final String password = proxyContext.getPassword().orElseThrow(() -> new IllegalArgumentException("Proxy password required"));
+ final String credentials = Credentials.basic(username, password);
+ final BasicProxyAuthenticator proxyAuthenticator = new BasicProxyAuthenticator(credentials);
+ okHttpClient = okHttpClient.newBuilder().proxyAuthenticator(proxyAuthenticator).build();
+ }
+ }
+
+ /**
+ * Set Redirect Handling strategy
+ *
+ * @param redirectHandling Redirect Handling strategy
+ */
+ public void setRedirectHandling(final RedirectHandling redirectHandling) {
+ Objects.requireNonNull(redirectHandling, "Redirect Handling required");
+ final boolean followRedirects = RedirectHandling.FOLLOWED == redirectHandling;
+ okHttpClient = okHttpClient.newBuilder().followRedirects(followRedirects).followSslRedirects(followRedirects).build();
+ }
+
+ /**
+ * Set TLS Context overrides system default TLS settings for HTTPS communication
+ *
+ * @param tlsContext TLS Context
+ */
+ public void setTlsContext(final TlsContext tlsContext) {
+ Objects.requireNonNull(tlsContext, "TLS Context required");
+ final X509TrustManager trustManager = Objects.requireNonNull(tlsContext.getTrustManager(), "Trust Manager required");
+ final SSLSocketFactory sslSocketFactory = sslSocketFactoryProvider.getSocketFactory(tlsContext);
+ okHttpClient = okHttpClient.newBuilder().sslSocketFactory(sslSocketFactory, trustManager).build();
+ }
+
+ /**
+ * Create HTTP Request builder starting with specified HTTP Request Method
+ *
+ * @param httpRequestMethod HTTP Request Method required
+ * @return HTTP Request URI Specification builder
+ */
+ @Override
+ public HttpRequestUriSpec method(final HttpRequestMethod httpRequestMethod) {
+ Objects.requireNonNull(httpRequestMethod, "HTTP Request Method required");
+ return new StandardHttpRequestUriSpec(httpRequestMethod);
+ }
+
+ /**
+ * Create HTTP Request builder starting with HTTP DELETE
+ *
+ * @return HTTP Request URI Specification builder
+ */
+ @Override
+ public HttpRequestUriSpec delete() {
+ return method(StandardHttpRequestMethod.DELETE);
+ }
+
+ /**
+ * Create HTTP Request builder starting with HTTP GET
+ *
+ * @return HTTP Request URI Specification builder
+ */
+ @Override
+ public HttpRequestUriSpec get() {
+ return method(StandardHttpRequestMethod.GET);
+ }
+
+ /**
+ * Create HTTP Request builder starting with HTTP PATCH
+ *
+ * @return HTTP Request URI Specification builder
+ */
+ @Override
+ public HttpRequestUriSpec patch() {
+ return method(StandardHttpRequestMethod.PATCH);
+ }
+
+ /**
+ * Create HTTP Request builder starting with HTTP POST
+ *
+ * @return HTTP Request URI Specification builder
+ */
+ public HttpRequestUriSpec post() {
+ return method(StandardHttpRequestMethod.POST);
+ }
+
+ /**
+ * Create HTTP Request builder starting with HTTP PUT
+ *
+ * @return HTTP Request URI Specification builder
+ */
+ public HttpRequestUriSpec put() {
+ return method(StandardHttpRequestMethod.PUT);
+ }
+
+ class StandardHttpRequestUriSpec implements HttpRequestUriSpec {
+ private final HttpRequestMethod httpRequestMethod;
+
+ StandardHttpRequestUriSpec(final HttpRequestMethod httpRequestMethod) {
+ this.httpRequestMethod = httpRequestMethod;
+ }
+
+ @Override
+ public HttpRequestBodySpec uri(final URI uri) {
+ Objects.requireNonNull(uri, "URI required");
+ return new StandardHttpRequestBodySpec(httpRequestMethod, uri);
+ }
+ }
+
+ class StandardHttpRequestBodySpec implements HttpRequestBodySpec {
+ private static final long UNKNOWN_CONTENT_LENGTH = -1;
+
+ private final HttpRequestMethod httpRequestMethod;
+
+ private final URI uri;
+
+ private final Headers.Builder headersBuilder;
+
+ private long contentLength = UNKNOWN_CONTENT_LENGTH;
+
+ private InputStream body;
+
+ StandardHttpRequestBodySpec(final HttpRequestMethod httpRequestMethod, final URI uri) {
+ this.httpRequestMethod = httpRequestMethod;
+ this.uri = uri;
+ this.headersBuilder = new Headers.Builder();
+ }
+
+ @Override
+ public HttpRequestHeadersSpec body(final InputStream body, final OptionalLong contentLength) {
+ this.body = Objects.requireNonNull(body, "Body required");
+ this.contentLength = Objects.requireNonNull(contentLength, "Content Length required").orElse(UNKNOWN_CONTENT_LENGTH);
+ return this;
+ }
+
+ @Override
+ public HttpRequestBodySpec header(final String headerName, final String headerValue) {
+ Objects.requireNonNull(headerName, "Header Name required");
+ Objects.requireNonNull(headerValue, "Header Value required");
+ headersBuilder.add(headerName, headerValue);
+ return this;
+ }
+
+ @Override
+ public HttpResponseEntity retrieve() {
+ final Request request = getRequest();
+ final Call call = okHttpClient.newCall(request);
+ final Response response = execute(call);
+
+ final int code = response.code();
+ final Headers responseHeaders = response.headers();
+ final HttpEntityHeaders headers = new StandardHttpEntityHeaders(responseHeaders.toMultimap());
+ final ResponseBody responseBody = response.body();
+ final InputStream body = responseBody == null ? new ByteArrayInputStream(EMPTY_BYTES) : responseBody.byteStream();
+
+ return new StandardHttpResponseEntity(code, headers, body);
+ }
+
+ private Response execute(final Call call) {
+ try {
+ return call.execute();
+ } catch (final IOException e) {
+ throw new WebClientServiceException("Request execution failed", e, uri, httpRequestMethod);
+ }
+ }
+
+ private Request getRequest() {
+ final HttpUrl url = HttpUrl.get(uri);
+ Objects.requireNonNull(url, "HTTP Request URI required");
+
+ final Headers headers = headersBuilder.build();
+ final RequestBody requestBody = getRequestBody();
+
+ return new Request.Builder()
+ .method(httpRequestMethod.getMethod(), requestBody)
+ .url(url)
+ .headers(headers)
+ .build();
+ }
+
+ private RequestBody getRequestBody() {
+ final RequestBody requestBody;
+
+ if (body == null) {
+ requestBody = null;
+ } else {
+ requestBody = new InputStreamRequestBody(body, contentLength);
+ }
+
+ return requestBody;
+ }
+ }
+}
diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/proxy/ProxyContext.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/proxy/ProxyContext.java
new file mode 100644
index 0000000000..570e587189
--- /dev/null
+++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/proxy/ProxyContext.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client.proxy;
+
+import java.net.Proxy;
+import java.util.Optional;
+
+/**
+ * Proxy Context provides information necessary to access sites through a Proxy with or without authentication
+ */
+public interface ProxyContext {
+ /**
+ * Get Proxy including Proxy Type and Proxy Server
+ *
+ * @return Proxy
+ */
+ Proxy getProxy();
+
+ /**
+ * Get Username for Proxy Authentication
+ *
+ * @return Username or empty when not configured
+ */
+ Optional<String> getUsername();
+
+ /**
+ * Get Password for Proxy Authentication
+ *
+ * @return Password or empty when not configured
+ */
+ Optional<String> getPassword();
+}
diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/redirect/RedirectHandling.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/redirect/RedirectHandling.java
new file mode 100644
index 0000000000..f7f2d23a98
--- /dev/null
+++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/redirect/RedirectHandling.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client.redirect;
+
+/**
+ * HTTP redirect handling strategy
+ */
+public enum RedirectHandling {
+ /** Follow HTTP location returned from an HTTP 300 series status */
+ FOLLOWED,
+
+ /** Ignore HTTP location returned from an HTTP 300 series status */
+ IGNORED
+}
diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/SSLSocketFactoryProvider.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/SSLSocketFactoryProvider.java
new file mode 100644
index 0000000000..a8a0538199
--- /dev/null
+++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/SSLSocketFactoryProvider.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client.ssl;
+
+import javax.net.ssl.SSLSocketFactory;
+
+/**
+ * SSLSocketFactory Provider
+ */
+public interface SSLSocketFactoryProvider {
+ /**
+ * Get SSLSocketFactory using provided TLS Context configuration
+ *
+ * @param tlsContext TLS Context configuration
+ * @return SSLSocketFactory
+ */
+ SSLSocketFactory getSocketFactory(TlsContext tlsContext);
+}
diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/StandardSSLSocketFactoryProvider.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/StandardSSLSocketFactoryProvider.java
new file mode 100644
index 0000000000..efa312d214
--- /dev/null
+++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/StandardSSLSocketFactoryProvider.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client.ssl;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509KeyManager;
+import javax.net.ssl.X509TrustManager;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Standard implementation of SSLSocketFactory Provider
+ */
+public class StandardSSLSocketFactoryProvider implements SSLSocketFactoryProvider {
+ /**
+ * Get SSLSocketFactory defaults to system Trust Manager and allows an empty Key Manager
+ *
+ * @param tlsContext TLS Context configuration
+ * @return SSLSocketFactory
+ */
+ @Override
+ public SSLSocketFactory getSocketFactory(final TlsContext tlsContext) {
+ Objects.requireNonNull(tlsContext, "TLS Context required");
+ final SSLContext sslContext = getSslContext(tlsContext);
+
+ try {
+ final Optional<X509KeyManager> keyManager = tlsContext.getKeyManager();
+ final KeyManager[] keyManagers = keyManager.map(x509KeyManager -> new KeyManager[]{x509KeyManager}).orElse(null);
+
+ final X509TrustManager trustManager = tlsContext.getTrustManager();
+ final TrustManager[] trustManagers = trustManager == null ? null : new TrustManager[]{trustManager};
+
+ final SecureRandom secureRandom = new SecureRandom();
+ sslContext.init(keyManagers, trustManagers, secureRandom);
+
+ return sslContext.getSocketFactory();
+ } catch (final KeyManagementException e) {
+ throw new IllegalArgumentException("SSLContext initialization failed", e);
+ }
+ }
+
+ private SSLContext getSslContext(final TlsContext tlsContext) {
+ final String protocol = Objects.requireNonNull(tlsContext.getProtocol(), "TLS Protocol required");
+ try {
+ return SSLContext.getInstance(protocol);
+ } catch (final NoSuchAlgorithmException e) {
+ throw new IllegalArgumentException(String.format("SSLContext protocol [%s] not supported", protocol), e);
+ }
+ }
+}
diff --git a/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/TlsContext.java b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/TlsContext.java
new file mode 100644
index 0000000000..604bfaff21
--- /dev/null
+++ b/nifi-commons/nifi-web-client/src/main/java/org/apache/nifi/web/client/ssl/TlsContext.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client.ssl;
+
+import javax.net.ssl.X509KeyManager;
+import javax.net.ssl.X509TrustManager;
+import java.util.Optional;
+
+/**
+ * TLS Context provides components necessary for TLS communication
+ */
+public interface TlsContext {
+ /**
+ * Get TLS Protocol
+ *
+ * @return TLS Protocol
+ */
+ String getProtocol();
+
+ /**
+ * Get X.509 Trust Manager
+ *
+ * @return X.509 Trust Manager
+ */
+ X509TrustManager getTrustManager();
+
+ /**
+ * Get X.509 Key Manager
+ *
+ * @return X.509 Key Manager or empty when not configured
+ */
+ Optional<X509KeyManager> getKeyManager();
+}
diff --git a/nifi-commons/nifi-web-client/src/test/java/org/apache/nifi/web/client/StandardHttpUriBuilderTest.java b/nifi-commons/nifi-web-client/src/test/java/org/apache/nifi/web/client/StandardHttpUriBuilderTest.java
new file mode 100644
index 0000000000..73cb81f0b7
--- /dev/null
+++ b/nifi-commons/nifi-web-client/src/test/java/org/apache/nifi/web/client/StandardHttpUriBuilderTest.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client;
+
+import org.apache.nifi.web.client.api.HttpUriBuilder;
+import org.junit.jupiter.api.Test;
+
+import java.net.URI;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class StandardHttpUriBuilderTest {
+ private static final String HTTP_SCHEME = "http";
+
+ private static final String LOCALHOST = "localhost";
+
+ private static final int PORT = 8080;
+
+ private static final String ENCODED_PATH = "/resources/search";
+
+ private static final String RESOURCES_PATH_SEGMENT = "resources";
+
+ private static final String PARAMETER_NAME = "search";
+
+ private static final String PARAMETER_VALUE = "terms";
+
+ private static final URI HTTP_LOCALHOST_URI = URI.create(String.format("%s://%s/", HTTP_SCHEME, LOCALHOST));
+
+ private static final URI HTTP_LOCALHOST_PORT_URI = URI.create(String.format("%s://%s:%d/", HTTP_SCHEME, LOCALHOST, PORT));
+
+ private static final URI HTTP_LOCALHOST_PORT_ENCODED_PATH_URI = URI.create(String.format("%s://%s:%d%s", HTTP_SCHEME, LOCALHOST, PORT, ENCODED_PATH));
+
+ private static final URI HTTP_LOCALHOST_RESOURCES_URI = URI.create(String.format("%s%s", HTTP_LOCALHOST_URI, RESOURCES_PATH_SEGMENT));
+
+ private static final URI HTTP_LOCALHOST_QUERY_URI = URI.create(String.format("%s?%s=%s", HTTP_LOCALHOST_RESOURCES_URI, PARAMETER_NAME, PARAMETER_VALUE));
+
+ private static final URI HTTP_LOCALHOST_QUERY_EMPTY_VALUE_URI = URI.create(String.format("%s?%s", HTTP_LOCALHOST_RESOURCES_URI, PARAMETER_NAME));
+
+ @Test
+ void testBuildIllegalStateException() {
+ final HttpUriBuilder builder = new StandardHttpUriBuilder();
+
+ assertThrows(IllegalStateException.class, builder::build);
+ }
+
+ @Test
+ void testBuildSchemeHost() {
+ final HttpUriBuilder builder = new StandardHttpUriBuilder()
+ .scheme(HTTP_SCHEME)
+ .host(LOCALHOST);
+
+ final URI uri = builder.build();
+
+ assertEquals(HTTP_LOCALHOST_URI, uri);
+ }
+
+ @Test
+ void testBuildSchemeHostPort() {
+ final HttpUriBuilder builder = new StandardHttpUriBuilder()
+ .scheme(HTTP_SCHEME)
+ .host(LOCALHOST)
+ .port(PORT);
+
+ final URI uri = builder.build();
+
+ assertEquals(HTTP_LOCALHOST_PORT_URI, uri);
+ }
+
+ @Test
+ void testBuildSchemeHostPortEncodedPath() {
+ final HttpUriBuilder builder = new StandardHttpUriBuilder()
+ .scheme(HTTP_SCHEME)
+ .host(LOCALHOST)
+ .port(PORT)
+ .encodedPath(ENCODED_PATH);
+
+ final URI uri = builder.build();
+
+ assertEquals(HTTP_LOCALHOST_PORT_ENCODED_PATH_URI, uri);
+ }
+
+ @Test
+ void testBuildSchemeHostPathSegment() {
+ final HttpUriBuilder builder = new StandardHttpUriBuilder()
+ .scheme(HTTP_SCHEME)
+ .host(LOCALHOST)
+ .addPathSegment(RESOURCES_PATH_SEGMENT);
+
+ final URI uri = builder.build();
+
+ assertEquals(HTTP_LOCALHOST_RESOURCES_URI, uri);
+ }
+
+ @Test
+ void testBuildSchemeHostPathSegmentQueryParameter() {
+ final HttpUriBuilder builder = new StandardHttpUriBuilder()
+ .scheme(HTTP_SCHEME)
+ .host(LOCALHOST)
+ .addPathSegment(RESOURCES_PATH_SEGMENT)
+ .addQueryParameter(PARAMETER_NAME, PARAMETER_VALUE);
+
+ final URI uri = builder.build();
+
+ assertEquals(HTTP_LOCALHOST_QUERY_URI, uri);
+ }
+
+ @Test
+ void testBuildSchemeHostPathSegmentQueryParameterNullValue() {
+ final HttpUriBuilder builder = new StandardHttpUriBuilder()
+ .scheme(HTTP_SCHEME)
+ .host(LOCALHOST)
+ .addPathSegment(RESOURCES_PATH_SEGMENT)
+ .addQueryParameter(PARAMETER_NAME, null);
+
+ final URI uri = builder.build();
+
+ assertEquals(HTTP_LOCALHOST_QUERY_EMPTY_VALUE_URI, uri);
+ }
+}
diff --git a/nifi-commons/nifi-web-client/src/test/java/org/apache/nifi/web/client/StandardWebClientServiceTest.java b/nifi-commons/nifi-web-client/src/test/java/org/apache/nifi/web/client/StandardWebClientServiceTest.java
new file mode 100644
index 0000000000..ae0a80fa6f
--- /dev/null
+++ b/nifi-commons/nifi-web-client/src/test/java/org/apache/nifi/web/client/StandardWebClientServiceTest.java
@@ -0,0 +1,434 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client;
+
+import okhttp3.Credentials;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.apache.nifi.web.client.api.HttpEntityHeaders;
+import org.apache.nifi.web.client.api.HttpRequestMethod;
+import org.apache.nifi.web.client.api.HttpRequestUriSpec;
+import org.apache.nifi.web.client.api.HttpResponseEntity;
+import org.apache.nifi.web.client.api.HttpResponseStatus;
+import org.apache.nifi.web.client.api.StandardHttpRequestMethod;
+import org.apache.nifi.web.client.api.WebClientServiceException;
+import org.apache.nifi.web.client.proxy.ProxyContext;
+import org.apache.nifi.web.client.redirect.RedirectHandling;
+import org.apache.nifi.web.client.ssl.TlsContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import javax.net.ssl.X509TrustManager;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.Proxy;
+import java.net.SocketTimeoutException;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.OptionalLong;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class StandardWebClientServiceTest {
+ private static final String ROOT_PATH = "/";
+
+ private static final String LOCALHOST = "localhost";
+
+ private static final byte[] EMPTY_BODY = new byte[0];
+
+ private static final String RESPONSE_BODY = String.class.getSimpleName();
+
+ private static final byte[] TEXT_BODY = RESPONSE_BODY.getBytes(StandardCharsets.UTF_8);
+
+ private static final Duration FAILURE_TIMEOUT = Duration.ofMillis(100);
+
+ private static final String ACCEPT_HEADER = "Accept";
+
+ private static final String ACCEPT_ANY_TYPE = "*/*";
+
+ private static final String CONTENT_LENGTH_HEADER = "content-length";
+
+ private static final String CONTENT_LENGTH_ZERO = "0";
+
+ private static final String LOCATION_HEADER = "Location";
+
+ private static final String PROXY_AUTHENTICATE_HEADER = "Proxy-Authenticate";
+
+ private static final String PROXY_AUTHENTICATE_BASIC_REALM = "Basic realm=\"Authentication Required\"";
+
+ private static final String PROXY_AUTHORIZATION_HEADER = "Proxy-Authorization";
+
+ private static final String TLS_PROTOCOL = "TLS";
+
+ private static final String TLS_PROTOCOL_UNSUPPORTED = "TLSv0";
+
+ private static final X509Certificate[] TRUSTED_ISSUERS = new X509Certificate[0];
+
+ @Mock
+ TlsContext tlsContext;
+
+ @Mock
+ ProxyContext proxyContext;
+
+ @Mock
+ X509TrustManager trustManager;
+
+ MockWebServer mockWebServer;
+
+ StandardWebClientService service;
+
+ @BeforeEach
+ void setServer() {
+ mockWebServer = new MockWebServer();
+ service = new StandardWebClientService();
+ }
+
+ @AfterEach
+ void shutdownServer() throws IOException {
+ mockWebServer.shutdown();
+ }
+
+ @Test
+ void testSetTlsContext() {
+ when(tlsContext.getProtocol()).thenReturn(TLS_PROTOCOL);
+ when(tlsContext.getTrustManager()).thenReturn(trustManager);
+ when(trustManager.getAcceptedIssuers()).thenReturn(TRUSTED_ISSUERS);
+
+ service.setTlsContext(tlsContext);
+ }
+
+ @Test
+ void testSetTlsContextProtocolNotSupported() {
+ when(tlsContext.getProtocol()).thenReturn(TLS_PROTOCOL_UNSUPPORTED);
+ when(tlsContext.getTrustManager()).thenReturn(trustManager);
+
+ assertThrows(IllegalArgumentException.class, () -> service.setTlsContext(tlsContext));
+ }
+
+ @Test
+ void testSocketTimeoutException() throws IOException {
+ mockWebServer.shutdown();
+
+ service.setConnectTimeout(FAILURE_TIMEOUT);
+ service.setReadTimeout(FAILURE_TIMEOUT);
+ service.setWriteTimeout(FAILURE_TIMEOUT);
+
+ when(proxyContext.getProxy()).thenReturn(Proxy.NO_PROXY);
+ service.setProxyContext(proxyContext);
+
+ final WebClientServiceException exception = assertThrows(WebClientServiceException.class, () ->
+ service.method(StandardHttpRequestMethod.GET)
+ .uri(getRootUri())
+ .retrieve()
+ );
+
+ assertInstanceOf(SocketTimeoutException.class, exception.getCause());
+ }
+
+ @Test
+ void testProxyAuthorization() throws IOException, InterruptedException {
+ final Proxy proxy = mockWebServer.toProxyAddress();
+ when(proxyContext.getProxy()).thenReturn(proxy);
+ final String username = String.class.getSimpleName();
+ final String password = String.class.getName();
+ when(proxyContext.getUsername()).thenReturn(Optional.of(username));
+ when(proxyContext.getPassword()).thenReturn(Optional.of(password));
+ service.setProxyContext(proxyContext);
+
+ mockWebServer.enqueue(new MockResponse()
+ .setResponseCode(HttpResponseStatus.PROXY_AUTHENTICATION_REQUIRED.getCode())
+ .setHeader(PROXY_AUTHENTICATE_HEADER, PROXY_AUTHENTICATE_BASIC_REALM)
+ );
+
+ runRequestMethod(service.get(), StandardHttpRequestMethod.GET, HttpResponseStatus.OK);
+
+ final RecordedRequest proxyAuthorizationRequest = mockWebServer.takeRequest();
+ final String proxyAuthorization = proxyAuthorizationRequest.getHeader(PROXY_AUTHORIZATION_HEADER);
+ final String credentials = Credentials.basic(username, password);
+ assertEquals(credentials, proxyAuthorization);
+ }
+
+ @Test
+ void testRedirectHandlingFollowed() throws InterruptedException {
+ service.setRedirectHandling(RedirectHandling.FOLLOWED);
+
+ final String location = mockWebServer.url(ROOT_PATH).newBuilder().host(LOCALHOST).build().toString();
+
+ final MockResponse movedResponse = new MockResponse()
+ .setResponseCode(HttpResponseStatus.MOVED_PERMANENTLY.getCode())
+ .setHeader(LOCATION_HEADER, location);
+ mockWebServer.enqueue(movedResponse);
+
+ final HttpResponseStatus httpResponseStatus = HttpResponseStatus.OK;
+ enqueueResponseStatus(httpResponseStatus);
+
+ final HttpResponseEntity httpResponseEntity = service.get()
+ .uri(getRootUri())
+ .retrieve();
+
+ assertRecordedRequestResponseStatus(httpResponseEntity, StandardHttpRequestMethod.GET, httpResponseStatus);
+ }
+
+ @Test
+ void testRedirectHandlingIgnored() throws InterruptedException {
+ service.setRedirectHandling(RedirectHandling.IGNORED);
+
+ final HttpResponseStatus httpResponseStatus = HttpResponseStatus.MOVED_PERMANENTLY;
+
+ enqueueResponseStatusBody(httpResponseStatus);
+
+ final HttpResponseEntity httpResponseEntity = service.get()
+ .uri(getRootUri())
+ .retrieve();
+
+ assertRecordedRequestResponseStatus(httpResponseEntity, StandardHttpRequestMethod.GET, httpResponseStatus);
+ }
+
+ @Test
+ void testDelete() throws InterruptedException, IOException {
+ runRequestMethod(service.delete(), StandardHttpRequestMethod.DELETE, HttpResponseStatus.NO_CONTENT);
+ }
+
+ @Test
+ void testDeleteMethodNotAllowed() throws InterruptedException, IOException {
+ runRequestMethod(service.delete(), StandardHttpRequestMethod.DELETE, HttpResponseStatus.METHOD_NOT_ALLOWED);
+ }
+
+ @Test
+ void testGet() throws InterruptedException, IOException {
+ runRequestMethod(service.get(), StandardHttpRequestMethod.GET, HttpResponseStatus.OK);
+ }
+
+ @Test
+ void testGetNotFound() throws InterruptedException, IOException {
+ runRequestMethod(service.get(), StandardHttpRequestMethod.GET, HttpResponseStatus.NOT_FOUND);
+ }
+
+ @Test
+ void testGetInternalServerError() throws InterruptedException, IOException {
+ runRequestMethod(service.get(), StandardHttpRequestMethod.GET, HttpResponseStatus.INTERNAL_SERVER_ERROR);
+ }
+
+ @Test
+ void testGetServiceUnavailable() throws InterruptedException, IOException {
+ runRequestMethod(service.get(), StandardHttpRequestMethod.GET, HttpResponseStatus.SERVICE_UNAVAILABLE);
+ }
+
+ @Test
+ void testPatch() throws InterruptedException, IOException {
+ runRequestMethodRequestBody(service.patch(), StandardHttpRequestMethod.PATCH, HttpResponseStatus.OK);
+ }
+
+ @Test
+ void testPatchBadRequest() throws InterruptedException, IOException {
+ runRequestMethodRequestBody(service.patch(), StandardHttpRequestMethod.PATCH, HttpResponseStatus.BAD_REQUEST);
+ }
+
+ @Test
+ void testPost() throws InterruptedException, IOException {
+ runRequestMethodRequestBody(service.post(), StandardHttpRequestMethod.POST, HttpResponseStatus.CREATED);
+ }
+
+ @Test
+ void testPostUnauthorized() throws InterruptedException, IOException {
+ runRequestMethodRequestBody(service.post(), StandardHttpRequestMethod.POST, HttpResponseStatus.UNAUTHORIZED);
+ }
+
+ @Test
+ void testPut() throws InterruptedException, IOException {
+ runRequestMethodRequestBody(service.put(), StandardHttpRequestMethod.PUT, HttpResponseStatus.OK);
+ }
+
+ @Test
+ void testPutForbidden() throws InterruptedException, IOException {
+ runRequestMethodRequestBody(service.put(), StandardHttpRequestMethod.PUT, HttpResponseStatus.FORBIDDEN);
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = StandardHttpRequestMethod.class, names = {"DELETE", "GET"})
+ void testHttpRequestMethod(final StandardHttpRequestMethod httpRequestMethod) throws InterruptedException, IOException {
+ runRequestMethod(service.method(httpRequestMethod), httpRequestMethod, HttpResponseStatus.NO_CONTENT);
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = StandardHttpRequestMethod.class, names = {"DELETE", "GET"})
+ void testHttpRequestMethodResponseBody(final StandardHttpRequestMethod httpRequestMethod) throws InterruptedException, IOException {
+ final HttpResponseStatus httpResponseStatus = HttpResponseStatus.ACCEPTED;
+ enqueueResponseStatusBody(httpResponseStatus);
+
+ final HttpResponseEntity httpResponseEntity = service.method(httpRequestMethod)
+ .uri(getRootUri())
+ .retrieve();
+
+ assertRecordedRequestResponseStatus(httpResponseEntity, httpRequestMethod, httpResponseStatus);
+
+ try (final InputStream body = httpResponseEntity.body()) {
+ final byte[] responseBody = new byte[TEXT_BODY.length];
+ final int bytesRead = body.read(responseBody);
+ assertEquals(TEXT_BODY.length, bytesRead);
+ assertArrayEquals(TEXT_BODY, responseBody);
+ }
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = StandardHttpRequestMethod.class, names = {"PATCH", "POST", "PUT"})
+ void testHttpRequestMethodRequestBodyEmpty(final StandardHttpRequestMethod httpRequestMethod) throws InterruptedException, IOException {
+ final HttpResponseStatus httpResponseStatus = HttpResponseStatus.ACCEPTED;
+ enqueueResponseStatus(httpResponseStatus);
+
+ final InputStream body = new ByteArrayInputStream(EMPTY_BODY);
+
+ try (final HttpResponseEntity httpResponseEntity = service.method(httpRequestMethod)
+ .uri(getRootUri())
+ .body(body, OptionalLong.empty())
+ .retrieve()
+ ) {
+ assertRecordedRequestResponseStatus(httpResponseEntity, httpRequestMethod, httpResponseStatus);
+ assertContentLengthHeaderFound(httpResponseEntity.headers());
+ }
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = StandardHttpRequestMethod.class, names = {"PATCH", "POST", "PUT"})
+ void testHttpRequestMethodRequestBody(final StandardHttpRequestMethod httpRequestMethod) throws InterruptedException, IOException {
+ final HttpResponseStatus httpResponseStatus = HttpResponseStatus.ACCEPTED;
+ enqueueResponseStatus(httpResponseStatus);
+
+ final InputStream body = new ByteArrayInputStream(TEXT_BODY);
+
+ try (final HttpResponseEntity httpResponseEntity = service.method(httpRequestMethod)
+ .uri(getRootUri())
+ .header(ACCEPT_HEADER, ACCEPT_ANY_TYPE)
+ .body(body, OptionalLong.of(TEXT_BODY.length))
+ .retrieve()
+ ) {
+
+ final HttpEntityHeaders headers = httpResponseEntity.headers();
+ assertContentLengthHeaderFound(headers);
+
+ final RecordedRequest recordedRequest = assertRecordedRequestResponseStatus(httpResponseEntity, httpRequestMethod, httpResponseStatus);
+
+ assertEquals(TEXT_BODY.length, recordedRequest.getBodySize());
+ final byte[] requestBody = recordedRequest.getBody().readByteArray();
+ assertArrayEquals(TEXT_BODY, requestBody);
+
+ final String acceptHeader = recordedRequest.getHeader(ACCEPT_HEADER);
+ assertEquals(ACCEPT_ANY_TYPE, acceptHeader);
+ }
+ }
+
+ private void runRequestMethod(
+ final HttpRequestUriSpec httpRequestUriSpec,
+ final HttpRequestMethod httpRequestMethod,
+ final HttpResponseStatus httpResponseStatus
+ ) throws IOException, InterruptedException {
+ enqueueResponseStatus(httpResponseStatus);
+
+ try (final HttpResponseEntity httpResponseEntity = httpRequestUriSpec
+ .uri(getRootUri())
+ .retrieve()
+ ) {
+ assertRecordedRequestResponseStatus(httpResponseEntity, httpRequestMethod, httpResponseStatus);
+ assertContentLengthHeaderFound(httpResponseEntity.headers());
+ }
+ }
+
+
+ private void runRequestMethodRequestBody(
+ final HttpRequestUriSpec httpRequestUriSpec,
+ final HttpRequestMethod httpRequestMethod,
+ final HttpResponseStatus httpResponseStatus
+ ) throws IOException, InterruptedException {
+ enqueueResponseStatus(httpResponseStatus);
+
+ final InputStream body = new ByteArrayInputStream(EMPTY_BODY);
+
+ try (final HttpResponseEntity httpResponseEntity = httpRequestUriSpec
+ .uri(getRootUri())
+ .body(body, OptionalLong.empty())
+ .retrieve()
+ ) {
+ assertRecordedRequestResponseStatus(httpResponseEntity, httpRequestMethod, httpResponseStatus);
+ assertContentLengthHeaderFound(httpResponseEntity.headers());
+ }
+ }
+
+ private RecordedRequest assertRecordedRequestResponseStatus(
+ final HttpResponseEntity httpResponseEntity,
+ final HttpRequestMethod httpRequestMethod,
+ final HttpResponseStatus httpResponseStatus
+ ) throws InterruptedException {
+ assertNotNull(httpResponseEntity);
+
+ final RecordedRequest recordedRequest = mockWebServer.takeRequest();
+ assertEquals(httpRequestMethod.getMethod(), recordedRequest.getMethod());
+
+ assertEquals(httpResponseStatus.getCode(), httpResponseEntity.statusCode());
+
+ return recordedRequest;
+ }
+
+ private void assertContentLengthHeaderFound(final HttpEntityHeaders headers) {
+ final Optional<String> contentLengthHeader = headers.getFirstHeader(CONTENT_LENGTH_HEADER);
+ assertTrue(contentLengthHeader.isPresent());
+ assertEquals(CONTENT_LENGTH_ZERO, contentLengthHeader.get());
+
+ final List<String> contentLengthHeaders = headers.getHeader(CONTENT_LENGTH_HEADER);
+ assertFalse(contentLengthHeaders.isEmpty());
+ assertEquals(Collections.singletonList(CONTENT_LENGTH_ZERO), contentLengthHeaders);
+
+ final Collection<String> headerNames = headers.getHeaderNames();
+ assertTrue(headerNames.contains(CONTENT_LENGTH_HEADER));
+ }
+
+ private void enqueueResponseStatus(final HttpResponseStatus httpResponseStatus) {
+ mockWebServer.enqueue(new MockResponse().setResponseCode(httpResponseStatus.getCode()));
+ }
+
+ private void enqueueResponseStatusBody(final HttpResponseStatus httpResponseStatus) {
+ mockWebServer.enqueue(new MockResponse()
+ .setResponseCode(httpResponseStatus.getCode())
+ .setBody(RESPONSE_BODY)
+ );
+ }
+
+ private URI getRootUri() {
+ return mockWebServer.url(ROOT_PATH).newBuilder().host(LOCALHOST).build().uri();
+ }
+}
diff --git a/nifi-commons/pom.xml b/nifi-commons/pom.xml
index 9c16bdc2be..1789e2fe4f 100644
--- a/nifi-commons/pom.xml
+++ b/nifi-commons/pom.xml
@@ -64,6 +64,8 @@
<module>nifi-utils</module>
<module>nifi-uuid5</module>
<module>nifi-vault-utils</module>
+ <module>nifi-web-client</module>
+ <module>nifi-web-client-api</module>
<module>nifi-web-utils</module>
<module>nifi-write-ahead-log</module>
<module>nifi-xml-processing</module>
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-standard-services-api-nar/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-standard-services-api-nar/pom.xml
index 0e76307c87..acbd7e46b3 100644
--- a/nifi-nar-bundles/nifi-standard-services/nifi-standard-services-api-nar/pom.xml
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-standard-services-api-nar/pom.xml
@@ -127,5 +127,11 @@
<version>1.18.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-web-client-provider-api</artifactId>
+ <version>1.18.0-SNAPSHOT</version>
+ <scope>compile</scope>
+ </dependency>
</dependencies>
</project>
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-api/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-api/pom.xml
new file mode 100644
index 0000000000..57b93c5b62
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-api/pom.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-web-client-provider-bundle</artifactId>
+ <version>1.18.0-SNAPSHOT</version>
+ </parent>
+ <artifactId>nifi-web-client-provider-api</artifactId>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-web-client-api</artifactId>
+ <version>1.18.0-SNAPSHOT</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-api</artifactId>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-api/src/main/java/org/apache/nifi/web/client/provider/api/WebClientServiceProvider.java b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-api/src/main/java/org/apache/nifi/web/client/provider/api/WebClientServiceProvider.java
new file mode 100644
index 0000000000..76382053f5
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-api/src/main/java/org/apache/nifi/web/client/provider/api/WebClientServiceProvider.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client.provider.api;
+
+import org.apache.nifi.controller.ControllerService;
+import org.apache.nifi.web.client.api.HttpUriBuilder;
+import org.apache.nifi.web.client.api.WebClientService;
+
+/**
+ * Web Client Service Provider abstracts configuration of Web Client Service instances
+ */
+public interface WebClientServiceProvider extends ControllerService {
+ /**
+ * Get new HTTP URI Builder
+ *
+ * @return New instance of HTTP URI Builder
+ */
+ HttpUriBuilder getHttpUriBuilder();
+
+ /**
+ * Get Web Client Service based on current configuration
+ *
+ * @return Configured Web Client Service
+ */
+ WebClientService getWebClientService();
+}
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service-nar/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service-nar/pom.xml
new file mode 100644
index 0000000000..f7ee3f865c
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service-nar/pom.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-web-client-provider-bundle</artifactId>
+ <version>1.18.0-SNAPSHOT</version>
+ </parent>
+ <artifactId>nifi-web-client-provider-service-nar</artifactId>
+ <packaging>nar</packaging>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-web-client-provider-service</artifactId>
+ <version>1.18.0-SNAPSHOT</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-standard-services-api-nar</artifactId>
+ <version>1.18.0-SNAPSHOT</version>
+ <type>nar</type>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/pom.xml
new file mode 100644
index 0000000000..e4aa47b172
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/pom.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-web-client-provider-bundle</artifactId>
+ <version>1.18.0-SNAPSHOT</version>
+ </parent>
+ <artifactId>nifi-web-client-provider-service</artifactId>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-web-client-provider-api</artifactId>
+ <version>1.18.0-SNAPSHOT</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-ssl-context-service-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-proxy-configuration-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-web-client-api</artifactId>
+ <version>1.18.0-SNAPSHOT</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-web-client</artifactId>
+ <version>1.18.0-SNAPSHOT</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-utils</artifactId>
+ <version>1.18.0-SNAPSHOT</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-mock</artifactId>
+ <version>1.18.0-SNAPSHOT</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.squareup.okhttp3</groupId>
+ <artifactId>mockwebserver</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/KeyManagerProvider.java b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/KeyManagerProvider.java
new file mode 100644
index 0000000000..9d6dddff45
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/KeyManagerProvider.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client.provider.service;
+
+import org.apache.nifi.ssl.SSLContextService;
+
+import javax.net.ssl.X509KeyManager;
+import java.util.Optional;
+
+/**
+ * Provider abstraction for loading a Key Manager
+ */
+interface KeyManagerProvider {
+ /**
+ * Get X.509 Key Manager
+ *
+ * @param sslContextService SSL Context Service
+ * @return X.509 Key Manager or empty when not configured
+ */
+ Optional<X509KeyManager> getKeyManager(SSLContextService sslContextService);
+}
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/StandardKeyManagerProvider.java b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/StandardKeyManagerProvider.java
new file mode 100644
index 0000000000..3db552494b
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/StandardKeyManagerProvider.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client.provider.service;
+
+import org.apache.nifi.ssl.SSLContextService;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.X509KeyManager;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.util.Arrays;
+import java.util.Optional;
+
+/**
+ * Standard implementation of Key Manager Provider
+ */
+class StandardKeyManagerProvider implements KeyManagerProvider {
+ /**
+ * Get X.509 Key Manager using SSL Context Service configuration properties
+ *
+ * @param sslContextService SSL Context Service
+ * @return X.509 Key Manager or empty when not configured
+ */
+ @Override
+ public Optional<X509KeyManager> getKeyManager(final SSLContextService sslContextService) {
+ final X509KeyManager keyManager;
+
+ if (sslContextService.isKeyStoreConfigured()) {
+ final KeyManagerFactory keyManagerFactory = getKeyManagerFactory();
+ final KeyStore keyStore = getKeyStore(sslContextService);
+ final char[] keyPassword = getKeyPassword(sslContextService);
+ try {
+ keyManagerFactory.init(keyStore, keyPassword);
+ } catch (final KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) {
+ throw new IllegalStateException("Key Manager Factory initialization failed", e);
+ }
+
+ final KeyManager[] keyManagers = keyManagerFactory.getKeyManagers();
+ final Optional<KeyManager> firstKeyManager = Arrays.stream(keyManagers).findFirst();
+ final KeyManager configuredKeyManager = firstKeyManager.orElse(null);
+ keyManager = configuredKeyManager instanceof X509KeyManager ? (X509KeyManager) configuredKeyManager : null;
+ } else {
+ keyManager = null;
+ }
+
+ return Optional.ofNullable(keyManager);
+ }
+
+ private KeyStore getKeyStore(final SSLContextService sslContextService) {
+ final String keyStoreType = sslContextService.getKeyStoreType();
+ final KeyStore keyStore = getKeyStore(keyStoreType);
+ final char[] keyStorePassword = sslContextService.getKeyStorePassword().toCharArray();
+ final String keyStoreFile = sslContextService.getKeyStoreFile();
+ try {
+ try (final InputStream inputStream = new FileInputStream(keyStoreFile)) {
+ keyStore.load(inputStream, keyStorePassword);
+ }
+ return keyStore;
+ } catch (final IOException e) {
+ throw new IllegalStateException(String.format("Key Store File [%s] reading failed", keyStoreFile), e);
+ } catch (final NoSuchAlgorithmException | CertificateException e) {
+ throw new IllegalStateException(String.format("Key Store File [%s] loading failed", keyStoreFile), e);
+ }
+ }
+
+ private KeyStore getKeyStore(final String keyStoreType) {
+ try {
+ return KeyStore.getInstance(keyStoreType);
+ } catch (final KeyStoreException e) {
+ throw new IllegalStateException(String.format("Key Store Type [%s] creation failed", keyStoreType), e);
+ }
+ }
+
+ private char[] getKeyPassword(final SSLContextService sslContextService) {
+ final String keyPassword = sslContextService.getKeyPassword();
+ final String keyStorePassword = sslContextService.getKeyStorePassword();
+ final String password = keyPassword == null ? keyStorePassword : keyPassword;
+ return password.toCharArray();
+ }
+
+ private KeyManagerFactory getKeyManagerFactory() {
+ try {
+ return KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ } catch (final NoSuchAlgorithmException e) {
+ throw new IllegalArgumentException("Key Manager Factory creation failed", e);
+ }
+ }
+}
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/StandardWebClientServiceProvider.java b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/StandardWebClientServiceProvider.java
new file mode 100644
index 0000000000..fc51ee8f1a
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/java/org/apache/nifi/web/client/provider/service/StandardWebClientServiceProvider.java
@@ -0,0 +1,206 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client.provider.service;
+
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnEnabled;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.PropertyValue;
+import org.apache.nifi.controller.AbstractControllerService;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.proxy.ProxyConfiguration;
+import org.apache.nifi.proxy.ProxyConfigurationService;
+import org.apache.nifi.ssl.SSLContextService;
+import org.apache.nifi.web.client.StandardHttpUriBuilder;
+import org.apache.nifi.web.client.api.HttpUriBuilder;
+import org.apache.nifi.web.client.proxy.ProxyContext;
+import org.apache.nifi.web.client.StandardWebClientService;
+import org.apache.nifi.web.client.redirect.RedirectHandling;
+import org.apache.nifi.web.client.ssl.TlsContext;
+import org.apache.nifi.web.client.api.WebClientService;
+import org.apache.nifi.web.client.provider.api.WebClientServiceProvider;
+
+import javax.net.ssl.X509KeyManager;
+import javax.net.ssl.X509TrustManager;
+import java.net.Proxy;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.nifi.proxy.ProxyConfigurationService.PROXY_CONFIGURATION_SERVICE;
+
+@CapabilityDescription("Web Client Service Provider with support for configuring standard HTTP connection properties")
+@Tags({ "HTTP", "Web", "Client" })
+public class StandardWebClientServiceProvider extends AbstractControllerService implements WebClientServiceProvider {
+
+ static final PropertyDescriptor CONNECT_TIMEOUT = new PropertyDescriptor.Builder()
+ .name("connect-timeout")
+ .displayName("Connect Timeout")
+ .description("Maximum amount of time to wait before failing during initial socket connection")
+ .required(true)
+ .defaultValue("10 secs")
+ .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR)
+ .build();
+
+ static final PropertyDescriptor READ_TIMEOUT = new PropertyDescriptor.Builder()
+ .name("read-timeout")
+ .displayName("Read Timeout")
+ .description("Maximum amount of time to wait before failing while reading socket responses")
+ .required(true)
+ .defaultValue("10 secs")
+ .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR)
+ .build();
+
+ static final PropertyDescriptor WRITE_TIMEOUT = new PropertyDescriptor.Builder()
+ .name("write-timeout")
+ .displayName("Write Timeout")
+ .description("Maximum amount of time to wait before failing while writing socket requests")
+ .required(true)
+ .defaultValue("10 secs")
+ .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR)
+ .build();
+
+ static final PropertyDescriptor REDIRECT_HANDLING_STRATEGY = new PropertyDescriptor.Builder()
+ .name("redirect-handling-strategy")
+ .displayName("Redirect Handling Strategy")
+ .description("Handling strategy for responding to HTTP 301 or 302 redirects received with a Location header")
+ .required(true)
+ .defaultValue(RedirectHandling.FOLLOWED.name())
+ .allowableValues(RedirectHandling.values())
+ .build();
+
+ static final PropertyDescriptor SSL_CONTEXT_SERVICE = new PropertyDescriptor.Builder()
+ .name("ssl-context-service")
+ .displayName("SSL Context Service")
+ .description("SSL Context Service overrides system default TLS settings for HTTPS communication")
+ .required(false)
+ .identifiesControllerService(SSLContextService.class)
+ .build();
+
+ static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = Arrays.asList(
+ CONNECT_TIMEOUT,
+ READ_TIMEOUT,
+ WRITE_TIMEOUT,
+ REDIRECT_HANDLING_STRATEGY,
+ SSL_CONTEXT_SERVICE,
+ PROXY_CONFIGURATION_SERVICE
+ );
+
+ private static final KeyManagerProvider keyManagerProvider = new StandardKeyManagerProvider();
+
+ private WebClientService webClientService;
+
+ @OnEnabled
+ public void onEnabled(final ConfigurationContext context) {
+ final StandardWebClientService standardWebClientService = new StandardWebClientService();
+
+ final Duration connectTimeout = getDuration(context, CONNECT_TIMEOUT);
+ standardWebClientService.setConnectTimeout(connectTimeout);
+
+ final Duration readTimeout = getDuration(context, READ_TIMEOUT);
+ standardWebClientService.setReadTimeout(readTimeout);
+
+ final Duration writeTimeout = getDuration(context, WRITE_TIMEOUT);
+ standardWebClientService.setReadTimeout(writeTimeout);
+
+ final String redirectHandlingStrategy = context.getProperty(REDIRECT_HANDLING_STRATEGY).getValue();
+ final RedirectHandling redirectHandling = RedirectHandling.valueOf(redirectHandlingStrategy);
+ standardWebClientService.setRedirectHandling(redirectHandling);
+
+ final PropertyValue sslContextServiceProperty = context.getProperty(SSL_CONTEXT_SERVICE);
+ if (sslContextServiceProperty.isSet()) {
+ final SSLContextService sslContextService = sslContextServiceProperty.asControllerService(SSLContextService.class);
+ final TlsContext tlsContext = getTlsContext(sslContextService);
+ standardWebClientService.setTlsContext(tlsContext);
+ }
+
+ final PropertyValue proxyConfigurationServiceProperty = context.getProperty(PROXY_CONFIGURATION_SERVICE);
+ if (proxyConfigurationServiceProperty.isSet()) {
+ final ProxyConfigurationService proxyConfigurationService = context.getProperty(PROXY_CONFIGURATION_SERVICE).asControllerService(ProxyConfigurationService.class);
+ final ProxyConfiguration proxyConfiguration = proxyConfigurationService.getConfiguration();
+ final ProxyContext proxyContext = getProxyContext(proxyConfiguration);
+ standardWebClientService.setProxyContext(proxyContext);
+ }
+
+ webClientService = standardWebClientService;
+ }
+
+ @Override
+ public HttpUriBuilder getHttpUriBuilder() {
+ return new StandardHttpUriBuilder();
+ }
+
+ @Override
+ public WebClientService getWebClientService() {
+ return webClientService;
+ }
+
+ @Override
+ protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+ return PROPERTY_DESCRIPTORS;
+ }
+
+ private Duration getDuration(final ConfigurationContext context, final PropertyDescriptor propertyDescriptor) {
+ final long millis = context.getProperty(propertyDescriptor).asTimePeriod(TimeUnit.MILLISECONDS);
+ return Duration.ofMillis(millis);
+ }
+
+ private TlsContext getTlsContext(final SSLContextService sslContextService) {
+ final X509TrustManager trustManager = sslContextService.createTrustManager();
+ final Optional<X509KeyManager> keyManager = keyManagerProvider.getKeyManager(sslContextService);
+
+ return new TlsContext() {
+ @Override
+ public String getProtocol() {
+ return sslContextService.getSslAlgorithm();
+ }
+
+ @Override
+ public X509TrustManager getTrustManager() {
+ return trustManager;
+ }
+
+ @Override
+ public Optional<X509KeyManager> getKeyManager() {
+ return keyManager;
+ }
+ };
+ }
+
+ private ProxyContext getProxyContext(final ProxyConfiguration proxyConfiguration) {
+ return new ProxyContext() {
+ @Override
+ public Proxy getProxy() {
+ return proxyConfiguration.createProxy();
+ }
+
+ @Override
+ public Optional<String> getUsername() {
+ return Optional.ofNullable(proxyConfiguration.getProxyUserName());
+ }
+
+ @Override
+ public Optional<String> getPassword() {
+ return Optional.ofNullable(proxyConfiguration.getProxyUserPassword());
+ }
+ };
+ }
+}
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
new file mode 100644
index 0000000000..53db7395ea
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
@@ -0,0 +1,15 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+org.apache.nifi.web.client.provider.service.StandardWebClientServiceProvider
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/test/java/org/apache/nifi/web/client/provider/service/StandardKeyManagerProviderTest.java b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/test/java/org/apache/nifi/web/client/provider/service/StandardKeyManagerProviderTest.java
new file mode 100644
index 0000000000..ccb6601263
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/test/java/org/apache/nifi/web/client/provider/service/StandardKeyManagerProviderTest.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client.provider.service;
+
+import org.apache.nifi.security.util.TemporaryKeyStoreBuilder;
+import org.apache.nifi.security.util.TlsConfiguration;
+import org.apache.nifi.ssl.SSLContextService;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import javax.net.ssl.X509KeyManager;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class StandardKeyManagerProviderTest {
+ static TlsConfiguration tlsConfiguration;
+
+ @Mock
+ SSLContextService sslContextService;
+
+ StandardKeyManagerProvider provider;
+
+ @BeforeAll
+ static void setTlsConfiguration() {
+ tlsConfiguration = new TemporaryKeyStoreBuilder().build();
+ }
+
+ @BeforeEach
+ void setProvider() {
+ provider = new StandardKeyManagerProvider();
+ }
+
+ @Test
+ void testGetKeyManagerNotConfigured() {
+ when(sslContextService.isKeyStoreConfigured()).thenReturn(false);
+
+ final Optional<X509KeyManager> keyManager = provider.getKeyManager(sslContextService);
+
+ assertFalse(keyManager.isPresent());
+ }
+
+ @Test
+ void testGetKeyManager() {
+ when(sslContextService.isKeyStoreConfigured()).thenReturn(true);
+ when(sslContextService.getKeyStoreType()).thenReturn(tlsConfiguration.getKeystoreType().getType());
+ when(sslContextService.getKeyStoreFile()).thenReturn(tlsConfiguration.getKeystorePath());
+ when(sslContextService.getKeyStorePassword()).thenReturn(tlsConfiguration.getKeystorePassword());
+
+ final Optional<X509KeyManager> keyManager = provider.getKeyManager(sslContextService);
+
+ assertTrue(keyManager.isPresent());
+ }
+}
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/test/java/org/apache/nifi/web/client/provider/service/StandardWebClientServiceProviderTest.java b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/test/java/org/apache/nifi/web/client/provider/service/StandardWebClientServiceProviderTest.java
new file mode 100644
index 0000000000..2c74b10e44
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/nifi-web-client-provider-service/src/test/java/org/apache/nifi/web/client/provider/service/StandardWebClientServiceProviderTest.java
@@ -0,0 +1,245 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.client.provider.service;
+
+import okhttp3.Credentials;
+import okhttp3.HttpUrl;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.apache.nifi.proxy.ProxyConfiguration;
+import org.apache.nifi.proxy.ProxyConfigurationService;
+import org.apache.nifi.reporting.InitializationException;
+import org.apache.nifi.security.util.SslContextFactory;
+import org.apache.nifi.security.util.TemporaryKeyStoreBuilder;
+import org.apache.nifi.security.util.TlsConfiguration;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.ssl.SSLContextService;
+import org.apache.nifi.util.NoOpProcessor;
+import org.apache.nifi.util.TestRunner;
+import org.apache.nifi.util.TestRunners;
+import org.apache.nifi.web.client.api.HttpResponseEntity;
+import org.apache.nifi.web.client.api.HttpResponseStatus;
+import org.apache.nifi.web.client.api.HttpUriBuilder;
+import org.apache.nifi.web.client.api.WebClientService;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509TrustManager;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.URI;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class StandardWebClientServiceProviderTest {
+ private static final String SERVICE_ID = StandardWebClientServiceProvider.class.getSimpleName();
+
+ private static final String SSL_CONTEXT_SERVICE_ID = SSLContextService.class.getSimpleName();
+
+ private static final String PROXY_SERVICE_ID = ProxyConfigurationService.class.getSimpleName();
+
+ private static final String LOCALHOST = "localhost";
+
+ private static final String HTTPS = "https";
+
+ private static final int PORT = 8443;
+
+ private static final String PATH_SEGMENT = "resources";
+
+ private static final String PARAMETER_NAME = "search";
+
+ private static final String PARAMETER_VALUE = "search";
+
+ private static final String ROOT_PATH = "/";
+
+ private static final URI LOCALHOST_URI = URI.create(String.format("%s://%s:%d/%s?%s=%s", HTTPS, LOCALHOST, PORT, PATH_SEGMENT, PARAMETER_NAME, PARAMETER_VALUE));
+
+ private static final String PROXY_AUTHENTICATE_HEADER = "Proxy-Authenticate";
+
+ private static final String PROXY_AUTHENTICATE_BASIC_REALM = "Basic realm=\"Authentication Required\"";
+
+ private static final String PROXY_AUTHORIZATION_HEADER = "Proxy-Authorization";
+
+ private static final boolean TUNNEL_PROXY_DISABLED = false;
+
+ static TlsConfiguration tlsConfiguration;
+
+ static SSLContext sslContext;
+
+ static X509TrustManager trustManager;
+
+ @Mock
+ SSLContextService sslContextService;
+
+ @Mock
+ ProxyConfigurationService proxyConfigurationService;
+
+ TestRunner runner;
+
+ MockWebServer mockWebServer;
+
+ StandardWebClientServiceProvider provider;
+
+ @BeforeAll
+ static void setTlsConfiguration() throws TlsException {
+ tlsConfiguration = new TemporaryKeyStoreBuilder().build();
+ sslContext = SslContextFactory.createSslContext(tlsConfiguration);
+ trustManager = SslContextFactory.getX509TrustManager(tlsConfiguration);
+ }
+
+ @BeforeEach
+ void setRunner() throws InitializationException {
+ mockWebServer = new MockWebServer();
+
+ runner = TestRunners.newTestRunner(NoOpProcessor.class);
+
+ provider = new StandardWebClientServiceProvider();
+ runner.addControllerService(SERVICE_ID, provider);
+ }
+
+ @AfterEach
+ void shutdownServer() throws IOException {
+ mockWebServer.shutdown();
+ }
+
+ @Test
+ void testEnable() {
+ runner.enableControllerService(provider);
+ }
+
+ @Test
+ void testGetHttpUriBuilder() {
+ runner.enableControllerService(provider);
+
+ final HttpUriBuilder httpUriBuilder = provider.getHttpUriBuilder();
+
+ final URI uri = httpUriBuilder.scheme(HTTPS)
+ .host(LOCALHOST)
+ .port(PORT)
+ .addPathSegment(PATH_SEGMENT)
+ .addQueryParameter(PARAMETER_NAME, PARAMETER_VALUE)
+ .build();
+
+ assertEquals(LOCALHOST_URI, uri);
+ }
+
+ @Test
+ void testGetWebServiceClientGetUri() throws InterruptedException {
+ runner.enableControllerService(provider);
+
+ final WebClientService webClientService = provider.getWebClientService();
+
+ assertNotNull(webClientService);
+
+ assertGetUriCompleted(webClientService);
+ }
+
+ @Test
+ void testGetWebServiceClientSslContextServiceConfiguredGetUri() throws InitializationException, InterruptedException {
+ when(sslContextService.getIdentifier()).thenReturn(SSL_CONTEXT_SERVICE_ID);
+ when(sslContextService.getSslAlgorithm()).thenReturn(tlsConfiguration.getProtocol());
+ when(sslContextService.createTrustManager()).thenReturn(trustManager);
+
+ runner.addControllerService(SSL_CONTEXT_SERVICE_ID, sslContextService);
+ runner.enableControllerService(sslContextService);
+
+ runner.setProperty(provider, StandardWebClientServiceProvider.SSL_CONTEXT_SERVICE, SSL_CONTEXT_SERVICE_ID);
+ runner.enableControllerService(provider);
+
+ final WebClientService webClientService = provider.getWebClientService();
+
+ assertNotNull(webClientService);
+
+ final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
+ mockWebServer.useHttps(sslSocketFactory, TUNNEL_PROXY_DISABLED);
+
+ assertGetUriCompleted(webClientService);
+ }
+
+ @Test
+ void testGetWebServiceClientProxyConfigurationGetUri() throws InitializationException, InterruptedException {
+ final Proxy proxy = mockWebServer.toProxyAddress();
+ final InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address();
+
+ final ProxyConfiguration proxyConfiguration = new ProxyConfiguration();
+ proxyConfiguration.setProxyType(Proxy.Type.HTTP);
+ proxyConfiguration.setProxyServerHost(proxyAddress.getHostName());
+ proxyConfiguration.setProxyServerPort(proxyAddress.getPort());
+
+ final String username = String.class.getSimpleName();
+ final String password = String.class.getName();
+ proxyConfiguration.setProxyUserName(username);
+ proxyConfiguration.setProxyUserPassword(password);
+
+ when(proxyConfigurationService.getIdentifier()).thenReturn(PROXY_SERVICE_ID);
+ when(proxyConfigurationService.getConfiguration()).thenReturn(proxyConfiguration);
+
+ mockWebServer.enqueue(new MockResponse()
+ .setResponseCode(HttpResponseStatus.PROXY_AUTHENTICATION_REQUIRED.getCode())
+ .setHeader(PROXY_AUTHENTICATE_HEADER, PROXY_AUTHENTICATE_BASIC_REALM)
+ );
+
+ runner.addControllerService(PROXY_SERVICE_ID, proxyConfigurationService);
+ runner.enableControllerService(proxyConfigurationService);
+
+ runner.setProperty(provider, ProxyConfigurationService.PROXY_CONFIGURATION_SERVICE, PROXY_SERVICE_ID);
+ runner.enableControllerService(provider);
+
+ final WebClientService webClientService = provider.getWebClientService();
+
+ assertNotNull(webClientService);
+
+ assertGetUriCompleted(webClientService);
+
+ final RecordedRequest proxyAuthorizationRequest = mockWebServer.takeRequest();
+ final String proxyAuthorization = proxyAuthorizationRequest.getHeader(PROXY_AUTHORIZATION_HEADER);
+ final String credentials = Credentials.basic(username, password);
+ assertEquals(credentials, proxyAuthorization);
+ }
+
+ private void assertGetUriCompleted(final WebClientService webClientService) throws InterruptedException {
+ final URI uri = mockWebServer.url(ROOT_PATH).newBuilder().host(LOCALHOST).build().uri();
+
+ final HttpResponseStatus httpResponseStatus = HttpResponseStatus.OK;
+ final MockResponse mockResponse = new MockResponse().setResponseCode(httpResponseStatus.getCode());
+ mockWebServer.enqueue(mockResponse);
+
+ final HttpResponseEntity httpResponseEntity = webClientService.get().uri(uri).retrieve();
+
+ assertNotNull(httpResponseEntity);
+ assertEquals(httpResponseStatus.getCode(), httpResponseEntity.statusCode());
+
+ final RecordedRequest request = mockWebServer.takeRequest();
+ final HttpUrl requestUrl = request.getRequestUrl();
+ assertNotNull(requestUrl);
+
+ final URI requestUri = requestUrl.uri();
+ assertEquals(uri.getPort(), requestUri.getPort());
+ }
+}
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/pom.xml
new file mode 100644
index 0000000000..6db83509db
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-web-client-provider-bundle/pom.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-standard-services</artifactId>
+ <version>1.18.0-SNAPSHOT</version>
+ </parent>
+ <artifactId>nifi-web-client-provider-bundle</artifactId>
+ <packaging>pom</packaging>
+ <modules>
+ <module>nifi-web-client-provider-api</module>
+ <module>nifi-web-client-provider-service</module>
+ <module>nifi-web-client-provider-service-nar</module>
+ </modules>
+</project>
diff --git a/nifi-nar-bundles/nifi-standard-services/pom.xml b/nifi-nar-bundles/nifi-standard-services/pom.xml
index 524fc4fbb3..6e210e8f7d 100644
--- a/nifi-nar-bundles/nifi-standard-services/pom.xml
+++ b/nifi-nar-bundles/nifi-standard-services/pom.xml
@@ -54,5 +54,6 @@
<module>nifi-hadoop-dbcp-service-bundle</module>
<module>nifi-kerberos-user-service-api</module>
<module>nifi-kerberos-user-service-bundle</module>
+ <module>nifi-web-client-provider-bundle</module>
</modules>
</project>