Monday, July 7, 2014

OSGI servlet bridge sample


Servlet bridge is a mechanism for extending your servlet functionality adding other servlets. In this case your main servlet is not affected within some changes however the number of processed request is increased by newly added servlets (modules).
We don't want to have some sophisticated way to register/unregister new modules, don't want to be forced to rebuild the whole application when the new servlet is registered. OSGI is a keyword which helps us to avoid of this issues. In the scope of Servlet Bridge feature OSGI is a powerful mechanism which can help you to extend the functionality of your java servlet application. It can be achieved by using HttpService implementation provided by OSGI standards. Image source: http://www.jayway.com/2007/05/01/osgi-not-just-a-four-letter-word/
(image from: http://www.jayway.com/2007/05/01/osgi-not-just-a-four-letter-word/)
This post is a good way to start with theoretical basics of OSGI, let's try to implement the OSGI servlet bridge.



I'd like to use Apache Felix libraries to implement it; there is some guidline and reference infromation about the felix services: http://felix.apache.org/documentation/subprojects/apache-felix-http-service.html On the felix web page you can find a code snippet with an example how to implement only a "bridged" servlet; in this article you also can find a full sample with all required nodes for servlet-bridge.

<Part 1> Servlet bridge
Create simple maven web project

mvn archetype:generate -DgroupId={project-packaging} -DartifactId={project-name} -DarchetypeArtifactId=maven-archetype-webapp -DinteractiveMode=false

Download felix http proxy jar (http://mvnrepository.com/artifact/org.apache.felix/org.apache.felix.http.proxy) put this jar to src/main/webapp/WEB-INF/lib/ folder.

Modify web.xml (src/main/webapp/WEB-INF/web.xml)

<web-app>
  <display-name>Archetype Created Web Application</display-name>
   <listener>
        <listener-class>com.vbashur.serv.StartupListener</listener-class>
    </listener>

    <listener>
        <listener-class>org.apache.felix.http.proxy.ProxyListener</listener-class>
    </listener>

    <servlet>
        <servlet-name>proxy</servlet-name>
        <servlet-class>org.apache.felix.http.proxy.ProxyServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>proxy</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
</web-app>

Now all your web requests will be processed by felix http proxy.
Take a look on listeners in web.xml. There is one custom listener (StartupListener) which is going to be an entry point for OSGI framework initialization. Felix provides embedding of OSGI framework and that's what we gonna have in the code.

public class StartupListener implements ServletContextListener {

 private FrameworkService service;

 public StartupListener() {
 }

 public void contextDestroyed(ServletContextEvent arg0) {  
  this.service.stop();

 }

 public void contextInitialized(ServletContextEvent arg0) {  
  this.service = new FrameworkService(arg0.getServletContext());
  this.service.start();

 }
}

FrameworkService configures OSGI settings

public final class FrameworkService
{
    private final ServletContext context;
    private Felix felix;

    public FrameworkService(ServletContext context)
    {
        this.context = context;
    }

    public void start()
    {
        try {
            doStart();
        } catch (Exception e) {
            System.err.println("("Failed to start framework", e);
        }
    }

    public void stop()
    {
        try {
            doStop();
        } catch (Exception e) {
            System.err.println("("Error stopping framework", e);
        }
    }

    private void doStart()
        throws Exception
    {
     Felix tmp = new Felix(createConfig());       
        tmp.start();
        this.felix = tmp;
    }

    private void doStop()
        throws Exception
    {
        if (this.felix != null) {
            this.felix.stop();
        }
    }

    private Map<String, Object> createConfig()
        throws Exception
    {
        Properties props = new Properties();        
        props.load(this.context.getResourceAsStream("/WEB-INF/framework.properties"));

        HashMap<String, Object> map = new HashMap<String, Object>();
        for (Object key : props.keySet()) {
            map.put(key.toString(), props.get(key));
        }              
        map.put(FelixConstants.SYSTEMBUNDLE_ACTIVATORS_PROP, Arrays.asList(new ProvisionActivator(this.context)));
        return map;
    }
}

Did you see the resourse path in the code above? What is the framework.properties file? This is about how your OSGI framework launches and works, more information is here

framework.properties
org.osgi.framework.storage.clean = onFirstInit
org.osgi.framework.system.packages.extra = javax.servlet;org.json;org.xml.sax;javax.servlet.http;version=3.0.1;javax.servlet.descriptor

org.apache.felix.http.debug = true

Provision activator installs your bundles using bundle context.

public class ProvisionActivator implements BundleActivator {

 private final static String B_PATH = "/WEB-INF/bundles/";

 private final ServletContext servletContext;

 public ProvisionActivator(ServletContext servletContext) {
  this.servletContext = servletContext;
 }

 public void start(BundleContext context) throws Exception {
  servletContext.setAttribute(BundleContext.class.getName(), context);
  ArrayList<Bundle> installed = new ArrayList<Bundle>();
  for (URL url : findBundles()) {
   this.servletContext.log("Installing bundle [" + url + "]");
   Bundle bundle = context.installBundle(url.toExternalForm());
   installed.add(bundle);
  }

  for (Bundle bundle : installed) {
   if (!isFragment(bundle)) {   
    bundle.start();
   }
  }
 }

 public void stop(BundleContext arg0) throws Exception {
  // TODO Auto-generated method stub

 }
 
 private boolean isFragment(Bundle bundle) {
  return bundle.getHeaders().get(Constants.FRAGMENT_HOST) != null;
 }

 private List<URL> findBundles() throws Exception {
  ArrayList<URL> list = new ArrayList<URL>();
  for (Object o : this.servletContext.getResourcePaths(B_PATH)) {
   String name = (String) o;
   if (name.endsWith(".jar")) {
    URL url = this.servletContext.getResource(name);
    if (url != null) {
     list.add(url);
    }
   }
  }

  return list;
 }

}
B_PATH variable points to the directory with your bundles. Having only sevlet bundle here is not enough for our bridge, we also need to add felix http bridge bundle and its dependency org.osgi.compendium. Get it using the links below:
http://mvnrepository.com/artifact/org.apache.felix/org.apache.felix.http.bridge
http://mvnrepository.com/artifact/org.apache.felix/org.osgi.compendium

pom.xml dependencies

        <dependencies>
  <dependency>
   <groupId>javax.servlet</groupId>
   <artifactId>javax.servlet-api</artifactId>
   <version>3.0.1</version>
   <scope>provided</scope>
  </dependency>

  <dependency>
   <groupId>org.apache.felix</groupId>
   <artifactId>org.apache.felix.framework</artifactId>
   <version>4.4.0</version>
  </dependency>

  <dependency>
   <groupId>org.apache.felix</groupId>
   <artifactId>org.apache.felix.main</artifactId>
   <version>4.4.0</version>
  </dependency>
 </dependencies>


<Part 2> Servlet bundle
Now we're done with bridge, let's make a bundle. Use maven again.

mvn archetype:generate -DgroupId={project-packaging} -DartifactId={project-name} -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false

We only need three classes for the simple demo.

1. Activator (implements BundleActivator) - this class is required for handling such events from OSGI framework as start and stop the bundle. Calling bundle.start() from ProvisionActivator will be handled by 'start' method here.

public class Activator implements BundleActivator {

 private static BundleContext context;

 private ServiceTracker httpServiceTracker;

 static BundleContext getContext() {
  return context;
 }

 public void start(BundleContext bundleContext) throws Exception {  
  Activator.context = bundleContext;
  httpServiceTracker = new HttpServiceTracker(context);
  httpServiceTracker.open();  
 }

 public void stop(BundleContext bundleContext) throws Exception {
  Activator.context = null;
  httpServiceTracker.close();
 }

}

2. HttpServiceTracker (extends ServiceTracker) checks whether HttpService instance is available in a bundle context, once it's found we can use the instance of HttpService to register our servlets.

public class HttpServiceTracker extends ServiceTracker {
 
 private BundleContext context;

 public HttpServiceTracker(BundleContext context) {
  super(context, HttpService.class.getName(), null);
  this.context = context;  
 }

 public Object addingService(ServiceReference reference) {
  HttpService httpService = (HttpService) context.getService(reference);
  System.out.println("Adding HTTP Service");
  try {
   httpService.registerServlet(TestServlet.SERVLET_ALIAS, new TestServlet(), null, null);
  } catch (ServletException | NamespaceException e) {
   System.err.println("Servlet couldn't be registered: " + e.getMessage());
  } 
  return httpService;
 }

 public void removedService(ServiceReference reference, Object service) {
  super.removedService(reference, service);  
 } 
}

3. Finally TestServlet

public class TestServlet extends HttpServlet {
 
    public static final String SERVLET_ALIAS = "/service";
 
    private static final long serialVersionUID = 1L;
 
    public TestServlet() {        
    }
    
    @Override
    public void init(ServletConfig config)
        throws ServletException
    {        
        super.init(config);  
    }

    @Override
    public void destroy()
    {        
        super.destroy();
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse res)
        throws ServletException, IOException
    {
        res.setContentType("text/plain");
        PrintWriter out = res.getWriter();

        out.println("Request = " + req);
        out.println("PathInfo = " + req.getPathInfo());
    }
}

We're not done yet, one important part is a bundle build, maven-bundle-plugin rulezzz.
pom.xml

<build>
 <plugins>
  <plugin>
   <groupId>org.apache.felix</groupId>
   <artifactId>maven-bundle-plugin</artifactId>
   <extensions>true</extensions>
   <configuration>
    <instructions>
     <Bundle-Activator>
      com.vbashur.bundle.Activator
     </Bundle-Activator>
     <Private-Package>
      com.vbashur.bundle.*
     </Private-Package>
     <Import-Package>
      *;resolution:=optional
     </Import-Package>
    </instructions>
   </configuration>
  </plugin>
  <plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-source-plugin</artifactId>
   <version>2.0.4</version>
   <executions>
    <execution>
     <id>attach-sources</id>
     <goals>
      <goal>jar</goal>
     </goals>
    </execution>
   </executions>
  </plugin>
 </plugins>
</build>


<dependencies>
        <dependency>
  <groupId>javax.servlet</groupId>
  <artifactId>javax.servlet-api</artifactId>
  <version>3.0.1</version>
  <scope>provided</scope>
 </dependency>  
 
 <dependency>
  <groupId>org.apache.felix</groupId>
  <artifactId>org.osgi.compendium</artifactId>
  <version>1.4.0</version>
 </dependency>
 <dependency>
  <groupId>org.apache.felix</groupId>
  <artifactId>org.apache.felix.http.api</artifactId>
  <version>2.3.0</version>
 </dependency>
</dependencies>

And don't forget to specify packaging type

<packaging>bundle</packaging>

Build the servlet bundle with 'mvn clean install' command and put the result jar to the bundles directory of the servlet bridge (/WEB-INF/bundles/).

In this sample if you deploy servletbridge on the web server and type http://localhost:8080/servletbridge/service in a browser you'll be taken to the page processed by servlet bundle. And that's the meaning of the servlet bridge.

In enterprise software production code I saw a sample when the core part and logic is implemented in a main servlet (servletmain) and  some additional features are implemented in a bundle servlets, between these items there is a servlet bridge layer. Bridge and the main servlet are running in a one web application container. Frankly speaking I don't think that it's a good idea to do so, cause it makes you to have two java applications deployed in one servlet contatiner. Therefore you may use one web application to serve all your needs setting up URL mapping to handle requests to the main servlet and to the servlet bridge. For example:

<listener>
 <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener> 
<listener>
        <listener-class>com.vbashur.serv.StartupListener.StartupListener</listener-class>
</listener> 
<listener>
        <listener-class>org.apache.felix.http.proxy.ProxyListener</listener-class>
</listener>
<servlet>
        <servlet-name>module</servlet-name>
        <servlet-class>org.apache.felix.http.proxy.ProxyServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
</servlet>
<servlet>
 <servlet-name>dispatcher</servlet-name>
 <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>  
 <load-on-startup>2</load-on-startup>
</servlet> 
<servlet-mapping>
 <servlet-name>dispatcher</servlet-name>
 <url-pattern>/</url-pattern>
</servlet-mapping>
 
<servlet-mapping>
 <servlet-name>module</servlet-name>
 <url-pattern>/module/*</url-pattern>
</servlet-mapping>

Anyway, if you prefer two different applications you can try and play with it using the source code from bitbucket (see link below) launching servletmain and servletbridge instances: localhost:8080/servletmain takes you to a main servlet web page, localhost:8080/servletbridge/service takes you to a servlet bundle.

Good luck!

Source code: https://bitbucket.org/vbashur/diff/src

1 comment:

  1. I followed the steps mentioned and was able to run the plugins which were already available in the bundle directory at the time of tomcat startup.
    What change is required to install bundle at runtime (when server is running)?

    ReplyDelete