Prev Next

Microservices


This tutorial is Maven and command-line based; the reader may follow this verbatim or use their favorite Java IDE.

Introduction

Using the enRoute Archetypes this tutorial walks through the creation of a REST Microservice comprised of the following structural elements:

  • An API module
  • A DAO Implementation module
  • A REST Service Implementation module
  • The Composite Application module

with each module having a POM that describes its dependencies.

We start by creating the required project skeleton.

For this tutorial we put the project in the ~ (AKA /home/user) directory. If you put your project in a different directory, be sure to replace the ~ with your directory path when it appears in shell snippets in the tutorial.

Creating the Project

Using the bare-project Archetype, in your project root directory create the microservice project:

~ $ mvn archetype:generate \
    -DarchetypeGroupId=org.osgi.enroute.archetype \
    -DarchetypeArtifactId=project-bare \
    -DarchetypeVersion=7.0.0

with the following values:

Define value for property 'groupId': org.osgi.enroute.examples
Define value for property 'artifactId': microservice
Define value for property 'version' 1.0-SNAPSHOT: : 0.0.1-SNAPSHOT
Define value for property 'package' org.osgi.enroute.examples: :
Confirm properties configuration:
groupId: org.osgi.enroute.examples
artifactId: microservice
version: 0.0.1-SNAPSHOT
package: org.osgi.enroute.examples
Y: :

Note - if you use alternative groupId, artifactId values remember to update the package-info.java and import statements in the files used throughout the rest of this tutorial.

We now create the required modules.

The DAO API

Change directory into the newly created microservice project directory; then create the api module using the api Archetype as shown:

~ $ cd microservice
~/microservice $ mvn archetype:generate \
    -DarchetypeGroupId=org.osgi.enroute.archetype \
    -DarchetypeArtifactId=api \
    -DarchetypeVersion=7.0.0

with the following values:

Define value for property 'groupId': org.osgi.enroute.examples.microservice
Define value for property 'artifactId': dao-api
Define value for property 'version' 1.0-SNAPSHOT: : 0.0.1-SNAPSHOT
Define value for property 'package' org.osgi.enroute.examples.microservice.dao.api: :org.osgi.enroute.examples.microservice.dao
Confirm properties configuration:
groupId: org.osgi.enroute.examples.microservice
artifactId: dao-api
version: 0.0.1-SNAPSHOT
package: org.osgi.enroute.examples.microservice.dao
Y: :

Now create the following two files:

~/microservice/dao-api/src/main/java/org/osgi/enroute/examples/microservice/dao/PersonDao.java

package org.osgi.enroute.examples.microservice.dao;

import java.util.List;

import org.osgi.annotation.versioning.ProviderType;
import org.osgi.enroute.examples.microservice.dao.dto.PersonDTO;

@ProviderType
public interface PersonDao {
    
    public List<PersonDTO> select();

    public PersonDTO findByPK(Long pk) ;

    public Long save(PersonDTO data);

    public void update(PersonDTO data);

    public void delete(Long pk) ;
}

~/microservice/dao-api/src/main/java/org/osgi/enroute/examples/microservice/dao/AddressDao.java

package org.osgi.enroute.examples.microservice.dao;

import java.util.List;

import org.osgi.annotation.versioning.ProviderType;
import org.osgi.enroute.examples.microservice.dao.dto.AddressDTO;

@ProviderType
public interface AddressDao {
    
    public List<AddressDTO> select(Long personId);

    public AddressDTO findByPK(String emailAddress);

    public void save(Long personId,AddressDTO data);

    public void update(Long personId,AddressDTO data);

    public void delete(Long personId) ;

}

Dependencies

dao-api has no dependencies.

Visibility

dao-api is an API package which is imported by RestComponentImpl, PersonDaoImpl & AddressDaoImpl; hence it must must be exported. This is indicated by the automatically generated file ~/microservice/dao-api/src/main/java/org/osgi/enroute/examples/microservice/dao/package-info.java:

@org.osgi.annotation.bundle.Export
@org.osgi.annotation.versioning.Version("1.0.0")
package org.osgi.enroute.examples.microservice.dao;

For further detail see Semantic Versioning.

Defining the DTO

Data transfer between the components is achieved via the use of Data Transfer Objects (DTOs).

To achieve this create the following two files:

~/microservice/dao-api/src/main/java/org/osgi/enroute/examples/microservice/dao/dto/PersonDTO.java

package org.osgi.enroute.examples.microservice.dao.dto;

import java.util.ArrayList;
import java.util.List;

public class PersonDTO {

	public long personId;
	public String firstName;
	public String lastName;

	public List<AddressDTO> addresses = new ArrayList<>();
}

~/microservice/dao-api/src/main/java/org/osgi/enroute/examples/microservice/dao/dto/AddressDTO.java

package org.osgi.enroute.examples.microservice.dao.dto;

public class AddressDTO {

	public long personId;
	public String emailAddress;
	public String city;
	public String country;
}

and again, we advertise this Capability by creating the following package-info.java file:

~/microservice/dao-api/src/main/java/org/osgi/enroute/examples/microservice/dao/dto/package-info.java

@org.osgi.annotation.bundle.Export
@org.osgi.annotation.versioning.Version("1.0.0")
package org.osgi.enroute.examples.microservice.dao.dto;

The DAO Implementation

Now, in the microservice project directory, create the impl module using the ds-component Archetype:

~/microservice $ mvn archetype:generate \
    -DarchetypeGroupId=org.osgi.enroute.archetype \
    -DarchetypeArtifactId=ds-component \
    -DarchetypeVersion=7.0.0

with the following values:

Define value for property 'groupId': org.osgi.enroute.examples.microservice
Define value for property 'artifactId': dao-impl
Define value for property 'version' 1.0-SNAPSHOT: : 0.0.1-SNAPSHOT
Define value for property 'package' org.osgi.enroute.examples.microservice.dao.impl: :
Confirm properties configuration:
groupId: org.osgi.enroute.examples.microservice
artifactId: dao-impl
version: 0.0.1-SNAPSHOT
package: org.osgi.enroute.examples.microservice.dao.impl
Y: :

