package com.temenos.interaction.jdbc.producer;
/*
* JDBC producer class.
*
* This wraps a spring Jdbc template. Its called with OData parameters and recovers the requested information from a SQL server.
*/
/*
* #%L
* interaction-jdbc-producer
* %%
* Copyright (C) 2012 - 2013 Temenos Holdings N.V.
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.naming.NamingException;
import javax.sql.DataSource;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response.Status;
import org.odata4j.producer.EntityQueryInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.rowset.SqlRowSet;
import org.springframework.jndi.JndiTemplate;
import com.temenos.interaction.core.command.InteractionContext;
import com.temenos.interaction.core.entity.Entity;
import com.temenos.interaction.core.entity.EntityProperties;
import com.temenos.interaction.core.entity.EntityProperty;
import com.temenos.interaction.core.resource.CollectionResource;
import com.temenos.interaction.core.resource.EntityResource;
import com.temenos.interaction.jdbc.ServerMode;
import com.temenos.interaction.jdbc.exceptions.JdbcException;
import com.temenos.interaction.jdbc.producer.sql.ColumnTypesMap;
import com.temenos.interaction.jdbc.producer.sql.SqlBuilder;
import com.temenos.interaction.jdbc.producer.sql.SqlBuilderFactory;
import com.temenos.interaction.odataext.odataparser.ODataParser;
import com.temenos.interaction.odataext.odataparser.ODataParser.UnsupportedQueryOperationException;
import com.temenos.interaction.odataext.odataparser.data.AccessProfile;
import com.temenos.interaction.odataext.odataparser.data.FieldName;
import com.temenos.interaction.odataext.odataparser.data.OrderBy;
import com.temenos.interaction.odataext.odataparser.data.RowFilters;
public class JdbcProducer {
// Somewhere to store connection
private JdbcTemplate template;
// Current compatibility mode.
private ServerMode serverMode;
private ServerMode h2ServerMode = null;
private static final Logger LOGGER = LoggerFactory.getLogger(JdbcProducer.class);
/*
* Constructor called when a DataSource object to be obtained from Jndi.
*/
public JdbcProducer(JndiTemplate jndiTemplate, String dataSourceName) throws ClassNotFoundException, JdbcException,
NamingException {
this((DataSource) jndiTemplate.lookup(dataSourceName));
}
/*
* Constructor called when a DataSource object is available.
*/
public JdbcProducer(DataSource dataSource) throws ClassNotFoundException, JdbcException {
template = new JdbcTemplate(dataSource);
serverMode = getServerMode();
}
/*
* Constructor USED IN TESTING.
*
* When using H2 there does not appear to be any way to read the server's
* current compatibility mode. So enable the test to pass this as a
* parameter. If it turns out that this information can be read from the
* server remove this constructor and add the read to getServerMode().
*/
public JdbcProducer(DataSource dataSource, ServerMode h2ServerMode) throws ClassNotFoundException, JdbcException {
template = new JdbcTemplate(dataSource);
this.h2ServerMode = h2ServerMode;
serverMode = getServerMode();
}
/*
* Query method for raw SQL commands
*/
public SqlRowSet query(String command) {
return template.queryForRowSet(command);
}
/*
* Query method for interaction context parameters returning collection of
* entities.
*/
public CollectionResource<Entity> queryEntities(String tableName, InteractionContext ctx, String returnEntityType)
throws UnsupportedQueryOperationException, JdbcException, Exception {
SqlRowSet rowSet = query(tableName, null, ctx);
return buildCollectionResource(returnEntityType, rowSet);
}
/*
* Query method for interaction context parameters returning a single
* entity.
*/
public EntityResource<Entity> queryEntity(String tableName, String key, InteractionContext ctx,
String returnEntityType) throws UnsupportedQueryOperationException, JdbcException, Exception {
SqlRowSet rowSet = query(tableName, key, ctx);
return createEntityResource(returnEntityType, rowSet);
}
/*
* Query method for interaction context parameters returning raw sql data.
*
* If given a key will return a single row.
*
* If given a null key will return all rows.
*/
public SqlRowSet query(String tableName, String key, InteractionContext ctx)
throws UnsupportedQueryOperationException, JdbcException, Exception {
// Not much point selecting from a null table
if (null == tableName) {
LOGGER.error("Jdbc producer cannot select from null table.");
throw new JdbcException(Status.INTERNAL_SERVER_ERROR, "Null table name");
}
// Get column types from Jdbc. We need these both for constructing the
// command and processing it's result set.
// We need the primary key for row ordering.
ColumnTypesMap colTypesMap = new ColumnTypesMap(this, tableName, true);
// Unpack the commands $filter and $select terms.
AccessProfile accessProfile = getAccessProfile(ctx);
// Get top and skip parameters (null if not specified).
MultivaluedMap<String, String> queryParams = ctx.getQueryParameters();
String top = queryParams.getFirst(ODataParser.TOP_KEY);
String skip = queryParams.getFirst(ODataParser.SKIP_KEY);
List<OrderBy> orderBy = ODataParser.parseOrderBy(queryParams.getFirst(ODataParser.ORDERBY_KEY));
// Build an SQL command from an appropriate builder
SqlBuilder sqlBuilder = SqlBuilderFactory.getSqlBuilder(tableName, key, accessProfile, colTypesMap, top, skip,
orderBy, serverMode);
String sqlCommand = sqlBuilder.getCommand();
LOGGER.info("Jdbc producer about to execute \"" + sqlCommand + "\"");
// Execute the SQL command
return query(sqlCommand);
}
public DataSource getDataSource() {
return template.getDataSource();
}
/*
* Unpack a contexts $filter and $select terms.
*/
private AccessProfile getAccessProfile(InteractionContext ctx) throws UnsupportedQueryOperationException {
EntityQueryInfo queryInfo = ODataParser.getEntityQueryInfo(ctx);
RowFilters filters = new RowFilters(queryInfo.filter);
Set<FieldName> selects = ODataParser.parseSelect(queryInfo.select);
return new AccessProfile(filters, selects);
}
/*
* Convert result to a single entry.
*/
EntityResource<Entity> createEntityResource(String entityType, SqlRowSet rowSet) throws JdbcException {
// Extract the returned column names. May be a subset of the ones
// requested.
String[] columnNames = rowSet.getMetaData().getColumnNames();
// Set cursor to first row
if (!rowSet.next()) {
throw new JdbcException(Status.NOT_FOUND, "Row not found. Entry with given key possibly not present.");
}
// Build up properties for this row
EntityProperties properties = new EntityProperties();
// For all columns in this row.
for (String columnName : columnNames) {
Object value = rowSet.getObject(columnName);
// Only return non null values
if (null != value) {
// Add object to the property. getObject() returns an object
// with the correct java type for each sql type. So we don't
// need to cast.
properties.setProperty(new EntityProperty(columnName, value));
}
}
// Make an entity
Entity entity = new Entity(entityType, properties);
// Make an entity resource
EntityResource<Entity> entityResource = new EntityResource<Entity>(entityType, entity);
// Check for additional rows. Not expected for a 'single'
// command.
if (rowSet.next()) {
throw new JdbcException(Status.INTERNAL_SERVER_ERROR, "Multiple rows returned for a single entity");
}
return entityResource;
}
/*
* Convert result set into a collection of entities.
*/
private CollectionResource<Entity> buildCollectionResource(String entityType, SqlRowSet rowSet) {
List<EntityResource<Entity>> results = new ArrayList<EntityResource<Entity>>();
// Extract the returned column names. May be a subset of the ones
// requested.
String[] columnNames = rowSet.getMetaData().getColumnNames();
// For all rows returned add an entity to the collection.
while (rowSet.next()) {
EntityProperties properties = new EntityProperties();
// For all columns in this row.
for (String columnName : columnNames) {
Object value = rowSet.getObject(columnName);
// Only return non null values
if (null != value) {
// Add object to the property. getObject() returns an object
// with the correct java type for each sql type. So we don't
// need to cast.
properties.setProperty(new EntityProperty(columnName, value));
}
}
// Create entity.
// Note: Despite the variable name the first arg of both these is
// the entity type name. Not it's key.
Entity entity = new Entity(entityType, properties);
results.add(new EntityResource<Entity>(entity.getName(), entity));
}
// Note: This line looks a bit odd but the {} at the end is required.
return new CollectionResource<Entity>(results) {
};
}
/*
* Utility to work out the current server mode.
*
* This is probably untestable.
*/
private ServerMode getServerMode() throws JdbcException {
// If a server comparability mode has been passed use it.
if (null != h2ServerMode) {
return h2ServerMode;
}
// Look for real servers
String url = null;
// Get the connection URL
Connection connection = null;
try {
connection = template.getDataSource().getConnection();
} catch (SQLException ex) {
throw new JdbcException(Status.INTERNAL_SERVER_ERROR, "Could get connection to datasource. ", ex);
}
try {
url = connection.getMetaData().getURL();
} catch (SQLException ex) {
throw new JdbcException(Status.INTERNAL_SERVER_ERROR, "Could not get server URL. ", ex);
} finally {
closeConnection(connection);
}
// Extract server type from URL
String[] tokens = url.split(":");
if (tokens != null && tokens.length > 1 && tokens[1] != null) {
String serverType = tokens[1];
if ("oracle".equals(serverType)) {
return ServerMode.ORACLE;
}
if ("mssql".equals(serverType) || "sqlserver".equals(serverType)) {
return ServerMode.MSSQL;
}
if ("h2".equals(serverType)) {
LOGGER.warn("Running under H2 but no server compatibility mode specified. Defaulting to emulated MSSQL mode.");
return ServerMode.H2_MSSQL;
}
throw new JdbcException(Status.PRECONDITION_FAILED, "JDBC Server type \"" + serverType
+ "\" not supported.");
}
throw new JdbcException(Status.INTERNAL_SERVER_ERROR,
"Failed to detect JDBC server type from connection URL \"" + url + "\".");
}
private static void closeConnection(Connection connection) {
try {
if (null != connection) {
connection.close();
}
} catch (SQLException ex) {
throw new JdbcException(Status.INTERNAL_SERVER_ERROR, "Could not close connection to datasource. ", ex);
}
}
}