Search code examples
javajettyjndiembedded-jetty

JNDI Lookup Failing For Embedded Jetty Server


I have an integration test in one of my projects that I want to run against an embedded jetty server. I followed along with the example here: https://www.eclipse.org/jetty/documentation/jetty-9/index.html#jndi-embedded but when I go to actually run my test it fails with the error:

javax.naming.NameNotFoundException: env is not bound; remaining name 'env/jdbc/NavDS'
    at org.eclipse.jetty.jndi.NamingContext.getContext(NamingContext.java:241)
    at org.eclipse.jetty.jndi.NamingContext.lookup(NamingContext.java:491)
    at org.eclipse.jetty.jndi.NamingContext.lookup(NamingContext.java:491)
    at org.eclipse.jetty.jndi.NamingContext.lookup(NamingContext.java:505)
    at org.eclipse.jetty.jndi.java.javaRootURLContext.lookup(javaRootURLContext.java:101)
    at javax.naming.InitialContext.lookup(InitialContext.java:417)
    at com.tura.eyerep.test.TestWebServices.setUpBeforeClass(TestWebServices.java:63)

I'm sure there must be a simple mistake I'm making somewhere but I just can't seem to spot it. Can anyone give a suggestion of what I'm doing wrong here?

In my test I'm setting up the server with:

@BeforeClass
public static void setUpBeforeClass() throws Exception {
            
    server = new Server(8080);
    ClassList classList = ClassList.setServerDefault(server);
    classList.addAfter("org.eclipse.jetty.webapp.FragmentConfiguration", "org.eclipse.jetty.plus.webapp.EnvConfiguration", "org.eclipse.jetty.plus.webapp.PlusConfiguration");
    
    WebAppContext context = new WebAppContext();
    context.setExtractWAR(false);
    context.setDescriptor("src/main/webapp/WEB-INF/web.xml");
    context.setResourceBase("src/main/webapp");
    context.setConfigurationDiscovered(false);  
    
    BasicDataSource ds = null;      
    ds = new BasicDataSource();
    ds.setUrl("jdbc:h2:mem:myDB;create=true;MODE=MSSQLServer;DATABASE_TO_UPPER=FALSE;");
    org.eclipse.jetty.plus.jndi.Resource mydatasource = new org.eclipse.jetty.plus.jndi.Resource(context, "jdbc/NavDS",ds);
    
    server.setHandler(context);
    server.start();
}

@Test
public void testLookup()
{
    InitialContext ctx = new InitialContext();
    DataSource myds = (DataSource) ctx.lookup("java:comp/env/jdbc/NavDS");
    assertNotNull( myds);
}

In my web.xml I have a resource ref entry:

  <resource-ref>
    <description>Nav Datasource</description>
    <res-ref-name>jdbc/NavDS</res-ref-name>
    <res-type>javax.sql.DataSource</res-type>
    <res-auth>Container</res-auth>
  </resource-ref>

