Skip to content

Commit

Permalink
Merge pull request #507 from OpenNTF/feature/issue-55-webxml-servlets
Browse files Browse the repository at this point in the history
Add ability to specify Servlets in web.xml (Issue #55)
  • Loading branch information
jesse-gallagher committed Dec 27, 2023
2 parents 989206a + ee08719 commit b79675a
Show file tree
Hide file tree
Showing 9 changed files with 207 additions and 61 deletions.
Expand Up @@ -20,6 +20,7 @@ Import-Package: com.ibm.commons.util,
javax.faces.context,
javax.servlet;version="2.5.0",
javax.servlet.http;version="2.5.0",
org.apache.tomcat.util.descriptor.web;version="9.0.84",
org.openntf.xsp.cdi;version="2.5.0",
org.osgi.framework;version="1.8.0"
Export-Package: org.openntf.xsp.jakarta.servlet.webapp;version="2.15.0"
Expand Up @@ -16,11 +16,15 @@
package org.openntf.xsp.jakarta.servlet.nsf;

import java.io.UncheckedIOException;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import javax.servlet.ServletException;

Expand All @@ -29,13 +33,16 @@
import jakarta.servlet.annotation.WebInitParam;
import jakarta.servlet.annotation.WebServlet;

import org.apache.tomcat.util.descriptor.web.ServletDef;
import org.apache.tomcat.util.descriptor.web.WebXml;
import org.openntf.xsp.cdi.CDILibrary;
import org.openntf.xsp.jakarta.servlet.ServletLibrary;
import org.openntf.xsp.jakartaee.servlet.ServletUtil;
import org.openntf.xsp.jakartaee.util.LibraryUtil;
import org.openntf.xsp.jakartaee.util.ModuleUtil;

import com.ibm.commons.util.PathUtil;
import com.ibm.commons.util.StringUtil;
import com.ibm.designer.runtime.domino.adapter.ComponentModule;
import com.ibm.designer.runtime.domino.adapter.IServletFactory;
import com.ibm.designer.runtime.domino.adapter.ServletMatch;
Expand All @@ -47,8 +54,10 @@
* @since 2.5.0
*/
public class ServletServletFactory implements IServletFactory {
private static final Logger log = Logger.getLogger(ServletServletFactory.class.getName());

private ComponentModule module;
private Map<WebServlet, Class<? extends Servlet>> servletClasses;
private Map<ServletInfo, Class<? extends Servlet>> servletClasses;
private Map<Class<? extends Servlet>, javax.servlet.Servlet> servlets;
private long lastUpdate;

Expand All @@ -61,7 +70,7 @@ public void init(ComponentModule module) {
public final ServletMatch getServletMatch(String contextPath, String path) throws javax.servlet.ServletException {
try {
if(LibraryUtil.usesLibrary(ServletLibrary.LIBRARY_ID, module)) {
for(Map.Entry<WebServlet, Class<? extends Servlet>> entry : getModuleServlets().entrySet()) {
for(Map.Entry<ServletInfo, Class<? extends Servlet>> entry : getModuleServlets().entrySet()) {
String match = matches(entry.getKey(), path);
if(match != null) {
javax.servlet.Servlet servlet = getExecutorServlet(entry.getKey(), entry.getValue());
Expand All @@ -77,18 +86,15 @@ public final ServletMatch getServletMatch(String contextPath, String path) throw
return null;
}

private String matches(WebServlet mapping, String path) {
private String matches(ServletInfo mapping, String path) {
// Context path is like /some/db.nsf
// Path is like /xsp/someservlet (no query string)

if(path == null || path.length() < 5) {
return null;
}

String[] patterns = mapping.value();
if(patterns == null || patterns.length == 0) {
patterns = mapping.urlPatterns();
}
List<String> patterns = mapping.patterns;

if(patterns != null) {
for(String pattern : patterns) {
Expand Down Expand Up @@ -142,7 +148,7 @@ private String findPathInfo(String path, String pattern) {
return null;
}

private javax.servlet.Servlet getExecutorServlet(WebServlet mapping, Class<? extends Servlet> c) {
private javax.servlet.Servlet getExecutorServlet(ServletInfo mapping, Class<? extends Servlet> c) {
checkInvalidate();
return this.servlets.computeIfAbsent(c, key -> {
try {
Expand All @@ -154,31 +160,52 @@ private javax.servlet.Servlet getExecutorServlet(WebServlet mapping, Class<? ext
}
Servlet wrapper = new XspServletWrapper(module, delegate);

Map<String, String> params = Arrays.stream(mapping.initParams())
.collect(Collectors.toMap(
WebInitParam::name,
WebInitParam::value
));
Map<String, String> params = mapping.def.getParameterMap();

return module.createServlet(ServletUtil.newToOld(wrapper), mapping.name(), params);
return module.createServlet(ServletUtil.newToOld(wrapper), mapping.def.getServletName(), params);
} catch (InstantiationException | IllegalAccessException | ServletException e) {
throw new RuntimeException(e);
}
});
}

@SuppressWarnings("unchecked")
private synchronized Map<WebServlet, Class<? extends Servlet>> getModuleServlets() {
private synchronized Map<ServletInfo, Class<? extends Servlet>> getModuleServlets() {
checkInvalidate();

if(this.servletClasses == null) {
this.servletClasses = ModuleUtil.getClasses(this.module)
Map<ServletInfo, Class<? extends Servlet>> result = new HashMap<>();
ModuleUtil.getClasses(this.module)
.filter(c -> c.isAnnotationPresent(WebServlet.class))
.filter(Servlet.class::isAssignableFrom)
.collect(Collectors.toMap(
c -> c.getAnnotation(WebServlet.class),
c -> (Class<? extends Servlet>)c
));
.forEach(c -> result.put(toServletDef(c), (Class<? extends Servlet>)c));

ClassLoader cl = module.getModuleClassLoader();
if(cl != null) {
WebXml webXml = ServletUtil.getWebXml(module);

// mappings is pattern -> name, so reverse for our needs
Map<String, String> mappings = webXml.getServletMappings();
Map<String, List<String>> nameToPattern = new HashMap<>();
mappings.forEach((pattern, name) -> nameToPattern.computeIfAbsent(name, k -> new ArrayList<>()).add(pattern));

Map<String, ServletDef> defs = webXml.getServlets();
nameToPattern.forEach((name, patterns) -> {
ServletInfo info = new ServletInfo();
info.def = defs.get(name);
info.patterns = patterns;

try {
result.put(info, (Class<? extends Servlet>)cl.loadClass(info.def.getServletClass()));
} catch (ClassNotFoundException e) {
if(log.isLoggable(Level.SEVERE)) {
log.log(Level.SEVERE, MessageFormat.format("Encountered exception loading Servlet class {0}", info.def.getServletClass()), e);
}
}
});
}

this.servletClasses = result;
}
return this.servletClasses;
}
Expand All @@ -192,5 +219,45 @@ private void checkInvalidate() {
this.servletClasses = null;
}
}

private ServletInfo toServletDef(Class<?> c) {
WebServlet annotation = c.getAnnotation(WebServlet.class);

ServletDef def = new ServletDef();

def.setAsyncSupported(Boolean.toString(annotation.asyncSupported()));
def.setDescription(annotation.description());
def.setDisplayName(annotation.displayName());
def.setEnabled(Boolean.toString(true));
def.setLargeIcon(annotation.largeIcon());
def.setLoadOnStartup(Integer.toString(annotation.loadOnStartup()));
def.setSmallIcon(annotation.smallIcon());
def.setServletClass(c.getName());

String name = annotation.name();
if(StringUtil.isEmpty(name)) {
name = c.getName();
}
def.setServletName(name);


for(WebInitParam param : annotation.initParams()) {
def.addInitParameter(param.name(), param.value());
}

ServletInfo info = new ServletInfo();
info.def = def;
String[] patterns = annotation.value();
if(patterns == null || patterns.length == 0) {
patterns = annotation.urlPatterns();
}
info.patterns = new ArrayList<>(Arrays.asList(patterns));
return info;
}

private static class ServletInfo {
private ServletDef def;
private List<String> patterns;
}

}
Expand Up @@ -25,6 +25,7 @@
import java.util.Enumeration;
import java.util.EventListener;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

Expand Down Expand Up @@ -55,6 +56,8 @@
public enum ServletUtil {
;

public static final String KEY_WEBXML = ServletUtil.class.getPackage().getName() + "_webXml"; //$NON-NLS-1$

private static final Logger log = Logger.getLogger(ServletUtil.class.getName());

public static javax.servlet.Servlet newToOld(jakarta.servlet.Servlet servlet) {
Expand Down Expand Up @@ -507,48 +510,65 @@ public static boolean isClosedConnection(Throwable t) {
*/
public static void populateWebXmlParams(ComponentModule module, jakarta.servlet.ServletContext context) {
if(module != null) {
WebXml result = AccessController.doPrivileged((PrivilegedAction<WebXml>)() -> {
ClassLoader tccl = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(ServletUtil.class.getClassLoader());
WebXml webXml = new WebXml();
try {
WebXmlParser parser = new WebXmlParser(false, false, false);
WebXml result = getWebXml(module);
// Now populate resolved params
result.getContextParams().forEach(context::setInitParameter);
}
}

/**
* Process any WEB-INF/web.xml and META-INF/web-fragment.xml files in the module
* into a deployment descriptor.
*
* @param module the module to load resources from
* @return a {@link WebXml} descriptor object for the module
* @since 2.15.0
*/
public static WebXml getWebXml(ComponentModule module) {
if(module == null) {
return new WebXml();
}

Map<String, Object> attrs = module.getAttributes();
return (WebXml)attrs.computeIfAbsent(KEY_WEBXML, key -> AccessController.doPrivileged((PrivilegedAction<WebXml>)() -> {
ClassLoader tccl = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(ServletUtil.class.getClassLoader());
WebXml webXml = new WebXml();
try {
WebXmlParser parser = new WebXmlParser(false, false, false);

InputStream is = module.getResourceAsStream("/WEB-INF/web.xml"); //$NON-NLS-1$
if(is != null) {
if(log.isLoggable(Level.FINE)) {
log.fine(MessageFormat.format("Processing web.xml in {0}", module.getModuleName()));
}
InputSource source = new InputSource(is);
parser.parseWebXml(source, webXml, false);
}

InputStream is = module.getResourceAsStream("/WEB-INF/web.xml"); //$NON-NLS-1$
if(is != null) {
// Look for META-INF/web-fragment.xml files
ClassLoader cl = module.getModuleClassLoader();
if(cl != null) {
Enumeration<URL> fragments = cl.getResources("META-INF/web-fragment.xml"); //$NON-NLS-1$
for(URL url : Collections.list(fragments)) {
if(log.isLoggable(Level.FINE)) {
log.fine(MessageFormat.format("Processing web.xml in {0}", module.getModuleName()));
log.fine(MessageFormat.format("Processing web-fragment.xml {0} in {1}", url, module.getModuleName()));
}
InputSource source = new InputSource(is);
parser.parseWebXml(source, webXml, false);
}

// Look for META-INF/web-fragment.xml files
ClassLoader cl = module.getModuleClassLoader();
if(cl != null) {
Enumeration<URL> fragments = cl.getResources("META-INF/web-fragment.xml"); //$NON-NLS-1$
for(URL url : Collections.list(fragments)) {
if(log.isLoggable(Level.FINE)) {
log.fine(MessageFormat.format("Processing web-fragment.xml {0} in {1}", url, module.getModuleName()));
}
InputStream fis = url.openStream();
if(fis != null) {
InputSource source = new InputSource(fis);
parser.parseWebXml(source, webXml, true);
}
InputStream fis = url.openStream();
if(fis != null) {
InputSource source = new InputSource(fis);
parser.parseWebXml(source, webXml, true);
}
}
} catch(Exception e) {
if(log.isLoggable(Level.WARNING)) {
log.log(Level.WARNING, MessageFormat.format("Encountered exception processing web.xml in {0}", module.getModuleName()), e);
}
} finally {
Thread.currentThread().setContextClassLoader(tccl);
}
return webXml;
});
// Now populate resolved params
result.getContextParams().forEach(context::setInitParameter);
}
} catch(Exception e) {
if(log.isLoggable(Level.WARNING)) {
log.log(Level.WARNING, MessageFormat.format("Encountered exception processing web.xml in {0}", module.getModuleName()), e);
}
} finally {
Thread.currentThread().setContextClassLoader(tccl);
}
return webXml;
}));
}
}
@@ -0,0 +1,23 @@
package servlet;

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

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

public class WebXmlServlet extends HttpServlet {
private static final long serialVersionUID = 1L;

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String param = getInitParameter("initGuy");

resp.setContentType("text/plain");
try(PrintWriter w = resp.getWriter()) {
w.print("I am the web.xml Servlet, and I was initialized with " + param);
}
}
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<note xmlns="http://www.lotus.com/dxl" class="form">
<item name="$TITLE">
<text>servlet/WebXmlServlet.java</text>
</item>
<item name="$Flags">
<text>34567Cg~[</text>
</item>
<item name="$FileNames">
<text>servlet/WebXmlServlet.java</text>
</item>
</note>
13 changes: 13 additions & 0 deletions eclipse/nsfs/nsf-jakartaee-example/odp/WebContent/WEB-INF/web.xml
Expand Up @@ -13,4 +13,17 @@
<param-name>jakarta.faces.FACELETS_LIBRARIES</param-name>
<param-value>/WEB-INF/exampleFacelets.taglib.xml</param-value>
</context-param>

<servlet>
<servlet-name>webxml-servlet</servlet-name>
<servlet-class>servlet.WebXmlServlet</servlet-class>
<init-param>
<param-name>initGuy</param-name>
<param-value>I was set by web.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>webxml-servlet</servlet-name>
<url-pattern>/webXmlServlet</url-pattern>
</servlet-mapping>
</web-app>
Expand Up @@ -152,7 +152,7 @@ public void testNotFound() {
WebTarget target = client.target(getRootUrl(null, TestDatabase.MAIN) + "/somefakepage.xhtml");
Response response = target.request().get();

assertEquals(404, response.getStatus());
checkResponse(404, response);

String content = response.readEntity(String.class);
assertFalse(StringUtil.isEmpty(content));
Expand Down

0 comments on commit b79675a

Please sign in to comment.