com.ibm.db2j.MongoDB.java Source code

Java tutorial

Introduction

Here is the source code for com.ibm.db2j.MongoDB.java

Source

/*
 * (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);
    }

}