JPA (Java Persistence API) is a major part of the Java EE specifications. Its utilization on Java EE servers, like Wildfly, JBoss EAP or Red Hat Fuse on JBoss EAP, became one of the most common persistence solutions. However, on OSGi platforms, like Apache Karaf or Red Hat Fuse on Apache Karaf, it was quite difficult until recently to use JPA providers like OpenJPA or Hibernate. With Red Hat Fuse 7.1 and Karaf 4.2 there is no more any reason to not choose JPA as your application persistence solution. This article shows how and gives you a consistent approach, as well as a reliable template, of developing OSGi applications, using Hiberante as a JPA provider, with Red Hat Fuse on Apache Karaf platforms. For illustration purposes, I’ll be using a sample application, named customer-manager which, as its name implies, handles customers and implements its associated CRUD (Create, Read, Update, delete) API. The code may be found here: https://github.com/nicolasduminil/Red-Hat-Fuse-7.1
The Software Architecture
The UML diagram below shows the software architecture of the sample application.
The UML diagram above shows the software architecture of the sample application, as follows:
- The master POM which defines dependencies and plugins and drives the build process
- The data module bundle including the model classes and its interface. The model consists in only one class, the Customer class. The interface named CustomerService represents the API via which the service may be invoked by its clients. It was included in the model because it seemed a very natural place for that but it also could have been included in a separate module or in the one hosting the implementation class.
- The service module bundle containing the class CustomerServiceImpl. This is the implementation of the CustomerService interface and provides support for the persistence operations.
- The command module bundle uses the Karaf shell extension feature in order to provide a set of commands which expose the customer API. These commands allow to create, search, list and remove customers.
- The features module provides the Karaf repository required to install and start all the project bundles. This is a very convenient way of handling several bundles grouped together.
The Master POM
This POM drives the overall sample application build process and defines the common dependencies and plugins. It imports the Fuse 7.1 BOM (Billing of Material), as follows:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.jboss.fuse</groupId>
<artifactId>jboss-fuse-parent</artifactId>
<version>7.1.0.fuse-710023-redhat-00001</version>
<scope>import</scope>
<type>pom</type>
</dependency>
…
</dependencyManagement>
In order that maven finds the BOM you need the following in your maven configuration file :
<repositories>
<repository>
<id>jboss-ga-repository</id>
<name>JBoss GA Tech Preview Maven Repository</name>
<url>https://maven.repository.redhat.com/ga/</url>
<layout>default</layout>
<releases>
<enabled>true</enabled>
<updatePolicy>never</updatePolicy>
</releases>
<snapshots>
<enabled>false</enabled>
<updatePolicy>never</updatePolicy>
</snapshots>
</repository>
<repository>
<id>jboss-release-repository</id>
<url>https://repository.jboss.org/nexus/service/local/staging/deploy/maven2</url>
<layout>default</layout>
<releases>
<enabled>true</enabled>
<updatePolicy>never</updatePolicy>
</releases>
<snapshots>
<enabled>false</enabled>
<updatePolicy>never</updatePolicy>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>jboss-ga-plugin-repository</id>
<name>JBoss 7 Maven Plugin Repository</name>
<url>https://maven.repository.redhat.com/ga/</url>
<layout>default</layout>
<releases>
<enabled>true</enabled>
<updatePolicy>never</updatePolicy>
</releases>
<snapshots>
<enabled>false</enabled>
<updatePolicy>never</updatePolicy>
</snapshots>
</pluginRepository>
</pluginRepositories>
Here we define two repositories:
- A GA (General Availability) repository at https://maven.repository.redhat.com/ga/ which contains dependency artifacts as well as plugins
- A staging repository at https://repository.jboss.org/nexus/service/local/staging/deploy/maven2/ which contains only dependency artifacts.
These two repositories are both Red Hat public release repositories. The snapshots are disabled as we don’t want to build against snapshots. The provided URLs may change, and they do often, which is a problem sometimes, so please make sure you use the current ones. You may find all the required information on the Red Hat web site.
The rest of the POM doesn’t contain any special element, it only defines the current versions of the dependencies and of the plugins to be used. A particular attention shall be paid to the configuration of the karaf-services-maven-plugin plugin, as shown below:
<plugin>
<groupId>org.apache.karaf.tooling</groupId>
<artifactId>karaf-services-maven-plugin</artifactId>
<version>4.2.1</version>
<executions>
<execution>
<id>service-metadata-generate</id>
<phase>process-classes</phase>
<goals>
<goal>service-metadata-generate</goal>
</goals>
</execution>
</executions>
</plugin>
This plugin is used to generate the metadata required by the OSGi shell extensions. The configuration above is mandatory and omitting it would prevent your shell extension commands, defined in the osgi-customer-management-command project, to work.
The Data Bundle
This bundle is created by building the osgi-customer-management-command maven project. This project contains the domain objects, its API and its entities. In this sample we use a very simple domain consisting in one entity: the Customer entity.
@XmlRootElement(name="customer")
@XmlAccessorType(XmlAccessType.PROPERTY)
@Entity
@Table(name="CUSTOMERS")
public class Customer implements Serializable
{
private static final long serialVersionUID = 1L;
private BigInteger id;
private String firstName;
private String lastName;
private String street;
private String city;
private String state;
private String zip;
private String country;
public Customer()
{
}
public Customer(String firstName, String lastName,
String street, String city, String state, String zip, String country)
{
this.firstName = firstName;
this.lastName = lastName;
this.street = street;
this.city = city;
this.state = state;
this.zip = zip;
this.country = country;
}
public Customer (Customer customer)
{
this (customer.firstName, customer.lastName, customer.street,
customer.city, customer.state, customer.zip, customer.country);
}
@Id
@SequenceGenerator(name = "CUSTOMERS_ID_GENERATOR", sequenceName = "CUSTOMERS_SEQ")
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "CUSTOMERS_ID_GENERATOR")
@Column(name = "CUSTOMER_ID", unique = true, nullable = false, length = 8)
public BigInteger getId()
{
return id;
}
public void setId(BigInteger id)
{
this.id = id;
}
@XmlElement
@Column(name = "FIRST_NAME", nullable = false, length = 40)
public String getFirstName()
{
return firstName;
}
public void setFirstName(String firstName)
{
this.firstName = firstName;
}
@XmlElement
@Column(name = "LAST_NAME", nullable = false, length = 40)
public String getLastName()
{
return lastName;
}
public void setLastName(String lastName)
{
this.lastName = lastName;
}
@XmlElement
@Column(name = "ADDRESS_STREET", nullable = false, length = 80)
public String getStreet()
{
return street;
}
public void setStreet(String street)
{
this.street = street;
}
@XmlElement
@Column(name = "ADDRESS_CITY", nullable = false, length = 80)
public String getCity()
{
return city;
}
public void setCity(String city)
{
this.city = city;
}
@XmlElement
@Column(name = "ADDRESS_STATE", nullable = false, length = 40)
public String getState()
{
return state;
}
public void setState(String state)
{
this.state = state;
}
@XmlElement
@Column(name = "ADDRESS_ZIP", nullable = false, length = 8)
public String getZip()
{
return zip;
}
public void setZip(String zip)
{
this.zip = zip;
}
@XmlElement
@Column(name = "ADDRESS_COUNTRY", nullable = false, length = 40)
public String getCountry()
{
return country;
}
public void setCountry(String country)
{
this.country = country;
}
}
Its API is very simple as well, as follows:
public interface CustomerManagementService
{
public Customer createCustomer (Customer customer);
public void removeCustomer (Customer customer);
public void removeCustomerById (BigInteger id);
public Customer findCustomer (BigInteger customerId);
public List<Customer> findCustomers();
public void updateCustomer (Customer customer);
}
The POM file which drives this bundle’s build process uses the maven-bundle-plugin having the following configuration:
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<configuration>
<instructions>
<Export-Package>fr.simplex_software.osgi.fuse.jpa.model</Export-Package>
<Import-Package>
javax.persistence;version="[2,3)",
org.hibernate.proxy,
javassist.util.proxy,
*
</Import-Package>
</instructions>
</configuration>
</plugin>
As you can see, the bundle configuration contains the required import statements for the JPA and Hibernate bundles. The notation ="[2,3)" means any release between version 2 and 3, including version 2 but excluding version 3. In our case we’ll be using JPA 2.1 coming out-of-the-box with Red Hat Fuse 7.1 but, as soon as the next Fuse release will be available and supposing it comes with JPA 2.2, the same POM file will be able to take advantage of it, without any modification.
The Service Bundle
This bundle is created by building the osgi-customer-management-service maven project. This project contains a single class, the CustomerManagementServiceImpl class which implements the interface CustomerManagementService defined by the osgi-customer-management-data project.
@Transactional
public class CustomerManagementServiceImpl implements CustomerManagementService
{
@PersistenceContext(unitName = "customers")
private EntityManager em;
@Override
public Customer createCustomer(Customer customer)
{
em.persist(customer);
em.flush();
return customer;
}
@Override
public void removeCustomer(Customer customer)
{
em.remove(customer);
}
@Transactional(TxType.SUPPORTS)
@Override
public Customer findCustomer(BigInteger customerId)
{
return em.find(Customer.class, customerId);
}
@Transactional(TxType.SUPPORTS)
@Override
public List<Customer> findCustomers()
{
CriteriaQuery<Customer> query = em.getCriteriaBuilder().createQuery(Customer.class);
return em.createQuery(query.select(query.from(Customer.class))).getResultList();
}
@Override
public void updateCustomer(Customer customer)
{
em.persist(customer);
}
@Override
public void removeCustomerById(BigInteger id)
{
em.remove(em.find(Customer.class, id));
}
}
The implementation is very straightforward and doesn’t require any explanation. The following persistence.xml file defines the required data-source:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="customers" transaction-type="JTA">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<jta-data-source>
osgi:service/javax.sql.DataSource/(osgi.jndi.service.name=customer-management)
</jta-data-source>
<class>fr.simplex_software.osgi.fuse.jpa.model.Customer</class>
<properties>
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
<property name="hibernate.hbm2ddl.auto" value="create-drop" />
</properties>
</persistence-unit>
</persistence>
The file above defines a persistence unit named “customers” of type JTA (Java Transaction API). This means that, as opposed to a RESSOURCE_LOCAL persistence unit type, which contains all the parameters required by the JDBC (Java DataBase Connectivity) layer, parameters like user name and password, data base name and schema, host name and TCP port, etc., a JTA persistence unit type only declares the name under which the associated data-source has been previously defined in the JNDI (Java Naming and Directory Interface) registry. This way, instead of defining all the JDBC details in the persistence.xml file, these details are pre-defined at the server level, using the JNDI registry.
Last but not least, we need to wire together the service interface and its implementation. This may be done either using Spring DM (Dynamic Modules) or the OSGi Blueprint notation. In our sample we’ve chosen to use the OSGi Blueprint notation, hence the following resourcesOSGI-INFblueprintblueprint.xml file:
<?xml version="1.0" encoding="UTF-8"?>
<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
xmlns:jpa="http://aries.apache.org/xmlns/jpa/v2.0.0"
xmlns:tx="http://aries.apache.org/xmlns/transactions/v1.2.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.osgi.org/xmlns/blueprint/v1.0.0
https://osgi.org/xmlns/blueprint/v1.0.0/blueprint.xsd">
<jpa:enable />
<tx:enable-annotations />
<service ref="customerManagementService"
interface="fr.simplex_software.osgi.fuse.jpa.model.CustomerManagementService" />
<bean id="customerManagementService"
class="fr.simplex_software.osgi.fuse.jpa.service.CustomerManagementServiceImpl" />
</blueprint>
What this file is doing is that it enables JPA as well as the transaction specific annotations. It also declares a bean named CustomerManagementServiceImpl, having the name of customerManagementService and implementing the interface named CustomerManagementService.
The Command Bundle
In order to use the defined service, its API shall be exposed somehow. Generally, this API is exposed as a REST API in the form of JAX-RS (Java API for Rest Services) endpoints. With Apache Kara OSGi platforms, and hence with Red Hat Fuse, we have the choice of a CLI (Command Line Interface). Such a CLI might be interesting as more practical and convenient compared with the REST API which requires communication through HTTP. Accordingly, the osgi-customer-management-command project defines the required commands to create, search, list and remove customers. Here is the create class.
@Service
@Command(scope = "customer-management", name = "create", description = "Create a Customer")
public class CreateCommand implements Action
{
@Reference
private CustomerManagementService customerManagementService;
@Argument(index = 0, name = "firstName", description = "Customer first name",
required = true, multiValued = false)
private String firstName;
@Argument(index = 1, name = "lastName", description = "Customer last name",
required = true, multiValued = false)
private String lastName;
@Argument(index = 2, name = "street", description = "Customer address (street)",
required = true, multiValued = false)
private String street;
@Argument(index = 3, name = "city", description = "Customer address (city)",
required = true, multiValued = false)
private String city;
@Argument(index = 4, name = "state", description = "Customer address (state)",
required = true, multiValued = false)
private String state;
@Argument(index = 5, name = "zip", description = "Customer address (zip)",
required = true, multiValued = false)
private String zip;
@Argument(index = 6, name = "country", description = "Customer address (country)",
required = true, multiValued = false)
private String country;
@Override
public Object execute() throws Exception
{
customerManagementService.createCustomer(new Customer(firstName, lastName, street,
city, state, zip, country));
return null;
}
}
An OSGi shell command is defined by its scope and its name. There are different ways to define these values but the most convenient one is by the org.apache.karaf.shell.api.action.Command annotation. Also, a command should be annotated with the org.apache.karaf.shell.api.action.Service annotation and has to implement the org.apache.karaf.shell.api.action.Action interface. Each command has arguments defined by their index and their name. For example, the listing above defines the argument having the index 0 and the name “firstName”. This argument is mandatory and it takes a simple value, a string, as opposed to a multi-value, in the form of a list of strings. The only required method in the execute() method which concretely implements the command operation. In the listing above it instantiates a Customer object using the values of its arguments and it persists this object using the CustomerManagementService which reference is injected.
Another very convenient feature of the OSGi shell extension is their auto-complete capability. This means that typing the TAB key the user may get help through auto-completion functionality, allowing this way to avoid having to know by heart the command syntax. Here is what a completer class is looking like:
@Service
public class CustomerIdCompleter implements Completer
{
@Reference
private CustomerManagementService customerManagementService;
@Override
public int complete(Session session, CommandLine commandLine, List<String> candidates)
{
StringsCompleter delegate = new StringsCompleter();
for (Customer customer : customerManagementService.findCustomers())
delegate.getStrings().add(String.valueOf(customer.getId()));
return delegate.complete(session, commandLine, candidates);
}
}
As shown by the listing above, a completer is a very simple class annotated with the org.apache.karaf.shell.api.action.Service annotation and implementing the org.apache.karaf.shell.api.action.Completer interface. Its only mandatory method, the complete() method, simply uses a delegate in order to fulfill the completion operation. This delegate of class org.apache.karaf.shell.api.action.StringsCompleter is initialized with the values on which we want to apply completion. In the example above, we’re applying auto-completion on the customer ID values, meaning that typing a customer-management command like findById or removeCustomer and then TAB, will display all the customer ID values that applies in the context of the given command.
The Feature Module
The osgi-customer-management-feature project packs and deploys in the local maven repository the feature.xml file which contains the description all the features required to install and run the sample application. It uses the build-helper-maven-plugin for these purposes. Here the is the feature.xml file :
<?xml version="1.0" encoding="UTF-8"?>
<features name="osgi-customer-management-${project.version}"
xmlns="http://karaf.apache.org/xmlns/features/v1.3.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://karaf.apache.org/xmlns/features/v1.3.0
http://karaf.apache.org/xmlns/features/v1.3.0">
<feature name="osgi-customer-management-datasource" version="${project.version}">
<config name="org.ops4j.datasource-customer-management">
osgi.jdbc.driver.class=org.h2.Driver
databaseName=customer-management;create=true
dataSourceName=customer-management
</config>
<capability>
osgi.service;
objectClass=javax.sql.DataSource;
effective:=active;
osgi.jndi.service.name=customer-management
</capability>
</feature>
<feature name="osgi-customer-management-data" version="${project.version}">
<feature>transaction</feature>
<feature>jndi</feature>
<feature>pax-jdbc-config</feature>
<feature>pax-jdbc-h2</feature>
<feature>pax-jdbc-pool-dbcp2</feature>
<feature>jdbc</feature>
<feature dependency="true">aries-blueprint</feature>
<feature version="[2,3)">jpa</feature>
<feature version="[5,6)">hibernate</feature>
<bundle>
mvn:fr.simplex-software.osgi.fuse.aries/osgi-customer-management-data/${project.version}
</bundle>
</feature>
<feature name="osgi-customer-management-service" version="${project.version}">
<feature version="${project.version}">osgi-customer-management-data</feature>
<bundle>
mvn:fr.simplex-software.osgi.fuse.aries/
osgi-customer-management-service/${project.version}
</bundle>
</feature>
<feature name="osgi-customer-management-command" version="${project.version}">
<feature version="${project.version}">osgi-customer-management-data</feature>
<bundle>
mvn:fr.simplex-software.osgi.fuse.aries/
osgi-customer-management-command/${project.version}
</bundle>
</feature>
</features>
A “feature” is a provisioning artifact used by Apache Karaf and, hence, by Red Hat Fuse on Karaf, in order to simplify the OSGi bundles resolving and deployment. A “feature” repository is an XML file which describes in a well-defined notation, based on a specific grammar, what are the resources (OSGi bundles and other features) that an application requires. The file above describes the following resources:
- A feature named osgi-customer-management-datasource defining the JDBC connection string used by the application in order to access the database. Here we are using a H2 database engine. Notice that the dataSourceName property’s value, here “customer-management”, is the same as the one of the jndi.service.name property, defined in the persistence.xml file, the <jta-data-source> element.
- A feature named osgi-customer-management-data consisting in the OSGi bundle with the same name and its dependencies: jndi, pax-jdbc-config, jpa, hibernate, etc. Here you can appreciate how convenient the use of the features is as opposed to the individual handling, one by one, of the required bundles.
- A feature named osgi-customer-management-service consisting in the bundle of the same name and the feature osgi-customer-management-data defined previously.
A feature named osgi-customer-management-command consisting in the bundle of the same name and the feature osgi-customer-management-data defined previously.
Installing and running the sample application
The screen copy below shows how to install and run the application:
As you can see above, the steps of installing and running the sample application are as follows:
- Declaring the feature repository associated with the application. This feature repository is our osgi-customer-management-feature maven artifact that we installed in our local maven repository. It contains the xml file explained above.
- Install the feature osgi-customer-management-datasource
- Install the feature osgi-customer-management-command
- Install the feature osgi-customer-management-service
Once that all the required features are successfully installed, we can use our shell extension commands, i.e. customer-management:create, customer-management:findAll, customer-management:findById and customer-management:removeCustomer in order to test the CRUD. Pressing the TAB key let you experience the auto-completion facility.
This concludes our sample. Enjoy !