Skip to content

Writing a simple MySQL Data Provider

Pradeeban Kathiravelu edited this page Aug 24, 2018 · 5 revisions

Create new Plugin Project

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.

Create directory for spring bean definitions

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. 

Edit MANIFEST entries

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"

 

Bundle Activator

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 ......");
    }

Write SimpleMySQLDataProvider

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.

  1. Copy the mysql-connector-java-3.0.16-ga-bin.jar into the project's home directory
  2. 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;

Write QueryHandler

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);
        }
    }

Write DeleteHandler

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);
        }
    }
}

Spring Bean Configuration

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>

Testing

For detailed instructions, please follow this screencast.

Exporting project as osgi bundle from eclipse

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.

Source Code

The source code of this demo can be found in the Bindaas repository under : source/projects/demos/SimpleMySQLDataProvider

 

Clone this wiki locally