How to Customize and Extend opentaps 2 with your own OSGi Bundle

From Opentaps Wiki
Revision as of 22:52, 9 March 2012 by Sichen (talk | contribs)
Jump to navigationJump to search

Ok, you've been patient and reading through all the other tutorials. But at some point, you've probably wondered -- what's the big idea here?

This tutorial will show you.

The most important thing for successful enterprise software is modularity: Separate and maintainable modules for each part of the application. OSGi lets you extend and customize your application with your own bundle. Even better, it lets you manage those bundles in real time, so you can manage your application by loading and unloading bundles from the Geronimo console.

In this tutorial, we will add security permissions to our opentaps 2 Notes application and then show you how to implement an OSGi bundle which overrides the base permission with your own. You can then see how you can deploy and undeploy the bundle in real time to change the security permissions of your system without restarting the server.

The first step is to create a new package, org.opentaps.notes.security, and a new interface NoteSecurity. All it does is define some operations and a security method:

public interface NoteSecurity {

    public static enum Operation {CREATE, UPDATE, DELETE};
    
    public boolean hasPermission(Note note, Operation permissionId);

    public String getErrorMessage();    
}

Then, I implement my security feature with a new module in notes/impl/security. I follow the steps from [[]] to create a new module. The code itself is trivial:

public class NoteSecurityImpl implements NoteSecurity {

    @Override
    public boolean hasPermission(Note note, String permissionId) {
        Log.logInfo("This test implementation will always return true, so I will let you create your note.");
        return true;
    }

}

Next, I modify my CreateNoteServiceImpl to check security before creating the Note:

        if (!security.hasPermission(note, NoteSecurity.Operation.CREATE)) {
            throw new ServiceException(security.getErrorMessage());
        }

But how do we get the security implementation into the service? This is done in an Inversion Of Control (IOC) pattern. First, we provide for the security with a hook in CreateNoteServiceImpl:

    private volatile NoteSecurity security = null;

    public void setNoteSecurity(NoteSecurity noteSecurity) {
        if (security == null && noteSecurity != null) {
            synchronized (this) {
                if (security == null) {
                    security = noteSecurity;
                }
            }
        }
    }

Now, we won't put anything in the code for where the security there. This is good, because when we change the security implementation later, we don't have to change the create note service. Instead, we'll use OSGI blueprint to wire the two together. I'll put some directives to modules/notes/impl/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml:

  <reference id="noteSecurityService" interface="org.opentaps.notes.security.NoteSecurity"/>

  <bean id="createNoteServiceImpl" class="org.opentaps.notes.services.impl.CreateNoteServiceImpl">
        <property name="noteSecurity" ref="noteSecurityService"/>
    </bean>   

What we're saying is that there is something called noteSecurityService, it is org.opentaps.notes.security.NoteSecurity, and we will use the CreateNoteServiceImpl.setNoteSecurity method to set it. This is equivalent to:

CreateNoteServiceImpl obj = new CreateNoteServiceImpl();
obj.setNoteRepository(InitialContext.lookup("org.opentaps.notes.repository.NoteRepository"));

where org.opentaps.notes.repository.NoteRepository in value of property osgi.jndi.service.name of the service.

In my security implementation module, I will also have a blueprint XML file, in modules/notes/impl/security/src/main/resources/OSGI-INF/blueprint/blueprint.xml. Here is where I define:

   <bean id="noteSecurityImpl" class="org.opentaps.notes.security.impl.NoteSecurityImpl">
   </bean>

   <service id="noteSecurityService" ref="noteSecurityImpl" interface="org.opentaps.notes.security.NoteSecurity">
        <service-properties>
            <entry key="osgi.jndi.service.name" value="org.opentaps.notes.security.NoteSecurity"/>
        </service-properties>
    </service>

Which says "I am implementing org.opentaps.notes.security.NoteSecurity with org.opentaps.notes.security.impl.NoteSecurityImpl." Note you have to define the entry key for osgi.jndi.service.name, or this name can be used to find service with service registry but not with JNDI lookup. Also, it is important to note that it is the full interface class name (org.opentaps.notes.security.NoteSecurity) which is used to link the implementation to the interface, not the service or bean id attribute.