Now create the following four files:

~/microservice/dao-impl/src/main/java/org/osgi/enroute/examples/microservice/dao/impl/PersonTable.java

package org.osgi.enroute.examples.microservice.dao.impl;
public interface PersonTable {

	String TABLE_NAME = "PERSONS";

	String SQL_SELECT_ALL_PERSONS = "SELECT * FROM " + TABLE_NAME;

	String SQL_DELETE_PERSON_BY_PK = "DELETE FROM " + TABLE_NAME + " where PERSON_ID=?";

	String SQL_SELECT_PERSON_BY_PK = "SELECT * FROM " + TABLE_NAME + " where PERSON_ID=?";

	String SQL_INSERT_PERSON = "INSERT INTO " + TABLE_NAME + "(FIRST_NAME,LAST_NAME) VALUES(?,?)";

	String SQL_UPDATE_PERSON_BY_PK = "UPDATE " + TABLE_NAME + " SET FIRST_NAME=?, LAST_NAME=? WHERE PERSON_ID=?";

	String PERSON_ID = "person_id";

	String FIRST_NAME = "first_name";

	String LAST_NAME = "last_name";

	String INIT = "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" //
			+ "person_id bigint GENERATED BY DEFAULT AS IDENTITY," //
			+ "first_name varchar(255) NOT NULL," //
			+ "last_name varchar(255) NOT NULL," //
			+ "PRIMARY KEY (person_id)" //
			+ ") ;";
}

~/microservice/dao-impl/src/main/java/org/osgi/enroute/examples/microservice/dao/impl/PersonDaoImpl.java

package org.osgi.enroute.examples.microservice.dao.impl;

import static java.sql.Statement.RETURN_GENERATED_KEYS;
import static org.osgi.enroute.examples.microservice.dao.impl.PersonTable.FIRST_NAME;
import static org.osgi.enroute.examples.microservice.dao.impl.PersonTable.INIT;
import static org.osgi.enroute.examples.microservice.dao.impl.PersonTable.LAST_NAME;
import static org.osgi.enroute.examples.microservice.dao.impl.PersonTable.PERSON_ID;
import static org.osgi.enroute.examples.microservice.dao.impl.PersonTable.SQL_DELETE_PERSON_BY_PK;
import static org.osgi.enroute.examples.microservice.dao.impl.PersonTable.SQL_INSERT_PERSON;
import static org.osgi.enroute.examples.microservice.dao.impl.PersonTable.SQL_SELECT_ALL_PERSONS;
import static org.osgi.enroute.examples.microservice.dao.impl.PersonTable.SQL_SELECT_PERSON_BY_PK;
import static org.osgi.enroute.examples.microservice.dao.impl.PersonTable.SQL_UPDATE_PERSON_BY_PK;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;

import org.osgi.enroute.examples.microservice.dao.AddressDao;
import org.osgi.enroute.examples.microservice.dao.PersonDao;
import org.osgi.enroute.examples.microservice.dao.dto.PersonDTO;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.transaction.control.TransactionControl;
import org.osgi.service.transaction.control.jdbc.JDBCConnectionProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Component
public class PersonDaoImpl implements PersonDao {
    
    private static final Logger logger = LoggerFactory.getLogger(PersonDaoImpl.class);

    @Reference
    TransactionControl transactionControl;

    @Reference(name="provider")
    JDBCConnectionProvider jdbcConnectionProvider;

    @Reference
    AddressDao addressDao;

    Connection connection;

    @Activate
    void start(Map<String, Object> props) throws SQLException {
        connection = jdbcConnectionProvider.getResource(transactionControl);
        transactionControl.supports(() -> connection.prepareStatement(INIT).execute());
    }

    @Override
    public List<PersonDTO> select() {

        return transactionControl.notSupported(() -> {

            List<PersonDTO> dbResults = new ArrayList<>();

            ResultSet rs = connection.createStatement().executeQuery(SQL_SELECT_ALL_PERSONS);

            while (rs.next()) {
                PersonDTO personDTO = mapRecordToPerson(rs);
                personDTO.addresses = addressDao.select(personDTO.personId);
                dbResults.add(personDTO);
            }

            return dbResults;
        });
    }

    @Override
    public void delete(Long primaryKey) {

        transactionControl.required(() -> {
            PreparedStatement pst = connection.prepareStatement(SQL_DELETE_PERSON_BY_PK);
            pst.setLong(1, primaryKey);
            pst.executeUpdate();
            addressDao.delete(primaryKey);
            logger.info("Deleted Person with ID : {}", primaryKey);
            return null;
        });
    }

    @Override
    public PersonDTO findByPK(Long pk) {

       return transactionControl.supports(() -> {

            PersonDTO personDTO = null;

            PreparedStatement pst = connection.prepareStatement(SQL_SELECT_PERSON_BY_PK);
            pst.setLong(1, pk);

            ResultSet rs = pst.executeQuery();

            if (rs.next()) {
                personDTO = mapRecordToPerson(rs);
                personDTO.addresses = addressDao.select(pk);
            }

            return personDTO;
        });
    }

    @Override
    public Long save(PersonDTO data) {

        return transactionControl.required(() -> {

            PreparedStatement pst = connection.prepareStatement(SQL_INSERT_PERSON, RETURN_GENERATED_KEYS);

            pst.setString(1, data.firstName);
            pst.setString(2, data.lastName);

            pst.executeUpdate();

            AtomicLong genPersonId = new AtomicLong(data.personId);

            if (genPersonId.get() <= 0) {
                ResultSet genKeys = pst.getGeneratedKeys();

                if (genKeys.next()) {
                    genPersonId.set(genKeys.getLong(1));
                }
            }

            logger.info("Saved Person with ID : {}", genPersonId.get());

            if (genPersonId.get() > 0) {
                data.addresses.stream().forEach(address -> {
                    address.personId = genPersonId.get();
                    addressDao.save(genPersonId.get(), address);
                });
            }

            return genPersonId.get();
        });
    }

