Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,21 @@

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;

import org.glassfish.embeddable.Deployer;
import org.glassfish.embeddable.GlassFish;
Expand All @@ -41,6 +46,7 @@
import org.glassfish.embeddable.web.HttpListener;
import org.glassfish.embeddable.web.WebContainer;
import org.glassfish.tests.embedded.scatteredarchive.contextInitialized.ContextInitializedTestServlet;
import org.glassfish.tests.embedded.scatteredarchive.fragment.FragmentServlet;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -174,6 +180,64 @@ public void testContextInitialized() throws Exception {
LABEL_CONTEXT_INITIALIZED_COUNTER, "1");
}

/**
* Reproducer for issue #24592: an annotated component bundled in a nested JAR (a web fragment)
* in {@code WEB-INF/lib} of a {@code ScatteredArchive} must be scanned for annotations and
* deployed.
*/
@Test
public void testWebFragmentInNestedJar() throws Exception {
ScatteredArchive sa = createDefaultArchive("scatteredarchive");

// Bundle an annotated servlet inside a nested JAR (web fragment) placed in WEB-INF/lib.
// The servlet class is NOT on the archive's WEB-INF/classes, so it can only be discovered
// by scanning the nested JAR.
File fragmentJar = createFragmentJar("fragment-with-servlet.jar", FragmentServlet.class);
sa.addClassPath(fragmentJar);

URI warURI = sa.toURI();
printContents(warURI);

Deployer deployer = glassfish.getDeployer();
String appname = deployer.deploy(warURI);
logger.log(INFO, "Deployed [" + appname + "]");
assertEquals("scatteredarchive", appname);

get("http://localhost:8080/satest/" + FragmentServlet.class.getSimpleName(),
FragmentServlet.MESSAGE);
}

/**
* Builds a web-fragment JAR (containing a {@code META-INF/web-fragment.xml} and the given
* compiled classes read from {@code target/test-classes}) under the {@code target} directory.
*/
private File createFragmentJar(String jarName, Class<?>... classes) throws IOException {
File jar = new File(PROJECT_DIR, "target/" + jarName);
try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(jar))) {
jos.putNextEntry(new ZipEntry("META-INF/web-fragment.xml"));
jos.write(WEB_FRAGMENT_XML.getBytes(StandardCharsets.UTF_8));
jos.closeEntry();
for (Class<?> clazz : classes) {
String entryName = clazz.getName().replace('.', '/') + ".class";
File classFile = new File(PROJECT_DIR, "target/test-classes/" + entryName);
jos.putNextEntry(new ZipEntry(entryName));
jos.write(Files.readAllBytes(classFile.toPath()));
jos.closeEntry();
}
}
return jar;
}

private static final String WEB_FRAGMENT_XML =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<web-fragment xmlns=\"https://jakarta.ee/xml/ns/jakartaee\"\n"
+ " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
+ " xsi:schemaLocation=\"https://jakarta.ee/xml/ns/jakartaee"
+ " https://jakarta.ee/xml/ns/jakartaee/web-fragment_6_0.xsd\"\n"
+ " version=\"6.0\">\n"
+ " <name>fragmentWithServlet</name>\n"
+ "</web-fragment>\n";

