-
Notifications
You must be signed in to change notification settings - Fork 11
Writing a simple MySQL Data Provider
- 1 Setup Bindaas Development Environment
- 2 Create new Plugin Project
- 3 Bundle Activator
- 4 Write SimpleMySQLDataProvider
- 5 Write QueryHandler
- 6 Write DeleteHandler
- 7 Spring Bean Configuration
- 8 Testing
- 9 Exporting project as osgi bundle from eclipse
- 10 Source Code
This screencast provide more information. However, be aware that it is outdated.
In Eclipse, start by creating a new Plugin-in Project
. Go with the
default configuration but remember to change
the Target Platform
to Equinox
. As you go through the wizard, in the
second window you will be asked to specify the name of the Activator
class. This must be a fully qualified name. For this exercise, we will
name it edu.emory.cci.sample.mysql.bundle.Activator.
Bindaas relies on spring-framework for managing application beans.
Although, it is not mandatory, but as a matter of consistency all
Bindaas projects should use spring to manage lifecycle of application
beans. The spring-framework is configured to pickup any *.xml file
under $PROJECT_HOME/META-INF/spring
directory. Create this directory.
After creating a brand new project, $PROJECT_HOME/META-INF/MANIFEST.MF
must be modified to import packages that are
required by this project to develop a Data Provider. These packages are
as follows. NOTE: You can edit these files directly from the tab
"MANIFEST.MF"
Import-Package: com.google.gson;version="2.1.0",
com.google.gson.annotations;version="2.1.0",
com.google.gson.internal;version="2.1.0",
com.google.gson.internal.bind;version="2.1.0",
com.google.gson.reflect;version="2.1.0",
com.google.gson.stream;version="2.1.0",
edu.emory.cci.bindaas.framework.api,
edu.emory.cci.bindaas.framework.event,
edu.emory.cci.bindaas.framework.event.listeners,
edu.emory.cci.bindaas.framework.model,
edu.emory.cci.bindaas.framework.provider.exception,
edu.emory.cci.bindaas.framework.util,
org.apache.commons.logging;version="1.1.1",
org.osgi.framework;version="1.3.0"
One more entry needs to be added in order to tell Bindaas where to pick-up spring configuration files :
Spring-Context: META-INF/spring/*.xml
Putting it all together, the final MANIFEST file should look something like :
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: SimpleMySQLDataProvider
Bundle-SymbolicName: SimpleMySQLDataProvider
Bundle-Version: 1.0.0.qualifier
Bundle-Activator: edu.emory.cci.sample.mysql.bundle.Activator
Bundle-ActivationPolicy: lazy
Spring-Context: META-INF/spring/*.xml
Bundle-RequiredExecutionEnvironment: JavaSE-1.6
Import-Package: com.google.gson;version="2.1.0",
com.google.gson.annotations;version="2.1.0",
com.google.gson.internal;version="2.1.0",
com.google.gson.internal.bind;version="2.1.0",
com.google.gson.reflect;version="2.1.0",
com.google.gson.stream;version="2.1.0",
edu.emory.cci.bindaas.framework.api,
edu.emory.cci.bindaas.framework.event,
edu.emory.cci.bindaas.framework.event.listeners,
edu.emory.cci.bindaas.framework.model,
edu.emory.cci.bindaas.framework.provider.exception,
edu.emory.cci.bindaas.framework.util,
org.apache.commons.logging;version="1.1.1",
org.osgi.framework;version="1.3.0"
The bundle activator is the entry point of the project. It is invoked by
OSGi framework when the bundle is started. The start
method is called
and an instance of BundleContext
is passed on to it. By default,
eclipse creates a simple Activator class with a static method that
returns an instance of BundleContext. BundleContext is the most critical
class provided by OSGi framework as it's the only way to access OSGi
Service registry. We shall add a logging statement [ line #10 ] when
the bundle starts to ensure that bundle indeed started successfully.
public class Activator implements BundleActivator {
private static BundleContext context;
private Log log = LogFactory.getLog(getClass());
static public BundleContext getContext() {
return context;
}
public void start(BundleContext bundleContext) throws Exception {
Activator.context = bundleContext;
log.info("The Simple MySQL Data Provider bundle is started ......");
}
In order to proceed, we will need the JDBC driver for MySQL database in project's classpath. Since, it is not available for import from other OSGi bundles, we shall embed the jar directly into the project.
For more information, follow this screencast.
- Copy the mysql-connector-java-3.0.16-ga-bin.jar into the project's home directory
- Edit the MANIFEST file and add this jar to the runtime
Create a new class edu.emory.cci.sample.mysql.core.SimpleMySQLDataProvider as follows :
// package imports omitted for brevity
public class SimpleMySQLDataProvider implements IProvider{
private IQueryHandler queryHandler;
private IDeleteHandler deleteHandler;
private Log log = LogFactory.getLog(getClass());
private static Driver driver;
static{
try {
driver = new Driver(); // Initializing the MySQL database driver
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
The queryHandler and deleteHandler will be injected using Spring. The static code block initializes the MySQL database driver.
// init method is invoked by spring-framework after calling the constructor to create an instance of the object
public void init() throws Exception
{
Dictionary<String, String> props = new Hashtable<String, String>();
props.put("class", getClass().getName()); // required property to be registered with every Data Provider implementation
props.put("driver", driver.getClass().getName()); // optional
props.put("driver-version(major/minor)", driver.getMajorVersion() + "/" + driver.getMinorVersion()); // optional
// Registering this object as a service in OSGi registry against IProvider interface. Unless this registration is done, Bindaas will not be able to locate this Data Provider
Activator.getContext().registerService(IProvider.class.getName(), this, props);
log.info("SimpleMySQLDataProvider initialized");
}
// Methods overridden from IProvider
@Override
public JsonObject getDataSourceSchema() {
return new JsonObject(); // return a empty instance of JsonObject
}
@Override
public IDeleteHandler getDeleteHandler() {
return deleteHandler; // return instance of deleteHandler
}
@Override
public JsonObject getDocumentation() {
return new JsonObject(); // return a empty instance of JsonObject
}
@Override
public String getId() {
return SimpleMySQLDataProvider.class.getName(); // returning the class name of Data Provider
}
@Override
public IQueryHandler getQueryHandler() {
return queryHandler; // return instance of queryHandler
}
@Override
public ISubmitHandler getSubmitHandler() {
throw new RuntimeException(new MethodNotImplementedException(SimpleMySQLDataProvider.class.getName(), 1)); // submitHandler not implemented for this Data Provider
}
@Override
public int getVersion() {
return 1; // Data Provider version
}
// This method is called when query author create an instance of Data Provider by providing data source configuration
// The configuration properties are validated and initialized in this method
@Override
public Profile validateAndInitializeProfile(Profile profile)
throws ProviderException {
JsonObject dataSourceProps = profile.getDataSource(); // retrieve Data Source configuration
DataSourceConfiguration configuration = GSONUtil.getGSONInstance().fromJson(dataSourceProps, DataSourceConfiguration.class); // Cast JSON to DataSourceConfiguration type
Connection connection = null;
try {
configuration.validate(); // validate properties
connection = getConnection(configuration); // get JDBC connection instance using these properties
} catch (Exception e) {
log.error(e);
throw new ProviderException(this.getClass().getName() , this.getVersion() , e);
}
finally{
if(connection!=null)
{
try {
connection.close(); // close connection after the transaction is complete
} catch (SQLException e) {
log.error(e);
throw new ProviderException(this.getClass().getName() , this.getVersion() ,e);
}
}
}
return profile;
}
Line 8: Notice that dataSourceProps is casted to an instance of DataSourceConfiguration. DataSourceConfiguration is a class that models the information required to configure the Data Provider. This information comes from the query author when a new Data Provider instance is created. As a matter of practice, DataSourceConfiguration should be created for every Data Provider implementation, which should capture information necessary to communicate with the underlying database technology. Since we are using MySQL database, the properties we are interested in are username,password and jdbcUrl.
public class DataSourceConfiguration {
@Expose private String username;
@Expose private String password;
@Expose private String url;
The QueryHandler must implement
the edu.emory.cci.bindaas.framework.api.IQueryHandler
interface.
public class QueryHandler implements IQueryHandler{
private Log log = LogFactory.getLog(getClass());
.....
The validateAndInitializeQueryEndpoint method is implemented to validate the query author provider parameters at the time of creation. While writing a QueryHandler one must design a proper schema for IQueryHandler.getOutputFormat() . In this example, we are going to implement two formats : xml & json. Thus we choose the following data structure to capture this information from the query author :
{ "format" : "<format>" }
format :: xml | json
Implement validateAndInitializeQueryEndpoint as follows:
@Override
public QueryEndpoint validateAndInitializeQueryEndpoint(QueryEndpoint queryEndpoint)
throws ProviderException {
try {
JsonObject of = queryEndpoint.getOutputFormat();
if(of.has("format"))
{
String format = of.get("format").getAsString();
if(format.equals("xml") || format.equals("json"))
{
return queryEndpoint;
}
else
{
throw new Exception("invalid format specified");
}
}
else
{
throw new Exception("format not specified");
}
} catch (Exception e) {
log.error(e);
throw new ProviderException(SimpleMySQLDataProvider.class.getName() , 1 , "Validation of Input Parameters failed", e);
}
}
Implement the query method as follows:
@Override
public QueryResult query(JsonObject dataSource,JsonObject outputFormatProps, String queryToExecute, Map<String,String> runtimeParameters, RequestContext requestContext) throws AbstractHttpCodeException {
try {
// cast dataSource into DataSourceConfiguration type
DataSourceConfiguration configuration = GSONUtil.getGSONInstance().fromJson(dataSource,DataSourceConfiguration.class);
// create new JDBC connection
final Connection connection = SimpleMySQLDataProvider.getConnection(configuration);
// create JDBC statement
Statement statement = connection.createStatement(ResultSet.TYPE_FORWARD_ONLY,ResultSet.CONCUR_READ_ONLY);
// execute the SQL query
ResultSet resultSet = statement.executeQuery(queryToExecute);
// extract the format field
String format = outputFormatProps.get("format").getAsString();
QueryResult queryResult = new QueryResult();
if(format.equals("json"))
{
// serialize resultSet into JsonArray
JsonArray arr = convert(resultSet);
String data = arr.toString();
// set the serialized data bytes
queryResult.setData(new ByteArrayInputStream(data.getBytes()));
// set the mimeType of HTTP response
queryResult.setMimeType(StandardMimeType.JSON.toString());
}
else if (format.equals("xml"))
{
// serialize resultSet into XML format
String data = toXML(resultSet);
// set the serialized data bytes
queryResult.setData(new ByteArrayInputStream(data.getBytes()));
// set the mimeType of HTTP response
queryResult.setMimeType(StandardMimeType.JSON.toString());
}
else
throw new Exception("invalid format specified");
return queryResult;
} catch (Exception e) {
log.error(e);
throw new QueryExecutionFailedException(SimpleMySQLDataProvider.class.getName() , 1 , "Query Not Executed", e);
}
}
Writing a DeleteHandler is similar to QueryHandler, except one does not
have to deal with outputFormats. Conceptually, DeleteHandler represents
a delete operation. Hence, our implementation must work with DML queries
like SQL DELETE or UPDATE
.
public class DeleteHandler implements IDeleteHandler {
private Log log = LogFactory.getLog(getClass());
@Override
public QueryResult delete(JsonObject dataSource, String deleteQueryToExecute, Map<String,String> runtimeParamters , RequestContext requestContext) throws AbstractHttpCodeException {
try {
Connection connection = null;
try {
DataSourceConfiguration configuration = GSONUtil.getGSONInstance().fromJson(dataSource,DataSourceConfiguration.class);
connection = SimpleMySQLDataProvider.getConnection(configuration); // get connection
Statement statement = connection.createStatement(); // create statement
int status = statement.executeUpdate(deleteQueryToExecute); // execute delete/update query
QueryResult queryResult = new QueryResult();
queryResult.setMimeType(StandardMimeType.JSON.toString()); // set mimeType to JSON
queryResult.setData( new ByteArrayInputStream(String.format("{ 'result' : 'success' , 'rowsDeleted' : '%s' }",status + "").getBytes())); // set the count of rowsDeleted in the response
return queryResult;
} catch (Exception er) {
log.error(er);
throw er;
} finally {
if (connection != null)
connection.close(); // always close the connection before exiting
}
} catch (Exception e) {
log.error(e);
throw new DeleteExecutionFailedException(SimpleMySQLDataProvider.class.getName() , 1 , "Delete operation failed", e);
}
}
}
After writing all the classes the final step is to create a spring bean
configuration file that will take care of initializing all bean
definition and setting up dependencies. Create a new spring
configuration
file mysql-beans.xml
in $PROJECT_HOME/META-INF/spring
directory as
follows:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:osgi="http://www.springframework.org/schema/osgi"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/osgi
http://www.springframework.org/schema/osgi/spring-osgi.xsd">
<!-- Provider Bean -->
<bean id="simpleMysqlProvider" class="edu.emory.cci.sample.mysql.core.SimpleMySQLDataProvider" init-method="init">
<property name="queryHandler" ref="queryHandler"></property>
<property name="deleteHandler" ref="deleteHandler"></property>
</bean>
<bean id="queryHandler" class="edu.emory.cci.sample.mysql.core.QueryHandler"></bean>
<bean id="deleteHandler" class="edu.emory.cci.sample.mysql.core.DeleteHandler"></bean>
</beans>
For detailed instructions, please follow this screencast.
For detailed instructions, please follow this screencast.
After exporting the project as an OSGi bundle one can deploy this plugin
by placing it under $BINDAAS_DIST_HOME/bundles/pickup
directory.
Although Bindaas supports hot deployment it is recommended to restart
Bindaas after the installing any new plugin.
The source code of this demo can be found in the Bindaas repository
under : source/projects/demos/SimpleMySQLDataProvider