    @Override
    public void update(PersonDTO data) {

        transactionControl.required(() -> {

            PreparedStatement pst = connection.prepareStatement(SQL_UPDATE_PERSON_BY_PK);
            pst.setString(1, data.firstName);
            pst.setString(2, data.lastName);
            pst.setLong(3, data.personId);
            pst.executeUpdate();

            logger.info("Updated person : {}", data);

            data.addresses.stream().forEach(address -> addressDao.update(data.personId, address));
            
            return null;
        });
    }

    protected PersonDTO mapRecordToPerson(ResultSet rs) throws SQLException {
        PersonDTO personDTO = new PersonDTO();
        personDTO.personId = rs.getLong(PERSON_ID);
        personDTO.firstName = rs.getString(FIRST_NAME);
        personDTO.lastName = rs.getString(LAST_NAME);
        return personDTO;
    }
}

~/microservice/dao-impl/src/main/java/org/osgi/enroute/examples/microservice/dao/impl/AddressTable.java

package org.osgi.enroute.examples.microservice.dao.impl;
public interface AddressTable {

	String TABLE_NAME = "ADDRESSES";

	String SQL_SELECT_ADDRESS_BY_PERSON = "SELECT * FROM " + TABLE_NAME + " WHERE PERSON_ID = ? ";

	String SQL_DELETE_ADDRESS = "DELETE FROM " + TABLE_NAME + " WHERE EMAIL_ADDRESS = ? AND  PERSON_ID=?";

	String SQL_DELETE_ALL_ADDRESS_BY_PERSON_ID = "DELETE FROM " + TABLE_NAME + " WHERE PERSON_ID=?";

	String SQL_SELECT_ADDRESS_BY_PK = "SELECT * FROM " + TABLE_NAME + " where EMAIL_ADDRESS=?";

	String SQL_ADD_ADDRESS = "INSERT INTO " + TABLE_NAME + "(EMAIL_ADDRESS,PERSON_ID,CITY,COUNTRY) VALUES(?,?,?,?)";

	String SQL_UPDATE_ADDRESS_BY_PK_AND_PERSON_ID = "UPDATE " + TABLE_NAME + " SET CITY=?, COUNTRY=? "
			+ "WHERE EMAIL_ADDRESS = ? AND  PERSON_ID=?";

	String PERSON_ID = "person_id";

	String EMAIL_ADDRESS = "email_address";

	String CITY = "city";

	String COUNTRY = "country";

	String INIT = "CREATE TABLE IF NOT EXISTS " + TABLE_NAME +" (" //
			+ "email_address varchar(255) NOT NULL," //
			+ "person_id  bigint NOT NULL," //
			+ "city varchar(100) NOT NULL," //
			+ "country varchar(2) NOT NULL," //
			+ "PRIMARY KEY (email_address)" + ") ;";
}

~/microservice/dao-impl/src/main/java/org/osgi/enroute/examples/microservice/dao/impl/AddressDaoImpl.java

package org.osgi.enroute.examples.microservice.dao.impl;

import static org.osgi.enroute.examples.microservice.dao.impl.AddressTable.SQL_ADD_ADDRESS;
import static org.osgi.enroute.examples.microservice.dao.impl.AddressTable.SQL_DELETE_ALL_ADDRESS_BY_PERSON_ID;
import static org.osgi.enroute.examples.microservice.dao.impl.AddressTable.SQL_SELECT_ADDRESS_BY_PERSON;
import static org.osgi.enroute.examples.microservice.dao.impl.AddressTable.SQL_SELECT_ADDRESS_BY_PK;
import static org.osgi.enroute.examples.microservice.dao.impl.AddressTable.SQL_UPDATE_ADDRESS_BY_PK_AND_PERSON_ID;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.osgi.enroute.examples.microservice.dao.AddressDao;
import org.osgi.enroute.examples.microservice.dao.dto.AddressDTO;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.transaction.control.TransactionControl;
import org.osgi.service.transaction.control.jdbc.JDBCConnectionProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Component
public class AddressDaoImpl implements AddressDao {

    private static final Logger logger = LoggerFactory.getLogger(AddressDaoImpl.class);

	@Reference
	TransactionControl transactionControl;

	@Reference(name="provider")
	JDBCConnectionProvider jdbcConnectionProvider;

	Connection connection;

	@Activate
	void activate(Map<String, Object> props) throws SQLException {
		connection = jdbcConnectionProvider.getResource(transactionControl);
		transactionControl.supports( () -> connection.prepareStatement(AddressTable.INIT).execute());
	}

	@Override
	public List<AddressDTO> select(Long personId) {

		return transactionControl.notSupported(() -> {

			List<AddressDTO> dbResults = new ArrayList<>();

			PreparedStatement pst = connection.prepareStatement(SQL_SELECT_ADDRESS_BY_PERSON);
			pst.setLong(1, personId);

			ResultSet rs = pst.executeQuery();

			while (rs.next()) {
				AddressDTO addressDTO = mapRecordToAddress(rs);
				dbResults.add(addressDTO);
			}

			return dbResults;
		});
	}

	@Override
	public AddressDTO findByPK(String pk) {

		return transactionControl.supports(() -> {

			AddressDTO addressDTO = null;

			PreparedStatement pst = connection.prepareStatement(SQL_SELECT_ADDRESS_BY_PK);
			pst.setString(1, pk);

			ResultSet rs = pst.executeQuery();

			if (rs.next()) {
				addressDTO = mapRecordToAddress(rs);
			}

			return addressDTO;
		});
	}

	@Override
	public void save(Long personId, AddressDTO data) {
	    
	    transactionControl.required(() -> {
	        PreparedStatement pst = connection.prepareStatement(SQL_ADD_ADDRESS);
	        pst.setString(1, data.emailAddress);
	        pst.setLong(2, data.personId);
	        pst.setString(3, data.city);
	        pst.setString(4, data.country);
	        logger.info("Saved Person with id {}  and Address : {}", personId, data);
	        pst.executeUpdate();
	        
	        return null;
	    });
	}