I use $ mvn clean install to build and create my packages. When I try to deploy my EBA with Geronimo, though, I get this error message:

The system cannot provision the EBA org.opentaps.notes.eba because the following problems in the dependency chain were detected: 
The package dependency org.opentaps.notes.security required by bundle org.opentaps.notes.services.impl_2.0.1.SNAPSHOT cannot be resolved.

At first, this seemed to be a strange error, because my org.opentaps.notes.security is in the jar file of the org.opentaps.notes.services package:


$ jar tvf modules/notes/api/services/target/org.opentaps.notes.services.api-2.0.1-SNAPSHOT.jar
 ...
  1258 Wed Mar 07 16:29:08 PST 2012 org/opentaps/notes/security/NoteSecurity$Operation.class
   395 Wed Mar 07 16:29:08 PST 2012 org/opentaps/notes/security/NoteSecurity.class
 ...

and this package was being exported during $ mvn install:

[INFO] Copying artifact[org.opentaps, org.opentaps:notes.services.api:jar:2.0.1-SNAPSHOT, compile]

The problem, though, was that this package was not marked for export, so it wasn't loading when the modules were being deployed. I fixed it by adding it to the list of export packages:

        <plugins>
            <plugin>
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-bundle-plugin</artifactId>
                <extensions>true</extensions>
                <configuration>
                    <instructions>
                        <Export-Package>
                            org.opentaps.notes.services,
                            org.opentaps.notes.services.security,
                            org.opentaps.notes.security
                        </Export-Package>
                    </instructions>
                </configuration>
            </plugin>
        </plugins>

This got it to build and deploy successfully, but when I tried to create a note, I started to get these errors:

2012-03-08 08:45:47,762 ERROR [core] osgi:service/org.opentaps.notes.services.CreateNoteService
javax.naming.NameNotFoundException: osgi:service/org.opentaps.notes.services.CreateNoteService
	at org.apache.aries.jndi.url.ServiceRegistryContext.lookup(ServiceRegistryContext.java:110)
	at org.apache.aries.jndi.url.ServiceRegistryContext.lookup(ServiceRegistryContext.java:141)
	at org.apache.aries.jndi.DelegateContext.lookup(DelegateContext.java:161)
	at javax.naming.InitialContext.lookup(InitialContext.java:392)
	at org.opentaps.notes.rest.NoteResource.createNote(NoteResource.java:153)

...
2012-03-08 08:50:32,039 ERROR [BlueprintContainerImpl] Unable to start blueprint container for bundle 
org.opentaps.notes.services.impl due to unresolved dependencies [(objectClass=org.opentaps.notes.repository.NoteRepository), 
(objectClass=org.opentaps.validation.services.ValidationService), (objectClass=org.opentaps.notes.security.NoteSecurity)]
java.util.concurrent.TimeoutException
	at org.apache.aries.blueprint.container.BlueprintContainerImpl$1.run(BlueprintContainerImpl.java:287)

The problem, as it turns out, is that I have not deployed my implementation of org.opentaps.notes.security.NoteSecurity to the EBA. Think of the EBA as a set of bundles running in separate OSGi framework inside main OSGi framework. Its dependencies with compile scope (or without scope as "compile" is default) are assembled into single EBA archive at runtime. Thus, maven does not detect that all the necessary implementations are there, because this happens at runtime. Looking through my $ mvn install log, I notice that there is org.opentaps.notes.security.impl.jar being copied:

