/*
* (C) Copyright IBM Corp. 2014
*
* LICENSE: Eclipse Public License v1.0
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.ibm.db2j;
import java.net.ConnectException;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Date;
import java.util.Map;
import org.apache.derby.iapi.error.StandardException;
import org.apache.derby.iapi.store.access.Qualifier;
import org.apache.derby.iapi.types.DataValueDescriptor;
import org.apache.derby.iapi.types.Orderable;
import org.apache.derby.vti.Pushable;
import org.apache.derby.vti.VTIEnvironment;
import com.ibm.gaiandb.DataSourcesManager;
import com.ibm.gaiandb.GaianDBConfigProcedures;
import com.ibm.gaiandb.GaianResultSetMetaData;
import com.ibm.gaiandb.Logger;
import com.ibm.gaiandb.mongodb.MongoMessages;
import com.ibm.gaiandb.mongodb.MongoConnectionFactory;
import com.ibm.gaiandb.mongodb.MongoConnectionParams;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
/**
* This class implements a Virtual Table Interface allowing GaianDB
* to retrieve data from MongoDB databases.
*
* @author Paul Stone
*/
public class MongoDB extends AbstractVTI implements Pushable {
// Use PROPRIETARY notice if class contains a main() method, otherwise use COPYRIGHT notice.
public static final String COPYRIGHT_NOTICE = "(c) Copyright IBM Corp. 2014";
private static final Logger logger = new Logger( "MongoDB", 10 );
public enum CombinationOperator { OR, AND };
//define string constants which map to the configuration parameters
static final String PROP_ADDRESS = "address";
static final String PROP_PORT = "port";
static final String PROP_DB_NAME = "db";
static final String PROP_COLLECTION_NAME = "collection";
static final String PROP_USER = "user";
static final String PROP_PASSWORD = "password";
// constant representing an "unconstrained" mongoDB query - will match any document in a mongo collection.
private static final BasicDBObject QUERY_ANYTHING = new BasicDBObject();
// instanceMongoQuery is a handle to the mongoDB collection, queries can be executed
// against this handle to retrieve data.
DBCollection instanceMongoCollection;
// instanceMongoQuery is used to hold the qualifier values in a format suitable for querying mongo.
// an empty object matches all documents in mongo.
BasicDBObject instanceMongoQuery;
// mongoAllColumns is used to hold the field names to return for the whole logical table. Note that the "id" field is always returned
// from mongo for each document.
BasicDBObject mongoAllColumns;
// mongoQueryColumns is used to hold the field names to return from the latest specific mongo query. .
BasicDBObject mongoQueryColumns;
// mongoResults references the results of the latest mongoDB query. The result data can
// be retrieved from this object.
DBCursor mongoResults;
/**
* This method creates a connection to the appropriate mongo process/database/collection
* according to the parameters in the Gaian Configuration file.
* @param vtiArgs - contains the Datasource "_ARGS" field from the config file.
* @throws This will throw an exception if we are unable to connect to the specified,
* configured Mongo database process.
*/
public MongoDB(String vtiArgs) throws Exception {
// The super class constructor sets up the VTI parameters from the configuration file.
super(vtiArgs, "MongoDBVTI");
// Get the connection details from the configuration.
MongoConnectionParams connDetails =
new MongoConnectionParams (
getVTIProperty (PROP_ADDRESS),
Integer.parseInt(getVTIProperty (PROP_PORT)),
getVTIPropertyWithReplacements (PROP_DB_NAME),
getVTIPropertyWithReplacements (PROP_COLLECTION_NAME),
getVTIPropertyNullable (PROP_USER),
getVTIPropertyNullable (PROP_PASSWORD) );
// Connect to the mongo collection, reference the collection as an instance variable
// so other methods - particularly executeAsFastPath can use it.
instanceMongoCollection = MongoConnectionFactory.getMongoCollection(connDetails);
if (instanceMongoCollection == null) throw new ConnectException(MongoMessages.DSWRAPPER_MONGODB_COLLECTION_ACCESS_ERROR);
//work out which fields we should be extracting. Limit the future queries to these fields.
GaianResultSetMetaData rowDescription;
try {
rowDescription = getMetaData();
} catch (SQLException e1) {
logger.logException(MongoMessages.DSWRAPPER_MONGODB_META_DATA_ERROR,MongoMessages.DSWRAPPER_MONGODB_META_DATA_ERROR, e1);
return;
}
mongoAllColumns = new BasicDBObject();
// Go through the items defined in the config, restrict the query to these rows only.
for ( int columnId = 0; columnId < rowDescription.getColumnCount(); columnId++){
String fieldName = rowDescription.getColumnName(columnId+1);
mongoAllColumns.put(fieldName, 1);
}
// Assume that the query will return all columns - this will be changed if pushProjection is called.
mongoQueryColumns = mongoAllColumns;
}
/**
* This method will execute a query against the connected Mongo Process.
* By this stage we should have connected to the Mongo database and collection and
* have processed any qualifiers and projections.
* Any qualifiers passed in "setQualifiers" and any column projections passed in "pushProjection"
* are used to constrain the query.
* The result of the query is held as an instance variable for further access using the nextRow() method.
* @throws SQLException if there is no connection to a mongo process or the query fails.
* @return "true" will always be returned unless an exception is raised.
*/
@Override
public boolean executeAsFastPath() throws SQLException {
if (instanceMongoCollection == null) {
// We don't have a valid Mongo Collection so we can't execute the query.
throw new SQLException(MongoMessages.DSWRAPPER_MONGODB_NOT_CONNECTED);
}
//Initialise these in case we have not been passed qualifiers or projected columns
BasicDBObject mongoQuery = instanceMongoQuery;
if (mongoQuery == null) mongoQuery = new BasicDBObject();
if (mongoQueryColumns == null) {
//Call mongo to find any document matching our query
mongoResults = instanceMongoCollection.find(mongoQuery);
} else {
//Call mongo to find any document matching our query
mongoResults = instanceMongoCollection.find(mongoQuery, mongoQueryColumns);
}
if (mongoResults == null) {
// for some reason the
throw new SQLException(MongoMessages.DSWRAPPER_RESULTSET_NOT_CONNECTED);
}
mongoResults.batchSize(1000); //configure mongo client to pull back 1000 results at a time.
return true;
}
/**
* This method maps from a derby Qualifier operator to a mongoDB operator string.
* @param qualifierOperator - a derby format operator.
* @return String - the equivalent mongoDB text to the qualifierOperator.
*/
private String mongoOperatorLookup (int qualifierOperator) {
switch (qualifierOperator) {
case (Orderable.ORDER_OP_LESSTHAN): return ("$lt");
case (Orderable.ORDER_OP_LESSOREQUALS):return ("$lte");
case (Orderable.ORDER_OP_GREATERTHAN):return ("$gt");
case (Orderable.ORDER_OP_GREATEROREQUALS):return ("$gte");
default:
logger.logImportant("MongoDB.mongoOperatorLookup - qualifierOperator: " + qualifierOperator + " is not known.");
throw new IllegalArgumentException("qualiferOperator: " + qualifierOperator);
}
}
/**
* This method adds an operator to the mongo query to enforce the derby qualifier specified.
* @param mongoConditions - Object representing the conditions of a mongoDB query.
* This will be modified to enforce the qualifier condition
* @param qual - A derby format qualifier, holding a condition that should be added to the query.
*/
public void addMongoOperator(BasicDBObject mongoConditions, Qualifier qual) {
try {
// work out the two component parts - the field and the value being compared
String mongoFieldName = getMetaData().getColumnName(qual.getColumnId() + 1);
Object mongoValue = qual.getOrderable().getObject();
if (qual.getOperator()== Orderable.ORDER_OP_EQUALS){
//equals operator is a special case
if (qual.negateCompareResult()){
mongoConditions.append(mongoFieldName, new BasicDBObject ("$ne", mongoValue)); //$ne is the mongo "not equals" operator, so this is "field != value"
} else {
mongoConditions.append(mongoFieldName, mongoValue); //add a condition that field = value
}
} else {
String mongoOperatorText = mongoOperatorLookup(qual.getOperator());
if (qual.negateCompareResult()){
// add a condition comparing the field and value with the negated operator
mongoConditions.append(mongoFieldName, new BasicDBObject("$not", new BasicDBObject (mongoOperatorText, mongoValue)));
} else {
// add a condition comparing the field and value with the operator
mongoConditions.append(mongoFieldName, new BasicDBObject (mongoOperatorText, mongoValue));
}
}
} catch (SQLException e) {
// This happens if we cannot resolve the qualifier metadata.
// log the exception and carry on without this qualifier.
logger.logException(MongoMessages.DSWRAPPER_MONGODB_QUALIFIER_META_DATA_ERROR, "Meta Data resolution failed for " + getPrefix(),e);
} catch (StandardException e) {
// This happens when the qualifier.getOrderable throws an error.
// log the exception and carry on without this qualifier.
logger.logException(MongoMessages.DSWRAPPER_MONGODB_QUALIFIER_ACCESS_ERROR, "Qualifier error for: " + qual.toString(),e);
}
}
/**
* This method adds an operator to the mongo query to enforce the derby qualifiers specified.
* @param mongoConditions - Object representing the conditions of a mongoDB query.
* This will be modified to enforce the qualRow conditions
* @param qualRow - an array of derby database qualifiers, holding a set of conditions
* that should be added to the query.
*/
public void addMongoOperators (BasicDBList mongoConditions, Qualifier qualRow[]){
if (qualRow != null) {
for (Qualifier qual : qualRow) {
BasicDBObject insideOperator = new BasicDBObject();
addMongoOperator(insideOperator, qual);
mongoConditions.add(insideOperator);
}
}
}
/**
* This method adds an operator to the mongo query to enforce the derby qualifiers specified.
* @param query - Object representing the conditions of a mongoDB query.
* This will be modified to enforce the qualRow conditions
* @param qualRow - an array of derby database qualifiers, holding a set of conditions
* that should be added to the query.
* @param operator - indicates how the qualRow conditions should be combined with the query - "OR" or "AND".
*/
public void addMongoOperatorRow(BasicDBObject query, Qualifier qualRow[], CombinationOperator operator){
if (qualRow == null || qualRow.length == 0){
// Don't include any entry for this row.
} else if (qualRow.length == 1){
// just include the single entry
addMongoOperator(query, qualRow[0]);
} else {
// we have > 1 qualifier so they need including in a list, prefixed by the
// necessary operator.
String combinationText;
switch (operator){
case OR: combinationText = "$or";
break;
case AND: combinationText = "$and";
break;
default: // throw exception
combinationText = "$invalid";
}
// construct a separate sub-list of conditions to hold the qualifiers.
BasicDBList insideOperatorList = new BasicDBList();
addMongoOperators(insideOperatorList, qualRow);
query.append(combinationText,insideOperatorList);
}
}
/**
* This method takes the derby qualifiers specified and determines how to apply the same conditions to
* a mongo query so that the correct, matching mongo documents are returned. The generated mongo query
* is held as an instance variable in "instanceMongoQuery" for when the query is executed.
* @param vtie - I handle to VTI Environment parameters - not used for this VTI.
* @param qualMatrix - a two dimensional array of qualifiers determining the conditions for matching
* query results. This is held in Conjunctive Normal Form (CNF) and is described
* the java doc for the org.apache.derby.iapi.store.access.Qualifier class
*/
@Override
public void setQualifiers(VTIEnvironment vtie, Qualifier[][] qualMatrix){
// a local variable to build the mongo query structure.
BasicDBObject mongoQueryOperators = new BasicDBObject();
if (qualMatrix != null) {
// The first slot of the 2 dimensional qual array is a list of "and" conditions.
// The remaining slots of the 2 dimensional qual array are list of "or" conditions.
// all slots are combined by "and"ing them together.
// represent mongo conditions as {{row-1-conditions},{row-2-conditions},...,{row-n-conditions}}
for (int index = 0; index < qualMatrix.length; index++){
Qualifier qualRow[] = qualMatrix[index];
// work out what logical operator is used to join multiple conditions. This is determined by derby
// see derby javadoc for qualifiers.
CombinationOperator operator;
if (index ==0){
operator = CombinationOperator.AND; // the first row of qualifiers are combined by "and" conditions
} else {
operator = CombinationOperator.OR; // other qualifier rows are combined by "or" conditions
}
addMongoOperatorRow(mongoQueryOperators,qualRow,operator);
}
}
instanceMongoQuery = mongoQueryOperators;
logger.logInfo("MongoVTI - query will use qualifiers: "+ mongoQueryOperators);
}
/**
* This method takes a mongo document resulting from a query and converts it into a row of data suitable to be
* returned to Derby.
* The datasource meta data is used to determine how fields in the mongo document map to derby columns.
* This method performs type casting from mongo to derby data types.
* Fields that are unsuccessfully parsed are returned as null in the derbyRow.
* @param mongoDoc - A mongo Document returned froma query
* @param derbyRow - An array of DataValueDescriptors, used to pass data results to Derby. This is updated.
* @return boolean - indicates the success of the document translation.
*/
private boolean parseBSONMongoDocument (DBObject mongoDoc, DataValueDescriptor[] derbyRow ) {
//get meta data to determine which fields and columns we expect
GaianResultSetMetaData rowDescription;
try {
rowDescription = getMetaData();
} catch (SQLException e1) {
logger.logException(MongoMessages.DSWRAPPER_MONGODB_META_DATA_ERROR, "MongoDB.parseBSONMongoDocument Failed to get table meta data", e1);
return false;
}
// Go through the items in the derby row and see if we have a matching field in the result row from mongo.
for ( int columnId = 0; columnId < rowDescription.getColumnCount(); columnId++){
String fieldName = rowDescription.getColumnName(columnId+1); //TBD this call has a "log" statement - should optimise to call it infrequently
//find the field in the mongo result row
Object mongoField = mongoDoc.get(fieldName);
if(mongoField != null) {
try {
// Set the value of the derby row according to the correct data type.
switch (rowDescription.getColumnType(columnId+1)) {
case java.sql.Types.VARCHAR:
String value = null;
if (mongoField instanceof java.lang.String) {
value = (String)mongoField;
} else if (mongoField instanceof org.bson.types.ObjectId) {
value = ((org.bson.types.ObjectId)mongoField).toStringMongod();
} else if (mongoField instanceof com.mongodb.BasicDBObject) {
value = ((com.mongodb.BasicDBObject)mongoField).toString();
} else if (mongoField instanceof com.mongodb.BasicDBList) {
value = ((com.mongodb.BasicDBList)mongoField).toString();
} else if (mongoField instanceof org.bson.types.BSONTimestamp) {
value = ((org.bson.types.BSONTimestamp)mongoField).toString();
} else {
logger.logWarning(MongoMessages.DSWRAPPER_MONGODB_INCOMPATIBLE_TYPE_ERROR,
"Could not map to VARCHAR, field named "+ fieldName +" of type: "+ mongoField.getClass() );
}
derbyRow[columnId].setValue( value );
break;
case java.sql.Types.CLOB:
String valueCLOB = null;
if (mongoField instanceof java.lang.String) {
valueCLOB = (String)mongoField;
} else if (mongoField instanceof org.bson.types.ObjectId) {
valueCLOB = ((org.bson.types.ObjectId)mongoField).toStringMongod();
} else if (mongoField instanceof com.mongodb.BasicDBObject) {
valueCLOB = ((com.mongodb.BasicDBObject)mongoField).toString();
} else if (mongoField instanceof com.mongodb.BasicDBList) {
valueCLOB = ((com.mongodb.BasicDBList)mongoField).toString();
} else if (mongoField instanceof org.bson.types.BSONTimestamp) {
value = ((org.bson.types.BSONTimestamp)mongoField).toString();
} else {
logger.logWarning(MongoMessages.DSWRAPPER_MONGODB_INCOMPATIBLE_TYPE_ERROR,
"Could not map to CLOB, field named "+ fieldName +" of type: "+ mongoField.getClass() );
}
derbyRow[columnId].setValue( valueCLOB );
break;
case java.sql.Types.INTEGER:
int valuei;
if (mongoField instanceof Integer) {
valuei = ((Integer)mongoField).intValue();
} else if (mongoField instanceof org.bson.types.BSONTimestamp) {
valuei = ((org.bson.types.BSONTimestamp)mongoField).getTime(); // sql.timestamp is in milliseconds, the mongo timestamp is in seconds.
} else {
logger.logWarning(MongoMessages.DSWRAPPER_MONGODB_INCOMPATIBLE_TYPE_ERROR,
"Could not map to INTEGER, field named "+ fieldName +" of type: "+ mongoField.getClass() );
valuei = - Integer.MIN_VALUE;
}
derbyRow[columnId].setValue( valuei );
break;
case java.sql.Types.DOUBLE:
double valued = ((Double)mongoField).doubleValue();
derbyRow[columnId].setValue( valued );
break;
case java.sql.Types.BOOLEAN:
boolean valueb = ((Boolean)mongoField).booleanValue();
derbyRow[columnId].setValue( valueb );
break;
case java.sql.Types.DATE:
java.sql.Date valueDate = new java.sql.Date(((Date)mongoField).getTime());
derbyRow[columnId].setValue( valueDate );
break;
case java.sql.Types.TIMESTAMP:
Timestamp valueTS = null;
if (mongoField instanceof org.bson.types.BSONTimestamp) {
valueTS = new Timestamp (((org.bson.types.BSONTimestamp)mongoField).getTime()*1000); // sql.timestamp is in milliseconds, the mongo timestamp is in seconds.
} else {
logger.logWarning(MongoMessages.DSWRAPPER_MONGODB_INCOMPATIBLE_TYPE_ERROR,
"Could not map to TIMESTAMP, field named "+ fieldName +" of type: "+ mongoField.getClass() );
}
derbyRow[columnId].setValue( valueTS );
break;
default:
//This is a type we are not expecting.
logger.logWarning(MongoMessages.DSWRAPPER_MONGODB_INCOMPATIBLE_TYPE_ERROR,
"Could not map a field named "+ fieldName +" of type: "+
mongoField.getClass()+", to type "+rowDescription.getColumnType(columnId+1) );
break;
}
} catch (StandardException e) {
// This error is thrown when we unable to set a value in the derby row.
e.printStackTrace();
logger.logException(MongoMessages.DSWRAPPER_MONGODB_VALUE_CONVERSION_ERROR,
"Could not convert result to Derby Type. Field: " + fieldName +", Value: " + mongoField.toString(), e);
} catch (Exception e){
// This error is thrown when we unable to set a value in the derby row.
e.printStackTrace();
logger.logException(MongoMessages.DSWRAPPER_MONGODB_VALUE_CONVERSION_ERROR,
"Unknown Error converting result to Derby Type. Field: " + fieldName +", Value: " + mongoField.toString(), e);
}
}
}
return true;
}
/**
* This method returns a result row from the executed query.
* @param arg0 - A data structure which is populated with the result row.
* @return a success status flag (see org.apache.derby.vti.IFastPath javadoc for valid values).
*/
@Override
public int nextRow(DataValueDescriptor[] arg0) throws StandardException,
SQLException {
// parsedValidRow is the flag indicating success - initialise to false.
boolean parsedValidRow = false;
if (mongoResults != null) {
while (mongoResults.hasNext()&& !parsedValidRow) {
//Parse the Mongo result into a DataValueDescriptor format for Derby
DBObject resultRow = mongoResults.next();
parsedValidRow = parseBSONMongoDocument(resultRow, arg0);
}
}
if (parsedValidRow){
return GOT_ROW;
} else {
return SCAN_COMPLETED;
}
}
/**
* This method returns the number of rows which have resulted from the last executed query
* @return int - count of satisfying rows.
*/
@Override
public int getRowCount() throws Exception {
if (mongoResults != null){
return mongoResults.count();
} else {
return 0;
}
}
/**
* This method returns an estimate of the row instantiation cost - used by the derby query
* optimiser.
* Currently returns a fixed value - 100.0!
* @return double - returns 100.0.
*/
@Override
public double getEstimatedCostPerInstantiation(VTIEnvironment arg0)
throws SQLException {
// This is just a simple implementation - could be improved with real statistics.
return 100.0d;
}
/**
* This method returns an estimate of the rows that would be fetched - used by the derby query
* optimiser.
* Currently returns a fixed value - 1.0!
* @return double - returns 1.0.
*/
@Override
public double getEstimatedRowCount(VTIEnvironment arg0) throws SQLException {
// This is just a simple implementation - could be improved with real statistics.
return 1.0d;
}
/**
* This method returns an estimate of the rows that would be fetched - used by the derby query
* planner.
* Currently returns a fixed value - false!
* @return boolean - returns false.
*/ @Override
public boolean supportsMultipleInstantiations(VTIEnvironment arg0)
throws SQLException {
// This is just a simple implementation - could be improved.
return false;
}
// This method resets the state of all VTI instance variables apart from the
// instanceMongoCollection, which is kept open to allow the VTI to be reused
// for other queries to the same MongoDB collection.
@Override
public boolean reinitialise() {
instanceMongoQuery = null;
// Assume that the next query will return all columns - this will be changed if pushProjection is called.
mongoQueryColumns = mongoAllColumns;
if (mongoResults !=null) {mongoResults.close();} //important to close the database cursor to free resources.
mongoResults = null;
return true;
}
@Override
public void close() throws SQLException {
super.close();
reinitialise();// closes all the local resources apart from the instanceMongoCollection.
MongoConnectionFactory.closeMongoCollection(instanceMongoCollection);
}
/**
* This method is called to inform the MongoVTI which columns that must be returned by the active query.
* This method is called only during the runtime execution of the VTI, after it has been constructed and before the executeQuery() method is called.
* The column identifiers contained in projectedColumns map to the columns described by the VTI's PreparedStatement's ResultSetMetaData.
* The JDBC column numbering scheme (1 based) is used for projectedColumns.
*
* The column fields passed are used to reduce the data retrieved from Mongo in the subsequent call
* to executeAsFastPath().
*
* @throws java.sql.SQLException - Error processing the request.
* @return false indicating that we do not implement getXXX() methods.
* NOTE! ==> The return value is ignored by Derby if the VTI implements IFastPath (because it will by-pass the ResultSet getXXX() methods).
*/
@Override
public boolean pushProjection(VTIEnvironment vtiEnvironment, int[] projectedColumns) throws SQLException{
//work out which fields we should be extracting. Limit the future queries to these fields.
GaianResultSetMetaData rowDescription = getMetaData();
mongoQueryColumns = new BasicDBObject();
// Go through the items defined in the config, restrict the query to these rows only.
for ( int columnIndex = 0; columnIndex < projectedColumns.length; columnIndex++){
int columnID = projectedColumns[columnIndex];
String fieldName = rowDescription.getColumnName(columnID);
mongoQueryColumns.put(fieldName, 1);
}
logger.logInfo("MongoVTI - query will fetch column: "+ mongoQueryColumns);
return false;
}
/**
* This method analyses the result document from Mongo and determines a suitable
* initial Logical Table definition from it.
*
* @param resultDoc - the mongo document that has been retrieved from the collection.
* @return a String representing a Logical Table definition appropriate to the resultDoc.
*/
//
//
private static String generateLTDefFromMongoDocument (DBObject resultDoc){
StringBuilder ltDef = new StringBuilder();
for (String fieldName : resultDoc.keySet()){
// The field gives us the column name
Object mongoField = resultDoc.get(fieldName);
if(mongoField != null) {
if (mongoField instanceof java.lang.String){
ltDef = ltDef.append(fieldName).append(" VARCHAR(255), ");
} else if (mongoField instanceof java.lang.Integer){
ltDef = ltDef.append(fieldName).append(" INTEGER, ");
} else if (mongoField instanceof java.lang.Double){
ltDef = ltDef.append(fieldName).append(" DOUBLE, ");
} else if (mongoField instanceof java.lang.Boolean){
ltDef = ltDef.append(fieldName).append(" BOOLEAN, ");
} else if (mongoField instanceof java.util.Date){
ltDef = ltDef.append(fieldName).append(" DATE, ");
} else if (mongoField instanceof org.bson.types.BSONTimestamp){
ltDef = ltDef.append(fieldName).append(" TIMESTAMP, ");
} else if(mongoField instanceof org.bson.types.ObjectId ||
mongoField instanceof com.mongodb.BasicDBObject ||
mongoField instanceof com.mongodb.BasicDBList) {
ltDef = ltDef.append(fieldName).append(" VARCHAR(255), ");
}
}
}
//remove a trailing ", "..
int defLength = ltDef.length();
if (defLength>2){
ltDef = ltDef.delete(defLength-2, defLength-1);
}
return ltDef.toString();
}
/**
* This method sends a request to a mongoDB instance, reads the data returned by the
* web service, and determines which columns can represent the
* associated logical table. It also writes the necessary properties into the
* gaian property file in order to query the logical table later on.
*
* @param ltName - Name of the generated logical table.
* @param url - Url accessing the mongo process.
* Expected format: {user}:{password}@{MongoURL}:{Port}/{Database}/{Collection}
* The {user}:{password}@ portion is optional, and implies that authentication is required.
* @param fields - an optional list of fields to be extracted for the logical table.
* @throws Exception on some sub-method failure.
*/
public static synchronized void setLogicalTableForMongoDB( String ltName, String url, String fields) throws Exception {
ltName = ltName.toUpperCase();
logger.logInfo("Obtaining tableDef for mongo process: " + url);
MongoConnectionParams connDetails = new MongoConnectionParams (url);
DBCollection mongoCollection = MongoConnectionFactory.getMongoCollection(connDetails);
// If we get this far without exception then the connection parameters must all be valid. Now work out the logical table definition.
String ltDef ="";
DBObject mongoResult;
if (fields == null || fields =="") {
mongoResult = mongoCollection.findOne(QUERY_ANYTHING); //retrieves just the first row
} else {
BasicDBObject keys = new BasicDBObject();
for (String field : fields.split(",")){
keys.put(field.trim(), 1);
}
mongoResult = mongoCollection.findOne(QUERY_ANYTHING, keys ); //retrieves just the first row matching the "keys"
}
if (mongoResult != null) {
ltDef = generateLTDefFromMongoDocument(mongoResult);
}
// Logical Table properties
Map<String, String> ltProperties = GaianDBConfigProcedures.prepareLogicalTable( ltName, ltDef, "" );
// Data Source Definition properties
ltProperties.put(ltName+"_DS0_ARGS",ltName+"conf, "+connDetails.getDatabaseName()+", "+connDetails.getCollectionName());
ltProperties.put(ltName+"_DS0_VTI",MongoDB.class.getName());
// VTI definition properties
String vtiPropertiesPrefix = MongoDB.class.getSimpleName() + "." + ltName + "conf.";
ltProperties.put(vtiPropertiesPrefix + "schema", ltDef);
ltProperties.put(vtiPropertiesPrefix + PROP_ADDRESS, connDetails.getHostAddress());
ltProperties.put(vtiPropertiesPrefix + PROP_PORT, connDetails.getHostPort().toString());
ltProperties.put(vtiPropertiesPrefix + PROP_DB_NAME, "$0"); // $0 is a replacement token to get the config to the DS0_ARGS field
ltProperties.put(vtiPropertiesPrefix + PROP_COLLECTION_NAME, "$1");// $0 is a replacement token to get the config to the DS0_ARGS field
// Note that if user or password is null, any existing key will be deleted, which is what we want to happen.
ltProperties.put(vtiPropertiesPrefix + PROP_USER, connDetails.getUserName());
ltProperties.put(vtiPropertiesPrefix + PROP_PASSWORD, connDetails.getPassword());
// Now actually update and save the configuration.
GaianDBConfigProcedures.setConfigProperties(ltProperties);
// GaianDBConfigProcedures.persistAndApplyConfigUpdates(ltProperties);
// The following call ensures that the configuration is fully loaded
// and we are ready to query the table. Without this, an immediate call
// to query the table can fail.
DataSourcesManager.checkUpdateLogicalTableViewsOnAllDBs();
MongoConnectionFactory.closeMongoCollection(mongoCollection);
}
}