	@Override
	public void update(Long personId, AddressDTO data) {
	    
	    transactionControl.required(() -> {
	        PreparedStatement pst = connection.prepareStatement(SQL_UPDATE_ADDRESS_BY_PK_AND_PERSON_ID);
	        pst.setString(1, data.city);
	        pst.setString(2, data.country);
	        pst.setString(3, data.emailAddress);
	        pst.setLong(4, data.personId);
	        logger.info("Updated Person Address : {}", data);
	        pst.executeUpdate();
	        
	        return null;
	    });
	}

	@Override
	public void delete(Long personId) {
	    
		transactionControl.required(() -> {
			PreparedStatement pst = connection.prepareStatement(SQL_DELETE_ALL_ADDRESS_BY_PERSON_ID);
			pst.setLong(1, personId);
			logger.info("Deleted Person {} Addresses", personId);
			pst.executeUpdate();

			return null;
		});
	}

	protected AddressDTO mapRecordToAddress(ResultSet rs) throws SQLException {
		AddressDTO addressDTO = new AddressDTO();
		addressDTO.personId = rs.getLong(AddressTable.PERSON_ID);
		addressDTO.emailAddress = rs.getString(AddressTable.EMAIL_ADDRESS);
		addressDTO.city = rs.getString(AddressTable.CITY);
		addressDTO.country = rs.getString(AddressTable.COUNTRY);
		return addressDTO;
	}
}

Dependencies

The dao-impl has a dependency on dao-api. Also PersonalDaoImpl.java and AddressDaoImpl.java implementations (see below) have dependencies on the slf4j logging API. This dependency information is added to the <dependencies> section of dao-impl/pom.xml: i.e. dao-impl’s repository.

<dependency>
    <groupId>org.osgi.enroute.examples.microservice</groupId>
    <artifactId>dao-api</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.25</version>
</dependency>

Visibility

Implementations should NOT be shared; hence no package-info.java file.

The REST Service

In the microservice project directory now create the rest-component module using the rest-component Archetype:

~/microservice $ mvn archetype:generate \
    -DarchetypeGroupId=org.osgi.enroute.archetype \
    -DarchetypeArtifactId=rest-component \
    -DarchetypeVersion=7.0.0

with the following values:

Define value for property 'groupId': org.osgi.enroute.examples.microservice
Define value for property 'artifactId': rest-service
Define value for property 'version' 1.0-SNAPSHOT: : 0.0.1-SNAPSHOT
Define value for property 'package' org.osgi.enroute.examples.microservice.rest.service: : org.osgi.enroute.examples.microservice.rest
Confirm properties configuration:
groupId: org.osgi.enroute.examples.microservice
artifactId: rest-service
version: 0.0.1-SNAPSHOT
package: org.osgi.enroute.examples.microservice.rest
Y: :

Now create the following two files:

~/microservice/rest-service/src/main/java/org/osgi/enroute/examples/microservice/rest/RestComponentImpl.java

package org.osgi.enroute.examples.microservice.rest;

import java.util.List;

import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import org.osgi.enroute.examples.microservice.dao.PersonDao;
import org.osgi.enroute.examples.microservice.dao.dto.PersonDTO;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.http.whiteboard.propertytypes.HttpWhiteboardResource;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JSONRequired;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;

@Component(service=RestComponentImpl.class)
@JaxrsResource
@Path("person")
@Produces(MediaType.APPLICATION_JSON)
@JSONRequired
@HttpWhiteboardResource(pattern="/microservice/*", prefix="static")
public class RestComponentImpl {
    
	@Reference
	private PersonDao personDao;

	@GET
	@Path("{person}")
	public PersonDTO getPerson(@PathParam("person") Long personId) {
		return personDao.findByPK(personId);
	}

	@GET
	public List<PersonDTO> getPerson() {
		return personDao.select();
	}

	@DELETE
	@Path("{person}")
	public boolean deletePerson(@PathParam("person") long personId) {
		personDao.delete(personId);
		return true;
	}

	@POST
	public PersonDTO postPerson(PersonDTO person) {
		if (person.personId > 0) {
			personDao.update(person);
			return person;
		}
		else {
			long id = personDao.save(person);
			person.personId = id;
			return person;
		}
	}
}

~/microservice/rest-service/src/main/java/org/osgi/enroute/examples/microservice/rest/JsonpConvertingPlugin.java

package org.osgi.enroute.examples.microservice.rest;

import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
import static org.osgi.service.component.annotations.ServiceScope.PROTOTYPE;
import static org.osgi.util.converter.ConverterFunction.CANNOT_HANDLE;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonArrayBuilder;
import javax.json.JsonNumber;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
import javax.json.JsonReader;
import javax.json.JsonString;
import javax.json.JsonStructure;
import javax.json.JsonValue;
import javax.json.JsonValue.ValueType;
import javax.json.JsonWriter;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;

import org.osgi.service.component.annotations.Component;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsExtension;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsMediaType;
import org.osgi.util.converter.Converter;
import org.osgi.util.converter.Converters;
import org.osgi.util.converter.TypeReference;

@Component(scope = PROTOTYPE)
@JaxrsExtension
@JaxrsMediaType(APPLICATION_JSON)
public class JsonpConvertingPlugin<T> implements MessageBodyReader<T>, MessageBodyWriter<T> {

    private final Converter converter = Converters.newConverterBuilder()
            .rule(JsonValue.class, this::toJsonValue)
            .rule(this::toScalar)
            .build();

