diff --git a/fineract-provider/dependencies.gradle b/fineract-provider/dependencies.gradle
index 0a24ad13b0e..79a77d68b7e 100644
--- a/fineract-provider/dependencies.gradle
+++ b/fineract-provider/dependencies.gradle
@@ -86,8 +86,6 @@ dependencies {
// Although fineract (at the time of writing) doesn't have any compile time dep. on this,
// it's useful to have this for the Spring Boot TestRestTemplate http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#boot-features-rest-templates-test-utility
[group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.3.5'],
- // Once we've switched to Java 8 this dep can be removed.
- //[group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.0']
[group: 'io.swagger', name: 'swagger-jersey-jaxrs', version: '1.5.15'],
[group: 'org.springframework', name:'spring-jms', version: '4.0.7.RELEASE'],
@@ -100,9 +98,9 @@ dependencies {
testCompile 'junit:junit:4.11',
'junit:junit-dep:4.11',
'org.mockito:mockito-core:1.9.5',
+ 'io.github.classgraph:classgraph:4.8.43',
'com.jayway.restassured:rest-assured:2.3.3',
[group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: springBootVersion],
[group: 'com.mockrunner', name: 'mockrunner-jms', version: '1.0.6'],
[group: 'com.mockrunner', name: 'mockrunner-jdbc', version: '1.0.6']
-
}
diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/classdupes/ClasspathHellDuplicatesCheckRule.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/classdupes/ClasspathHellDuplicatesCheckRule.java
new file mode 100644
index 00000000000..62efb3e7efc
--- /dev/null
+++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/classdupes/ClasspathHellDuplicatesCheckRule.java
@@ -0,0 +1,67 @@
+/**
+ * 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.fineract.infrastructure.classdupes;
+
+import java.util.List;
+import java.util.Map;
+import junit.framework.AssertionFailedError;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/**
+ * JUnit Rule to run detect duplicate entries on the classpath. Usage:
+ *
+ *
public static {@literal @}ClassRule ClasspathHellDuplicatesCheckRule
+ * dupes = new ClasspathHellDuplicatesCheckRule();
+ *
+ * NB that the basepom/duplicate-finder-maven-plugin already runs as part of odlparent.
+ * It has a similar purpose, but covers build time instead of runtime testing. This JUnit Rule class is
+ * thus recommended to be used in particular in tests which previously ran into JAR Hell issues, and for
+ * which non-regression with a clear failure message in case of future similar problems is important.
+ * (This provides more details at runtime than duplicate-finder-maven-plugin does at build time.)
+ *
+ * @author Michael Vorburger.ch
+ */
+public class ClasspathHellDuplicatesCheckRule implements TestRule {
+
+ private final ClasspathHellDuplicatesChecker checker;
+
+ public ClasspathHellDuplicatesCheckRule(ClasspathHellDuplicatesChecker checker) {
+ this.checker = checker;
+ }
+
+ public ClasspathHellDuplicatesCheckRule() {
+ this(ClasspathHellDuplicatesChecker.INSTANCE);
+ }
+
+ @Override
+ public Statement apply(Statement base, Description description) {
+ checkClasspath();
+ return base;
+ }
+
+ protected void checkClasspath() {
+ Map> dupes = checker.getDuplicates();
+ if (!dupes.isEmpty()) {
+ throw new AssertionFailedError("Classpath duplicates detected: " + checker.toString(dupes));
+ }
+ }
+}
+
diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/classdupes/ClasspathHellDuplicatesCheckRuleTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/classdupes/ClasspathHellDuplicatesCheckRuleTest.java
new file mode 100644
index 00000000000..48639f34d01
--- /dev/null
+++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/classdupes/ClasspathHellDuplicatesCheckRuleTest.java
@@ -0,0 +1,35 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.classdupes;
+
+import org.junit.ClassRule;
+import org.junit.Test;
+
+/**
+ * {@link ClasspathHellDuplicatesCheckRule} test.
+ *
+ * @author Michael Vorburger
+ */
+public class ClasspathHellDuplicatesCheckRuleTest {
+
+ @ClassRule public static ClasspathHellDuplicatesCheckRule jHades = new ClasspathHellDuplicatesCheckRule();
+
+ @Test // we just need this because JUnit doesn't like a *Test class with only a Rule
+ public void testAnything() throws Exception { }
+}
diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/classdupes/ClasspathHellDuplicatesChecker.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/classdupes/ClasspathHellDuplicatesChecker.java
new file mode 100644
index 00000000000..ed7d9c26f23
--- /dev/null
+++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/classdupes/ClasspathHellDuplicatesChecker.java
@@ -0,0 +1,143 @@
+/**
+ * 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.fineract.infrastructure.classdupes;
+
+import io.github.classgraph.ClassGraph;
+import io.github.classgraph.ResourceList;
+import io.github.classgraph.ScanResult;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.stream.Collectors;
+
+/**
+ * Check classpath for duplicates.
+ *
+ * @author Michael Vorburger.ch
+ */
+public class ClasspathHellDuplicatesChecker {
+
+ public static final ClasspathHellDuplicatesChecker INSTANCE = new ClasspathHellDuplicatesChecker();
+
+ private final Map> duplicates;
+
+ public ClasspathHellDuplicatesChecker() {
+ duplicates = recheck();
+ }
+
+ public Map> getDuplicates() {
+ return duplicates;
+ }
+
+ public String toString(Map> map) {
+ StringBuilder sb = new StringBuilder();
+ for (Entry> entry : map.entrySet()) {
+ sb.append(entry.getKey());
+ sb.append('\n');
+ for (String location : entry.getValue()) {
+ sb.append(" ");
+ sb.append(location);
+ sb.append('\n');
+ }
+ }
+ return sb.toString();
+ }
+
+ private Map> recheck() {
+ Map> dupes = new HashMap<>();
+ // To debug this scanner, use ClassGraph().verbose()
+ // We intentionally do not use .classFilesOnly(), or .nonClassFilesOnly(), to check both
+ try (ScanResult scanResult = new ClassGraph().scan()) {
+ for (Entry dupe : scanResult.getAllResources().findDuplicatePaths()) {
+ String path = dupe.getKey();
+ if (!isHarmlessDuplicate(path)) {
+ dupes.put(path, dupe.getValue().stream()
+ .map(resource -> resource.getURL().toExternalForm()).collect(Collectors.toList()));
+ }
+ }
+ return dupes;
+ }
+ }
+
+ protected boolean isHarmlessDuplicate(String resourcePath) {
+ // list from org.jhades.reports.DuplicatesReport
+ return resourcePath.equals("META-INF/MANIFEST.MF")
+ || resourcePath.equals("META-INF/INDEX.LIST")
+ || resourcePath.equals("META-INF/ORACLE_J.SF")
+ || resourcePath.toUpperCase().startsWith("META-INF/NOTICE")
+ || resourcePath.toUpperCase().startsWith("META-INF/LICENSE")
+ || resourcePath.toUpperCase().startsWith("LICENSE")
+ || resourcePath.toUpperCase().startsWith("LICENSE/NOTICE")
+ // list formerly in ClasspathHellDuplicatesCheckRule (moved here in INFRAUTILS-52)
+ || resourcePath.endsWith(".txt")
+ || resourcePath.endsWith("LICENSE")
+ || resourcePath.endsWith("license.html")
+ || resourcePath.endsWith("about.html")
+ || resourcePath.endsWith("readme.html")
+ || resourcePath.startsWith("META-INF/services")
+ || resourcePath.equals("META-INF/DEPENDENCIES")
+ || resourcePath.equals("META-INF/git.properties")
+ || resourcePath.equals("META-INF/io.netty.versions.properties")
+ || resourcePath.equals("META-INF/jersey-module-version")
+ || resourcePath.startsWith("OSGI-INF/blueprint/")
+ || resourcePath.startsWith("org/opendaylight/blueprint/")
+ || resourcePath.equals("WEB-INF/web.xml")
+ || resourcePath.endsWith("reference.conf") // in Akka's JARs
+ || resourcePath.equals("META-INF/eclipse.inf")
+ || resourcePath.equals("META-INF/ECLIPSE_.SF")
+ || resourcePath.equals("META-INF/ECLIPSE_.RSA")
+ || resourcePath.equals("META-INF/BC2048KE.DSA")
+ || resourcePath.equals("META-INF/BC2048KE.SF")
+ || resourcePath.equals("META-INF/BC1024KE.SF")
+ || resourcePath.equals("OSGI-INF/bundle.info")
+ // Something doesn't to be a perfectly clean in Maven Surefire:
+ || resourcePath.startsWith("META-INF/maven/")
+ || resourcePath.contains("surefire")
+ // org.slf4j.impl.StaticLoggerBinder.class in testutils for the LogCaptureRule
+ || resourcePath.equals("org/slf4j/impl/StaticLoggerBinder.class")
+ // INFRAUTILS-35: JavaLaunchHelper is both in java and libinstrument.dylib (?) on Mac OS X
+ || resourcePath.contains("JavaLaunchHelper")
+ // javax.annotation is a big mess... :( E.g. javax.annotation.Resource (and some others)
+ // are present both in rt.jar AND javax.annotation-api-1.3.2.jar and similar - BUT those
+ // JARs cannot just be excluded, because they contain some additional annotations, in the
+ // (reserved!) package javax.annotation, such as javax.annotation.Priority et al. The
+ // super proper way to address this cleanly would be to make our own JAR for javax.annotation
+ // and have it contain ONLY what is not already in package javax.annotation in rt.jar.. but for now:
+ || resourcePath.equals("javax/annotation/Resource$AuthenticationType.class")
+ // NEUTRON-205: javax.inject is a mess :( because of javax.inject:javax.inject (which we widely use in ODL)
+ // VS. org.glassfish.hk2.external:javax.inject (which Glassfish Jersey has dependencies on). Attempts to
+ // cleanly exclude glassfish.hk2's javax.inject and align everything on only depending on
+ // javax.inject:javax.inject have failed, because the OSGi bundle
+ // org.glassfish.jersey.containers.jersey-container-servlet-core (2.25.1) has a non-optional Package-Import
+ // for javax.inject, but we made javax.inject:javax.inject true in odlparent, and don't bundle it.
+ || resourcePath.startsWith("javax/inject/")
+ // Java 9 modules
+ || resourcePath.endsWith("module-info.class")
+ || resourcePath.contains("findbugs")
+ // list newly introduced in INFRAUTILS-52, because classgraph scans more than JHades did
+ || resourcePath.equals("plugin.properties")
+ || resourcePath.equals(".api_description")
+ // errorprone with Java 11 integration leaks to classpath, which causes a conflict between
+ // checkerframework/checker-qual and checkerframework/dataflow
+ || resourcePath.startsWith("org/checkerframework/dataflow/qual/")
+ ;
+ }
+}
+