[INFO] --- eba-maven-plugin:0.3:eba (default-eba) @ notes.eba ---
...
[INFO] Copying artifact[org.opentaps, org.opentaps:core:jar:2.0.1-SNAPSHOT, compile]
[INFO] Copying artifact[org.opentaps, org.opentaps:entity:jar:2.0.1-SNAPSHOT, compile]
[INFO] Copying artifact[org.opentaps, org.opentaps:rest:jar:2.0.1-SNAPSHOT, compile]
[INFO] Copying artifact[org.opentaps, org.opentaps:notes.rest:jar:2.0.1-SNAPSHOT, compile]
[INFO] Copying artifact[org.opentaps, org.opentaps:notes.domain:jar:2.0.1-SNAPSHOT, compile]
[INFO] Copying artifact[org.opentaps, org.opentaps:notes.services.api:jar:2.0.1-SNAPSHOT, compile]
[INFO] Copying artifact[org.opentaps, org.opentaps:notes.repository.impl:jar:2.0.1-SNAPSHOT, compile]
[INFO] Copying artifact[org.opentaps, org.opentaps:notes.services.impl:jar:2.0.1-SNAPSHOT, compile]
[INFO] Copying artifact[org.opentaps, org.opentaps:validation.api:jar:2.0.1-SNAPSHOT, compile]
[INFO] Copying artifact[org.opentaps, org.opentaps:validation.impl:jar:2.0.1-SNAPSHOT, compile]
[INFO] Building zip: /Users/sichen/git/opentaps-2/modules/notes/eba/target/org.opentaps.notes.eba-2.0.1-SNAPSHOT.eba

So I have to add it to the EBA's pom.xml in modules/notes/eba/pom.xml, with project groupId and version which are defined in the notes/pom.xml:

        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>notes.security.impl</artifactId>
            <version>${project.version}</version>
        </dependency>

When I build it again with $ mvn install, I see my org.opentaps.security.impl being copied into the EBA:

[INFO] --- eba-maven-plugin:0.3:eba (default-eba) @ notes.eba ---
...
[INFO] Copying artifact[org.opentaps, org.opentaps:core:jar:2.0.1-SNAPSHOT, compile]
[INFO] Copying artifact[org.opentaps, org.opentaps:entity:jar:2.0.1-SNAPSHOT, compile]
[INFO] Copying artifact[org.opentaps, org.opentaps:rest:jar:2.0.1-SNAPSHOT, compile]
[INFO] Copying artifact[org.opentaps, org.opentaps:notes.rest:jar:2.0.1-SNAPSHOT, compile]
[INFO] Copying artifact[org.opentaps, org.opentaps:notes.domain:jar:2.0.1-SNAPSHOT, compile]
[INFO] Copying artifact[org.opentaps, org.opentaps:notes.services.api:jar:2.0.1-SNAPSHOT, compile]
[INFO] Copying artifact[org.opentaps, org.opentaps:notes.repository.impl:jar:2.0.1-SNAPSHOT, compile]
[INFO] Copying artifact[org.opentaps, org.opentaps:notes.services.impl:jar:2.0.1-SNAPSHOT, compile]
[INFO] Copying artifact[org.opentaps, org.opentaps:notes.security.impl:jar:2.0.1-SNAPSHOT, compile]
[INFO] Copying artifact[org.opentaps, org.opentaps:validation.api:jar:2.0.1-SNAPSHOT, compile]
[INFO] Copying artifact[org.opentaps, org.opentaps:validation.impl:jar:2.0.1-SNAPSHOT, compile]

When I deploy my new EBA and create a note, I'll see this message telling me my security implementation is running:

2012-03-09 12:46:44,207 INFO  [core] This test implementation will always return true, so I will let you create your note.

Success! I've earned myself a cookie break. When I come back, I'll work on extending the security implementation with a separate bundle and deploying it at runtime.

Now let's start with an alternative implementation of the security interface. First I create a modules/notes/impl/security2 directory and copy everything over from modules/notes/impl/security:

$ cp -r modules/notes/impl/security/ modules/notes/impl/security2/

Then I need to give its pom.xml a different artifactId:

<artifactId>notes.security.impl2</artifactId>
   

When I import this new bundle into Eclipse, it will show up as notes.security.impl2, right below notes.security.impl