    private JsonValue toJsonValue(Object value, Type targetType) {
        if (value == null) {
           return JsonValue.NULL;
        } else if (value instanceof String) {
            return Json.createValue(value.toString());
        } else if (value instanceof Boolean) {
            return ((Boolean) value) ? JsonValue.TRUE : JsonValue.FALSE;
        } else if (value instanceof Number) {
            Number n = (Number) value;
            if (value instanceof Float || value instanceof Double) {
                return Json.createValue(n.doubleValue());
            } else if (value instanceof BigDecimal) {
                return Json.createValue((BigDecimal) value);
            } else if (value instanceof BigInteger) {
                return Json.createValue((BigInteger) value);
            } else {
                return Json.createValue(n.longValue());
            }
        } else if (value instanceof Collection || value.getClass().isArray()) {
            return toJsonArray(value);
        } else {
            return toJsonObject(value);
        }
    }

    private JsonArray toJsonArray(Object o) {
        List<?> l = converter.convert(o).to(List.class);
    
        JsonArrayBuilder builder = Json.createArrayBuilder();
        l.forEach(v -> builder.add(toJsonValue(v, JsonValue.class)));
        return builder.build();
    }

    private JsonObject toJsonObject(Object o) {

        Map<String, Object> m = converter.convert(o).to(new TypeReference<Map<String, Object>>(){});

        JsonObjectBuilder jsonBuilder = Json.createObjectBuilder();
        m.entrySet().stream().forEach(e -> jsonBuilder.add(e.getKey(), toJsonValue(e.getValue(), JsonValue.class)));
        return jsonBuilder.build();
    }

    private Object toScalar(Object o, Type t) {

        if (o instanceof JsonNumber) {
            JsonNumber jn = (JsonNumber) o;
            return converter.convert(jn.bigDecimalValue()).to(t);
        } else if (o instanceof JsonString) {
            JsonString js = (JsonString) o;
            return converter.convert(js.getString()).to(t);
        } else if (o instanceof JsonValue) {
            JsonValue jv = (JsonValue) o;
            if (jv.getValueType() == ValueType.NULL) {
                return null;
            } else if (jv.getValueType() == ValueType.TRUE) {
                return converter.convert(Boolean.TRUE).to(t);
            } else if (jv.getValueType() == ValueType.FALSE) {
                return converter.convert(Boolean.FALSE).to(t);
            }
        }
        return CANNOT_HANDLE;
    }

    @Override
    public boolean isWriteable(Class<?> c, Type t, Annotation[] a, MediaType mediaType) {
        return APPLICATION_JSON_TYPE.isCompatible(mediaType) || mediaType.getSubtype().endsWith("+json");
    }

    @Override
    public boolean isReadable(Class<?> c, Type t, Annotation[] a, MediaType mediaType) {
        return APPLICATION_JSON_TYPE.isCompatible(mediaType) || mediaType.getSubtype().endsWith("+json");
    }

    @Override
    public void writeTo(T o, Class<?> arg1, Type arg2, Annotation[] arg3, MediaType arg4,
            MultivaluedMap<String, java.lang.Object> arg5, OutputStream out)
            throws IOException, WebApplicationException {

        JsonValue jv = converter.convert(o).to(JsonValue.class);

        try (JsonWriter jw = Json.createWriter(out)) {
            jw.write(jv);
        }
    }

    @SuppressWarnings("unchecked")
    @Override
    public T readFrom(Class<T> arg0, Type arg1, Annotation[] arg2, MediaType arg3, MultivaluedMap<String, String> arg4,
            InputStream in) throws IOException, WebApplicationException {

        try (JsonReader jr = Json.createReader(in)) {
            JsonStructure read = jr.read();
            return (T) converter.convert(read).to(arg1);
        }
    }
}

Create the directory ~/microservice/rest-service/src/main/resources/static/main/html and added the following file:

~/microservice/rest-service/src/main/resources/static/main/html/person.html

<link rel="import" href="https://cdn.rawgit.com/download/polymer-cdn/2.6.0.2/lib/iron-ajax/iron-ajax.html">
<link rel="import" href="https://cdn.rawgit.com/download/polymer-cdn/2.6.0.2/lib/iron-input/iron-input.html">
<link rel="import" href="https://cdn.rawgit.com/download/polymer-cdn/2.6.0.2/lib/polymer/polymer-element.html">

