This is an automated email from the ASF dual-hosted git repository.

exceptionfactory 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 868a6c84d4 NIFI-14682 Improved JDBC Driver error message in Connection 
Pool Services (#10039)
868a6c84d4 is described below

commit 868a6c84d4e378dba6eed0bb52b5249c8de4bb28
Author: Pierre Villard <[email protected]>
AuthorDate: Fri Aug 22 00:01:25 2025 +0200

    NIFI-14682 Improved JDBC Driver error message in Connection Pool Services 
(#10039)
    
    Signed-off-by: David Handermann <[email protected]>
---
 .../nifi-extension-utils/nifi-dbcp-base/pom.xml    |   5 +
 .../nifi/dbcp/AbstractDBCPConnectionPool.java      |  30 ++-
 .../nifi-extension-utils/nifi-dbcp-utils/pom.xml   |  26 +++
 .../org/apache/nifi/dbcp/utils/DriverUtils.java    | 209 +++++++++++++++++++++
 .../nifi-extension-utils/pom.xml                   |   1 +
 .../nifi-dbcp-service/pom.xml                      |   6 +-
 .../org/apache/nifi/dbcp/DBCPConnectionPool.java   |  32 +++-
 .../java/org/apache/nifi/dbcp/DBCPServiceTest.java |  41 ++++
 .../nifi-hikari-dbcp-service/pom.xml               |   5 +
 .../apache/nifi/dbcp/HikariCPConnectionPool.java   |  28 ++-
 10 files changed, 369 insertions(+), 14 deletions(-)

diff --git a/nifi-extension-bundles/nifi-extension-utils/nifi-dbcp-base/pom.xml 
b/nifi-extension-bundles/nifi-extension-utils/nifi-dbcp-base/pom.xml
index f57d2235d8..0d8fc61c40 100644
--- a/nifi-extension-bundles/nifi-extension-utils/nifi-dbcp-base/pom.xml
+++ b/nifi-extension-bundles/nifi-extension-utils/nifi-dbcp-base/pom.xml
@@ -34,6 +34,11 @@
             <artifactId>nifi-dbcp-service-api</artifactId>
             <version>2.6.0-SNAPSHOT</version>
         </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-dbcp-utils</artifactId>
+            <version>2.6.0-SNAPSHOT</version>
+        </dependency>
         <dependency>
             <groupId>org.apache.nifi</groupId>
             <artifactId>nifi-security-kerberos</artifactId>
diff --git 
a/nifi-extension-bundles/nifi-extension-utils/nifi-dbcp-base/src/main/java/org/apache/nifi/dbcp/AbstractDBCPConnectionPool.java
 
b/nifi-extension-bundles/nifi-extension-utils/nifi-dbcp-base/src/main/java/org/apache/nifi/dbcp/AbstractDBCPConnectionPool.java
index e3e924e2ff..babdb9b1de 100644
--- 
a/nifi-extension-bundles/nifi-extension-utils/nifi-dbcp-base/src/main/java/org/apache/nifi/dbcp/AbstractDBCPConnectionPool.java
+++ 
b/nifi-extension-bundles/nifi-extension-utils/nifi-dbcp-base/src/main/java/org/apache/nifi/dbcp/AbstractDBCPConnectionPool.java
@@ -31,10 +31,12 @@ import org.apache.nifi.annotation.lifecycle.OnEnabled;
 import org.apache.nifi.components.ConfigVerificationResult;
 import org.apache.nifi.components.PropertyDescriptor;
 import org.apache.nifi.components.PropertyValue;
+import org.apache.nifi.components.resource.ResourceReferences;
 import org.apache.nifi.controller.AbstractControllerService;
 import org.apache.nifi.controller.ConfigurationContext;
 import org.apache.nifi.controller.VerifiableControllerService;
 import org.apache.nifi.dbcp.utils.DataSourceConfiguration;
+import org.apache.nifi.dbcp.utils.DriverUtils;
 import org.apache.nifi.kerberos.KerberosUserService;
 import org.apache.nifi.logging.ComponentLog;
 import org.apache.nifi.processor.exception.ProcessException;
@@ -45,6 +47,7 @@ import org.apache.nifi.security.krb.KerberosUser;
 
 import static 
org.apache.nifi.components.ConfigVerificationResult.Outcome.FAILED;
 import static 
org.apache.nifi.components.ConfigVerificationResult.Outcome.SUCCESSFUL;
+import static org.apache.nifi.dbcp.utils.DBCPProperties.DB_DRIVERNAME;
 import static org.apache.nifi.dbcp.utils.DBCPProperties.DB_DRIVER_LOCATION;
 import static org.apache.nifi.dbcp.utils.DBCPProperties.KERBEROS_USER_SERVICE;
 
@@ -58,6 +61,7 @@ public abstract class AbstractDBCPConnectionPool extends 
AbstractControllerServi
         List<ConfigVerificationResult> results = new ArrayList<>();
 
         KerberosUser kerberosUser = null;
+
         try {
             kerberosUser = getKerberosUser(context);
             if (kerberosUser != null) {
@@ -101,16 +105,30 @@ public abstract class AbstractDBCPConnectionPool extends 
AbstractControllerServi
                         .build());
             }
         } catch (final Exception e) {
-            String message = "Failed to configure Data Source.";
-            if (e.getCause() instanceof ClassNotFoundException) {
-                message += String.format("  Ensure changes to the '%s' 
property are applied before verifying",
-                        DB_DRIVER_LOCATION.getDisplayName());
+            StringBuilder messageBuilder = new StringBuilder("Failed to 
configure Data Source.");
+            verificationLogger.error(messageBuilder.toString(), e);
+
+            final String driverName = 
context.getProperty(DB_DRIVERNAME).evaluateAttributeExpressions().getValue();
+            final ResourceReferences driverResources = 
context.getProperty(DB_DRIVER_LOCATION).evaluateAttributeExpressions().asResources();
+
+            if (StringUtils.isNotBlank(driverName) && 
driverResources.getCount() != 0) {
+                List<String> availableDrivers = 
DriverUtils.findDriverClassNames(driverResources);
+                if (!availableDrivers.isEmpty() && 
!availableDrivers.contains(driverName)) {
+                    messageBuilder.append(String.format(" Driver class [%s] 
not found in provided resources. Available driver classes found: %s",
+                            driverName, String.join(", ", availableDrivers)));
+                } else if (e.getCause() instanceof ClassNotFoundException && 
availableDrivers.contains(driverName)) {
+                    messageBuilder.append(" Driver Class found but not loaded: 
Apply configuration before verifying.");
+                } else {
+                    messageBuilder.append(String.format(" Exception: %s", 
e.getMessage()));
+                }
+            } else {
+                messageBuilder.append(String.format(" No driver name specified 
or no driver resources provided. Exception: %s", e.getMessage()));
             }
-            verificationLogger.error(message, e);
+
             results.add(new ConfigVerificationResult.Builder()
                     .verificationStepName("Configure Data Source")
                     .outcome(FAILED)
-                    .explanation(message + ": " + e.getMessage())
+                    .explanation(messageBuilder.toString())
                     .build());
         } finally {
             try {
diff --git 
a/nifi-extension-bundles/nifi-extension-utils/nifi-dbcp-utils/pom.xml 
b/nifi-extension-bundles/nifi-extension-utils/nifi-dbcp-utils/pom.xml
new file mode 100644
index 0000000000..68bfdb2c7a
--- /dev/null
+++ b/nifi-extension-bundles/nifi-extension-utils/nifi-dbcp-utils/pom.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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>
+        <artifactId>nifi-extension-utils</artifactId>
+        <groupId>org.apache.nifi</groupId>
+        <version>2.6.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>nifi-dbcp-utils</artifactId>
+
+</project>
diff --git 
a/nifi-extension-bundles/nifi-extension-utils/nifi-dbcp-utils/src/main/java/org/apache/nifi/dbcp/utils/DriverUtils.java
 
b/nifi-extension-bundles/nifi-extension-utils/nifi-dbcp-utils/src/main/java/org/apache/nifi/dbcp/utils/DriverUtils.java
new file mode 100644
index 0000000000..68834e3197
--- /dev/null
+++ 
b/nifi-extension-bundles/nifi-extension-utils/nifi-dbcp-utils/src/main/java/org/apache/nifi/dbcp/utils/DriverUtils.java
@@ -0,0 +1,209 @@
+/*
+ * 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.dbcp.utils;
+
+import org.apache.nifi.components.resource.ResourceReference;
+import org.apache.nifi.components.resource.ResourceReferences;
+import org.apache.nifi.components.resource.ResourceType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.sql.Driver;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Scanner;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.regex.Pattern;
+
+public class DriverUtils {
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(DriverUtils.class);
+    private static final String DRIVER_CLASS_PATTERN = "(?i).*(driver|jdbc).*";
+    private static final Pattern DRIVER_PATTERN = 
Pattern.compile(DRIVER_CLASS_PATTERN);
+
+    /**
+     * Discovers JDBC driver classes using static JAR scanning only (no class
+     * loading) This is used during validation when resources aren't loaded 
into
+     * classpath yet
+     */
+    public static List<String> findDriverClassNames(final ResourceReferences 
driverResources) {
+        final Set<String> driverClasses = new TreeSet<>();
+
+        if (driverResources == null || driverResources.getCount() == 0) {
+            LOGGER.debug("No driver resources provided for static discovery");
+            return new ArrayList<>();
+        }
+        LOGGER.debug("Starting static driver discovery for {} resources", 
driverResources.getCount());
+
+        for (final ResourceReference resource : 
driverResources.flattenRecursively().asList()) {
+            try {
+                LOGGER.debug("Processing resource: {} (type: {})", 
resource.getLocation(), resource.getResourceType());
+                final List<File> jarFiles = getJarFilesFromResource(resource);
+                LOGGER.debug("Found {} JAR files in resource {}", 
jarFiles.size(), resource.getLocation());
+                for (final File jarFile : jarFiles) {
+                    LOGGER.debug("Scanning JAR file: {}", 
jarFile.getAbsolutePath());
+                    final Set<String> foundDrivers = 
scanJarForDriverClassesUsingMetadata(jarFile);
+                    LOGGER.debug("Found {} potential driver classes in {}", 
foundDrivers.size(), jarFile.getName());
+                    driverClasses.addAll(foundDrivers);
+                }
+            } catch (final Exception e) {
+                LOGGER.warn("Error processing resource {} for static driver 
discovery", resource.getLocation(), e);
+            }
+        }
+
+        LOGGER.debug("Static driver discovery completed. Found {} potential 
driver classes", driverClasses.size());
+        return new ArrayList<>(driverClasses);
+    }
+
+    /**
+     * Scans a JAR file for potential JDBC driver class names using static 
analysis
+     * only This uses improved heuristics including checking META-INF/services 
and
+     * common patterns
+     */
+    private static Set<String> scanJarForDriverClassesUsingMetadata(final File 
jarFile) {
+        final Set<String> driverClasses = new TreeSet<>();
+        try (final JarFile jar = new JarFile(jarFile)) {
+            // Check META-INF/services/java.sql.Driver for registered drivers
+            // This is the most reliable method
+            final JarEntry servicesEntry = 
jar.getJarEntry("META-INF/services/java.sql.Driver");
+            if (servicesEntry != null) {
+                try (Scanner scanner = new 
Scanner(jar.getInputStream(servicesEntry))) {
+                    while (scanner.hasNextLine()) {
+                        final String line = scanner.nextLine().trim();
+                        if (!line.isEmpty() && !line.startsWith("#")) {
+                            driverClasses.add(line);
+                            LOGGER.debug("Found driver in META-INF/services: 
{}", line);
+                        }
+                    }
+                }
+            }
+            return driverClasses;
+        } catch (final Exception e) {
+            LOGGER.warn("Error scanning JAR file {} for driver classes", 
jarFile.getAbsolutePath(), e);
+        }
+        return driverClasses;
+    }
+
+    /**
+     * Gets JAR files from a ResourceReference for scanning. It is expected 
that the
+     * submitted resource is coming from the listing of
+     * driverResources.flattenRecursively().asList() so that this method does 
not
+     * need to be recursive
+     */
+    private static List<File> getJarFilesFromResource(final ResourceReference 
resource) {
+        final List<File> jarFiles = new ArrayList<>();
+        if (resource.getResourceType() == ResourceType.FILE) {
+            final File file = new File(resource.getLocation());
+            if (file.exists() && file.canRead() && 
file.getName().toLowerCase().endsWith(".jar")) {
+                jarFiles.add(file);
+            }
+        }
+        // we don't need to scan for directory since we already did a 
recursive flatten
+        // and we don't want to deal with URLs in this case
+        return jarFiles;
+    }
+
+    /**
+     * Discovers all potential JDBC driver classes in the provided resources 
Since
+     * dynamicallyModifiesClasspath=true, we scan the resources directly 
rather than
+     * trying to load classes, as NiFi handles classpath modification
+     */
+    public static List<String> discoverDriverClasses(final ResourceReferences 
driverResources) {
+        final Set<String> driverClasses = new TreeSet<>(); // Use TreeSet for 
sorted results
+
+        if (driverResources == null || driverResources.getCount() == 0) {
+            return new ArrayList<>();
+        }
+
+        for (final ResourceReference resource : 
driverResources.flattenRecursively().asList()) {
+            try {
+                final List<File> jarFiles = 
DriverUtils.getJarFilesFromResource(resource);
+                for (final File jarFile : jarFiles) {
+                    driverClasses.addAll(scanJarForDriverClasses(jarFile));
+                }
+            } catch (final Exception e) {
+                LOGGER.warn("Error processing resource {} for driver 
discovery", resource.getLocation(), e);
+            }
+        }
+
+        return new ArrayList<>(driverClasses);
+    }
+
+    /**
+     * Scans a JAR file for potential JDBC driver class names (without loading 
them)
+     */
+    private static Set<String> scanJarForDriverClasses(final File jarFile) {
+        final Set<String> actualDriverClasses = new TreeSet<>();
+
+        try (JarFile jar = new JarFile(jarFile)) {
+            final Enumeration<JarEntry> entries = jar.entries();
+            while (entries.hasMoreElements()) {
+                final JarEntry entry = entries.nextElement();
+                final String entryName = entry.getName();
+
+                // Look for .class files (exclude inner classes)
+                if (entryName.endsWith(".class") && !entryName.contains("$")) {
+                    String className = entryName.substring(0, 
entryName.length() - 6).replace('/', '.');
+
+                    // First filter with heuristics to avoid loading every 
class
+                    if (isPotentialDriverClass(className)) {
+                        // Now actually verify it's a JDBC driver
+                        if (isActualDriverClass(className)) {
+                            actualDriverClasses.add(className);
+                        }
+                    }
+                }
+            }
+        } catch (final Exception e) {
+            LOGGER.warn("Error scanning JAR file {} for driver classes", 
jarFile.getAbsolutePath(), e);
+        }
+
+        return actualDriverClasses;
+    }
+
+    /**
+     * Uses heuristics to identify potential JDBC driver classes without 
loading
+     * them This is a first-pass filter to avoid loading every single class
+     */
+    private static boolean isPotentialDriverClass(final String className) {
+        return DRIVER_PATTERN.matcher(className).matches();
+    }
+
+    /**
+     * Actually loads and checks if a class implements java.sql.Driver Since
+     * dynamicallyModifiesClasspath=true, the class should be available
+     */
+    private static boolean isActualDriverClass(final String className) {
+        try {
+            final Class<?> clazz = Class.forName(className);
+
+            // Check if it implements Driver interface and is not 
abstract/interface
+            return Driver.class.isAssignableFrom(clazz)
+                    && !clazz.isInterface()
+                    && 
!java.lang.reflect.Modifier.isAbstract(clazz.getModifiers());
+        } catch (final Throwable e) {
+            // Class couldn't be loaded or has issues - not a valid driver
+            return false;
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/nifi-extension-bundles/nifi-extension-utils/pom.xml 
b/nifi-extension-bundles/nifi-extension-utils/pom.xml
index 327f0a95c0..95399087a9 100644
--- a/nifi-extension-bundles/nifi-extension-utils/pom.xml
+++ b/nifi-extension-bundles/nifi-extension-utils/pom.xml
@@ -32,6 +32,7 @@
         <module>nifi-database-utils</module>
         <module>nifi-database-test-utils</module>
         <module>nifi-dbcp-base</module>
+        <module>nifi-dbcp-utils</module>
         <module>nifi-event-listen</module>
         <module>nifi-event-put</module>
         <module>nifi-event-transport</module>
diff --git 
a/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/pom.xml
 
b/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/pom.xml
index 38ad2df65a..7286d64b65 100644
--- 
a/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/pom.xml
+++ 
b/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/pom.xml
@@ -35,7 +35,11 @@
             <artifactId>nifi-dbcp-base</artifactId>
             <version>2.6.0-SNAPSHOT</version>
         </dependency>
-
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-dbcp-utils</artifactId>
+            <version>2.6.0-SNAPSHOT</version>
+        </dependency>
         <dependency>
             <groupId>org.apache.nifi</groupId>
             <artifactId>nifi-service-utils</artifactId>
diff --git 
a/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/src/main/java/org/apache/nifi/dbcp/DBCPConnectionPool.java
 
b/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/src/main/java/org/apache/nifi/dbcp/DBCPConnectionPool.java
index ee430e8621..28fef941f3 100644
--- 
a/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/src/main/java/org/apache/nifi/dbcp/DBCPConnectionPool.java
+++ 
b/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/src/main/java/org/apache/nifi/dbcp/DBCPConnectionPool.java
@@ -37,9 +37,13 @@ import org.apache.nifi.annotation.documentation.Tags;
 import org.apache.nifi.components.PropertyDescriptor;
 import org.apache.nifi.components.PropertyValue;
 import org.apache.nifi.components.RequiredPermission;
+import org.apache.nifi.components.resource.ResourceReference;
+import org.apache.nifi.components.resource.ResourceReferences;
+import org.apache.nifi.components.resource.ResourceType;
 import org.apache.nifi.controller.ConfigurationContext;
 import org.apache.nifi.controller.VerifiableControllerService;
 import org.apache.nifi.dbcp.utils.DataSourceConfiguration;
+import org.apache.nifi.dbcp.utils.DriverUtils;
 import org.apache.nifi.expression.AttributeExpression;
 import org.apache.nifi.expression.ExpressionLanguageScope;
 import org.apache.nifi.migration.PropertyConfiguration;
@@ -203,7 +207,33 @@ public class DBCPConnectionPool extends 
AbstractDBCPConnectionPool implements DB
         try {
             clazz = Class.forName(driverName);
         } catch (final ClassNotFoundException e) {
-            throw new ProcessException("Driver class " + driverName + " is not 
found", e);
+            // Enhanced error message with discovery
+            ResourceReferences driverResources = null;
+            try {
+                // Try to get driver resources from current context if 
available
+                if (getConfigurationContext() != null) {
+                    driverResources = 
getConfigurationContext().getProperty(DB_DRIVER_LOCATION).evaluateAttributeExpressions().asResources();
+                }
+            } catch (Exception ignored) {
+                // Context might not be available, continue without it
+            }
+
+            final List<String> availableDrivers = (driverResources != null && 
driverResources.getCount() != 0) ? 
DriverUtils.discoverDriverClasses(driverResources) : List.of();
+
+            StringBuilder errorMessage = new StringBuilder("JDBC driver class 
'%s' not found.".formatted(driverName));
+
+            if (!availableDrivers.isEmpty()) {
+                errorMessage.append(" Available driver classes found in 
resources: %s.".formatted(String.join(", ", availableDrivers)));
+            } else if (driverResources != null && driverResources.getCount() 
!= 0) {
+                final List<ResourceReference> resourcesList = 
driverResources.asList();
+                if (resourcesList.stream().filter(r -> r.getResourceType() != 
ResourceType.URL).count() != 0) {
+                    errorMessage.append(" No JDBC driver classes found in the 
provided resources.");
+                }
+            } else if (driverResources == null) {
+                errorMessage.append(" The property 'Database Driver 
Location(s)' should be set.");
+            }
+
+            throw new ProcessException(errorMessage.toString(), e);
         }
 
         try {
diff --git 
a/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/src/test/java/org/apache/nifi/dbcp/DBCPServiceTest.java
 
b/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/src/test/java/org/apache/nifi/dbcp/DBCPServiceTest.java
index 490dba71bd..3c6e976ea3 100644
--- 
a/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/src/test/java/org/apache/nifi/dbcp/DBCPServiceTest.java
+++ 
b/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/src/test/java/org/apache/nifi/dbcp/DBCPServiceTest.java
@@ -18,13 +18,19 @@ package org.apache.nifi.dbcp;
 
 import java.io.File;
 import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
 import java.nio.file.Paths;
 import java.sql.Connection;
 import java.sql.DriverManager;
 import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.Statement;
+import java.util.Collection;
+import java.util.Map;
 import java.util.UUID;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ValidationResult;
 import org.apache.nifi.dbcp.utils.DBCPProperties;
 import org.apache.nifi.processor.exception.ProcessException;
 import org.apache.nifi.reporting.InitializationException;
@@ -205,6 +211,41 @@ public class DBCPServiceTest {
         service.getDataSource().close();
     }
 
+    @Test
+    public void testInvalidDriverDerby() throws URISyntaxException {
+        final URL driverURL = 
org.apache.derby.client.ClientAutoloadedDriver.class
+                .getProtectionDomain()
+                .getCodeSource()
+                .getLocation();
+
+        final File jarFile = new File(driverURL.toURI());
+
+        runner.setProperty(service, DBCPProperties.DB_DRIVER_LOCATION, 
jarFile.getAbsolutePath());
+        runner.setProperty(service, DBCPProperties.DB_DRIVERNAME, 
"a.very.bad.jdbc.Driver");
+
+        final Collection<ConfigVerificationResult> verificationResults = 
runner.verify(service, Map.of());
+        assertEquals(1, verificationResults.size());
+
+        final ConfigVerificationResult result = 
verificationResults.stream().filter(r -> 
r.getVerificationStepName().equals("Configure Data Source")).findFirst().get();
+        
assertTrue(result.getExplanation().contains("org.apache.derby.client.ClientAutoloadedDriver"));
+    }
+
+    @Test
+    public void testInvalidDriverNoResource() {
+        runner.setProperty(service, DBCPProperties.DB_DRIVERNAME, 
"a.very.bad.jdbc.Driver");
+
+        final Collection<ValidationResult> validationResults = 
runner.validate(service);
+        assertEquals(0, validationResults.size());
+
+        AssertionFailedError thrown = assertThrows(AssertionFailedError.class, 
() -> {
+            runner.enableControllerService(service);
+        });
+
+        // Verify the underlying cause is what we expect
+        assertTrue(thrown.getMessage().contains("ProcessException"));
+        assertTrue(thrown.getMessage().contains("JDBC driver class 
'a.very.bad.jdbc.Driver' not found. The property 'Database Driver Location(s)' 
should be set."));
+    }
+
     private void assertConnectionNotNullDynamicProperty(final String 
propertyName, final String propertyValue) throws SQLException {
         runner.setProperty(service, propertyName, propertyValue);
 
diff --git 
a/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-hikari-dbcp-service/pom.xml
 
b/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-hikari-dbcp-service/pom.xml
index 56a4ac976b..f9ce52f864 100644
--- 
a/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-hikari-dbcp-service/pom.xml
+++ 
b/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-hikari-dbcp-service/pom.xml
@@ -48,5 +48,10 @@
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-lang3</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-dbcp-utils</artifactId>
+            <version>2.6.0-SNAPSHOT</version>
+        </dependency>
     </dependencies>
 </project>
diff --git 
a/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-hikari-dbcp-service/src/main/java/org/apache/nifi/dbcp/HikariCPConnectionPool.java
 
b/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-hikari-dbcp-service/src/main/java/org/apache/nifi/dbcp/HikariCPConnectionPool.java
index 6c7ec2312e..58e65c7aff 100644
--- 
a/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-hikari-dbcp-service/src/main/java/org/apache/nifi/dbcp/HikariCPConnectionPool.java
+++ 
b/nifi-extension-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-hikari-dbcp-service/src/main/java/org/apache/nifi/dbcp/HikariCPConnectionPool.java
@@ -32,10 +32,12 @@ import org.apache.nifi.components.PropertyDescriptor;
 import org.apache.nifi.components.PropertyValue;
 import org.apache.nifi.components.RequiredPermission;
 import org.apache.nifi.components.resource.ResourceCardinality;
+import org.apache.nifi.components.resource.ResourceReferences;
 import org.apache.nifi.components.resource.ResourceType;
 import org.apache.nifi.controller.AbstractControllerService;
 import org.apache.nifi.controller.ConfigurationContext;
 import org.apache.nifi.controller.VerifiableControllerService;
+import org.apache.nifi.dbcp.utils.DriverUtils;
 import org.apache.nifi.expression.AttributeExpression;
 import org.apache.nifi.expression.ExpressionLanguageScope;
 import org.apache.nifi.kerberos.KerberosUserService;
@@ -47,6 +49,7 @@ import org.apache.nifi.security.krb.KerberosLoginException;
 import org.apache.nifi.security.krb.KerberosUser;
 
 import javax.security.auth.login.LoginException;
+
 import java.sql.Connection;
 import java.sql.SQLException;
 import java.util.ArrayList;
@@ -361,16 +364,29 @@ public class HikariCPConnectionPool extends 
AbstractControllerService implements
                         .build());
             }
         } catch (final Exception e) {
-            String message = "Failed to configure Data Source.";
-            if (e.getCause() instanceof ClassNotFoundException) {
-                message += String.format("  Ensure changes to the '%s' 
property are applied before verifying",
-                        DB_DRIVER_LOCATION.getDisplayName());
+            StringBuilder messageBuilder = new StringBuilder("Failed to 
configure Data Source.");
+            verificationLogger.error(messageBuilder.toString(), e);
+
+            final String driverName = 
context.getProperty(DB_DRIVERNAME).evaluateAttributeExpressions().getValue();
+            final ResourceReferences driverResources = 
context.getProperty(DB_DRIVER_LOCATION).evaluateAttributeExpressions().asResources();
+
+            if (StringUtils.isNotBlank(driverName) && 
driverResources.getCount() != 0) {
+                List<String> availableDrivers = 
DriverUtils.findDriverClassNames(driverResources);
+                if (!availableDrivers.isEmpty() && 
!availableDrivers.contains(driverName)) {
+                    messageBuilder.append(" Driver class [%s] not found in 
provided resources. Available driver classes found: %s".formatted(driverName, 
String.join(", ", availableDrivers)));
+                } else if (e.getCause() instanceof ClassNotFoundException && 
availableDrivers.contains(driverName)) {
+                    messageBuilder.append(" Driver Class found but not loaded: 
Apply configuration before verifying.");
+                } else {
+                    messageBuilder.append(" Exception: 
%s".formatted(e.getMessage()));
+                }
+            } else {
+                messageBuilder.append(" No driver name specified or no driver 
resources provided. Exception: %s".formatted(e.getMessage()));
             }
-            verificationLogger.error(message, e);
+
             results.add(new ConfigVerificationResult.Builder()
                     .verificationStepName("Configure Data Source")
                     .outcome(FAILED)
-                    .explanation(message + ": " + e.getMessage())
+                    .explanation(messageBuilder.toString())
                     .build());
         } finally {
             try {

Reply via email to