6 Replies Latest reply on Sep 16, 2010 4:30 PM by Marcel Kolsteren

    Instance lookup doesn't find implementations in WAR file.

    steven verborgh Newbie

      I have a enterprise project with an ejb and web project in it.
      The webproject uses what is an attempt at an CDI MVC framework i'm working on. The MVC framework is it's own jar. referenced by the webproject.


      in the mcv framework i have a @ApplicationScoped



      public class QualifierMappersProducer {
          @Inject
          @Any
          Instance<de.feo.mvc.spi.QualifierMapper> mappers;
      
      
          @Produces
          public  List<QualifierMapper> getMappers() {
              List<QualifierMapper> mappersLst = new LinkedList<QualifierMapper>();
              
              Iterator<de.feo.mvc.spi.QualifierMapper> mappersIter = mappers.iterator();
              while (mappersIter.hasNext()) {
      
                  de.feo.mvc.spi.QualifierMapper mapper = mappersIter.next();
                  System.out.println(mapper);
                  mappersLst.add(mapper.getClass().getAnnotation(QualifierMapper.class));
              }
      
              Collections.sort(mappersLst, new Comparator<QualifierMapper>() {
                  @Override
                  public int compare(QualifierMapper o1, QualifierMapper o2) {
                      return new Integer(o1.priority()).compareTo(new Integer(o2.priority()));
                  }
              });
              
              return mappersLst;
          }
      }



      There is also an implementation of a default QualifierMapper in this mvc project:



      @de.feo.mvc.spi.annotations.QualifierMapper(name="ControllerActionURL", priority=99)
      public class ControllerActionURLQualifierMapper implements QualifierMapper {
          private static class ControllerQualifier extends AnnotationLiteral<Controller> implements Controller {
      
              private final String value;
      
              public ControllerQualifier(String value) {
                  this.value = value;
              }
      
              @Override
              public String value() {
                  return value;
              }
          }
      
          private static class ActionQualifier extends AnnotationLiteral<Action> implements Action {
      
              private final String value;
              private final String method;
      
              public ActionQualifier(String value, String method) {
                  this.value = value;
                  this.method = method;
              }
      
              @Override
              public String value() {
                  return value;
              }
      
              @Override
              public String method() {
                  return method;
              }
      
      
          }
      
          private static final Pattern URL_PATTERN = Pattern.compile("/([^/]+)(/([^/]+))?.*");
          
          @Inject
          private HttpServletRequest req;
          
          @Override
          public boolean isRelevant() {
              String path = req.getPathInfo();
              Matcher controllerMatcher = URL_PATTERN.matcher(path);
              return controllerMatcher.matches();
          }
      
          @Override
          public List<? extends Annotation> getQualifiers() {
              String path = req.getPathInfo();
              String controllerName = null;
              String actionName = null;
              if (path != null) {
                  Matcher controllerMatcher = URL_PATTERN.matcher(path);
                  if (controllerMatcher.find()) {
                      controllerName = controllerMatcher.group(1);
                      actionName = controllerMatcher.group(3);
                  }
              }
              controllerName = controllerName == null ? "index" : controllerName;
              actionName = actionName == null ? "index" : actionName;
      
              //DETERMINE METHOD
              String httpMethod = "GET";
              String methodParam = req.getParameter("_method");
              if(methodParam == null || methodParam.trim().isEmpty()) {
                  methodParam = req.getMethod();
              } else if(Arrays.asList("DELETE", "PUT", "GET", "POST").contains(methodParam)) {
                  httpMethod = methodParam;
              }
      
      
              
              ControllerQualifier controllerQualifier = new ControllerQualifier(controllerName);
              ActionQualifier actionQualifier = new ActionQualifier(actionName, httpMethod);
              return Arrays.asList(controllerQualifier, actionQualifier);
          }
      
      }



      In the web application I have a custom QualifierMapper:


      @de.feo.mvc.spi.annotations.QualifierMapper(name = "ApplicationMapper", priority = 25)
      public class ApplicationMapper implements QualifierMapper {
      
          private static class ControllerQualifier extends AnnotationLiteral<Controller> implements Controller {
      
              private final String value;
      
              public ControllerQualifier(String value) {
                  this.value = value;
              }
      
              @Override
              public String value() {
                  return value;
              }
          }
      
          private static class ActionQualifier extends AnnotationLiteral<Action> implements Action {
      
              private final String value;
              private final String method;
      
              public ActionQualifier(String value, String method) {
                  this.value = value;
                  this.method = method;
              }
      
              @Override
              public String value() {
                  return value;
              }
      
              @Override
              public String method() {
                  return method;
              }
          }
      
          @Inject
          private HttpServletRequest req;
          
          @Override
          public boolean isRelevant() {
              String path = req.getPathInfo();
              return (path != null && (path.startsWith("/v2/")));
          }
      
          @Override
          public List<? extends Annotation> getQualifiers() {
              String path = req.getPathInfo();
              //main
              Pattern MAIN_PATTERN = Pattern.compile("/v2/main$");
              if (path != null) {
                  Matcher controllerMatcher = MAIN_PATTERN.matcher(path);
                  if (controllerMatcher.find()) {
                      ControllerQualifier controllerQualifier = new ControllerQualifier("main");
                      ActionQualifier actionQualifier = new ActionQualifier("show", req.getMethod());
                      return Arrays.asList(controllerQualifier, actionQualifier);
                  }
              }
      
              // SOME MORE PATHS
              //...
      
              // UNRECOGNIZED PATH
              return Collections.emptyList();
          }
      }




      When I run this through a servlet:


      @WebServlet(name = "DispatcherServlet", urlPatterns = {"/mvc/*"})
      @MultipartConfig(location="/Applications/NetBeans/glassfish-3.0.1/glassfish/domains/domain1/upload")
      public class DispatcherServlet extends HttpServlet {
          @Inject
          private BeanManager beanManager;
          
          @Inject
          private Event<ActionEvent> event;
      
          @Inject
          private List<de.feo.mvc.spi.annotations.QualifierMapper> mapperAnnotations;
      
          @Inject
          @Any
          private Instance<QualifierMapper> mappers;
      
          @Override
          protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              //DETERMINE QUALIFIERS FROM URLMAPPERS
              Annotation[] annotations = new Annotation[0];
              for (de.feo.mvc.spi.annotations.QualifierMapper mapperAnnotation : mapperAnnotations) {
                  System.out.println(mapperAnnotation.name());
                  QualifierMapper mapper = mappers.select(mapperAnnotation).get();
                  if(mapper.isRelevant()) {
                      annotations = (Annotation[]) mapper.getQualifiers().toArray();
                      break;
                  }
              }
      
              //FIRE EVENT WITH QUALIFIERS
      
              event.select(annotations).fire(new ActionEvent());
      
          }
      }



      It only finds the QualifierMapper in the mvc lib, and not the one in the web project


      (in a side note, i have notices that META-INF/service/javax.whatever.Extension is ignored in a war file that is inside an ear file, not the case for a standalone war file.)

        • 1. Re: Instance lookup doesn't find implementations in WAR file.
          steven verborgh Newbie

          ok, so my question was, Is this a intended behavior?


          I'm running this project on glassfish.

          • 2. Re: Instance lookup doesn't find implementations in WAR file.
            steven verborgh Newbie

            I have made a simple test project to confirm i wasn't delirious yesterday. This problem only happens when using the war project form an ear file.


            I have a jar file called TestInstance-cdi.jar with in it:



            • empty /META-INF/beans.xml

            • class de.feo.jar.TestInterface with:



            package de.feo.jar;
            
            import javax.enterprise.context.ApplicationScoped;
            import javax.enterprise.inject.Instance;
            import javax.inject.Inject;
            
            
            @ApplicationScoped
            public class TestFinder{
            
                @Inject
                Instance<TestInterface> instances;
                
                public void test() {
                    for (TestInterface testInterface : instances) {
                        System.out.println("TEST: " + testInterface.getTest());
                    }
                }
            }




            • interface de.feo.jar.TestInterface



            package de.feo.jar;
            
            public interface TestInterface {
                public String getTest();
            }




            • class de.feo.jar.Test1Impl



            package de.feo.jar;
            
            
            public class Test1Impl implements TestInterface {
            
                public String getTest() {
                    return "test1";
                }
            
            }



            I have a war file called TestInstance-web.war with in it:



            • empty /WEB-INF/beans.xml

            • class de.feo.web.Test2Impl with in it:



            package de.feo.web;
            
            import de.feo.jar.TestInterface;
            
            public class Test2Impl implements TestInterface{
            
                @Override
                public String getTest() {
                    return "Test2";
                }
            
            }




            • class de.feo.web.TestServlet with in it:



            package de.feo.web;
            
            import de.feo.jar.TestFinder;
            import java.io.IOException;
            import javax.inject.Inject;
            import javax.servlet.ServletException;
            import javax.servlet.annotation.WebServlet;
            import javax.servlet.http.HttpServlet;
            import javax.servlet.http.HttpServletRequest;
            import javax.servlet.http.HttpServletResponse;
            
            @WebServlet(name="TestServlet", urlPatterns={"/test"})
            public class TestServlet extends HttpServlet {
            
                @Inject
                TestFinder finder;
            
                @Override
                protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
                     throws ServletException, IOException {
                    finder.test();
                }
               
            }
            



            When running the project stand alone, I see both Test1 and Test2 message.
            When running the project inside a ear file i only see Test1.

            • 3. Re: Instance lookup doesn't find implementations in WAR file.
              steven verborgh Newbie

              There is also a difference in behavior depending on the packaging for ear files.


              When using the normal packaging: adding the TestInstance-cdi.jar in the lib folder of the ear it prints test1.
              When overriding the default packaging and forcing the TestInstance-cdi.jar into the WEB-INF/lib of TestInstance-web.war it prints both messages.


              • 4. Re: Instance lookup doesn't find implementations in WAR file.
                Marcel Kolsteren Apprentice

                I think that the behaviour that you see is what you'd expect when taking into account the class loader structure related to an ear deployment. The libraries in the lib folder of the ear live together in a class loader, but each of the war modules will have a separate classloader, which is a child of the ear libraries class loader. So, the ear libraries are visible from the war modules, but not the other way around.


                More background on this can be found here (section 21.3)


                In terms of CDI beans, you could say that a class loader corresponds to what is called a Bean Deployment Archive (BDA) in CDI. Beans and bean instances belong to Bean Deployment Archives, and those archives form a graph, with visibility relationships between the nodes of the graph. See Appendix A.1.1 of the Weld documentation.


                So in case of an ear packaging, the expected behaviour is that bean instances from the regular war classes can see bean instances from the war libraries and vice versa (they're in the same class loader and thus in the same BDA). The ear-level libraries are visible from beans that belong to the war modules, but not the other way around (they're in class loaders that have a parent-child relationship and thus in different BDAs).


                So, I think you should include your MVC framework in the war module as a library, or find a solution where the MVC framework doesn't need to inject beans that are in the application. In the latter case, you might investigate an event-based solution. Probably an event fired from BDA 1 can be observed by another BDA 2, where BDA 2 can see BDA 1 but not the other way around. But I'm not sure about that.

                • 5. Re: Instance lookup doesn't find implementations in WAR file.
                  steven verborgh Newbie

                  I'm aware of the classloader problem. To my mind it's a showstopper. It means when you create a framework, distributed in a jar with beans.xml in <ear>/lib, where you allow the user to supply a custom implementation of a certain interface. e.g. a ViewResolver that you try to load in the framework via Instance<ViewResolver> you will only find the ViewResolvers in the 'lib' folder of the ear, not those supplied in de WEB-INF/classes of the user. You could work around it by requiring the user to put his custom ViewResolver also in a jar file (that ends up in <ear>/lib)



                  Probably an event fired from BDA 1 can be observed by another BDA 2, where BDA 2 can see BDA 1 but not the other way around. But I'm not sure about that.

                  I tried something similar, but alas.


                  I tried registering them through an Extension with ProcessManagedBean, filtering based on the annotation, this gives me the class, but to create it you need to use bean.create, but ... this is fun ... the class is not know in the classloader of the injected beanManager...


                  I could require the user to supply a provider for List<ViewResolver>, but thats just unnecessary plumbing code.


                  (I tried every possible combination of manipulating the classloader too. through sun-web.xml, through the Class-Path entry in manifest. And i still doesn't allow me to use interceptors and decorators inside an war/ear combo)


                  anyway, time wasted, lessons learned, moving on :)

                  • 6. Re: Instance lookup doesn't find implementations in WAR file.
                    Marcel Kolsteren Apprentice

                    For deployments that rely on EJB 3.0 ear packaging, I can imagine that this is a problem. But as from EJB 3.1, enterprise beans can live everywhere, so why not just include the framework library in the lib directory of the war file? In most deployments, you have only one war inside of an ear, so there's not really something to share at ear level.


                    In fact, the Seam 3 framework is also being built on top of CDI. I wonder what the experienced Seam 3 developers have to say about the framework packaging problem you're experiencing.