<dom-module id="person-app">
  <template>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <iron-ajax id="xhr" handle-as="json" content-type="application/json"></iron-ajax>

    <div>
      <div class="list-group">
        <template is="dom-repeat" items="[[data]]">
          <div class="list-group-item">
            <a href="#" on-click="selectPerson" class="[[item == selected ? 'active' : '']]">
              <div>First name: <span>[[item.firstName]]</span></div>
              <div>Last name: <span>[[item.lastName]]</span></div>
            </a>
            <button type="button" class="btn btn-danger" on-click="deletePerson">Delete</button>
          </div>
        </template>
      </div>
      <div>
        <template is="dom-if" if="[[selected]]">
          <h4>Addresses for [[selected.firstName]] [[selected.lastName]]:</h4>
          <ul class="list-group">
            <template is="dom-repeat" items="[[selected.addresses]]" as="address">
              <li class="list-group-item">
                <ul>
                  <li>Email: <span>[[address.emailAddress]]</span></li>
                  <li>City: <span>[[address.city]]</span></li>
                  <li>Country: <span>[[address.country]]</span></li>
                </ul>
              </li>
            </template>
          </ul>
        </template>
      </div>
    </div>

    <div>
      <h4>Add a Person</h4>
      <input type="text" class="form-control" id="fName" placeholder="First Name">
      <input type="text" class="form-control" id="lName" placeholder="Last Name">
      <h5>Addresses</h5>
      <button class="btn btn-default" type="button" on-click="addAddress">+</button>
      <template is="dom-repeat" items="[[addresses]]" as="address">
        <div class="input-group">
          <input type="email" class="form-control" id="[[address]]-email" placeholder="Email">
          <input type="text" class="form-control" id="[[address]]-city" placeholder="City">
          <input type="text" class="form-control" id="[[address]]-country" placeholder="Country">
        </div>
      </template>
      <button type="submit" class="btn btn-default" on-click="addPerson">Add</button>
      </form>
    </div>

  </template>
  <script>
    class Person extends Polymer.Element {
      static get is() {
        return 'person-app';
      }
      
      static get properties() {
        return {
          selected: {
            type: Object
          },
          addresses: {
            type: Array,
            value: []
          },
          data: {
            type: Array,
            value: []
          }
        }
      }
      
      ready() {
        super.ready();
        this.getData(this);
      }
      
      getData(self) {
        this.$.xhr.url = this.ownerDocument.location.origin + '/person';
        this.$.xhr.method = 'GET';
        this.$.xhr.body = null;
        this.$.xhr.generateRequest().completes.then(function(request) { self.data = request.response;});
      }

      selectPerson(event) {
        this.selected = event.model.item;
      }

      deletePerson(event) {
      
        if(this.selected && this.selected.personId == event.model.item.personId) {
          selected = null;
        }
        
        var self = this;

        this.$.xhr.url = this.ownerDocument.location.origin + '/person/' + event.model.item.personId;
        this.$.xhr.method = 'DELETE';
        this.$.xhr.body = null;
        this.$.xhr.generateRequest().completes.then(function() { self.getData(self); });
      }
      
      addAddress() {
        this.push('addresses', 'address' + this.addresses.length);
      }
      
      addPerson() {
        var person = { firstName: this.$.fName.value, lastName: this.$.lName.value, addresses: []};
        
        var shadowRoot = this.shadowRoot;
        
        this.addresses.forEach(function(id) {
            person.addresses.push(
              {
                emailAddress: shadowRoot.getElementById(id + '-email').value,
                city: shadowRoot.getElementById(id + '-city').value,
                country: shadowRoot.getElementById(id + '-country').value
              });
          });
          
        this.addresses = [];
        this.$.fName.value = null;
        this.$.lName.value = null;
      
        var self = this;
      
        this.$.xhr.url = this.ownerDocument.location.origin + '/person/';
        this.$.xhr.method = 'POST';
        this.$.xhr.body = person;
        this.$.xhr.generateRequest().completes.then(function() { self.getData(self); });
      }
    }
    customElements.define(Person.is, Person);
  </script>
</dom-module>

And also the ~/microservice/rest-service/src/main/resources/static/css directory for the following style.css file

~/microservice/rest-service/src/main/resources/static/css/style.css

/*
	osgi.enroute.examples.component Style Sheet
*/

/* Space out content a bit */
body {
  padding-top: 20px;
  padding-bottom: 20px;
}

/* Everything but the jumbotron gets side spacing for mobile first views */
.header,
.marketing,
.footer {
  padding-right: 15px;
  padding-left: 15px;
}

/* Custom page header */
.header {
  border-bottom: 1px solid #e5e5e5;
}
/* Make the masthead heading the same height as the navigation */
.header h3 {
  padding-bottom: 19px;
  margin-top: 0;
  margin-bottom: 0;
  line-height: 40px;
}

/* Custom page footer */
.footer {
  padding-top: 19px;
  color: #777;
  border-top: 1px solid #e5e5e5;
}

/* Customize container */
@media (min-width: 768px) {
  .container {
    max-width: 730px;
  }
}
.container-narrow > hr {
  margin: 30px 0;
}

/* Main marketing message and sign up button */
.jumbotron {
  text-align: center;
  border-bottom: 1px solid #e5e5e5;
}
.jumbotron .btn {
  padding: 14px 24px;
  font-size: 21px;
}

/* Supporting marketing content */
.marketing {
  margin: 40px 0;
}
.marketing p + h4 {
  margin-top: 28px;
}

/* Responsive: Portrait tablets and up */
@media screen and (min-width: 768px) {
  /* Remove the padding we set earlier */
  .header,
  .marketing,
  .footer {
    padding-right: 0;
    padding-left: 0;
  }
  /* Space out the masthead */
  .header {
    margin-bottom: 30px;
  }
  /* Remove the bottom border on the jumbotron for visual effect */
  .jumbotron {
    border-bottom: 0;
  }
}

Finally, place the following index.html file in directory ~/microservice/rest-service/src/main/resources/static

~/microservice/rest-service/src/main/resources/static/index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>OSGI enRoute quickstart example</title>
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  <link rel="import" href="main/html/person.html">
</head>

<body>
  <div class="container">

    <div class="row">
      <div class="col-md-1">
        <img src="main/img/enroute-logo-64.png" class="img-responsive"/>
      </div>
      <div class="col-md-3">
        <h2> OSGi enRoute</h2>
      </div>
    </div>
    <div class="row center-block">
      <h3 class="text-muted">The Microservice Example</h3>
    </div>
    
    <div class="row center-block">
      <person-app></person-app>
    </div>

    <div class="row">
      <p>&copy; OSGi Alliance 2017</p>
    </div>
  </div>

  <script src="https://cdn.rawgit.com/download/polymer-cdn/2.6.0.2/lib/webcomponentsjs/webcomponents-loader.js"></script>
</body>

</html>

and create the directory ~/microservice/rest-service/src/main/resources/static/main/img into which save the following icon with the name enroute-logo-64.png.

enRoute logo

Dependencies

As the rest-service module has dependencies on the dao-api and json-api these dependencies are added to the <dependencies> section in ~/microservice/rest-service/pom.xml. A JSON-P implementation dependency is also included so that the rest-service can be unit tested.

<dependency>
    <groupId>org.apache.servicemix.specs</groupId>
    <artifactId>org.apache.servicemix.specs.json-api-1.1</artifactId>
    <version>2.9.0</version>
</dependency>
<dependency>
    <groupId>org.osgi.enroute.examples.microservice</groupId>
    <artifactId>dao-api</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
    <groupId>org.apache.johnzon</groupId>
    <artifactId>johnzon-core</artifactId>
    <version>1.1.0</version>
</dependency>

Visibility

Implementation types should NOT be shared; hence we have no package-info.java file in the REST component.

The Composite Application

We now pull these Modules together to create the Composite Application.