Solution

  • Lets cleanup your testcase first.

    // meaningless when you have a WAR that is a directory.
    context.setExtractWAR(false); // remove this line
    
    // This prevents Servlet 3 behaviors when using Servlet 2.x descriptors
    context.setConfigurationDiscovered(false); // Remove this
    

    The error you are getting ...

    javax.naming.NameNotFoundException: env is not bound; 
      remaining name 'env/jdbc/NavDS'
    

    That likely means that a server is still running somewhere, probably forgot to stop/cleanup the previous server instance. (Look at the example junit5 testcase below, for how it deals with non-fixed ports, how to reference a non-fixed port, and how it stops the lifecycle of the server).

    Next, be aware of your scopes.

    new org.eclipse.jetty.plus.jndi.Resource(context, "jdbc/NavDS",ds);
    

    That will bind the resource entry to jdbc/NavDS on the scope context.

    Which means if you look up the resource outside of the WebAppContext scope, like you do with testLookup() method it will exist at initialContext.lookup("jdbc/NavDS"), and nowhere else, the java:comp/env prefix/tree doesn't even exist to that testLookup() method scope.

    Inside of your webapp, such as in a Filter or Servlet, that context specific resource is bound and available at jdbc:comp/env/jdbc/NavDS.

    You have 3 typical scopes.

    Order Scope EnvEntry or Resource first parameter
    1 WebApp new EnvEntry(webappContext, ...) or new Resource(webappContext, ...)
    2 Server new EnvEntry(server, ...) or new Resource(server, ...)
    3 JVM new EnvEntry(null, ...) or new Resource(null, ...)

    If the value doesn't exist at the WebApp scope, the Server scope is checked, and then the JVM scope is checked.

    Your Server can have a value for the name val/foo and a specific webapp can have a different value for the same name val/foo, simply by how the scopes are defined.

    Next, there's the binding in the Servlet spec, you have specify the <resource-ref> and this combined with the declaration at the server side, bound to context means you can access java:comp/env/jdbc/NavDS from your servlet in that specific webapp.

    To see this a different way, in code ...

    package jetty.jndi;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.PrintStream;
    import java.net.HttpURLConnection;
    import java.nio.charset.StandardCharsets;
    import java.nio.file.Paths;
    import java.util.List;
    import javax.naming.InitialContext;
    import javax.naming.NameNotFoundException;
    import javax.naming.NamingException;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.eclipse.jetty.plus.webapp.EnvConfiguration;
    import org.eclipse.jetty.plus.webapp.PlusConfiguration;
    import org.eclipse.jetty.server.Server;
    import org.eclipse.jetty.util.IO;
    import org.eclipse.jetty.util.component.LifeCycle;
    import org.eclipse.jetty.util.resource.PathResource;
    import org.eclipse.jetty.webapp.Configuration;
    import org.eclipse.jetty.webapp.FragmentConfiguration;
    import org.eclipse.jetty.webapp.WebAppContext;
    import org.junit.jupiter.api.AfterAll;
    import org.junit.jupiter.api.BeforeAll;
    import org.junit.jupiter.api.Test;
    
    public class WebAppWithJNDITest
    {
        private static Server server;
        private static WebAppContext context;
    
        public static class JndiDumpServlet extends HttpServlet
        {
            @Override
            protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
            {
                resp.setCharacterEncoding("utf-8");
                resp.setContentType("text/plain");
                PrintStream out = new PrintStream(resp.getOutputStream(), false, StandardCharsets.UTF_8);
                try
                {
                    dumpJndi(out);
                }
                catch (NamingException e)
                {
                    throw new ServletException(e);
                }
            }
        }
    
        @BeforeAll
        public static void startServer() throws Exception
        {
            server = new Server(0); // let os/jvm pick a port
            Configuration.ClassList classList = Configuration.ClassList.setServerDefault(server);
            classList.addAfter(FragmentConfiguration.class.getName(),
                EnvConfiguration.class.getName(),
                PlusConfiguration.class.getName());
    
            context = new WebAppContext();
            context.setContextPath("/");
            // This directory only has WEB-INF/web.xml
            context.setBaseResource(new PathResource(Paths.get("src/main/webroots/jndi-root")));
            context.addServlet(JndiDumpServlet.class, "/jndi-dump");
    
    
            new org.eclipse.jetty.plus.jndi.Resource(null, "val/foo", Integer.valueOf(707));
            new org.eclipse.jetty.plus.jndi.Resource(server, "val/foo", Integer.valueOf(808));
            new org.eclipse.jetty.plus.jndi.Resource(context, "val/foo", Integer.valueOf(909));
    
            new org.eclipse.jetty.plus.jndi.EnvEntry(null, "entry/foo", Integer.valueOf(440), false);
            new org.eclipse.jetty.plus.jndi.EnvEntry(server, "entry/foo", Integer.valueOf(550), false);
            new org.eclipse.jetty.plus.jndi.EnvEntry(context, "entry/foo", Integer.valueOf(660), false);
    
            server.setHandler(context);
            server.start();
        }
    
        @AfterAll
        public static void stopServer()
        {
            LifeCycle.stop(server);
        }
    
        public static void dumpJndi(PrintStream out) throws NamingException
        {
            InitialContext ctx = new InitialContext();
    
            List<String> paths = List.of("val/foo", "entry/foo");
            List<String> prefixes = List.of("java:comp/env/", "");
    
            for (String prefix : prefixes)
            {
                for (String path : paths)
                {
                    try
                    {
                        Integer val = (Integer)ctx.lookup(prefix + path);
                        out.printf("lookup(\"%s%s\") = %s%n", prefix, path, val);
                    }
                    catch (NameNotFoundException e)
                    {
                        out.printf("lookup(\"%s%s\") = NameNotFound: %s%n", prefix, path, e.getMessage());
                    }
                }
            }
        }
    
        @Test
        public void testLookup() throws NamingException, IOException
        {
            System.out.println("-- Dump from WebApp Scope");
            HttpURLConnection http = (HttpURLConnection)server.getURI().resolve("/jndi-dump").toURL().openConnection();
            try (InputStream in = http.getInputStream())
            {
                String body = IO.toString(in, StandardCharsets.UTF_8);
                System.out.println(body);
            }
    
            System.out.println("-- Dump from Test scope");
            dumpJndi(System.out);
        }
    }
    

    Contents of src/main/webroot/jndi-root/WEB-INF/web.xml

    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
             http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
      version="3.1">
    
      <resource-ref>
        <description>My Foo Resource</description>
        <res-ref-name>val/foo</res-ref-name>
        <res-type>java.lang.Integer</res-type>
        <res-auth>Container</res-auth>
      </resource-ref>
    </web-app>
    

    The output looks like ...

    2021-10-26 17:05:16.834:INFO:oejs.Server:main: jetty-9.4.44.v20210927; built: 2021-06-30T11:07:22.254Z; git: 526006ecfa3af7f1a27ef3a288e2bef7ea9dd7e8; jvm 11.0.12+7
    2021-10-26 17:05:17.012:INFO:oejsh.ContextHandler:main: Started o.e.j.w.WebAppContext@19932c16{/,file:///home/joakim/code/jetty/junk/src/main/webroots/jndi-root/,AVAILABLE}
    2021-10-26 17:05:17.033:INFO:oejs.AbstractConnector:main: Started ServerConnector@212b5695{HTTP/1.1, (http/1.1)}{0.0.0.0:45387}
    2021-10-26 17:05:17.034:INFO:oejs.Server:main: Started @816ms
    -- Dump from WebApp Scope
    lookup("java:comp/env/val/foo") = 909
    lookup("java:comp/env/entry/foo") = 660
    lookup("val/foo") = 707
    lookup("entry/foo") = 440
    
    -- Dump from Test scope
    lookup("java:comp/env/val/foo") = NameNotFound: env is not bound
    lookup("java:comp/env/entry/foo") = NameNotFound: env is not bound
    lookup("val/foo") = 707
    lookup("entry/foo") = 440
    2021-10-26 17:05:17.209:INFO:oejs.AbstractConnector:main: Stopped ServerConnector@212b5695{HTTP/1.1, (http/1.1)}{0.0.0.0:0}
    2021-10-26 17:05:17.210:INFO:oejs.session:main: node0 Stopped scavenging
    2021-10-26 17:05:17.214:INFO:oejsh.ContextHandler:main: Stopped o.e.j.w.WebAppContext@19932c16{/,file:///home/joakim/code/jetty/junk/src/main/webroots/jndi-root/,STOPPED}
    

    Hopefully the scope differences are obvious above.

    A variation of the above test is now available at the Eclipse Jetty Embedded Cookbook project.

    https://github.com/jetty-project/embedded-jetty-cookbook/

    Available in 3 different Jetty flavors