The java class stays the same, except I need to change the class implementing security. I change its package to org.opentaps.notes.security.impl2 and the hasPermission method a little bit:

   public boolean hasPermission(Note note, Operation permissionId) {
       Log.logInfo("This is the alternate security implementation.  I will not let anonymous users do anything.");
       if (note.getCreatedByUserId() == null) {
           errorMessage = "Sorry, but anonymous users are not allowed.";
           return false;
       } else {
           Log.logInfo("I hereby give permission to user [" + note.getCreatedByUserId() + "] from [" + note.getUserIdType() + "].");
           return true;    
       }
       
   }

I also modified its blueprint.xml to say this class now implements the org.opentaps.notes.security.NoteSecurity interface:

   <bean id="noteSecurityImpl2" class="org.opentaps.notes.security.impl2.NoteSecurityImpl">
   </bean>
   <service id="noteSecurityService" ref="noteSecurityImpl2" interface="org.opentaps.notes.security.NoteSecurity">
       <service-properties>
           <entry key="osgi.jndi.service.name" value="org.opentaps.notes.security.NoteSecurity"/>
       </service-properties>
   </service>

I go into the security2 module and build it:

$ cd modules/notes/impl/security2
$ mvn install

Now this is the cool part. I copy over the result of the build to the geronimo/hotbundles directory:

$ cp modules/notes/impl/security2/target/org.opentaps.notes.security.impl2-2.0.1-SNAPSHOT.jar ~/geronimo-tomcat7-javaee6-3.0-beta-1/hotbundles/

and it will show up in the list of OSGi bundles in the console right away: Osgi-tutorial-security-impl2-bundle-installed.png

Bundle 1571 is the original security implementation and is ACTIVE and running. Bundle 1586 is the new one and is INSTALLED but not active. (By the way, if you remove it from the directory, it will automatically uninstall too!)

I click on the button to stop the org.opentaps.notes.security.impl bundle and click on the play button for org.opentaps.notes.security.impl2 ... and got this error:

The bundle "org.opentaps.notes.security.impl2_2.0.1.SNAPSHOT [1586]" could not be resolved. Reason: Missing Constraint: Import-Package: org.opentaps.notes.security.impl2; version="0.0.0"

What happened? Well, I forgot to mention the new Java package name in my bundle's pom.xml. So I fix that in the pom.xml:

   <build>
       <plugins>
           <plugin>
               <groupId>org.apache.felix</groupId>
               <artifactId>maven-bundle-plugin</artifactId>
               <extensions>true</extensions>
               <configuration>
                   <instructions>
                       <Private-Package>
                           org.opentaps.notes.security.impl2
                       </Private-Package>
                   </instructions>
               </configuration>
           </plugin>
       </plugins>
   </build>

Then I build the bundle again and copy its jar to geronimo/hotbundles/. Now the bundle starts properly -- you can see its status is ACTIVE:


(The other security bundle which I stopped is now in RESOLVED state, which means it is recognized by Geronimo but not running.)

When I try to create a note anonymously, I will get an error:

Unexpected Error Sorry, but anonymous users are not allowed.

and in the Geronimo log file

2012-03-09 14:36:07,701 INFO  [core] This is the alternate security implementation.  I will not let anonymous users do anything.
2012-03-09 14:36:07,702 ERROR [core] Sorry, but anonymous users are not allowed.
org.opentaps.core.service.ServiceException: Sorry, but anonymous users are not allowed.

If log into Facebook, though, I can post notes again:

2012-03-09 14:45:04,958 INFO  [core] This is the alternate security implementation.  I will not let anonymous users do anything.
2012-03-09 14:45:04,959 INFO  [core] I hereby give permission to user [639199434] from [facebook].

Finally, to switch back to the original security implementation, I just stop the org.opentaps.notes.security.impl2 bundle and start the org.opentaps.notes.security.impl bundle again. Sure enough, the note is created, and the log says

2012-03-09 14:46:14,472 INFO  [core] This test implementation will always return true, so I will let you create your note.

The most amazing thing is I never had to stop and restart my server this whole time! All I had to do was copy my bundle into hotbundles/ and then start/stop bundles from the management console.