In the microservice project directory create the application module using the application Archetype:

~/microservice $ mvn archetype:generate \
    -DarchetypeGroupId=org.osgi.enroute.archetype \
    -DarchetypeArtifactId=application \
    -DarchetypeVersion=7.0.0

with the following values:

Define value for property 'groupId': org.osgi.enroute.examples.microservice
Define value for property 'artifactId': rest-app
Define value for property 'version' 1.0-SNAPSHOT: : 0.0.1-SNAPSHOT
Define value for property 'package' org.osgi.enroute.examples.microservice: :
Define value for property 'impl-artifactId': rest-service
Define value for property 'impl-groupId' org.osgi.enroute.examples.microservice: :
Define value for property 'impl-version' 0.0.1-SNAPSHOT: :
Define value for property 'app-target-java-version' 8: :
Confirm properties configuration:
groupId: org.osgi.enroute.examples.microservice
artifactId: rest-app
version: 0.0.1-SNAPSHOT
package: org.osgi.enroute.examples.microservice
impl-artifactId: rest-service
impl-groupId: org.osgi.enroute.examples.microservice
impl-version: 0.0.1-SNAPSHOT
app-target-java-version: 8
Y: :

Define Runtime Entity

Our Microservice is composed of the following elements:

  • A REST Service
  • An implementation of JSON-P (org.apache.johnzon.core)
  • An in memory database (H2).

These dependencies are expressed as runtime Requirements in the ~/microservice/rest-app/rest-app.bndrun file:

index: target/index.xml

-standalone: ${index}

-resolve.effective: active

-runrequires: \
    osgi.identity;filter:='(osgi.identity=org.osgi.enroute.examples.microservice.rest-service)',\
    osgi.identity;filter:='(osgi.identity=org.apache.johnzon.core)',\
    osgi.identity;filter:='(osgi.identity=org.h2)',\
    osgi.identity;filter:='(osgi.identity=org.osgi.enroute.examples.microservice.rest-app)'
-runfw: org.eclipse.osgi
-runee: JavaSE-1.8

Dependencies

By adding the following dependencies inside the <dependencies> section of the file ~/microservice/rest-app/pom.xml, we add the necessary Capabilities to the rest-app’s repository.

<dependency>
    <groupId>org.osgi.enroute.examples.microservice</groupId>
    <artifactId>dao-impl</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
    <groupId>org.apache.johnzon</groupId>
    <artifactId>johnzon-core</artifactId>
    <version>1.1.0</version>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.196</version>
    <scope>runtime</scope>
</dependency>

Runtime Configuration

Finally, our Microservice will be configured using the new R7 Configurator mechanism.

The application Archetype enables this via ~/microservice/rest-app/src/main/java/config/package-info.java.

@RequireConfigurator
package config;

import org.osgi.service.configurator.annotations.RequireConfigurator;

To pass in the appropriate configuration, overwrite the contents of ~/microservice/rest-app/src/main/resources/OSGI-INF/configurator/configuration.json with the following:

{
    // Global Settings
    ":configurator:resource-version" : 1,
    ":configurator:symbolic-name" : "org.osgi.enroute.examples.microservice.config",
    ":configurator:version" : "0.0.1.SNAPSHOT",
    
    
    // Configure a JDBC resource provider
    "org.apache.aries.tx.control.jdbc.xa~microservice": {
           "name": "microservice.database",
           "osgi.jdbc.driver.class": "org.h2.Driver",
           "url": "jdbc:h2:./data/database" },
    
    // Target the Dao impls at the provider we configured
    "org.osgi.enroute.examples.microservice.dao.impl.PersonDaoImpl": {
           "provider.target": "(name=microservice.database)" },
    "org.osgi.enroute.examples.microservice.dao.impl.AddressDaoImpl": {
           "provider.target": "(name=microservice.database)" }
}

Build

Check the modules that make up your application build cleanly from the top level project directory

~/microservice $ mvn -pl !rest-app verify

