Skip to content

Commit

Permalink
Merge pull request #448 from OpenNTF/feature/issue-445-abstractosgimo…
Browse files Browse the repository at this point in the history
…dule

Modify checks for OSGIModule to look for AbstractOSGIModule (fixes #445)
  • Loading branch information
jesse-gallagher authored Jun 30, 2023
2 parents 6f1cdd4 + 672d476 commit c947302
Show file tree
Hide file tree
Showing 12 changed files with 250 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,11 @@
import org.openntf.xsp.jakartaee.util.ModuleUtil;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleException;
import org.osgi.framework.BundleReference;
import org.osgi.framework.wiring.BundleWiring;

import com.ibm.commons.util.StringUtil;
import com.ibm.designer.runtime.domino.adapter.ComponentModule;
import com.ibm.domino.xsp.adapter.osgi.OSGIModule;
import com.ibm.domino.xsp.adapter.osgi.AbstractOSGIModule;

import jakarta.el.ELResolver;
import jakarta.el.ExpressionFactory;
Expand Down Expand Up @@ -143,7 +142,7 @@ public static CDI<Object> getContainer(Bundle bundle) {
Set<String> bundleNames = new HashSet<>();
Set<String> classNames = new HashSet<>();
try {
addBundleBeans(bundle, weld, bundleNames, classNames);
addBundleBeans(bundle, weld, bundleNames, classNames, true);
} catch (BundleException e) {
if(log.isLoggable(Level.WARNING)) {
log.log(Level.WARNING, "Encountered exception loading bundle beans", e);
Expand Down Expand Up @@ -199,14 +198,14 @@ public static CDI<Object> getContainerUnchecked(ComponentModule module) {
return (CDI<Object>)module.getAttributes().get(ATTR_CONTEXTCONTAINER);
}

private static void addBundleBeans(Bundle bundle, Weld weld, Set<String> bundleNames, Set<String> classNames) throws BundleException {
private static void addBundleBeans(Bundle bundle, Weld weld, Set<String> bundleNames, Set<String> classNames, boolean nonExported) throws BundleException {
String symbolicName = bundle.getSymbolicName();
if(bundleNames.contains(symbolicName)) {
return;
}
bundleNames.add(symbolicName);
// Add classes from the bundle here
DiscoveryUtil.findBeanClasses(bundle)
DiscoveryUtil.findBeanClasses(bundle, nonExported)
.filter(t -> !classNames.contains(t.getName()))
.peek(t -> classNames.add(t.getName()))
.distinct()
Expand All @@ -221,7 +220,7 @@ private static void addBundleBeans(Bundle bundle, Weld weld, Set<String> bundleN
if(StringUtil.isNotEmpty(bundleName)) {
Optional<Bundle> dependency = LibraryUtil.getBundle(bundleName);
if(dependency.isPresent()) {
addBundleBeans(dependency.get(), weld, bundleNames, classNames);
addBundleBeans(dependency.get(), weld, bundleNames, classNames, false);
}
}
}
Expand All @@ -238,10 +237,12 @@ private static void addBundleBeans(Bundle bundle, Weld weld, Set<String> bundleN
public static CDI<Object> getContainer(ComponentModule module) {
// OSGi Servlets use their containing bundle, and we have to assume
// that it's from the current thread
if(module instanceof OSGIModule) {
if(module instanceof AbstractOSGIModule) {

ClassLoader cl = Thread.currentThread().getContextClassLoader();
if(cl instanceof BundleReference) {
return getContainer(((BundleReference) cl).getBundle());
Optional<Bundle> bundle = DiscoveryUtil.getBundleForClassLoader(cl);
if(bundle.isPresent()) {
return getContainer(bundle.get());
}
}

Expand All @@ -255,7 +256,7 @@ public static CDI<Object> getContainer(ComponentModule module) {

long refresh = module.getLastRefresh();

if(module instanceof OSGIModule || LibraryUtil.usesLibrary(CDILibrary.LIBRARY_ID, module)) {
if(module instanceof AbstractOSGIModule || LibraryUtil.usesLibrary(CDILibrary.LIBRARY_ID, module)) {
String bundleId = getApplicationCDIBundle(module);
if(StringUtil.isNotEmpty(bundleId)) {
Optional<Bundle> bundle = LibraryUtil.getBundle(bundleId);
Expand Down Expand Up @@ -292,7 +293,7 @@ public static CDI<Object> getContainer(ComponentModule module) {
Set<String> bundleNames = new HashSet<>();
Set<String> classNames = new HashSet<>();
try {
addBundleBeans(bundle, weld, bundleNames, classNames);
addBundleBeans(bundle, weld, bundleNames, classNames, false);
} catch (BundleException e) {
if(log.isLoggable(Level.WARNING)) {
log.log(Level.WARNING, "Encountered exception loading bundle beans", e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,18 @@
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
Expand All @@ -38,6 +42,7 @@
import org.openntf.xsp.jakartaee.util.LibraryUtil;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleException;
import org.osgi.framework.BundleReference;
import org.osgi.framework.Version;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
Expand Down Expand Up @@ -65,6 +70,7 @@ private enum ScanType {
}

private static final Map<Class<?>, Boolean> BEAN_DEFINING = new ConcurrentHashMap<>();
private static Field WEBAPP_BUNDLE_FIELD = null;

/**
* Searches through the provided bundle to find all exported class names that may
Expand All @@ -74,21 +80,25 @@ private enum ScanType {
* the bundle's {@code Export-Package} listing.</p>
*
* @param bundle a {@link Bundle} instance to query
* @param nonExported {@code true} to include classes not marked as exported from the bundle
* @return a {@link Stream} of discovered exported classes
* @throws BundleException if there is a problem parsing the bundle manifest
*/
public static Stream<String> findCandidateBeanClassNames(Bundle bundle) throws BundleException {
public static Stream<String> findCandidateBeanClassNames(Bundle bundle, boolean nonExported) throws BundleException {
ScanType scanType = determineScanType(bundle);

if(scanType == ScanType.ALL || scanType == ScanType.ANNOTATED) {
String exportPackages = bundle.getHeaders().get("Export-Package"); //$NON-NLS-1$
if(StringUtil.isNotEmpty(exportPackages)) {
if(StringUtil.isNotEmpty(exportPackages) || nonExported) {
// Restrict to exported packages for sanity's sake
ManifestElement[] elements = ManifestElement.parseHeader("Export-Package", exportPackages); //$NON-NLS-1$
Set<String> packages = Arrays.stream(elements)
.map(ManifestElement::getValue)
.filter(StringUtil::isNotEmpty)
.collect(Collectors.toSet());
Set<String> packages = null;
if(!nonExported) {
ManifestElement[] elements = ManifestElement.parseHeader("Export-Package", exportPackages); //$NON-NLS-1$
packages = Arrays.stream(elements)
.map(ManifestElement::getValue)
.filter(StringUtil::isNotEmpty)
.collect(Collectors.toSet());
}

URL jandexUrl = bundle.getResource("/META-INF/jandex.idx"); //$NON-NLS-1$
if(jandexUrl != null) {
Expand All @@ -99,24 +109,32 @@ public static Stream<String> findCandidateBeanClassNames(Bundle bundle) throws B
throw new UncheckedIOException(MessageFormat.format("Encountered exception reading jandex.idx for {0}", bundle.getSymbolicName()), e);
}

return packages.stream()
.map(p -> jandex.getClassesInPackage(p))
.flatMap(Collection::stream)
.map(c -> c.name().toString())
.distinct();
if(packages != null) {
return packages.stream()
.map(p -> jandex.getClassesInPackage(p))
.flatMap(Collection::stream)
.map(c -> c.name().toString())
.distinct();
} else {
return jandex.getKnownClasses()
.stream()
.map(c -> c.name().toString())
.distinct();
}

} else {
// Otherwise, do a manual crawl for class names
String baseUrl = bundle.getEntry("/").toString(); //$NON-NLS-1$
List<URL> entries = Collections.list(bundle.findEntries("/", "*.class", true)); //$NON-NLS-1$ //$NON-NLS-2$
Set<String> classNames = new HashSet<>();
Set<String> fpackages = packages;
return entries.stream()
.parallel()
.map(String::valueOf)
.map(url -> url.substring(baseUrl.length()))
.map(LibraryUtil::toClassName)
.filter(StringUtil::isNotEmpty)
.filter(className -> packages.contains(className.substring(0, className.lastIndexOf('.'))))
.filter(className -> fpackages == null || fpackages.contains(className.substring(0, className.lastIndexOf('.'))))
.filter(className -> !classNames.contains(className))
.peek(classNames::add)
.sequential();
Expand All @@ -135,15 +153,15 @@ public static Stream<String> findCandidateBeanClassNames(Bundle bundle) throws B
* the bundle's {@code Export-Package} listing.</p>
*
* @param bundle a {@link Bundle} instance to query
* @param force include classes even when there's no beans.xml
* @param nonExported {@code true} to include classes not marked as exported from the bundle
* @return a {@link Stream} of discovered exported classes
* @throws BundleException if there is a problem parsing the bundle manifest
* @since 2.3.0
*/
public static Stream<Class<?>> findBeanClasses(Bundle bundle) throws BundleException {
public static Stream<Class<?>> findBeanClasses(Bundle bundle, boolean nonExported) throws BundleException {
ScanType scanType = determineScanType(bundle);

return findCandidateBeanClassNames(bundle)
return findCandidateBeanClassNames(bundle, nonExported)
.map(className -> {
try {
return bundle.loadClass(className);
Expand All @@ -162,6 +180,30 @@ public static Stream<Class<?>> findBeanClasses(Bundle bundle) throws BundleExcep
.map(c -> (Class<?>)c);
}

public static Optional<Bundle> getBundleForClassLoader(ClassLoader cl) {
// Equinox Servlets
if(cl instanceof BundleReference) {
return Optional.of(((BundleReference) cl).getBundle());
}

// Bundle webapps
if("com.ibm.pvc.internal.webcontainer.webapp.BundleWebAppClassLoader".equals(cl.getClass().getName())) { //$NON-NLS-1$
return AccessController.doPrivileged((PrivilegedAction<Optional<Bundle>>)() -> {
try {
if(WEBAPP_BUNDLE_FIELD == null) {
WEBAPP_BUNDLE_FIELD = cl.getClass().getDeclaredField("bundle"); //$NON-NLS-1$
WEBAPP_BUNDLE_FIELD.setAccessible(true);
}
return Optional.ofNullable((Bundle)WEBAPP_BUNDLE_FIELD.get(cl));
} catch(Exception e) {
throw new RuntimeException(e);
}
});
}

return Optional.empty();
}

private static ScanType determineScanType(Bundle bundle) {
URL beansXml = bundle.getResource("/META-INF/beans.xml"); //$NON-NLS-1$
if(beansXml == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ public boolean isActive() {

@Override
public boolean isComponentEnabled(String componentId) {
// TODO consider using an NSFComponentModule surrounding an OSGIModule
ComponentModule module = ComponentModuleLocator.getDefault().get().getActiveModule();
if(module != null) {
return LibraryUtil.usesLibrary(componentId, module);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ public Collection<Class<?>> getBeanClasses() {
// Look for annotated beans in io.smallrye.health
Bundle bundle = FrameworkUtil.getBundle(ResponseProvider.class);
try {
return DiscoveryUtil.findBeanClasses(bundle)
// TODO filter to only annotated
return DiscoveryUtil.findBeanClasses(bundle, false)
.collect(Collectors.toList());
} catch (BundleException e) {
throw new RuntimeException(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public Collection<Class<?>> getBeanClasses() {
.map(FrameworkUtil::getBundle)
.flatMap(t -> {
try {
return DiscoveryUtil.findBeanClasses(t);
return DiscoveryUtil.findBeanClasses(t, false);
} catch (BundleException e) {
throw new RuntimeException(e);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Copyright (c) 2018-2023 Contributors to the XPages Jakarta EE Support Project
*
* Licensed 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 it.org.openntf.xsp.jakartaee.webapp.cdi;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.fail;

import java.io.StringReader;

import jakarta.json.Json;
import jakarta.json.JsonObject;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.WebTarget;
import jakarta.ws.rs.core.Response;

import org.junit.jupiter.api.Test;

import it.org.openntf.xsp.jakartaee.AbstractWebClientTest;
import it.org.openntf.xsp.jakartaee.TestDatabase;

@SuppressWarnings({ "nls" })
public class TestWebappBeanResource extends AbstractWebClientTest {

@Test
public void testBundleBean() {
int expectedIdentity = 0;
// First NSF - uses .cdibundle
{
JsonObject obj = getBean(getRootUrl(null, TestDatabase.OSGI_WEBAPP) + "/cdiServlet");
assertEquals("Hello from webappBean", obj.getString("hello"));
expectedIdentity = obj.getInt("identity");
assertNotEquals(0, expectedIdentity);
}
// Call this again to ensure that it uses the same bean
{
JsonObject obj = getBean(getRootUrl(null, TestDatabase.OSGI_WEBAPP) + "/cdiServlet");
assertEquals("Hello from webappBean", obj.getString("hello"));
int identity = obj.getInt("identity");
assertEquals(expectedIdentity, identity);
}
}

private JsonObject getBean(String base) {
Client client = getAnonymousClient();
WebTarget target = client.target(base);
Response response = target.request().get();

checkResponse(200, response);
String output = response.readEntity(String.class);
try {
return Json.createReader(new StringReader(output)).readObject();
} catch(Exception e) {
fail("Exception parsing JSON: " + output, e);
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,18 @@ Bundle-ManifestVersion: 2
Bundle-Name: Example Jakarta EE Webapp
Bundle-SymbolicName: org.openntf.xsp.jakarta.example.webapp;singleton:=true
Automatic-Module-Name: org.openntf.xsp.jakarta.example.webapp
Bundle-ActivationPolicy: lazy
Bundle-Activator: org.openntf.xsp.jakarta.example.webapp.WebappActivator
Bundle-Version: 2.13.0.qualifier
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Require-Bundle: org.openntf.xsp.jsp;bundle-version="2.8.0",
org.openntf.xsp.jakarta.servlet;bundle-version="2.8.0"
Import-Package: com.ibm.designer.runtime.domino.adapter
Import-Package: com.ibm.designer.runtime.domino.adapter,
jakarta.enterprise.context;version="3.0.0",
jakarta.enterprise.inject;version="3.0.0",
jakarta.enterprise.inject.spi;version="3.0.0",
jakarta.inject;version="2.0.0",
jakarta.json.bind;version="2.0.0",
org.openntf.xsp.cdi.provider;version="2.13.0",
org.osgi.framework;version="1.8.0"
DynamicImport-Package: *
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!--
Copyright (c) 2018-2023 Contributors to the XPages Jakarta EE Support Project
Licensed 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.
-->
<beans xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/beans_3_0.xsd"
version="3.0"
bean-discovery-mode="annotated">
</beans>
Loading

0 comments on commit c947302

Please sign in to comment.