Issue #23507 Implemented HK2Extension
- based on hk2-junitrunner
- portions of code were copy pasted as they are not public
- public classes were reused
- simplified own integration with tests respecting JUnit5 principles
diff --git a/nucleus/test-utils/utils/pom.xml b/nucleus/test-utils/utils/pom.xml
index 9c7e1bc..a0ccb71 100644
--- a/nucleus/test-utils/utils/pom.xml
+++ b/nucleus/test-utils/utils/pom.xml
@@ -64,5 +64,10 @@
<artifactId>junit-jupiter-engine</artifactId>
<scope>compile</scope>
</dependency>
+ <dependency>
+ <groupId>org.glassfish.hk2</groupId>
+ <artifactId>hk2-junitrunner</artifactId>
+ <scope>compile</scope>
+ </dependency>
</dependencies>
</project>
diff --git a/nucleus/test-utils/utils/src/main/java/org/glassfish/tests/utils/HK2Extension.java b/nucleus/test-utils/utils/src/main/java/org/glassfish/tests/utils/HK2Extension.java
new file mode 100644
index 0000000..1a3a083
--- /dev/null
+++ b/nucleus/test-utils/utils/src/main/java/org/glassfish/tests/utils/HK2Extension.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (c) 2021 Eclipse Foundation and/or its affiliates. All rights reserved.
+ * Copyright (c) 2021 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.tests.utils;
+
+import com.sun.enterprise.module.bootstrap.StartupContext;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+import org.glassfish.hk2.api.DynamicConfiguration;
+import org.glassfish.hk2.api.DynamicConfigurationService;
+import org.glassfish.hk2.api.ServiceLocator;
+import org.glassfish.hk2.utilities.DescriptorImpl;
+import org.glassfish.hk2.utilities.ServiceLocatorUtilities;
+import org.glassfish.server.ServerEnvironmentImpl;
+import org.junit.jupiter.api.extension.AfterAllCallback;
+import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.TestInstancePostProcessor;
+import org.jvnet.hk2.testing.junit.annotations.InhabitantFiles;
+import org.jvnet.hk2.testing.junit.annotations.Packages;
+import org.jvnet.hk2.testing.junit.internal.ClassVisitorImpl;
+import org.jvnet.hk2.testing.junit.internal.TestServiceLocator;
+import org.objectweb.asm.ClassReader;
+
+
+/**
+ * This JUnit5 extension is based on {@link TestServiceLocator} implementaion, which served for JUnit4.
+ * In the future it will probably move to HK2, but first it needs to gain some maturity in practice.
+ * The hk2-junitrunner will be refactored, so both usages will be possible.
+ *
+ * @author David Matejcek
+ */
+public class HK2Extension implements BeforeAllCallback, TestInstancePostProcessor, AfterAllCallback {
+ private static final String CLASS_PATH_PROP = "java.class.path";
+ private final static String DOT_CLASS = ".class";
+ private final ServiceLocator locator;
+ private DynamicConfiguration config;
+
+
+ /**
+ * Creates {@link ServiceLocator} with a name of this extension.
+ */
+ public HK2Extension() {
+ locator = ServiceLocatorUtilities.createAndPopulateServiceLocator(getClass().getSimpleName());
+ }
+
+
+ @Override
+ public void beforeAll(final ExtensionContext context) throws Exception {
+ final Class<?> testClass = context.getRequiredTestClass();
+ final ClassLoader loader = testClass.getClassLoader();
+ final Packages packagesAnnotation = testClass.getAnnotation(Packages.class);
+ final List<String> packages = packagesAnnotation == null ? List.of(testClass.getPackageName())
+ : Arrays.asList(packagesAnnotation.value());
+
+ config = locator.getService(DynamicConfigurationService.class).createDynamicConfiguration();
+ addServicesFromDefault(loader, Set.of(), getDefaultLocatorPaths(context));
+ addServicesFromPackage(packages, Set.of());
+ config.addActiveDescriptor(ServerEnvironmentImpl.class);
+ config.addActiveDescriptor(StartupContext.class);
+ config.commit();
+ }
+
+
+ @Override
+ public void postProcessTestInstance(final Object testInstance, final ExtensionContext context) throws Exception {
+ locator.inject(testInstance);
+ }
+
+
+ @Override
+ public void afterAll(final ExtensionContext context) throws Exception {
+ locator.shutdown();
+ }
+
+
+ private Set<String> getDefaultLocatorPaths(final ExtensionContext context) {
+ final HashSet<String> paths = new HashSet<>();
+ final InhabitantFiles iFiles = context.getRequiredTestClass().getAnnotation(InhabitantFiles.class);
+ if (iFiles == null) {
+ paths.add("META-INF/hk2-locator/default");
+ return paths;
+ }
+ for (final String iFile : iFiles.value()) {
+ paths.add(iFile);
+ }
+ return paths;
+ }
+
+
+ private void addServicesFromDefault(final ClassLoader loader, final Set<String> excludes, final Set<String> locatorFiles)
+ throws IOException {
+ for (final String locatorFile : locatorFiles) {
+ final Enumeration<URL> resources = loader.getResources(locatorFile);
+ readResources(resources, excludes);
+ }
+ }
+
+
+ private void readResources(final Enumeration<URL> resources, final Set<String> excludes) throws IOException {
+ while (resources.hasMoreElements()) {
+ final URL url = resources.nextElement();
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream()))) {
+ while (true) {
+ final DescriptorImpl bindMe = new DescriptorImpl();
+ final boolean goOn = bindMe.readObject(reader);
+ if (!goOn) {
+ break;
+ }
+ if (!excludes.contains(bindMe.getImplementation())) {
+ config.bind(bindMe);
+ }
+ }
+ }
+ }
+ }
+
+
+ private void addServicesFromPackage(final List<String> packages, final Set<String> excludes) throws IOException {
+ if (packages.isEmpty()) {
+ return;
+ }
+ final String classPath = System.getProperty(CLASS_PATH_PROP);
+ final StringTokenizer st = new StringTokenizer(classPath, File.pathSeparator);
+ while (st.hasMoreTokens()) {
+ addServicesFromPathElement(packages, st.nextToken(), excludes);
+ }
+ }
+
+
+ private void addServicesFromPathElement(final List<String> packages, final String path, final Set<String> excludes) throws IOException {
+ final File fileElement = new File(path);
+ if (!fileElement.exists()) {
+ return;
+ }
+
+ if (fileElement.isDirectory()) {
+ addServicesFromPathDirectory(packages, fileElement, excludes);
+ } else {
+ addServicesFromPathJar(packages, fileElement, excludes);
+ }
+ }
+
+
+ private void addServicesFromPathDirectory(final List<String> packages, final File directory, final Set<String> excludes) throws IOException {
+ for (final String pack : packages) {
+ final File searchDir = new File(directory, convertToFileFormat(pack));
+ if (!searchDir.exists()) {
+ continue;
+ }
+ if (!searchDir.isDirectory()) {
+ continue;
+ }
+
+ final File candidates[] = searchDir.listFiles((FilenameFilter) (dir, name) -> {
+ if (name == null) {
+ return false;
+ }
+ if (name.endsWith(DOT_CLASS)) {
+ return true;
+ }
+ return false;
+ });
+
+ if (candidates == null) {
+ continue;
+ }
+
+ for (final File candidate : candidates) {
+ try (FileInputStream fis = new FileInputStream(candidate)) {
+ addClassIfService(fis, excludes);
+ }
+ }
+ }
+ }
+
+
+ private void addServicesFromPathJar(final List<String> packages, final File jar, final Set<String> excludes) throws IOException {
+ try (JarFile jarFile = new JarFile(jar)) {
+ for (final String pack : packages) {
+ final String packAsFile = convertToFileFormat(pack);
+ final int packAsFileLen = packAsFile.length() + 1;
+
+ final Enumeration<JarEntry> entries = jarFile.entries();
+ while (entries.hasMoreElements()) {
+ final JarEntry entry = entries.nextElement();
+
+ final String entryName = entry.getName();
+ if (!entryName.startsWith(packAsFile)) {
+ // Not in the correct directory
+ continue;
+ }
+ if (entryName.substring(packAsFileLen).contains("/")) {
+ // Next directory down
+ continue;
+ }
+ if (!entryName.endsWith(DOT_CLASS)) {
+ // Not a class
+ continue;
+ }
+
+ try {
+ addClassIfService(jarFile.getInputStream(entry), excludes);
+ } catch (final IOException ioe) {
+ // Simply don't add it if we can't read it
+ }
+ }
+ }
+ }
+ }
+
+
+ private static String convertToFileFormat(final String clazzFormat) {
+ return clazzFormat.replaceAll("\\.", "/");
+ }
+
+
+ private void addClassIfService(final InputStream is, final Set<String> excludes) throws IOException {
+ final ClassReader reader = new ClassReader(is);
+ final ClassVisitorImpl cvi = new ClassVisitorImpl(locator, true, excludes);
+ reader.accept(cvi, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
+ }
+}