Note - If you are a linux/unix/*nix user, you might need to escape the exclamation point in the above command.

Now build the rest-app bundle using package command.

~/microservice $ mvn -pl rest-app package

Note - if this build fails then check your code and pom dependencies and try again.

We now generate the required OSGi indexes from the project dependencies, and resolve our application.

~/microservice $ mvn -pl rest-app -am bnd-indexer:index \
    bnd-indexer:index@test-index bnd-resolver:resolve

Note Don’t do a clean before running this step, it’s building the indexes and resolution from the bundles you made in the previous step. Also, you don’t need to run this step every time, just if your dependency graph needs to be recalculated.

And finally generate the runnable jar from the top level project directory.

~/microservice $ mvn verify

Re-inspecting ~/microservice/rest-app/rest-app.bndrun we can see that this now explicitly references the acceptable version range for each required OSGi bundle. At runtime the OSGi framework resolves these requirements against the capabilities in the specified target repository: i.e. target/index.xml.

index: target/index.xml;name="rest-app"

-standalone: ${index}

-resolve.effective: active

-runproperties: \
	logback.configurationFile=file:${.}/logback.xml

-runrequires: \
	osgi.identity;filter:='(osgi.identity=org.osgi.enroute.examples.microservice.rest-service)',\
	osgi.identity;filter:='(osgi.identity=org.osgi.enroute.examples.microservice.rest-app)',\
	osgi.identity;filter:='(osgi.identity=org.apache.johnzon.core)',\
	osgi.identity;filter:='(osgi.identity=org.h2)'
-runfw: org.eclipse.osgi
-runee: JavaSE-1.8
-runbundles: \
	ch.qos.logback.classic;version='[1.2.3,1.2.4)',\
	ch.qos.logback.core;version='[1.2.3,1.2.4)',\
	org.apache.aries.javax.jax.rs-api;version='[1.0.4,1.0.5)',\
	org.apache.felix.http.servlet-api;version='[1.1.2,1.1.3)',\
	org.osgi.service.jaxrs;version='[1.0.0,1.0.1)',\
	org.apache.felix.converter;version='[1.0.10,1.0.11)',\
	org.osgi.util.function;version='[1.1.0,1.1.1)',\
	org.osgi.util.promise;version='[1.1.0,1.1.1)',\
	slf4j.api;version='[1.7.25,1.7.26)',\
	tx-control-provider-jdbc-xa;version='[1.0.0,1.0.1)',\
	tx-control-service-xa;version='[1.0.0,1.0.1)',\
	org.h2;version='[1.4.196,1.4.197)',\
	org.apache.servicemix.specs.json-api-1.1;version='[2.9.0,2.9.1)',\
	org.apache.johnzon.core;version='[1.1.0,1.1.1)',\
	org.apache.geronimo.specs.geronimo-annotation_1.3_spec;version='[1.1.0,1.1.1)',\
	org.apache.aries.jax.rs.whiteboard;version='[1.0.6,1.0.7)',\
	org.apache.felix.configadmin;version='[1.9.16,1.9.17)',\
	org.apache.felix.configurator;version='[1.0.10,1.0.11)',\
	org.apache.felix.http.jetty;version='[4.0.14,4.0.15)',\
	org.apache.felix.scr;version='[2.1.16,2.1.17)',\
	org.osgi.enroute.examples.microservice.dao-api;version='[0.0.2,0.0.3)',\
	org.osgi.enroute.examples.microservice.dao-impl;version='[0.0.2,0.0.3)',\
	org.osgi.enroute.examples.microservice.rest-app;version='[0.0.2,0.0.3)',\
	org.osgi.enroute.examples.microservice.rest-service;version='[0.0.2,0.0.3)'

Run

To dynamically assemble and run the resultant REST Microservice simply change back to the top level project directory and type the command:

~/microservice $ java -jar rest-app/target/rest-app.jar

The REST service can be seen by pointing a browser to http://localhost:8080/microservice/index.html

Microservice demo

Stop the application using Ctrl+C in the console.

Finally, if we create and run the debug version of the Microservice we can see all of the OSGi bundles used in the actual runtime assembly.

g! lb
START LEVEL 1
   ID|State      |Level|Name
    0|Active     |    0|System Bundle (5.7.0.SNAPSHOT)|5.7.0.SNAPSHOT
    1|Active     |    1|Logback Classic Module (1.2.3)|1.2.3
    2|Active     |    1|Logback Core Module (1.2.3)|1.2.3
    3|Active     |    1|Apache Aries Javax Annotation API (0.0.1.201711291743)|0.0.1.201711291743
    4|Active     |    1|Apache Aries JAX-RS Specification API (0.0.1.201803231639)|0.0.1.201803231639
    5|Active     |    1|Apache Aries JAX-RS Whiteboard (0.0.1.201803231640)|0.0.1.201803231640
    6|Active     |    1|Apache Commons FileUpload (1.3.2)|1.3.2
    7|Active     |    1|Apache Commons IO (2.5.0)|2.5.0
    8|Active     |    1|Apache Felix Configuration Admin Service (1.9.0.SNAPSHOT)|1.9.0.SNAPSHOT
    9|Active     |    1|Apache Felix Configurer Service (0.0.1.SNAPSHOT)|0.0.1.SNAPSHOT
   10|Active     |    1|Apache Felix Gogo Command (1.0.2)|1.0.2
   11|Active     |    1|Apache Felix Gogo Runtime (1.0.10)|1.0.10
   12|Active     |    1|Apache Felix Gogo Shell (1.0.0)|1.0.0
   13|Active     |    1|Apache Felix Http Jetty (3.4.7.R7-SNAPSHOT)|3.4.7.R7-SNAPSHOT
   14|Active     |    1|Apache Felix Servlet API (1.1.2)|1.1.2
   15|Active     |    1|Apache Felix Inventory (1.0.4)|1.0.4
   16|Active     |    1|Apache Felix Declarative Services (2.1.0.SNAPSHOT)|2.1.0.SNAPSHOT
   17|Active     |    1|Apache Felix Web Management Console (4.3.4)|4.3.4
   18|Active     |    1|Apache Felix Web Console Service Component Runtime/Declarative Services Plugin (2.0.8)|2.0.8
   19|Active     |    1|Johnzon :: Core (1.1.0)|1.1.0
   20|Active     |    1|Apache ServiceMix :: Specs :: JSon API 1.1 (2.9.0)|2.9.0
   21|Active     |    1|H2 Database Engine (1.4.196)|1.4.196
   22|Active     |    1|dao-api (0.0.1.201803251221)|0.0.1.201803251221
   23|Active     |    1|dao-impl (0.0.1.201803251221)|0.0.1.201803251221
   24|Active     |    1|rest-app (0.0.1.201803251650)|0.0.1.201803251650
   25|Active     |    1|rest-service (0.0.1.201803251221)|0.0.1.201803251221
   26|Active     |    1|org.osgi:org.osgi.service.jaxrs (1.0.0.201803131808-SNAPSHOT)|1.0.0.201803131808-SNAPSHOT
   27|Active     |    1|org.osgi:org.osgi.util.converter (1.0.0.201803131810-SNAPSHOT)|1.0.0.201803131810-SNAPSHOT
   28|Active     |    1|org.osgi:org.osgi.util.function (1.1.0.201803131808-SNAPSHOT)|1.1.0.201803131808-SNAPSHOT
   29|Active     |    1|org.osgi:org.osgi.util.promise (1.1.0.201803131808-SNAPSHOT)|1.1.0.201803131808-SNAPSHOT
   30|Active     |    1|osgi.cmpn (4.3.1.201210102024)|4.3.1.201210102024
   31|Active     |    1|slf4j-api (1.7.25)|1.7.25
   32|Active     |    1|OSGi Transaction Control JDBC Resource Provider - XA Transactions (1.0.0.201801251821)|1.0.0.201801251821
   33|Active     |    1|Apache Aries OSGi Transaction Control Service - XA Transactions (1.0.0.201801251821)|1.0.0.201801251821