private ScatteredArchive createDefaultArchive(String ARCHIVE_NAME) throws IOException {
// Test Scattered Web Archive
ScatteredArchive sa = new ScatteredArchive(ARCHIVE_NAME,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2026 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.embedded.scatteredarchive.fragment;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;

/**
* A servlet that is bundled inside a nested JAR (a web fragment) of a {@code ScatteredArchive}.
* It must be discovered by annotation scanning of the nested JAR, see issue #24592.
*/
@WebServlet(name = "FragmentServlet", urlPatterns = "/FragmentServlet")
public class FragmentServlet extends HttpServlet {

public static final String MESSAGE = "Hi from FragmentServlet in a nested JAR";

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
try (PrintWriter out = response.getWriter()) {
out.println(MESSAGE);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, 2023 Contributors to the Eclipse Foundation
* Copyright (c) 2022, 2023, 2026 Contributors to the Eclipse Foundation
* Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
Expand Down Expand Up @@ -40,8 +40,11 @@
import com.sun.enterprise.deployment.web.SecurityConstraint;
import com.sun.enterprise.deployment.web.ServletFilter;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Set;

import org.glassfish.api.deployment.archive.ReadableArchive;
Expand Down Expand Up @@ -442,9 +445,34 @@ public void print(StringBuffer toStringBuffer) {
}
}

/**
* Returns the entries of the nested JAR file (in {@code WEB-INF/lib}) that this web fragment
* was loaded from, so that the classes it contains can be scanned for annotations.
* <p>
* Unlike a regular {@link WebBundleDescriptorImpl}, whose classes live under
* {@code WEB-INF/classes/}, a web fragment's classes live inside a nested JAR. The returned
* entry names are relative to the root of that nested JAR (e.g. {@code com/example/Foo.class}).
* The entry names are materialized eagerly so the sub-archive can be closed before this method
* returns.
*/
@Override
public Enumeration<String> getArchiveFileEntries(ReadableArchive archiveFile) {
return Collections.emptyEnumeration();
if (jarName == null || archiveFile == null) {
return Collections.emptyEnumeration();
}
try (ReadableArchive jarArchive = archiveFile.getSubArchive("WEB-INF/lib/" + jarName)) {
if (jarArchive == null) {
return Collections.emptyEnumeration();
}
List<String> entries = new ArrayList<>();
Enumeration<String> jarEntries = jarArchive.entries();
while (jarEntries.hasMoreElements()) {
entries.add(jarEntries.nextElement());
}
return Collections.enumeration(entries);
} catch (IOException e) {
return Collections.emptyEnumeration();
}
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, 2025 Contributors to the Eclipse Foundation.
* Copyright (c) 2022, 2025, 2026 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
Expand Down Expand Up @@ -37,6 +37,7 @@
import org.glassfish.web.LogFacade;
import org.glassfish.web.deployment.archivist.WebArchivist;
import org.glassfish.web.deployment.descriptor.WebBundleDescriptorImpl;
import org.glassfish.web.deployment.descriptor.WebFragmentDescriptor;
import org.jvnet.hk2.annotations.Service;


Expand Down Expand Up @@ -98,12 +99,15 @@ public void process(ReadableArchive archiveFile, WebBundleDescriptorImpl descrip
this.classLoader = classLoader;
this.elements.clear();
// in embedded mode, we don't scan archive, we just process all classes.
// For the web module itself the classes live under WEB-INF/classes/, while for a web
// fragment the entries come from its nested JAR and are relative to that JAR's root.
final String classPrefix = (descriptor instanceof WebFragmentDescriptor) ? "" : "WEB-INF/classes/";
Enumeration<String> fileEntries = descriptor.getArchiveFileEntries(archiveFile);
while (fileEntries.hasMoreElements()) {
String entry = fileEntries.nextElement();
if (entry.startsWith("WEB-INF/classes/") && entry.endsWith(".class")) {
if (entry.startsWith(classPrefix) && entry.endsWith(".class") && !entry.endsWith("module-info.class")) {
try {
elements.add(classLoader.loadClass(toClassName(entry)));
elements.add(classLoader.loadClass(toClassName(entry, classPrefix)));
} catch (ClassNotFoundException e) {
LOG.log(Level.WARNING, "Cannot load class " + entry, e);
}
Expand All @@ -117,8 +121,8 @@ protected void process(File archiveFile, WebBundleDescriptorImpl descriptor, Cla
}


private String toClassName(String entryName) {
String name = entryName.substring("WEB-INF/classes/".length(), entryName.length() - ".class".length());
private String toClassName(String entryName, String classPrefix) {
String name = entryName.substring(classPrefix.length(), entryName.length() - ".class".length());
return name.replaceAll("/", ".");
}

Expand Down