This is an automated email from the ASF dual-hosted git repository.
bbende 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 0548338cb0a NIFI-16201: Reload Parameter-driven extra classpath for
Connector Methods (#11574)
0548338cb0a is described below
commit 0548338cb0ac5f17559b8661d24f73b5c56c1553
Author: Mark Payne <[email protected]>
AuthorDate: Mon Aug 24 11:33:22 2026 -0400
NIFI-16201: Reload Parameter-driven extra classpath for Connector Methods
(#11574)
* NIFI-16201: Reload Parameter-driven extra classpath for Connector Methods
Stopped components keep a stale InstanceClassLoader after Parameter/asset
classpath changes, so Connector Method discovery and invocation fail with
LinkageError and can hang async verification.
* NIFI-16201: Wrap ClasspathVerifyIT lines that exceeded Checkstyle length
* NIFI-16201: Reload recursive referencers and wrap TypeNotPresentException
Parameter updates can change a Controller Service isolation key, so stopped
referencing components need the same recursive ClassLoader reload as
setProperties(). Connector Method argument resolution can also throw
TypeNotPresentException when a Class annotation member is missing from the
component ClassLoader.
---
.../nifi/controller/StandardProcessorNode.java | 8 +-
.../service/StandardControllerServiceNode.java | 8 +-
.../nifi/controller/AbstractComponentNode.java | 54 ++--
.../org/apache/nifi/controller/ProcessorNode.java | 2 +-
.../controller/service/ControllerServiceNode.java | 2 +-
.../StandaloneParameterContextFacade.java | 14 +-
.../StandaloneParameterContextFacadeTest.java | 110 ++++++++
.../web/api/concurrent/AsyncRequestManager.java | 8 +-
.../pom.xml | 19 +-
.../dynamicclasspath/DynamicallyLoadedType.java | 21 ++
.../tests/system/VerifyMethodSignatureService.java | 64 +++++
.../org.apache.nifi.controller.ControllerService | 1 +
.../nifi-system-test-extensions/pom.xml | 23 +-
.../tests/system/ClasspathVerifyConnector.java | 223 ++++++++++++++++
.../system/MethodSignatureVerifyConnector.java | 217 ++++++++++++++++
.../tests/system/VerifyClasspathResource.java | 125 +++++++++
.../system/VerifyMethodSignatureResource.java | 75 ++++++
.../org.apache.nifi.components.connector.Connector | 4 +-
.../services/org.apache.nifi.processor.Processor | 2 +
.../flows/method-signature-verify-connector.json | 88 +++++++
.../nifi-system-test-extensions-bundle/pom.xml | 1 +
nifi-system-tests/nifi-system-test-suite/pom.xml | 13 +
.../tests/system/connectors/ClasspathVerifyIT.java | 279 +++++++++++++++++++++
23 files changed, 1319 insertions(+), 42 deletions(-)
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/StandardProcessorNode.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/StandardProcessorNode.java
index 9fa0e0b4b2f..e24af1afea7 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/StandardProcessorNode.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/StandardProcessorNode.java
@@ -2023,7 +2023,7 @@ public class StandardProcessorNode extends ProcessorNode
implements Connectable
}
@Override
- public List<ConnectorMethod> getConnectorMethods() {
+ public List<ConnectorMethod> getConnectorMethods() throws
InvocationFailedException {
return getConnectorMethods(getProcessor().getClass());
}
@@ -2082,6 +2082,12 @@ public class StandardProcessorNode extends ProcessorNode
implements Connectable
} catch (final Exception e) {
throw new InvocationFailedException(e);
}
+ } catch (final TypeNotPresentException | LinkageError e) {
+ // MethodArgument.type() throws TypeNotPresentException when an
annotation Class member is absent from the component ClassLoader.
+ // Resolving the implementation Method's parameter types can still
throw LinkageError. Discovery already wraps LinkageError, and
+ // Errors thrown from the invoked method body are wrapped by
Method.invoke as InvocationTargetException.
+ throw new InvocationFailedException("Failed to invoke Connector
Method '" + methodName + "' on " + this
+ + " because a class required by the component could not be
loaded from the component's ClassLoader", e);
}
}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java
index 32c402dca95..b2010072027 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java
@@ -992,11 +992,17 @@ public class StandardControllerServiceNode extends
AbstractComponentNode impleme
} catch (final Exception e) {
throw new InvocationFailedException(e);
}
+ } catch (final TypeNotPresentException | LinkageError e) {
+ // MethodArgument.type() throws TypeNotPresentException when an
annotation Class member is absent from the component ClassLoader.
+ // Resolving the implementation Method's parameter types can still
throw LinkageError. Discovery already wraps LinkageError, and
+ // Errors thrown from the invoked method body are wrapped by
Method.invoke as InvocationTargetException.
+ throw new InvocationFailedException("Failed to invoke Connector
Method '" + methodName + "' on " + this
+ + " because a class required by the component could not be
loaded from the component's ClassLoader", e);
}
}
@Override
- public List<ConnectorMethod> getConnectorMethods() {
+ public List<ConnectorMethod> getConnectorMethods() throws
InvocationFailedException {
return
getConnectorMethods(getControllerServiceImplementation().getClass());
}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/AbstractComponentNode.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/AbstractComponentNode.java
index fd419477174..2e8ce97da57 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/AbstractComponentNode.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/AbstractComponentNode.java
@@ -32,6 +32,7 @@ import org.apache.nifi.components.ConfigurableComponent;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.components.ValidationContext;
import org.apache.nifi.components.ValidationResult;
+import org.apache.nifi.components.connector.InvocationFailedException;
import org.apache.nifi.components.connector.components.ConnectorMethod;
import org.apache.nifi.components.resource.ResourceContext;
import org.apache.nifi.components.resource.ResourceReferenceFactory;
@@ -1561,30 +1562,47 @@ public abstract class AbstractComponentNode implements
ComponentNode {
}
}
- protected List<ConnectorMethod> getConnectorMethods(final Class<?>
componentClass) {
- final List<ConnectorMethod> connectorMethods = new ArrayList<>();
- for (final Method method : componentClass.getDeclaredMethods()) {
- final ConnectorMethod annotation =
method.getAnnotation(ConnectorMethod.class);
- connectorMethods.add(annotation);
- }
+ protected List<ConnectorMethod> getConnectorMethods(final Class<?>
componentClass) throws InvocationFailedException {
+ try {
+ final List<ConnectorMethod> connectorMethods = new ArrayList<>();
+ for (final Method method : componentClass.getDeclaredMethods()) {
+ final ConnectorMethod annotation =
method.getAnnotation(ConnectorMethod.class);
+ if (annotation != null) {
+ connectorMethods.add(annotation);
+ }
+ }
- return connectorMethods;
+ final Class<?> superClass = componentClass.getSuperclass();
+ if (superClass != null && !Object.class.equals(superClass)) {
+ connectorMethods.addAll(getConnectorMethods(superClass));
+ }
+
+ return connectorMethods;
+ } catch (final LinkageError e) {
+ throw new InvocationFailedException("Failed to discover Connector
Methods on " + componentClass.getName()
+ + " because a class required by the component could not be
loaded from the component's ClassLoader", e);
+ }
}
- protected Method discoverConnectorMethod(final Class<?> componentClass,
final String connectorMethodName) {
- for (final Method method : componentClass.getDeclaredMethods()) {
- final ConnectorMethod annotation =
method.getAnnotation(ConnectorMethod.class);
- if (annotation != null &&
annotation.name().equals(connectorMethodName)) {
- return method;
+ protected Method discoverConnectorMethod(final Class<?> componentClass,
final String connectorMethodName) throws InvocationFailedException {
+ try {
+ for (final Method method : componentClass.getDeclaredMethods()) {
+ final ConnectorMethod annotation =
method.getAnnotation(ConnectorMethod.class);
+ if (annotation != null &&
annotation.name().equals(connectorMethodName)) {
+ return method;
+ }
}
- }
- final Class<?> superClass = componentClass.getSuperclass();
- if (superClass != null && !Object.class.equals(superClass)) {
- return discoverConnectorMethod(superClass, connectorMethodName);
- }
+ final Class<?> superClass = componentClass.getSuperclass();
+ if (superClass != null && !Object.class.equals(superClass)) {
+ return discoverConnectorMethod(superClass,
connectorMethodName);
+ }
- return null;
+ return null;
+ } catch (final LinkageError e) {
+ throw new InvocationFailedException("Failed to discover Connector
Method '" + connectorMethodName + "' on " + componentClass.getName()
+ + " because a class required by the component could not be
loaded from the component's ClassLoader", e);
+ }
}
protected void setAdditionalResourcesFingerprint(String
additionalResourcesFingerprint) {
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/ProcessorNode.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/ProcessorNode.java
index e3db0b42de5..b72859f75ce 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/ProcessorNode.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/ProcessorNode.java
@@ -353,5 +353,5 @@ public abstract class ProcessorNode extends
AbstractComponentNode implements Con
public abstract String invokeConnectorMethod(String methodName,
Map<String, String> jsonArguments, ProcessContext processContext) throws
InvocationFailedException;
- public abstract List<ConnectorMethod> getConnectorMethods();
+ public abstract List<ConnectorMethod> getConnectorMethods() throws
InvocationFailedException;
}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceNode.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceNode.java
index 0f1bd09535f..13dcb393234 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceNode.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceNode.java
@@ -278,5 +278,5 @@ public interface ControllerServiceNode extends
ComponentNode, VersionedComponent
String invokeConnectorMethod(String methodName, Map<String, String>
jsonArguments, ConfigurationContext configurationContext) throws
InvocationFailedException;
- List<ConnectorMethod> getConnectorMethods();
+ List<ConnectorMethod> getConnectorMethods() throws
InvocationFailedException;
}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/facades/standalone/StandaloneParameterContextFacade.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/facades/standalone/StandaloneParameterContextFacade.java
index b4387c74503..b7f4d027072 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/facades/standalone/StandaloneParameterContextFacade.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/facades/standalone/StandaloneParameterContextFacade.java
@@ -20,6 +20,7 @@ package
org.apache.nifi.components.connector.facades.standalone;
import org.apache.nifi.asset.Asset;
import org.apache.nifi.components.connector.components.ParameterContextFacade;
import org.apache.nifi.components.connector.components.ParameterValue;
+import org.apache.nifi.controller.ComponentNode;
import org.apache.nifi.controller.FlowController;
import org.apache.nifi.controller.ProcessorNode;
import org.apache.nifi.controller.serialization.AffectedComponentSet;
@@ -95,8 +96,19 @@ public class StandaloneParameterContextFacade implements
ParameterContextFacade
final Map<String, Parameter> updatedParameters =
createParameterMap(updatedValues);
managedProcessGroup.getParameterContext().setParameters(updatedParameters);
- allReferencingProcessors.forEach(ProcessorNode::resetValidationState);
+ // A component whose additional classpath resources are supplied by a
Parameter must be reloaded so that its ClassLoader reflects the
+ // updated Parameter values. Components that are not running are not
restarted below, so reloading here is the only point at which their
+ // ClassLoader is rebuilt. Changing a Controller Service property can
also change the Classloader Isolation Key of components that
+ // reference that service, matching
StandardControllerServiceNode.setProperties().
+ for (final ControllerServiceNode serviceNode : allReferencingServices)
{
+ serviceNode.reloadAdditionalResourcesIfNecessary();
+
serviceNode.getReferences().findRecursiveReferences(ComponentNode.class).forEach(ComponentNode::reloadAdditionalResourcesIfNecessary);
+ }
+
+
allReferencingProcessors.forEach(ProcessorNode::reloadAdditionalResourcesIfNecessary);
+
allReferencingServices.forEach(ControllerServiceNode::resetValidationState);
+ allReferencingProcessors.forEach(ProcessorNode::resetValidationState);
logger.info("Parameter Context updated {} parameter. Restarting {}
affected components.", updatedValues.size(), activeSet.getComponentCount());
activeSet.start();
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/facades/standalone/StandaloneParameterContextFacadeTest.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/facades/standalone/StandaloneParameterContextFacadeTest.java
new file mode 100644
index 00000000000..919b414044a
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/facades/standalone/StandaloneParameterContextFacadeTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.components.connector.facades.standalone;
+
+import org.apache.nifi.components.connector.components.ParameterValue;
+import org.apache.nifi.controller.ComponentNode;
+import org.apache.nifi.controller.FlowController;
+import org.apache.nifi.controller.ProcessorNode;
+import org.apache.nifi.controller.ScheduledState;
+import org.apache.nifi.controller.flow.FlowManager;
+import org.apache.nifi.controller.service.ControllerServiceNode;
+import org.apache.nifi.controller.service.ControllerServiceProvider;
+import org.apache.nifi.controller.service.ControllerServiceReference;
+import org.apache.nifi.controller.service.ControllerServiceState;
+import org.apache.nifi.flow.ExecutionEngine;
+import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.parameter.Parameter;
+import org.apache.nifi.parameter.ParameterContext;
+import org.apache.nifi.parameter.ParameterDescriptor;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Predicate;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class StandaloneParameterContextFacadeTest {
+
+ private static final String PARAMETER_NAME = "Classpath Resource";
+
+ @Test
+ public void
testUpdateParametersReloadsStoppedProcessorsThatReferenceUpdatedServices() {
+ final FlowController flowController = mock(FlowController.class);
+ final FlowManager flowManager = mock(FlowManager.class);
+ final ControllerServiceProvider controllerServiceProvider =
mock(ControllerServiceProvider.class);
+ when(flowController.getFlowManager()).thenReturn(flowManager);
+
when(flowController.getControllerServiceProvider()).thenReturn(controllerServiceProvider);
+
+ final ParameterContext parameterContext = mock(ParameterContext.class);
+ final ParameterDescriptor parameterDescriptor = new
ParameterDescriptor.Builder()
+ .name(PARAMETER_NAME)
+ .sensitive(false)
+ .build();
+ final Parameter existingParameter = new Parameter.Builder()
+ .descriptor(parameterDescriptor)
+ .value("old-asset")
+ .build();
+
when(parameterContext.getParameter(PARAMETER_NAME)).thenReturn(Optional.of(existingParameter));
+
+ final ProcessorNode referencingProcessor = mock(ProcessorNode.class);
+
when(referencingProcessor.getPhysicalScheduledState()).thenReturn(ScheduledState.STOPPED);
+ when(referencingProcessor.isRunning()).thenReturn(false);
+
when(referencingProcessor.isReferencingParameter(PARAMETER_NAME)).thenReturn(false);
+
+ final ControllerServiceReference serviceReferences =
mock(ControllerServiceReference.class);
+
when(serviceReferences.findRecursiveReferences(ComponentNode.class)).thenReturn(List.of(referencingProcessor));
+
+ final ControllerServiceNode referencingService =
mock(ControllerServiceNode.class);
+
when(referencingService.isReferencingParameter(PARAMETER_NAME)).thenReturn(true);
+
when(referencingService.getState()).thenReturn(ControllerServiceState.DISABLED);
+ when(referencingService.getReferences()).thenReturn(serviceReferences);
+
+ final ProcessGroup processGroup = mock(ProcessGroup.class);
+ when(processGroup.getParameterContext()).thenReturn(parameterContext);
+
when(processGroup.getExecutionEngine()).thenReturn(ExecutionEngine.STANDARD);
+
when(processGroup.referencesParameterContext(parameterContext)).thenReturn(true);
+
when(processGroup.getControllerServices(false)).thenReturn(Set.of(referencingService));
+ when(processGroup.getProcessors()).thenReturn(Set.of());
+ when(processGroup.findAllProcessGroups(any())).thenAnswer(invocation
-> {
+ final Predicate<ProcessGroup> predicate =
invocation.getArgument(0);
+ if (predicate.test(processGroup)) {
+ return List.of(processGroup);
+ }
+ return List.of();
+ });
+ when(referencingProcessor.getProcessGroup()).thenReturn(processGroup);
+ when(referencingService.getProcessGroup()).thenReturn(processGroup);
+
+ final StandaloneParameterContextFacade facade = new
StandaloneParameterContextFacade(flowController, processGroup);
+ final ParameterValue updatedParameter = new ParameterValue.Builder()
+ .name(PARAMETER_NAME)
+ .value("new-asset")
+ .sensitive(false)
+ .build();
+
+ facade.updateParameters(List.of(updatedParameter));
+
+ verify(referencingService).reloadAdditionalResourcesIfNecessary();
+ verify(referencingProcessor).reloadAdditionalResourcesIfNecessary();
+ }
+}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsyncRequestManager.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsyncRequestManager.java
index 7d58dce0dc2..1eae970f6a3 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsyncRequestManager.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsyncRequestManager.java
@@ -114,9 +114,11 @@ public class AsyncRequestManager<R, T> implements
RequestManager<R, T> {
SecurityContextHolder.getContext().setAuthentication(authentication);
task.accept(request);
- } catch (final Exception e) {
- logger.error("Failed to perform asynchronous task", e);
- request.fail("Encountered unexpected error when performing
asynchronous task: " + e);
+ } catch (final Throwable t) {
+ // Any Throwable must mark the request as failed. A Throwable
that escapes this Runnable is captured by the Future returned from
+ // submit() and is never surfaced, which would leave the
request incomplete and cause clients to poll it indefinitely.
+ logger.error("Failed to perform asynchronous task", t);
+ request.fail("Encountered unexpected error when performing
asynchronous task: " + t);
} finally {
// clear the authentication token
SecurityContextHolder.getContext().setAuthentication(null);
diff --git a/nifi-system-tests/nifi-system-test-extensions-bundle/pom.xml
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-dynamic-classpath/pom.xml
similarity index 59%
copy from nifi-system-tests/nifi-system-test-extensions-bundle/pom.xml
copy to
nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-dynamic-classpath/pom.xml
index bd7d295f8cf..9f3f529c3bf 100644
--- a/nifi-system-tests/nifi-system-test-extensions-bundle/pom.xml
+++
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-dynamic-classpath/pom.xml
@@ -13,23 +13,14 @@
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">
+<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">
<parent>
- <artifactId>nifi-system-tests</artifactId>
<groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-system-test-extensions-bundle</artifactId>
<version>2.12.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
- <artifactId>nifi-system-test-extensions-bundle</artifactId>
- <packaging>pom</packaging>
-
- <modules>
- <module>nifi-system-test-extensions</module>
- <module>nifi-system-test-extensions-nar</module>
- <module>nifi-system-test-extensions-services</module>
- <module>nifi-system-test-extensions-services-api</module>
- <module>nifi-system-test-extensions-services-nar</module>
- <module>nifi-system-test-extensions-services-api-nar</module>
- </modules>
-</project>
\ No newline at end of file
+ <artifactId>nifi-system-test-dynamic-classpath</artifactId>
+</project>
diff --git
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-dynamic-classpath/src/main/java/org/apache/nifi/tests/system/dynamicclasspath/DynamicallyLoadedType.java
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-dynamic-classpath/src/main/java/org/apache/nifi/tests/system/dynamicclasspath/DynamicallyLoadedType.java
new file mode 100644
index 00000000000..6206fe46591
--- /dev/null
+++
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-dynamic-classpath/src/main/java/org/apache/nifi/tests/system/dynamicclasspath/DynamicallyLoadedType.java
@@ -0,0 +1,21 @@
+/*
+ * 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.tests.system.dynamicclasspath;
+
+public class DynamicallyLoadedType {
+}
diff --git
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions-services/src/main/java/org/apache/nifi/cs/tests/system/VerifyMethodSignatureService.java
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions-services/src/main/java/org/apache/nifi/cs/tests/system/VerifyMethodSignatureService.java
new file mode 100644
index 00000000000..ad2566025f4
--- /dev/null
+++
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions-services/src/main/java/org/apache/nifi/cs/tests/system/VerifyMethodSignatureService.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.cs.tests.system;
+
+import org.apache.nifi.annotation.behavior.RequiresInstanceClassLoading;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.connector.components.ComponentState;
+import org.apache.nifi.components.connector.components.ConnectorMethod;
+import org.apache.nifi.components.connector.components.MethodArgument;
+import org.apache.nifi.components.resource.ResourceCardinality;
+import org.apache.nifi.components.resource.ResourceType;
+import org.apache.nifi.controller.AbstractControllerService;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.util.List;
+
+/**
+ * Controller Service used to verify that additional classpath resources are
available during {@code @ConnectorMethod}
+ * invocation after the classpath resource is set as a Controller Service
property.
+ */
+@RequiresInstanceClassLoading
+public class VerifyMethodSignatureService extends AbstractControllerService {
+
+ public static final PropertyDescriptor CLASSPATH_RESOURCE = new
PropertyDescriptor.Builder()
+ .name("Classpath Resource")
+ .description("An external resource to add to the Controller
Service classpath")
+ .required(false)
+ .dynamicallyModifiesClasspath(true)
+ .identifiesExternalResource(ResourceCardinality.SINGLE,
ResourceType.FILE)
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .build();
+
+ @Override
+ protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+ return List.of(CLASSPATH_RESOURCE);
+ }
+
+ @ConnectorMethod(
+ name = "loadClass",
+ description = "Attempts to load the given class from the
Controller Service classpath",
+ allowedStates = {ComponentState.STOPPED},
+ arguments = {
+ @MethodArgument(name = "className", type = String.class,
description = "Fully-qualified class name to load", required = true)
+ }
+ )
+ public String loadClass(final String className) throws
ClassNotFoundException {
+ final Class<?> clazz = Class.forName(className);
+ return clazz.getName();
+ }
+}
diff --git
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions-services/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions-services/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
index 99b5498bfe4..3e376b35485 100644
---
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions-services/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
+++
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions-services/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
@@ -26,3 +26,4 @@ org.apache.nifi.cs.tests.system.ModifyClasspathService
org.apache.nifi.cs.tests.system.MockCSVReader
org.apache.nifi.cs.tests.system.MockCSVWriter
org.apache.nifi.cs.tests.system.VerifyLocalClusterStateService
+org.apache.nifi.cs.tests.system.VerifyMethodSignatureService
diff --git
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/pom.xml
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/pom.xml
index 9ce5354f363..0c93f0d6658 100644
---
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/pom.xml
+++
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/pom.xml
@@ -24,6 +24,12 @@
<artifactId>nifi-system-test-extensions</artifactId>
<dependencies>
+ <dependency>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-system-test-dynamic-classpath</artifactId>
+ <version>2.12.0-SNAPSHOT</version>
+ <scope>provided</scope>
+ </dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-stateless-api</artifactId>
@@ -55,4 +61,19 @@
<scope>provided</scope>
</dependency>
</dependencies>
-</project>
\ No newline at end of file
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.rat</groupId>
+ <artifactId>apache-rat-plugin</artifactId>
+ <configuration>
+ <excludes combine.children="append">
+ <!-- Connector flow definition; JSON cannot carry an
Apache license header -->
+
<exclude>src/main/resources/flows/method-signature-verify-connector.json</exclude>
+ </excludes>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/connectors/tests/system/ClasspathVerifyConnector.java
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/connectors/tests/system/ClasspathVerifyConnector.java
new file mode 100644
index 00000000000..2c7fe849803
--- /dev/null
+++
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/connectors/tests/system/ClasspathVerifyConnector.java
@@ -0,0 +1,223 @@
+/*
+ * 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.connectors.tests.system;
+
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.connector.AbstractConnector;
+import org.apache.nifi.components.connector.BundleCompatibility;
+import org.apache.nifi.components.connector.ConfigurationStep;
+import org.apache.nifi.components.connector.ConnectorConfigurationContext;
+import org.apache.nifi.components.connector.ConnectorPropertyDescriptor;
+import org.apache.nifi.components.connector.ConnectorPropertyGroup;
+import org.apache.nifi.components.connector.FlowUpdateException;
+import org.apache.nifi.components.connector.InvocationFailedException;
+import org.apache.nifi.components.connector.PropertyType;
+import org.apache.nifi.components.connector.components.FlowContext;
+import org.apache.nifi.components.connector.components.ProcessorFacade;
+import org.apache.nifi.components.connector.util.VersionedFlowUtils;
+import org.apache.nifi.flow.Bundle;
+import org.apache.nifi.flow.Position;
+import org.apache.nifi.flow.VersionedExternalFlow;
+import org.apache.nifi.flow.VersionedProcessGroup;
+import org.apache.nifi.flow.VersionedProcessor;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.processors.tests.system.VerifyClasspathResource;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Test connector that exercises additional classpath loading during
configuration verification.
+ * Supports processor verification with property overrides, class loading
through a Connector Method,
+ * and consecutive Connector Method invocations that verify component instance
state is preserved.
+ */
+public class ClasspathVerifyConnector extends AbstractConnector {
+
+ public static final String STEP_NAME = "Classpath Configuration";
+ public static final String STRATEGY_PROCESSOR_VERIFY = "Processor Verify";
+ public static final String STRATEGY_CONNECTOR_METHOD = "Connector Method";
+ public static final String STRATEGY_CONNECTOR_METHOD_STATE = "Connector
Method State";
+ public static final String CONNECTOR_METHOD_STEP = "Invoke loadClass
Connector Method";
+ public static final String CONNECTOR_METHOD_STATE_STEP = "Preserve
Connector Method Component State";
+
+ public static final ConnectorPropertyDescriptor CLASSPATH_RESOURCE = new
ConnectorPropertyDescriptor.Builder()
+ .name("Classpath Resource")
+ .description("An asset JAR to place on the processor classpath")
+ .required(true)
+ .type(PropertyType.ASSET)
+ .build();
+
+ public static final ConnectorPropertyDescriptor CLASS_TO_LOAD = new
ConnectorPropertyDescriptor.Builder()
+ .name("Class to Load")
+ .description("Fully-qualified class name that must be loadable
from the classpath resource")
+ .required(true)
+ .type(PropertyType.STRING)
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .build();
+
+ public static final ConnectorPropertyDescriptor VERIFICATION_STRATEGY =
new ConnectorPropertyDescriptor.Builder()
+ .name("Verification Strategy")
+ .description("Whether verification should call
ProcessorFacade.verify, invoke the loadClass Connector Method, or verify
Connector Method component state")
+ .required(true)
+ .type(PropertyType.STRING)
+ .defaultValue(STRATEGY_PROCESSOR_VERIFY)
+ .allowableValues(STRATEGY_PROCESSOR_VERIFY,
STRATEGY_CONNECTOR_METHOD, STRATEGY_CONNECTOR_METHOD_STATE)
+ .build();
+
+ private static final String PROCESSOR_TYPE =
"org.apache.nifi.processors.tests.system.VerifyClasspathResource";
+ private static final String PROCESSOR_NAME = "Verify Classpath Resource";
+ private static final Bundle SYSTEM_TEST_EXTENSIONS_BUNDLE = new
Bundle("org.apache.nifi", "nifi-system-test-extensions-nar", "2.8.0-SNAPSHOT");
+
+ private static final ConnectorPropertyGroup PROPERTY_GROUP = new
ConnectorPropertyGroup.Builder()
+ .name("Classpath Settings")
+ .description("Classpath resource and verification strategy")
+ .properties(List.of(CLASSPATH_RESOURCE, CLASS_TO_LOAD,
VERIFICATION_STRATEGY))
+ .build();
+
+ private static final ConfigurationStep CONFIGURATION_STEP = new
ConfigurationStep.Builder()
+ .name(STEP_NAME)
+ .description("Configure classpath verification")
+ .propertyGroups(List.of(PROPERTY_GROUP))
+ .build();
+
+ @Override
+ public List<ConfigurationStep> getConfigurationSteps() {
+ return List.of(CONFIGURATION_STEP);
+ }
+
+ @Override
+ public VersionedExternalFlow getInitialFlow() {
+ return buildFlow(null, null);
+ }
+
+ @Override
+ public VersionedExternalFlow getActiveFlow(final FlowContext
activeFlowContext) {
+ final ConnectorConfigurationContext configurationContext =
activeFlowContext.getConfigurationContext();
+ final String classpathResource =
configurationContext.getProperty(STEP_NAME,
CLASSPATH_RESOURCE.getName()).getValue();
+ final String classToLoad = configurationContext.getProperty(STEP_NAME,
CLASS_TO_LOAD.getName()).getValue();
+ return buildFlow(classpathResource, classToLoad);
+ }
+
+ @Override
+ protected void onStepConfigured(final String stepName, final FlowContext
workingContext) throws FlowUpdateException {
+ final ConnectorConfigurationContext configurationContext =
workingContext.getConfigurationContext();
+ final String classpathResource =
configurationContext.getProperty(STEP_NAME,
CLASSPATH_RESOURCE.getName()).getValue();
+ final String classToLoad = configurationContext.getProperty(STEP_NAME,
CLASS_TO_LOAD.getName()).getValue();
+ getInitializationContext().updateFlow(workingContext,
buildFlow(classpathResource, classToLoad), BundleCompatibility.RESOLVE_BUNDLE);
+ }
+
+ @Override
+ public void applyUpdate(final FlowContext workingContext, final
FlowContext activeContext) throws FlowUpdateException {
+ final ConnectorConfigurationContext configurationContext =
workingContext.getConfigurationContext();
+ final String classpathResource =
configurationContext.getProperty(STEP_NAME,
CLASSPATH_RESOURCE.getName()).getValue();
+ final String classToLoad = configurationContext.getProperty(STEP_NAME,
CLASS_TO_LOAD.getName()).getValue();
+ getInitializationContext().updateFlow(activeContext,
buildFlow(classpathResource, classToLoad), BundleCompatibility.RESOLVE_BUNDLE);
+ }
+
+ @Override
+ public List<ConfigVerificationResult> verifyConfigurationStep(final String
stepName, final Map<String, String> propertyValueOverrides, final FlowContext
flowContext) {
+ final ConnectorConfigurationContext configurationContext =
flowContext.getConfigurationContext().createWithOverrides(stepName,
propertyValueOverrides);
+ final String classpathResource =
configurationContext.getProperty(STEP_NAME,
CLASSPATH_RESOURCE.getName()).getValue();
+ final String classToLoad = configurationContext.getProperty(STEP_NAME,
CLASS_TO_LOAD.getName()).getValue();
+ final String strategy = configurationContext.getProperty(STEP_NAME,
VERIFICATION_STRATEGY.getName()).getValue();
+
+ final ProcessorFacade processorFacade =
flowContext.getRootGroup().getProcessors().stream()
+ .filter(processor ->
processor.getDefinition().getType().endsWith("VerifyClasspathResource"))
+ .findFirst()
+ .orElseThrow(() -> new
IllegalStateException("VerifyClasspathResource processor not found in flow"));
+
+ if (STRATEGY_CONNECTOR_METHOD.equals(strategy)) {
+ return invokeLoadClassMethod(processorFacade, classToLoad);
+ }
+
+ if (STRATEGY_CONNECTOR_METHOD_STATE.equals(strategy)) {
+ return verifyConnectorMethodState(processorFacade);
+ }
+
+ return verifyWithPropertyOverrides(processorFacade, classpathResource,
classToLoad);
+ }
+
+ private List<ConfigVerificationResult> verifyWithPropertyOverrides(final
ProcessorFacade processorFacade, final String classpathResource, final String
classToLoad) {
+ final Map<String, String> propertyOverrides = new HashMap<>();
+ if (classpathResource != null) {
+
propertyOverrides.put(VerifyClasspathResource.CLASSPATH_RESOURCE.getName(),
classpathResource);
+ }
+
+ if (classToLoad != null) {
+
propertyOverrides.put(VerifyClasspathResource.CLASS_TO_LOAD.getName(),
classToLoad);
+ }
+
+ return processorFacade.verify(propertyOverrides, Map.of());
+ }
+
+ private List<ConfigVerificationResult> invokeLoadClassMethod(final
ProcessorFacade processorFacade, final String classToLoad) {
+ try {
+ final String loadedClass =
processorFacade.invokeConnectorMethod("loadClass", Map.of("className",
classToLoad), String.class);
+ return List.of(new ConfigVerificationResult.Builder()
+ .verificationStepName(CONNECTOR_METHOD_STEP)
+ .outcome(Outcome.SUCCESSFUL)
+ .explanation("Successfully loaded class " + loadedClass +
" via ConnectorMethod")
+ .build());
+ } catch (final InvocationFailedException e) {
+ return List.of(new ConfigVerificationResult.Builder()
+ .verificationStepName(CONNECTOR_METHOD_STEP)
+ .outcome(Outcome.FAILED)
+ .explanation("Failed to load class " + classToLoad + " via
ConnectorMethod: " + e.getMessage())
+ .build());
+ }
+ }
+
+ private List<ConfigVerificationResult> verifyConnectorMethodState(final
ProcessorFacade processorFacade) {
+ try {
+ final Integer firstInvocationCount =
processorFacade.invokeConnectorMethod("incrementInvocationCount", Map.of(),
Integer.class);
+ final Integer secondInvocationCount =
processorFacade.invokeConnectorMethod("incrementInvocationCount", Map.of(),
Integer.class);
+ final boolean statePreserved = firstInvocationCount == 1 &&
secondInvocationCount == 2;
+ return List.of(new ConfigVerificationResult.Builder()
+ .verificationStepName(CONNECTOR_METHOD_STATE_STEP)
+ .outcome(statePreserved ? Outcome.SUCCESSFUL :
Outcome.FAILED)
+ .explanation("Connector Method invocation counts were " +
firstInvocationCount + " and " + secondInvocationCount)
+ .build());
+ } catch (final InvocationFailedException e) {
+ return List.of(new ConfigVerificationResult.Builder()
+ .verificationStepName(CONNECTOR_METHOD_STATE_STEP)
+ .outcome(Outcome.FAILED)
+ .explanation("Failed to invoke stateful Connector Method:
" + e.getMessage())
+ .build());
+ }
+ }
+
+ private VersionedExternalFlow buildFlow(final String classpathResource,
final String classToLoad) {
+ final VersionedProcessGroup group =
VersionedFlowUtils.createProcessGroup("classpath-verify-flow-id", "Classpath
Verify Flow");
+ final VersionedProcessor processor =
VersionedFlowUtils.addProcessor(group, PROCESSOR_TYPE,
SYSTEM_TEST_EXTENSIONS_BUNDLE, PROCESSOR_NAME, new Position(0, 0));
+
+ if (classpathResource != null) {
+
processor.getProperties().put(VerifyClasspathResource.CLASSPATH_RESOURCE.getName(),
classpathResource);
+ }
+
+ if (classToLoad != null) {
+
processor.getProperties().put(VerifyClasspathResource.CLASS_TO_LOAD.getName(),
classToLoad);
+ }
+
+ final VersionedExternalFlow flow = new VersionedExternalFlow();
+ flow.setFlowContents(group);
+ flow.setParameterContexts(Map.of());
+ return flow;
+ }
+}
diff --git
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/connectors/tests/system/MethodSignatureVerifyConnector.java
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/connectors/tests/system/MethodSignatureVerifyConnector.java
new file mode 100644
index 00000000000..02b416fb093
--- /dev/null
+++
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/connectors/tests/system/MethodSignatureVerifyConnector.java
@@ -0,0 +1,217 @@
+/*
+ * 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.connectors.tests.system;
+
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.connector.AbstractConnector;
+import org.apache.nifi.components.connector.BundleCompatibility;
+import org.apache.nifi.components.connector.ConfigurationStep;
+import org.apache.nifi.components.connector.ConnectorConfigurationContext;
+import org.apache.nifi.components.connector.ConnectorPropertyDescriptor;
+import org.apache.nifi.components.connector.ConnectorPropertyGroup;
+import org.apache.nifi.components.connector.FlowUpdateException;
+import org.apache.nifi.components.connector.InvocationFailedException;
+import org.apache.nifi.components.connector.PropertyType;
+import org.apache.nifi.components.connector.components.ControllerServiceFacade;
+import org.apache.nifi.components.connector.components.FlowContext;
+import org.apache.nifi.components.connector.components.ProcessorFacade;
+import org.apache.nifi.components.connector.util.VersionedFlowUtils;
+import org.apache.nifi.flow.Bundle;
+import org.apache.nifi.flow.Position;
+import org.apache.nifi.flow.VersionedControllerService;
+import org.apache.nifi.flow.VersionedExternalFlow;
+import org.apache.nifi.flow.VersionedProcessGroup;
+import org.apache.nifi.flow.VersionedProcessor;
+import org.apache.nifi.processors.tests.system.VerifyMethodSignatureResource;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Test connector that exercises additional-classpath method discovery. The
Working flow contains either a
+ * processor or a Controller Service whose declared methods include a private
method returning a type available only
+ * through the configured asset. Invoking the unrelated, constant-returning
{@code returnConstant} Connector Method
+ * forces the framework to resolve every declared method signature, so the
asset must be present on the component
+ * classpath even though the invoked method itself does not reference the
dynamically loaded type.
+ */
+public class MethodSignatureVerifyConnector extends AbstractConnector {
+
+ public static final String STEP_NAME = "Method Signature Configuration";
+ public static final String APPLICATION_PARAMETER = "Parameter";
+ public static final String APPLICATION_PROCESSOR_PROPERTY = "Processor
Property";
+ public static final String APPLICATION_CONTROLLER_SERVICE_PROPERTY =
"Controller Service Property";
+ public static final String METHOD_SIGNATURE_STEP = "Discover Connector
Method With Dynamic Signature";
+
+ public static final ConnectorPropertyDescriptor CLASSPATH_RESOURCE = new
ConnectorPropertyDescriptor.Builder()
+ .name("Classpath Resource")
+ .description("An asset JAR to place on the component classpath")
+ .required(true)
+ .type(PropertyType.ASSET)
+ .build();
+
+ public static final ConnectorPropertyDescriptor CLASSPATH_APPLICATION =
new ConnectorPropertyDescriptor.Builder()
+ .name("Classpath Application")
+ .description("Whether the classpath asset is applied through a
Parameter, a Processor property, or a Controller Service property")
+ .required(true)
+ .type(PropertyType.STRING)
+ .defaultValue(APPLICATION_PARAMETER)
+ .allowableValues(APPLICATION_PARAMETER,
APPLICATION_PROCESSOR_PROPERTY, APPLICATION_CONTROLLER_SERVICE_PROPERTY)
+ .build();
+
+ private static final String FLOW_JSON_PATH =
"flows/method-signature-verify-connector.json";
+ private static final String CLASSPATH_PARAMETER_NAME = "Classpath
Resource";
+ private static final String PROCESSOR_TYPE =
"org.apache.nifi.processors.tests.system.VerifyMethodSignatureResource";
+ private static final String PROCESSOR_NAME = "Verify Method Signature
Resource";
+ private static final String CONTROLLER_SERVICE_TYPE =
"org.apache.nifi.cs.tests.system.VerifyMethodSignatureService";
+ private static final String CONTROLLER_SERVICE_NAME = "Verify Method
Signature Service";
+ private static final String DYNAMIC_CLASSPATH_CLASS =
"org.apache.nifi.tests.system.dynamicclasspath.DynamicallyLoadedType";
+ private static final Bundle SYSTEM_TEST_EXTENSIONS_BUNDLE = new
Bundle("org.apache.nifi", "nifi-system-test-extensions-nar", "2.8.0-SNAPSHOT");
+ private static final Bundle SYSTEM_TEST_EXTENSIONS_SERVICES_BUNDLE = new
Bundle("org.apache.nifi", "nifi-system-test-extensions-services-nar",
"2.8.0-SNAPSHOT");
+
+ private static final ConnectorPropertyGroup PROPERTY_GROUP = new
ConnectorPropertyGroup.Builder()
+ .name("Classpath Settings")
+ .description("Classpath resource providing the dynamically loaded
method-signature type")
+ .properties(List.of(CLASSPATH_RESOURCE, CLASSPATH_APPLICATION))
+ .build();
+
+ private static final ConfigurationStep CONFIGURATION_STEP = new
ConfigurationStep.Builder()
+ .name(STEP_NAME)
+ .description("Configure the classpath resource used for
method-signature discovery")
+ .propertyGroups(List.of(PROPERTY_GROUP))
+ .build();
+
+ @Override
+ public List<ConfigurationStep> getConfigurationSteps() {
+ return List.of(CONFIGURATION_STEP);
+ }
+
+ @Override
+ public VersionedExternalFlow getInitialFlow() {
+ return buildFlow(null, APPLICATION_PARAMETER);
+ }
+
+ @Override
+ public VersionedExternalFlow getActiveFlow(final FlowContext
activeFlowContext) {
+ return buildFlowFromContext(activeFlowContext);
+ }
+
+ @Override
+ protected void onStepConfigured(final String stepName, final FlowContext
workingContext) throws FlowUpdateException {
+ getInitializationContext().updateFlow(workingContext,
buildFlowFromContext(workingContext), BundleCompatibility.RESOLVE_BUNDLE);
+ }
+
+ @Override
+ public void applyUpdate(final FlowContext workingContext, final
FlowContext activeContext) throws FlowUpdateException {
+ getInitializationContext().updateFlow(activeContext,
buildFlowFromContext(workingContext), BundleCompatibility.RESOLVE_BUNDLE);
+ }
+
+ @Override
+ public List<ConfigVerificationResult> verifyConfigurationStep(final String
stepName, final Map<String, String> propertyValueOverrides, final FlowContext
flowContext) {
+ final ConnectorConfigurationContext configurationContext =
flowContext.getConfigurationContext().createWithOverrides(stepName,
propertyValueOverrides);
+ final String classpathApplication =
configurationContext.getProperty(STEP_NAME,
CLASSPATH_APPLICATION.getName()).getValue();
+
+ try {
+ final String result;
+ if
(APPLICATION_CONTROLLER_SERVICE_PROPERTY.equals(classpathApplication)) {
+ result =
findControllerService(flowContext).invokeConnectorMethod("loadClass",
Map.of("className", DYNAMIC_CLASSPATH_CLASS), String.class);
+ } else {
+ result =
findProcessor(flowContext).invokeConnectorMethod("returnConstant", Map.of(),
String.class);
+ }
+
+ return List.of(new ConfigVerificationResult.Builder()
+ .verificationStepName(METHOD_SIGNATURE_STEP)
+ .outcome(Outcome.SUCCESSFUL)
+ .explanation("Successfully invoked ConnectorMethod
returning " + result)
+ .build());
+ } catch (final InvocationFailedException e) {
+ return List.of(new ConfigVerificationResult.Builder()
+ .verificationStepName(METHOD_SIGNATURE_STEP)
+ .outcome(Outcome.FAILED)
+ .explanation("Failed to discover ConnectorMethod because a
declared method signature type was unavailable: " + e)
+ .build());
+ }
+ }
+
+ private VersionedExternalFlow buildFlowFromContext(final FlowContext
flowContext) {
+ final ConnectorConfigurationContext configurationContext =
flowContext.getConfigurationContext();
+ final String classpathResource =
configurationContext.getProperty(STEP_NAME,
CLASSPATH_RESOURCE.getName()).getValue();
+ final String classpathApplication =
configurationContext.getProperty(STEP_NAME,
CLASSPATH_APPLICATION.getName()).getValue();
+ return buildFlow(classpathResource, classpathApplication);
+ }
+
+ private VersionedExternalFlow buildFlow(final String classpathResource,
final String classpathApplication) {
+ if (APPLICATION_PROCESSOR_PROPERTY.equals(classpathApplication)) {
+ return buildProcessorPropertyFlow(classpathResource);
+ }
+
+ if
(APPLICATION_CONTROLLER_SERVICE_PROPERTY.equals(classpathApplication)) {
+ return buildControllerServicePropertyFlow(classpathResource);
+ }
+
+ return buildParameterFlow(classpathResource);
+ }
+
+ private VersionedExternalFlow buildParameterFlow(final String
classpathResource) {
+ final VersionedExternalFlow flow =
VersionedFlowUtils.loadFlowFromResource(FLOW_JSON_PATH);
+ if (classpathResource != null) {
+ VersionedFlowUtils.setParameterValues(flow,
Map.of(CLASSPATH_PARAMETER_NAME, classpathResource));
+ }
+
+ return flow;
+ }
+
+ private VersionedExternalFlow buildProcessorPropertyFlow(final String
classpathResource) {
+ final VersionedProcessGroup group =
VersionedFlowUtils.createProcessGroup("method-signature-verify-flow-id",
"Method Signature Verify Flow");
+ final VersionedProcessor processor =
VersionedFlowUtils.addProcessor(group, PROCESSOR_TYPE,
SYSTEM_TEST_EXTENSIONS_BUNDLE, PROCESSOR_NAME, new Position(0, 0));
+ if (classpathResource != null) {
+
processor.getProperties().put(VerifyMethodSignatureResource.CLASSPATH_RESOURCE.getName(),
classpathResource);
+ }
+
+ final VersionedExternalFlow flow = new VersionedExternalFlow();
+ flow.setFlowContents(group);
+ flow.setParameterContexts(Map.of());
+ return flow;
+ }
+
+ private VersionedExternalFlow buildControllerServicePropertyFlow(final
String classpathResource) {
+ final VersionedProcessGroup group =
VersionedFlowUtils.createProcessGroup("method-signature-verify-flow-id",
"Method Signature Verify Flow");
+ final VersionedControllerService controllerService =
VersionedFlowUtils.addControllerService(group, CONTROLLER_SERVICE_TYPE,
SYSTEM_TEST_EXTENSIONS_SERVICES_BUNDLE, CONTROLLER_SERVICE_NAME);
+ if (classpathResource != null) {
+ controllerService.getProperties().put(CLASSPATH_PARAMETER_NAME,
classpathResource);
+ }
+
+ final VersionedExternalFlow flow = new VersionedExternalFlow();
+ flow.setFlowContents(group);
+ flow.setParameterContexts(Map.of());
+ return flow;
+ }
+
+ private ProcessorFacade findProcessor(final FlowContext flowContext) {
+ return flowContext.getRootGroup().getProcessors().stream()
+ .filter(processor ->
processor.getDefinition().getType().endsWith("VerifyMethodSignatureResource"))
+ .findFirst()
+ .orElseThrow(() -> new
IllegalStateException("VerifyMethodSignatureResource processor not found in
flow"));
+ }
+
+ private ControllerServiceFacade findControllerService(final FlowContext
flowContext) {
+ return flowContext.getRootGroup().getControllerServices().stream()
+ .filter(controllerService ->
controllerService.getDefinition().getType().endsWith("VerifyMethodSignatureService"))
+ .findFirst()
+ .orElseThrow(() -> new
IllegalStateException("VerifyMethodSignatureService controller service not
found in flow"));
+ }
+}
diff --git
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/processors/tests/system/VerifyClasspathResource.java
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/processors/tests/system/VerifyClasspathResource.java
new file mode 100644
index 00000000000..4bf7bf06b61
--- /dev/null
+++
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/processors/tests/system/VerifyClasspathResource.java
@@ -0,0 +1,125 @@
+/*
+ * 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.processors.tests.system;
+
+import org.apache.nifi.annotation.behavior.RequiresInstanceClassLoading;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.connector.components.ComponentState;
+import org.apache.nifi.components.connector.components.ConnectorMethod;
+import org.apache.nifi.components.connector.components.MethodArgument;
+import org.apache.nifi.components.resource.ResourceCardinality;
+import org.apache.nifi.components.resource.ResourceType;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Processor used to verify that additional classpath resources are available
during configuration verification
+ * and {@code @ConnectorMethod} invocation.
+ */
+@RequiresInstanceClassLoading
+public class VerifyClasspathResource extends AbstractProcessor implements
VerifiableProcessor {
+
+ public static final PropertyDescriptor CLASSPATH_RESOURCE = new
PropertyDescriptor.Builder()
+ .name("Classpath Resource")
+ .description("An external resource to add to the processor
classpath")
+ .required(false)
+ .dynamicallyModifiesClasspath(true)
+ .identifiesExternalResource(ResourceCardinality.SINGLE,
ResourceType.FILE)
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .build();
+
+ public static final PropertyDescriptor CLASS_TO_LOAD = new
PropertyDescriptor.Builder()
+ .name("Class to Load")
+ .description("The fully-qualified class name that must be loadable
from the processor classpath")
+ .required(false)
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .build();
+
+ public static final String LOAD_CLASS_STEP = "Load Class From Classpath";
+
+ private final AtomicInteger connectorMethodInvocationCount = new
AtomicInteger();
+
+ @Override
+ protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+ return List.of(CLASSPATH_RESOURCE, CLASS_TO_LOAD);
+ }
+
+ @Override
+ public void onTrigger(final ProcessContext context, final ProcessSession
session) throws ProcessException {
+ }
+
+ @Override
+ public List<ConfigVerificationResult> verify(final ProcessContext context,
final ComponentLog verificationLogger, final Map<String, String> attributes) {
+ final String classToLoad =
context.getProperty(CLASS_TO_LOAD).getValue();
+ if (classToLoad == null || classToLoad.isBlank()) {
+ return List.of(new ConfigVerificationResult.Builder()
+ .verificationStepName(LOAD_CLASS_STEP)
+ .outcome(Outcome.FAILED)
+ .explanation("Class to Load is not configured")
+ .build());
+ }
+
+ try {
+ loadClass(classToLoad);
+ return List.of(new ConfigVerificationResult.Builder()
+ .verificationStepName(LOAD_CLASS_STEP)
+ .outcome(Outcome.SUCCESSFUL)
+ .explanation("Successfully loaded class " + classToLoad)
+ .build());
+ } catch (final ClassNotFoundException e) {
+ return List.of(new ConfigVerificationResult.Builder()
+ .verificationStepName(LOAD_CLASS_STEP)
+ .outcome(Outcome.FAILED)
+ .explanation("Failed to load class " + classToLoad + ": "
+ e.getMessage())
+ .build());
+ }
+ }
+
+ @ConnectorMethod(
+ name = "loadClass",
+ description = "Attempts to load the given class from the processor
classpath",
+ allowedStates = {ComponentState.STOPPED,
ComponentState.PROCESSOR_DISABLED},
+ arguments = {
+ @MethodArgument(name = "className", type = String.class,
description = "Fully-qualified class name to load", required = true)
+ }
+ )
+ public String loadClass(final String className) throws
ClassNotFoundException {
+ final Class<?> clazz = Class.forName(className);
+ return clazz.getName();
+ }
+
+ @ConnectorMethod(
+ name = "incrementInvocationCount",
+ description = "Increments and returns the number of invocations on
this Processor instance",
+ allowedStates = {ComponentState.STOPPED,
ComponentState.PROCESSOR_DISABLED}
+ )
+ public int incrementInvocationCount() {
+ return connectorMethodInvocationCount.incrementAndGet();
+ }
+}
diff --git
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/processors/tests/system/VerifyMethodSignatureResource.java
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/processors/tests/system/VerifyMethodSignatureResource.java
new file mode 100644
index 00000000000..cbc97799853
--- /dev/null
+++
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/processors/tests/system/VerifyMethodSignatureResource.java
@@ -0,0 +1,75 @@
+/*
+ * 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.processors.tests.system;
+
+import org.apache.nifi.annotation.behavior.RequiresInstanceClassLoading;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.connector.components.ComponentState;
+import org.apache.nifi.components.connector.components.ConnectorMethod;
+import org.apache.nifi.components.resource.ResourceCardinality;
+import org.apache.nifi.components.resource.ResourceType;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.tests.system.dynamicclasspath.DynamicallyLoadedType;
+
+import java.util.List;
+
+/**
+ * Processor whose declared methods include a private method that returns a
type available only through an additional
+ * classpath resource. Discovering any {@code @ConnectorMethod} on this
processor forces the JVM to resolve every
+ * declared method signature, so the additional classpath resource must be
present even to invoke the unrelated,
+ * constant-returning {@code returnConstant} method.
+ */
+@RequiresInstanceClassLoading
+public class VerifyMethodSignatureResource extends AbstractProcessor {
+
+ public static final PropertyDescriptor CLASSPATH_RESOURCE = new
PropertyDescriptor.Builder()
+ .name("Classpath Resource")
+ .description("An external resource to add to the processor
classpath")
+ .required(false)
+ .dynamicallyModifiesClasspath(true)
+ .identifiesExternalResource(ResourceCardinality.SINGLE,
ResourceType.FILE)
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .build();
+
+ @Override
+ protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+ return List.of(CLASSPATH_RESOURCE);
+ }
+
+ @Override
+ public void onTrigger(final ProcessContext context, final ProcessSession
session) throws ProcessException {
+ }
+
+ @ConnectorMethod(
+ name = "returnConstant",
+ description = "Returns a constant value without referencing any
dynamically loaded class",
+ allowedStates = {ComponentState.STOPPED,
ComponentState.PROCESSOR_DISABLED}
+ )
+ public String returnConstant() {
+ return "success";
+ }
+
+ @SuppressWarnings("unused")
+ private DynamicallyLoadedType dynamicallyLoadedMethod() {
+ return null;
+ }
+}
diff --git
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/META-INF/services/org.apache.nifi.components.connector.Connector
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/META-INF/services/org.apache.nifi.components.connector.Connector
index c617a26993b..e6b2787790e 100644
---
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/META-INF/services/org.apache.nifi.components.connector.Connector
+++
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/META-INF/services/org.apache.nifi.components.connector.Connector
@@ -18,13 +18,15 @@
org.apache.nifi.connectors.tests.system.AsymmetricFailureMigrationConnector
org.apache.nifi.connectors.tests.system.BacklogReportingTestConnector
org.apache.nifi.connectors.tests.system.BundleResolutionConnector
org.apache.nifi.connectors.tests.system.CalculateConnector
+org.apache.nifi.connectors.tests.system.ClasspathVerifyConnector
org.apache.nifi.connectors.tests.system.ComponentLifecycleConnector
org.apache.nifi.connectors.tests.system.DataQueuingConnector
org.apache.nifi.connectors.tests.system.GatedDataQueuingConnector
org.apache.nifi.connectors.tests.system.FailingConfigurationMigrationConnector
org.apache.nifi.connectors.tests.system.FailingStateMigrationConnector
-org.apache.nifi.connectors.tests.system.MigrationTargetConnector
+org.apache.nifi.connectors.tests.system.MethodSignatureVerifyConnector
org.apache.nifi.connectors.tests.system.MigratePropertiesConnector
+org.apache.nifi.connectors.tests.system.MigrationTargetConnector
org.apache.nifi.connectors.tests.system.NestedProcessGroupConnector
org.apache.nifi.connectors.tests.system.NonMigratingConnector
org.apache.nifi.connectors.tests.system.NopConnector
diff --git
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
index 2d0e1afa8ab..7d9389d9be3 100644
---
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
+++
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
@@ -63,8 +63,10 @@ org.apache.nifi.processors.tests.system.UpdateContent
org.apache.nifi.processors.tests.system.UpdateMetric
org.apache.nifi.processors.tests.system.UnzipFlowFile
org.apache.nifi.processors.tests.system.ValidateFileExists
+org.apache.nifi.processors.tests.system.VerifyClasspathResource
org.apache.nifi.processors.tests.system.VerifyContents
org.apache.nifi.processors.tests.system.VerifyEvenThenOdd
+org.apache.nifi.processors.tests.system.VerifyMethodSignatureResource
org.apache.nifi.processors.tests.system.WriteFlowFileCountToFile
org.apache.nifi.processors.tests.system.WriteLifecycleEvents
org.apache.nifi.processors.tests.system.WriteToFile
diff --git
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/flows/method-signature-verify-connector.json
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/flows/method-signature-verify-connector.json
new file mode 100644
index 00000000000..9f9ba1439fd
--- /dev/null
+++
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/flows/method-signature-verify-connector.json
@@ -0,0 +1,88 @@
+{
+ "flowContents": {
+ "identifier": "method-signature-verify-flow-id",
+ "name": "Method Signature Verify Flow",
+ "comments": "",
+ "position": {
+ "x": 0.0,
+ "y": 0.0
+ },
+ "processGroups": [],
+ "remoteProcessGroups": [],
+ "processors": [
+ {
+ "identifier": "5d3f8a2b-1c4e-4a6f-9b8d-2e7c1f0a3b5c",
+ "name": "Verify Method Signature Resource",
+ "comments": "",
+ "position": {
+ "x": 0.0,
+ "y": 0.0
+ },
+ "type":
"org.apache.nifi.processors.tests.system.VerifyMethodSignatureResource",
+ "bundle": {
+ "group": "org.apache.nifi",
+ "artifact": "nifi-system-test-extensions-nar",
+ "version": "2.8.0-SNAPSHOT"
+ },
+ "properties": {
+ "Classpath Resource": "#{Classpath Resource}"
+ },
+ "propertyDescriptors": {},
+ "style": {},
+ "schedulingPeriod": "0 sec",
+ "schedulingStrategy": "TIMER_DRIVEN",
+ "executionNode": "ALL",
+ "penaltyDuration": "30 sec",
+ "yieldDuration": "1 sec",
+ "bulletinLevel": "WARN",
+ "runDurationMillis": 0,
+ "concurrentlySchedulableTaskCount": 1,
+ "autoTerminatedRelationships": [],
+ "scheduledState": "ENABLED",
+ "retryCount": 10,
+ "retriedRelationships": [],
+ "backoffMechanism": "PENALIZE_FLOWFILE",
+ "maxBackoffPeriod": "10 mins",
+ "componentType": "PROCESSOR",
+ "groupIdentifier": "method-signature-verify-flow-id"
+ }
+ ],
+ "inputPorts": [],
+ "outputPorts": [],
+ "connections": [],
+ "labels": [],
+ "funnels": [],
+ "controllerServices": [],
+ "parameterContextName": "Method Signature Verify Parameters",
+ "defaultFlowFileExpiration": "0 sec",
+ "defaultBackPressureObjectThreshold": 10000,
+ "defaultBackPressureDataSizeThreshold": "1 GB",
+ "scheduledState": "ENABLED",
+ "executionEngine": "INHERITED",
+ "maxConcurrentTasks": 1,
+ "statelessFlowTimeout": "1 min",
+ "flowFileOutboundPolicy": "STREAM_WHEN_AVAILABLE",
+ "flowFileConcurrency": "UNBOUNDED",
+ "componentType": "PROCESS_GROUP"
+ },
+ "externalControllerServices": {},
+ "parameterContexts": {
+ "Method Signature Verify Parameters": {
+ "name": "Method Signature Verify Parameters",
+ "parameters": [
+ {
+ "name": "Classpath Resource",
+ "description": "The asset JAR to place on the processor
classpath",
+ "sensitive": false,
+ "provided": false
+ }
+ ],
+ "inheritedParameterContexts": [],
+ "description": "",
+ "componentType": "PARAMETER_CONTEXT"
+ }
+ },
+ "flowEncodingVersion": "1.0",
+ "parameterProviders": {},
+ "latest": false
+}
diff --git a/nifi-system-tests/nifi-system-test-extensions-bundle/pom.xml
b/nifi-system-tests/nifi-system-test-extensions-bundle/pom.xml
index bd7d295f8cf..749e45d4274 100644
--- a/nifi-system-tests/nifi-system-test-extensions-bundle/pom.xml
+++ b/nifi-system-tests/nifi-system-test-extensions-bundle/pom.xml
@@ -25,6 +25,7 @@
<packaging>pom</packaging>
<modules>
+ <module>nifi-system-test-dynamic-classpath</module>
<module>nifi-system-test-extensions</module>
<module>nifi-system-test-extensions-nar</module>
<module>nifi-system-test-extensions-services</module>
diff --git a/nifi-system-tests/nifi-system-test-suite/pom.xml
b/nifi-system-tests/nifi-system-test-suite/pom.xml
index 3064e9a4179..5de9717a222 100644
--- a/nifi-system-tests/nifi-system-test-suite/pom.xml
+++ b/nifi-system-tests/nifi-system-test-suite/pom.xml
@@ -173,6 +173,19 @@
<version>2.12.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
+ <!--
+ Provided scope makes the dynamic-classpath jar resolvable on the
test JVM classpath so ClasspathVerifyIT can
+ upload it as a Connector asset, without leaking
DynamicallyLoadedType onto the NiFi server runtime. The
+ system-test runtime assembly (src/test/assembly/dependencies.xml)
copies only runtime-scope artifacts named
+ in its includes allowlist, so this provided artifact never lands
in nifi-lib-assembly. The type is therefore
+ available to the running component only through the uploaded
asset's dynamic classpath.
+ -->
+ <dependency>
+ <groupId>org.apache.nifi</groupId>
+ <artifactId>nifi-system-test-dynamic-classpath</artifactId>
+ <version>2.12.0-SNAPSHOT</version>
+ <scope>provided</scope>
+ </dependency>
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
diff --git
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/connectors/ClasspathVerifyIT.java
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/connectors/ClasspathVerifyIT.java
new file mode 100644
index 00000000000..89f9aeab644
--- /dev/null
+++
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/connectors/ClasspathVerifyIT.java
@@ -0,0 +1,279 @@
+/*
+ * 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.tests.system.connectors;
+
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.tests.system.NiFiSystemIT;
+import org.apache.nifi.tests.system.dynamicclasspath.DynamicallyLoadedType;
+import org.apache.nifi.toolkit.client.NiFiClientException;
+import org.apache.nifi.web.api.dto.AssetReferenceDTO;
+import org.apache.nifi.web.api.dto.ConfigVerificationResultDTO;
+import org.apache.nifi.web.api.dto.ConnectorValueReferenceDTO;
+import org.apache.nifi.web.api.entity.AssetEntity;
+import org.apache.nifi.web.api.entity.ConnectorEntity;
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * System tests that verify additional classpath resources are loaded during
connector configuration verification,
+ * Connector Method discovery and invocation use the Working component
classpath after Parameter updates and after
+ * Processor or Controller Service property updates, and consecutive
invocations preserve component instance state.
+ */
+public class ClasspathVerifyIT extends NiFiSystemIT {
+
+ private static final String STEP_NAME = "Classpath Configuration";
+ private static final String METHOD_SIGNATURE_STEP_NAME = "Method Signature
Configuration";
+ private static final String STRATEGY_PROCESSOR_VERIFY = "Processor Verify";
+ private static final String STRATEGY_CONNECTOR_METHOD = "Connector Method";
+ private static final String STRATEGY_CONNECTOR_METHOD_STATE = "Connector
Method State";
+ private static final String LOAD_CLASS_STEP = "Load Class From Classpath";
+ private static final String CONNECTOR_METHOD_STEP = "Invoke loadClass
Connector Method";
+ private static final String CONNECTOR_METHOD_STATE_STEP = "Preserve
Connector Method Component State";
+ private static final String METHOD_SIGNATURE_STEP = "Discover Connector
Method With Dynamic Signature";
+
+ private static final String CLASSPATH_RESOURCE = "Classpath Resource";
+ private static final String CLASS_TO_LOAD = "Class to Load";
+ private static final String VERIFICATION_STRATEGY = "Verification
Strategy";
+ private static final String CLASSPATH_APPLICATION = "Classpath
Application";
+ private static final String APPLICATION_PROCESSOR_PROPERTY = "Processor
Property";
+ private static final String APPLICATION_CONTROLLER_SERVICE_PROPERTY =
"Controller Service Property";
+
+ private static final String DYNAMIC_CLASSPATH_CLASS =
DynamicallyLoadedType.class.getName();
+ private static final String UNKNOWN_CLASS =
"org.apache.nifi.tests.system.DoesNotExistOnClasspath";
+
+ @Test
+ public void testProcessorVerifyUsesTemporaryClasspathWhenUncommitted()
throws NiFiClientException, IOException, InterruptedException {
+ final ConnectorEntity connector =
getClientUtil().createConnector("ClasspathVerifyConnector");
+ final String assetId = uploadDynamicClasspathAsset(connector.getId());
+
+ final List<ConfigVerificationResultDTO> results =
getClientUtil().verifyConnectorStepConfigWithReferences(
+ connector.getId(), STEP_NAME, createVerifyReferences(assetId,
DYNAMIC_CLASSPATH_CLASS, STRATEGY_PROCESSOR_VERIFY));
+
+ assertSuccessfulClasspathStep(results, LOAD_CLASS_STEP);
+ }
+
+ @Test
+ public void testConnectorMethodLoadsClassFromCommittedClasspath() throws
NiFiClientException, IOException, InterruptedException {
+ final ConnectorEntity connector =
getClientUtil().createConnector("ClasspathVerifyConnector");
+ final String assetId = uploadDynamicClasspathAsset(connector.getId());
+
+ getClientUtil().configureConnectorWithReferences(connector.getId(),
STEP_NAME, createVerifyReferences(assetId, DYNAMIC_CLASSPATH_CLASS,
STRATEGY_CONNECTOR_METHOD));
+ getClientUtil().applyConnectorUpdate(connector);
+ getClientUtil().waitForValidConnector(connector.getId());
+
+ final List<ConfigVerificationResultDTO> results =
getClientUtil().verifyConnectorStepConfigWithReferences(
+ connector.getId(), STEP_NAME, createVerifyReferences(assetId,
DYNAMIC_CLASSPATH_CLASS, STRATEGY_CONNECTOR_METHOD));
+
+ assertSuccessfulClasspathStep(results, CONNECTOR_METHOD_STEP);
+ }
+
+ @Test
+ public void testConnectorMethodPreservesWorkingComponentState() throws
NiFiClientException, IOException, InterruptedException {
+ final ConnectorEntity connector =
getClientUtil().createConnector("ClasspathVerifyConnector");
+ final String assetId = uploadDynamicClasspathAsset(connector.getId());
+
+ getClientUtil().configureConnectorWithReferences(connector.getId(),
STEP_NAME, createVerifyReferences(assetId, DYNAMIC_CLASSPATH_CLASS,
STRATEGY_CONNECTOR_METHOD_STATE));
+
+ final List<ConfigVerificationResultDTO> results =
getClientUtil().verifyConnectorStepConfigWithReferences(
+ connector.getId(), STEP_NAME, createVerifyReferences(assetId,
DYNAMIC_CLASSPATH_CLASS, STRATEGY_CONNECTOR_METHOD_STATE));
+
+ assertSuccessfulClasspathStep(results, CONNECTOR_METHOD_STATE_STEP);
+ }
+
+ @Test
+ public void testConnectorMethodLoadsAssetWithMethodSignatureDependency()
throws NiFiClientException, IOException, InterruptedException {
+ final ConnectorEntity connector =
getClientUtil().createConnector("MethodSignatureVerifyConnector");
+ final String assetId = uploadDynamicClasspathAsset(connector.getId());
+
+ getClientUtil().configureConnectorWithReferences(connector.getId(),
METHOD_SIGNATURE_STEP_NAME, createAssetReferences(assetId));
+
+ final List<ConfigVerificationResultDTO> results =
getClientUtil().verifyConnectorStepConfigWithReferences(connector.getId(),
METHOD_SIGNATURE_STEP_NAME, createAssetReferences(assetId));
+
+ assertSuccessfulClasspathStep(results, METHOD_SIGNATURE_STEP);
+ }
+
+ @Test
+ public void testConnectorMethodLoadsAssetWhenSetAsProcessorProperty()
throws NiFiClientException, IOException, InterruptedException {
+ final ConnectorEntity connector =
getClientUtil().createConnector("MethodSignatureVerifyConnector");
+ final String assetId = uploadDynamicClasspathAsset(connector.getId());
+
+ getClientUtil().configureConnectorWithReferences(connector.getId(),
METHOD_SIGNATURE_STEP_NAME, createMethodSignatureReferences(assetId,
APPLICATION_PROCESSOR_PROPERTY));
+
+ final List<ConfigVerificationResultDTO> results =
getClientUtil().verifyConnectorStepConfigWithReferences(
+ connector.getId(), METHOD_SIGNATURE_STEP_NAME,
createMethodSignatureReferences(assetId, APPLICATION_PROCESSOR_PROPERTY));
+
+ assertSuccessfulClasspathStep(results, METHOD_SIGNATURE_STEP);
+ }
+
+ @Test
+ public void
testConnectorMethodLoadsAssetWhenSetAsControllerServiceProperty() throws
NiFiClientException, IOException, InterruptedException {
+ final ConnectorEntity connector =
getClientUtil().createConnector("MethodSignatureVerifyConnector");
+ final String assetId = uploadDynamicClasspathAsset(connector.getId());
+
+ getClientUtil().configureConnectorWithReferences(connector.getId(),
METHOD_SIGNATURE_STEP_NAME, createMethodSignatureReferences(assetId,
APPLICATION_CONTROLLER_SERVICE_PROPERTY));
+
+ final List<ConfigVerificationResultDTO> results =
getClientUtil().verifyConnectorStepConfigWithReferences(
+ connector.getId(), METHOD_SIGNATURE_STEP_NAME,
createMethodSignatureReferences(assetId,
APPLICATION_CONTROLLER_SERVICE_PROPERTY));
+
+ assertSuccessfulClasspathStep(results, METHOD_SIGNATURE_STEP);
+ }
+
+ @Test
+ public void testConnectorMethodFailsWhenClassMissingFromClasspath() throws
NiFiClientException, IOException, InterruptedException {
+ final ConnectorEntity connector =
getClientUtil().createConnector("ClasspathVerifyConnector");
+ final String assetId = uploadDynamicClasspathAsset(connector.getId());
+
+ getClientUtil().configureConnectorWithReferences(connector.getId(),
STEP_NAME, createVerifyReferences(assetId, UNKNOWN_CLASS,
STRATEGY_CONNECTOR_METHOD));
+ getClientUtil().applyConnectorUpdate(connector);
+ getClientUtil().waitForValidConnector(connector.getId());
+
+ final List<ConfigVerificationResultDTO> results =
getClientUtil().verifyConnectorStepConfigWithReferences(
+ connector.getId(), STEP_NAME, createVerifyReferences(assetId,
UNKNOWN_CLASS, STRATEGY_CONNECTOR_METHOD));
+
+ assertFailedClasspathStep(results, CONNECTOR_METHOD_STEP);
+ }
+
+ @Test
+ public void testProcessorVerifyUsesLiveClasspathWhenCommitted() throws
NiFiClientException, IOException, InterruptedException {
+ final ConnectorEntity connector =
getClientUtil().createConnector("ClasspathVerifyConnector");
+ final String assetId = uploadDynamicClasspathAsset(connector.getId());
+
+ getClientUtil().configureConnectorWithReferences(connector.getId(),
STEP_NAME, createVerifyReferences(assetId, DYNAMIC_CLASSPATH_CLASS,
STRATEGY_PROCESSOR_VERIFY));
+ getClientUtil().applyConnectorUpdate(connector);
+ getClientUtil().waitForValidConnector(connector.getId());
+
+ final List<ConfigVerificationResultDTO> results =
getClientUtil().verifyConnectorStepConfigWithReferences(
+ connector.getId(), STEP_NAME, createVerifyReferences(assetId,
DYNAMIC_CLASSPATH_CLASS, STRATEGY_PROCESSOR_VERIFY));
+
+ assertSuccessfulClasspathStep(results, LOAD_CLASS_STEP);
+ }
+
+ @Test
+ public void testProcessorVerifyFailsForUnknownClass() throws
NiFiClientException, IOException, InterruptedException {
+ final ConnectorEntity connector =
getClientUtil().createConnector("ClasspathVerifyConnector");
+ final String assetId = uploadDynamicClasspathAsset(connector.getId());
+
+ getClientUtil().configureConnectorWithReferences(connector.getId(),
STEP_NAME, createVerifyReferences(assetId, DYNAMIC_CLASSPATH_CLASS,
STRATEGY_PROCESSOR_VERIFY));
+ getClientUtil().applyConnectorUpdate(connector);
+ getClientUtil().waitForValidConnector(connector.getId());
+
+ final List<ConfigVerificationResultDTO> results =
getClientUtil().verifyConnectorStepConfigWithReferences(
+ connector.getId(), STEP_NAME, createVerifyReferences(assetId,
UNKNOWN_CLASS, STRATEGY_PROCESSOR_VERIFY));
+
+ assertFailedClasspathStep(results, LOAD_CLASS_STEP);
+ }
+
+ private String uploadDynamicClasspathAsset(final String connectorId)
throws IOException, NiFiClientException {
+ // DynamicallyLoadedType is available on the test JVM classpath
through a provided-scope dependency, which
+ // resolves to the module's built jar without adding the type to the
NiFi server runtime assembly. Uploading that
+ // jar as the Connector asset makes the type available to the running
component only through its dynamic classpath.
+ final File dynamicClasspathJar = locateDynamicClasspathJar();
+
+ final File assetCopy = new File("target/dynamic-classpath-" +
connectorId + ".jar");
+ Files.copy(dynamicClasspathJar.toPath(), assetCopy.toPath(),
StandardCopyOption.REPLACE_EXISTING);
+
+ final AssetEntity assetEntity =
getNifiClient().getConnectorClient().createAsset(connectorId,
assetCopy.getName(), assetCopy);
+ assertNotNull(assetEntity);
+ assertNotNull(assetEntity.getAsset());
+ return assetEntity.getAsset().getId();
+ }
+
+ private File locateDynamicClasspathJar() {
+ final URL location =
DynamicallyLoadedType.class.getProtectionDomain().getCodeSource().getLocation();
+ final File jarFile;
+ try {
+ jarFile = new File(location.toURI());
+ } catch (final URISyntaxException e) {
+ throw new IllegalStateException("Could not resolve the dynamic
classpath jar from location " + location, e);
+ }
+
+ if (!jarFile.isFile() || !jarFile.getName().endsWith(".jar")) {
+ throw new IllegalStateException("Expected the
nifi-system-test-dynamic-classpath dependency to resolve to a jar file but
found "
+ + jarFile.getAbsolutePath() + "; ensure the module is
installed so the provided-scope dependency resolves to a jar");
+ }
+
+ return jarFile;
+ }
+
+ private Map<String, ConnectorValueReferenceDTO>
createVerifyReferences(final String assetId, final String classToLoad, final
String strategy) {
+ final Map<String, ConnectorValueReferenceDTO> propertyValues = new
HashMap<>();
+ propertyValues.put(CLASSPATH_RESOURCE, createAssetReference(assetId));
+ propertyValues.put(CLASS_TO_LOAD,
createStringLiteralReference(classToLoad));
+ propertyValues.put(VERIFICATION_STRATEGY,
createStringLiteralReference(strategy));
+ return propertyValues;
+ }
+
+ private Map<String, ConnectorValueReferenceDTO>
createAssetReferences(final String assetId) {
+ final Map<String, ConnectorValueReferenceDTO> propertyValues = new
HashMap<>();
+ propertyValues.put(CLASSPATH_RESOURCE, createAssetReference(assetId));
+ return propertyValues;
+ }
+
+ private Map<String, ConnectorValueReferenceDTO>
createMethodSignatureReferences(final String assetId, final String
classpathApplication) {
+ final Map<String, ConnectorValueReferenceDTO> propertyValues =
createAssetReferences(assetId);
+ propertyValues.put(CLASSPATH_APPLICATION,
createStringLiteralReference(classpathApplication));
+ return propertyValues;
+ }
+
+ private ConnectorValueReferenceDTO createAssetReference(final String
assetId) {
+ final ConnectorValueReferenceDTO assetReference = new
ConnectorValueReferenceDTO();
+ assetReference.setValueType("ASSET_REFERENCE");
+ assetReference.setAssetReferences(List.of(new
AssetReferenceDTO(assetId)));
+ return assetReference;
+ }
+
+ private ConnectorValueReferenceDTO createStringLiteralReference(final
String value) {
+ final ConnectorValueReferenceDTO valueReference = new
ConnectorValueReferenceDTO();
+ valueReference.setValueType("STRING_LITERAL");
+ valueReference.setValue(value);
+ return valueReference;
+ }
+
+ private void assertSuccessfulClasspathStep(final
List<ConfigVerificationResultDTO> results, final String stepName) {
+ assertNotNull(results);
+ final ConfigVerificationResultDTO classpathResult = findStep(results,
stepName);
+ assertEquals(Outcome.SUCCESSFUL.name(), classpathResult.getOutcome(),
classpathResult.getExplanation());
+ }
+
+ private void assertFailedClasspathStep(final
List<ConfigVerificationResultDTO> results, final String stepName) {
+ assertNotNull(results);
+ final ConfigVerificationResultDTO classpathResult = findStep(results,
stepName);
+ assertEquals(Outcome.FAILED.name(), classpathResult.getOutcome(),
classpathResult.getExplanation());
+ }
+
+ private ConfigVerificationResultDTO findStep(final
List<ConfigVerificationResultDTO> results, final String stepName) {
+ return results.stream()
+ .filter(result ->
stepName.equals(result.getVerificationStepName()))
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("Expected verification
step '" + stepName + "' in results: " + results.stream()
+
.map(ConfigVerificationResultDTO::getVerificationStepName)
+ .toList()));
+ }
+}