org.eclipse.birt.data.oda.mongodb.internal.impl.MDbMetaData.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.birt.data.oda.mongodb.internal.impl.MDbMetaData.java

Source

/*
 *************************************************************************
 * Copyright (c) 2013 Actuate Corporation.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *  Actuate Corporation - initial API and implementation
 *  
 *************************************************************************
 */

package org.eclipse.birt.data.oda.mongodb.internal.impl;

import java.sql.Types;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Level;

import org.bson.BSON;
import org.eclipse.birt.data.oda.mongodb.impl.MDbConnection;
import org.eclipse.birt.data.oda.mongodb.impl.MDbQuery;
import org.eclipse.birt.data.oda.mongodb.impl.MongoDBDriver;
import org.eclipse.birt.data.oda.mongodb.internal.impl.QueryProperties.CommandOperationType;
import org.eclipse.birt.data.oda.mongodb.nls.Messages;
import org.eclipse.datatools.connectivity.oda.OdaException;
import org.eclipse.datatools.connectivity.oda.util.manifest.ManifestExplorer;

import com.mongodb.Bytes;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
import com.mongodb.MapReduceOutput;
import com.mongodb.MongoException;
import com.mongodb.ServerAddress;

/**
 * Utility class to retrieve the metadata of a MongoDB database and collections.
 */
public class MDbMetaData {
    public static final int DEFAULT_META_DATA_SEARCH_LIMIT = 1;

    private static final MDbMetaData sm_factory = new MDbMetaData();
    private static final DocumentsMetaData sm_emptyFields = sm_factory.new DocumentsMetaData();
    private static final FieldMetaData sm_emptyFieldMetaData = sm_factory.new FieldMetaData(
            DriverUtil.EMPTY_STRING);

    private static final String SYSTEM_NAMESPACE_PREFIX = "system."; //$NON-NLS-1$
    static final String FIELD_FULL_NAME_SEPARATOR = "."; //$NON-NLS-1$

    private static final Integer NULL_NATIVE_DATA_TYPE = Integer.valueOf(BSON.NULL);
    private static final Integer STRING_NATIVE_DATA_TYPE = Integer.valueOf(BSON.STRING);
    private static final Integer BOOLEAN_NATIVE_DATA_TYPE = Integer.valueOf(BSON.BOOLEAN);
    private static final Integer NUMBER_NATIVE_DATA_TYPE = Integer.valueOf(BSON.NUMBER);
    private static final Integer NUMBER_INT_NATIVE_DATA_TYPE = Integer.valueOf(BSON.NUMBER_INT);
    private static final Integer DATE_NATIVE_DATA_TYPE = Integer.valueOf(BSON.DATE);
    private static final Integer TIMESTAMP_NATIVE_DATA_TYPE = Integer.valueOf(BSON.TIMESTAMP);
    private static final Integer BINARY_NATIVE_DATA_TYPE = Integer.valueOf(BSON.BINARY);
    private static final Integer ARRAY_NATIVE_DATA_TYPE = Integer.valueOf(BSON.ARRAY);
    private static final Integer OBJECT_NATIVE_DATA_TYPE = Integer.valueOf(BSON.OBJECT);

    private DB m_connectedDB;

    public MDbMetaData(Properties connProperties) throws OdaException {
        m_connectedDB = MDbConnection.getMongoDatabase(connProperties);
    }

    public MDbMetaData(DB connectedDB) {
        if (connectedDB == null)
            throw new IllegalArgumentException("null"); //$NON-NLS-1$
        m_connectedDB = connectedDB;
    }

    private MDbMetaData() {
    }

    /**
     * Returns the name of the connected database.
     */
    public String getDatabaseName() {
        return m_connectedDB.getName();
    }

    /**
     * Returns the sorted list of collection names found in the 
     * connected database of this instance.
     * The returned list excludes system collections, by default.
     */
    public List<String> getCollectionsList() {
        return getCollectionsList(true);
    }

    /**
     * Returns the sorted list of collection names found in the 
     * connected database of this instance.
     * @param excludeSystemCollections  true indicates to exclude system collections
     *          from the returned list; false to include.
     */
    public List<String> getCollectionsList(boolean excludeSystemCollections) {
        Set<String> collectionNames;
        try {
            collectionNames = m_connectedDB.getCollectionNames();
        } catch (MongoException ex) {
            // log and ignore
            DriverUtil.getLogger().log(Level.INFO, "Ignoring error to get collection names from database.", ex); //$NON-NLS-1$
            return Collections.emptyList();
        }

        if (excludeSystemCollections) {
            List<String> filteredNames = new ArrayList<String>(collectionNames.size());
            for (String collectionName : collectionNames) {
                if (!collectionName.startsWith(SYSTEM_NAMESPACE_PREFIX))
                    filteredNames.add(collectionName);
            }
            return filteredNames;
        }

        // return the complete list returned by mongoDB, which is already sorted
        return new ArrayList<String>(collectionNames);
    }

    public DBCollection getCollection(String collectionName) {
        if (!m_connectedDB.collectionExists(collectionName))
            return null;
        return m_connectedDB.getCollectionFromString(collectionName);
    }

    /**
     * Returns all fields' name and corresponding metadata found in the specified collection.
     * @param collectionName name of MongoDB collection (i.e. table)
     * @param searchLimit maximum number of documents, i.e. rows to search for available fields;
     *          a zero or negative value would adopt the default limit 
     * @param runtimeProps  an instance of QueryProperties containing the data set runtime property values;
     *          may be null to apply all default values in finding the available fields metadata
     * @return  the DocumentsMetaData object that contains the list of available field names and 
     *          corresponding metadata; 
     *          an empty list is returned if no available fields are found, or 
     *          if the specified collection does not exist
     * @throws OdaException
     */
    public DocumentsMetaData getAvailableFields(String collectionName, int searchLimit,
            QueryProperties runtimeProps) throws OdaException {
        DBCollection collection = getCollection(collectionName);
        if (collection == null && !runtimeProps.hasRunCommand()) {
            if (runtimeProps.getOperationType() == CommandOperationType.RUN_DB_COMMAND
                    && runtimeProps.getOperationExpression().isEmpty())
                throw new OdaException(Messages.bind(Messages.mDbMetaData_missingCmdExprText,
                        runtimeProps.getOperationType().displayName()));
            else
                throw new OdaException(Messages.bind(Messages.mDbMetaData_invalidCollectionName, collectionName));
        }

        if (searchLimit <= 0) // no limit specified, applies meta data design-time default
            searchLimit = DEFAULT_META_DATA_SEARCH_LIMIT;

        // handle optional command operation
        if (runtimeProps.hasValidCommandOperation()) {
            QueryModel.validateCommandSyntax(runtimeProps.getOperationType(),
                    runtimeProps.getOperationExpression());

            Iterable<DBObject> commandResults = null;
            if (runtimeProps.hasAggregateCommand())
                commandResults = MDbOperation.callAggregateCmd(collection, runtimeProps);
            else if (runtimeProps.hasMapReduceCommand()) {
                MapReduceOutput mapReduceOut = MDbOperation.callMapReduceCmd(collection, runtimeProps);
                commandResults = mapReduceOut.results();
                // skip running $query on output collection in discovering metadata
            } else if (runtimeProps.hasRunCommand())
                commandResults = MDbOperation.callDBCommand(m_connectedDB, runtimeProps);

            if (commandResults != null)
                return getMetaData(commandResults, searchLimit);
            return sm_emptyFields;
        }

        // run search query operation by default
        DBCursor rowsCursor = collection.find();

        if (searchLimit > 0)
            rowsCursor.limit(searchLimit);

        QueryProperties mdCursorProps = runtimeProps != null ? runtimeProps : QueryProperties.defaultValues();
        MDbOperation.applyPropertiesToCursor(rowsCursor, mdCursorProps, false);

        return getMetaData(rowsCursor);
    }

    /**
     * Returns the default database port.
     */
    public static int defaultPort() {
        return ServerAddress.defaultPort();
    }

    static String[] splitFieldName(String fieldFullName) {
        if (fieldFullName == null || fieldFullName.isEmpty())
            return new String[0];
        return fieldFullName.split('\\' + FIELD_FULL_NAME_SEPARATOR);
    }

    static String getSimpleName(String fieldFullName) {
        String[] nameFragments = splitFieldName(fieldFullName);
        if (nameFragments.length == 0)
            return DriverUtil.EMPTY_STRING; // something is wrong; not able to find simple name
        return nameFragments[nameFragments.length - 1];
    }

    static String stripParentName(String fieldFullName, String parentName) {
        if (parentName == null || parentName.isEmpty())
            return fieldFullName; // nothing applicable to strip
        int stripFromIndex = parentName.length() + FIELD_FULL_NAME_SEPARATOR.length();
        if (stripFromIndex > fieldFullName.length()) // out of bound index
            return fieldFullName; // n/a in fieldFullName to strip
        return fieldFullName.substring(stripFromIndex);
    }

    static String formatFieldLevelNames(String[] fieldLevelNames, int fromIndex, int toIndex) {
        if (fromIndex < 0 || toIndex >= fieldLevelNames.length || fromIndex > toIndex)
            throw new IllegalArgumentException(
                    "MDbMetaData#formatFieldLevelNames: Index argument(s) out of range."); //$NON-NLS-1$

        StringBuffer fieldName = new StringBuffer();
        for (int i = fromIndex; i <= toIndex; i++) {
            if (fieldName.length() > 0)
                fieldName.append(FIELD_FULL_NAME_SEPARATOR);
            fieldName.append(fieldLevelNames[i]);
        }
        return fieldName.toString();
    }

    /**
     * Find and return the metadata of the specified field.
     * @param fieldFullName the full name of a field
     * @param fromDocMetaData   the metadata of documents found in a collection
     * @return  the FieldMetaData instance of the specified field full name
     */
    public static FieldMetaData findFieldByFullName(String fieldFullName, DocumentsMetaData fromDocMetaData) {
        String[] nameFragments = splitFieldName(fieldFullName);
        if (nameFragments.length == 0)
            return null; // something is wrong; not able to find a match

        FieldMetaData firstLevelMd = fromDocMetaData.getFieldMetaData(nameFragments[0]);
        if (nameFragments.length == 1) // specified field has only 1 level
            return firstLevelMd;

        // Sanity check for multiple-level no-data case:
        // getFieldMetaData() currently returns empty-field-metadata if metadata
        // is not found by the field name. So if field has more than two levels,
        // but the first level MD points to the static empty-field-metadata instance,
        // then return the first level MD (empty-field-metadata) like in 1-level case.
        if (firstLevelMd == sm_emptyFieldMetaData)
            return firstLevelMd;

        // expects the first level to be a parent field
        if (!firstLevelMd.hasChildDocuments())
            return null; // does not match metadata; not able to find a match
        // remove the parent name to get the next level child's full name
        String childFullName = stripParentName(fieldFullName, nameFragments[0]);
        return findFieldByFullName(childFullName, firstLevelMd.getChildMetaData());
    }

    /**
     * Indicates whether the specified field has flattening support.
     * @param fieldMd   field metadata; may be that of top-level array field or a child field
     * @param topLevelDocMD     top level document metadata returned 
     *          by {@link #getAvailableFields(String, int, QueryProperties)}
     */
    public static boolean isFlattenableNestedField(FieldMetaData fieldMd, DocumentsMetaData topLevelDocMD) {
        if (fieldMd == null)
            return false;
        DocumentsMetaData containingDocMD = fieldMd.getContainingMetaData();
        if (containingDocMD == null) // top-level field
        {
            containingDocMD = topLevelDocMD; // use top-level metadata
        }
        String cachedAncestorName = containingDocMD.getFlattenableFieldName();
        if (cachedAncestorName != null && cachedAncestorName.equals(fieldMd.getFullName()))
            return true;
        if (fieldMd.isChildField())
            return isFlattenableNestedField(fieldMd.getParentMetaData(), topLevelDocMD);
        return false;
    }

    /**
     * Find the FieldMetaData of each field specified by its full name. 
     * @param fieldFullNames    a list of fields in their full name.
     * @param fromDocMetaData   the metadata of documents found in a collection
     * @return  a flattened Map of each field's full name with its corresponding FieldMetaData.
     *          If a field name is not found in the specified DocumentsMetaData, 
     *          no entry is put in the returned Map.
     */
    public static Map<String, FieldMetaData> flattenFieldsMetaData(List<String> fieldFullNames,
            DocumentsMetaData fromDocMetaData) {
        if (fieldFullNames.isEmpty())
            return Collections.emptyMap(); // done; no fields to find

        Map<String, FieldMetaData> resultFieldsMD = new LinkedHashMap<String, FieldMetaData>(fieldFullNames.size());
        for (String fieldFullName : fieldFullNames) {
            FieldMetaData fieldMD = findFieldByFullName(fieldFullName, fromDocMetaData);
            if (fieldMD != null)
                resultFieldsMD.put(fieldFullName, fieldMD);
        }
        return resultFieldsMD;
    }

    /**
     * Flatten all the fields, including nested ones, in the specified DocumentsMetaData into the specified Map.
     * @param fromDocMetaData   the metadata of documents found in a collection
     * @param toResultFieldsMD  the Map to which append the entries for all fields in the specified DocumentsMetaData
     * @return  a flattened Map of each field's full name with its corresponding FieldMetaData.
     */
    public static Map<String, FieldMetaData> flattenFieldsMetaData(DocumentsMetaData fromDocMetaData,
            Map<String, FieldMetaData> toResultFieldsMD) {
        if (toResultFieldsMD == null)
            toResultFieldsMD = new LinkedHashMap<String, FieldMetaData>();
        for (FieldMetaData fieldMD : fromDocMetaData.m_fieldsMetaData.values()) {
            toResultFieldsMD.put(fieldMD.getFullName(), fieldMD);
            if (fieldMD.hasChildDocuments())
                toResultFieldsMD = flattenFieldsMetaData(fieldMD.getChildMetaData(), toResultFieldsMD);
        }
        return toResultFieldsMD;
    }

    /**
     * An internal utility method.
     * Discover and return the metadata of one or more documents in the specified MongoDB cursor, 
     * up to the searchLimit count.
     * @param resultCursor  an inactive MongoDB cursor, before having executed query, to iterate the results
     *                      expects caller to have already set searchLimit and other options on the result cursor
     * @return  a DocumentsMetaData representing all the fields and corresponding metadata 
     *          found in the specified iterated cursor
     */
    public static DocumentsMetaData getMetaData(DBCursor resultCursor) {
        if (resultCursor == null)
            return sm_emptyFields;

        DocumentsMetaData newMetaData = sm_factory.new DocumentsMetaData();
        // iterate thru available documents to discover metadata
        while (resultCursor.hasNext()) {
            DBObject doc = resultCursor.next();
            newMetaData.addDocumentMetaData(doc, null); // top-level doc has no parent
        }
        return newMetaData;
    }

    public static DocumentsMetaData getMetaData(Iterable<DBObject> resultObjs, int searchLimit) {
        if (resultObjs == null)
            return sm_emptyFields;

        DocumentsMetaData newMetaData = sm_factory.new DocumentsMetaData();
        // iterate thru searchLimit documents to discover metadata
        int count = 1;
        Iterator<DBObject> resultObjItr = resultObjs.iterator();
        while (resultObjItr.hasNext() && (searchLimit <= 0 || count <= searchLimit)) {
            DBObject doc = resultObjItr.next();
            newMetaData.addDocumentMetaData(doc, null); // top-level doc has no parent
            count++;
        }
        return newMetaData;
    }

    private static Integer getPreferredScalarNativeDataType(Set<Integer> nativeDataTypes) {
        if (nativeDataTypes.isEmpty())
            return NULL_NATIVE_DATA_TYPE; // none available
        if (nativeDataTypes.size() == 1)
            return nativeDataTypes.iterator().next(); // return the only data type available

        // more than one native data types in field

        if (nativeDataTypes.contains(STRING_NATIVE_DATA_TYPE))
            return STRING_NATIVE_DATA_TYPE; // String data type takes precedence over other scalar types

        // check if any of the native data types map to an ODA String
        Set<Integer> nonStringNativeDataTypes = new HashSet<Integer>(nativeDataTypes.size());
        for (Integer nativeDataType : nativeDataTypes) {
            if (nativeDataType == NULL_NATIVE_DATA_TYPE || nativeDataType == ARRAY_NATIVE_DATA_TYPE
                    || nativeDataType == OBJECT_NATIVE_DATA_TYPE)
                continue; // skip non-scalar data types
            int odaDataType = ManifestExplorer.getInstance().getDefaultOdaDataTypeCode(nativeDataType,
                    MongoDBDriver.ODA_DATA_SOURCE_ID, MDbQuery.ODA_DATA_SET_ID);
            if (odaDataType == Types.CHAR) // maps to ODA String data type
                return nativeDataType; // String data type takes precedence over other scalar types

            nonStringNativeDataTypes.add(nativeDataType);
        }

        if (nonStringNativeDataTypes.isEmpty())
            return NULL_NATIVE_DATA_TYPE; // none available
        if (nonStringNativeDataTypes.size() == 1)
            return nonStringNativeDataTypes.iterator().next(); // return first element by default

        // more than one native data types in field are not mapped to ODA String;
        // check if they have mixed data type categories.
        boolean isNumeric = nonStringNativeDataTypes.contains(NUMBER_NATIVE_DATA_TYPE)
                || nonStringNativeDataTypes.contains(NUMBER_INT_NATIVE_DATA_TYPE)
                || nonStringNativeDataTypes.contains(BOOLEAN_NATIVE_DATA_TYPE);
        boolean isDatetime = nonStringNativeDataTypes.contains(DATE_NATIVE_DATA_TYPE)
                || nonStringNativeDataTypes.contains(TIMESTAMP_NATIVE_DATA_TYPE);
        boolean isBinary = nonStringNativeDataTypes.contains(BINARY_NATIVE_DATA_TYPE);

        if (isNumeric && !isDatetime && !isBinary) // numeric only
        {
            if (nonStringNativeDataTypes.contains(NUMBER_NATIVE_DATA_TYPE))
                return NUMBER_NATIVE_DATA_TYPE; // Number takes precedence over other numeric data types
            return NUMBER_INT_NATIVE_DATA_TYPE; // Integer takes precedence over Boolean
        }

        if (!isNumeric && isDatetime && !isBinary) // Date and Timestamp data types only
        {
            return TIMESTAMP_NATIVE_DATA_TYPE; // Timestamp takes precedence over Date
        }

        // multiple non-String native data types must be of mixed data type categories
        return STRING_NATIVE_DATA_TYPE; // use String to handle mixed data types
    }

    /**
     * The metadata of all fields discovered in one or more documents 
     * found in a collection.
     */
    public class DocumentsMetaData {
        // an ordered Map w/ the field name as the key, 
        // and its corresponding metadata as value
        private Map<String, FieldMetaData> m_fieldsMetaData = new LinkedHashMap<String, FieldMetaData>();

        // flattening of nested collection is supported for only one applicable field in a document,
        // and is tracked in this variable to ensure consistency in metadata and fetching result set
        private String m_nestedCollFieldName;

        private void addDocumentMetaData(DBObject doc, FieldMetaData parentMd) {
            if (doc == null)
                return;

            Set<String> fieldNames = doc.keySet();
            for (String fieldName : fieldNames) {
                Object value = doc.get(fieldName);
                addDataTypeOfFieldValue(fieldName, value, parentMd);
            }
        }

        private FieldMetaData addDataTypeOfFieldValue(String fieldName, Object fieldValue, FieldMetaData parentMd) {
            // add the specified data type to existing set, if exists, for the specified field;
            // the same field name in a different doc may have a different data type 
            FieldMetaData fieldMd = m_fieldsMetaData.get(fieldName);
            if (fieldMd == null) {
                fieldMd = new FieldMetaData(fieldName);
                fieldMd.setParentMetaData(parentMd);
            }
            fieldMd.addDataType(fieldValue);
            m_fieldsMetaData.put(fieldMd.getSimpleName(), fieldMd);
            return fieldMd;
        }

        @SuppressWarnings("unused")
        private void removeField(String fieldName) {
            if (fieldName != null)
                m_fieldsMetaData.remove(fieldName);
        }

        /**
         * Returns the simple names of first level fields in the sequence that 
         * they were discovered from a collection.
         */
        public List<String> getFieldNames() {
            // maintain the ordering of the fields that they were discovered
            List<String> docFields = new ArrayList<String>(m_fieldsMetaData.size());
            for (String fieldName : m_fieldsMetaData.keySet()) {
                docFields.add(fieldName);
            }
            return docFields;
        }

        /**
         * Returns the simple names of first level fields in natural ascending order.
         */
        public List<String> getSortedFieldNames() {
            Set<String> attributeNames = m_fieldsMetaData.keySet();
            return sortFieldNames(attributeNames);
        }

        private List<String> sortFieldNames(Set<String> fieldNames) {
            if (fieldNames == null || fieldNames.isEmpty())
                return Collections.emptyList();

            String[] attrNamesArray = (String[]) fieldNames.toArray(new String[fieldNames.size()]);
            Arrays.sort(attrNamesArray);

            List<String> sortedAttrList = new ArrayList<String>(attrNamesArray.length);
            sortedAttrList.addAll(Arrays.asList(attrNamesArray));
            return sortedAttrList;
        }

        public FieldMetaData getFieldMetaData(String fieldSimpleName) {
            FieldMetaData fieldMd = m_fieldsMetaData.get(fieldSimpleName);
            return fieldMd != null ? fieldMd : sm_emptyFieldMetaData;
        }

        public void setFlattenableFields(Map<String, FieldMetaData> resultFieldsMD, boolean isTopLevelDoc) {
            if (resultFieldsMD == null || resultFieldsMD.isEmpty())
                return; // no result set fields; nothing to set

            FieldMetaData flattenableFieldMD = null;
            if (m_nestedCollFieldName != null)
                flattenableFieldMD = resultFieldsMD.get(m_nestedCollFieldName);

            if (flattenableFieldMD == null) {
                // iterate in the sequence of selected fields in result set
                for (FieldMetaData resultFieldMD : resultFieldsMD.values()) {
                    FieldMetaData fieldLevelMD = resultFieldMD;
                    while (fieldLevelMD != null) {
                        if (!m_fieldsMetaData.containsValue(fieldLevelMD)) // not a field in this document
                        {
                            fieldLevelMD = fieldLevelMD.getParentMetaData(); // check its parent field
                            continue;
                        }

                        // is a field of this document;
                        // determine the first field with nested collection of documents, whose result set may be flattened
                        if (fieldLevelMD.isArrayOfDocuments()) {
                            m_nestedCollFieldName = fieldLevelMD.getFullName();
                            flattenableFieldMD = fieldLevelMD;
                        }
                        // done checking on this field
                        fieldLevelMD = null;
                    }
                    if (flattenableFieldMD != null) // done finding the flattenable field
                        break;
                }
            }

            // set its child document's flattenable field
            if (flattenableFieldMD != null) {
                if (flattenableFieldMD.hasChildDocuments())
                    flattenableFieldMD.getChildMetaData().setFlattenableFields(resultFieldsMD, false);
                return;
            }

            // no nested collection of documents exist;
            // determine the top-level's first array field of scalar values that may be flattened
            if (m_nestedCollFieldName == null && isTopLevelDoc) {
                for (FieldMetaData resultFieldMD : resultFieldsMD.values()) {
                    if (resultFieldMD.isArrayOfScalarValues()) {
                        m_nestedCollFieldName = resultFieldMD.getFullName();
                        break;
                    }
                }
            }
        }

        public String getFlattenableFieldName() {
            return m_nestedCollFieldName;
        }

        /* (non-Javadoc)
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString() {
            StringBuffer buf = new StringBuffer("\n " + getClass().getSimpleName() + ":"); //$NON-NLS-1$ //$NON-NLS-2$
            buf.append("; flattenableFieldName: " + m_nestedCollFieldName); //$NON-NLS-1$
            for (Entry<String, FieldMetaData> entry : m_fieldsMetaData.entrySet()) {
                buf.append("\n  field key: " + entry.getKey()); //$NON-NLS-1$
                buf.append("; metadata: " + entry.getValue()); //$NON-NLS-1$
            }
            return buf.toString();
        }
    }

    /**
     * The metadata of an individual field found in one or more documents 
     * in a collection.
     * A field is uniquely identified by name within a document.
     * If a field value is a sub-document, this includes the sub-document's metadata.
     */
    public class FieldMetaData {
        private static final String ARRAY_NOTATION = "[]"; //$NON-NLS-1$

        private String m_simpleName;
        private Set<Integer> m_nativeDataTypes;
        private DocumentsMetaData m_childDocMetaData;
        private FieldMetaData m_parentMd; // may be null if no parent doc field
        private String[] m_nameFragments; // cached fragments of field full name

        private FieldMetaData(String simpleName) {
            m_simpleName = simpleName;
        }

        public String getSimpleName() {
            return m_simpleName;
        }

        public String getSimpleDisplayName() {
            return hasArrayDataType() ? getSimpleName() + ARRAY_NOTATION : getSimpleName();
        }

        public String getFullName() {
            if (m_parentMd == null)
                return getSimpleName();

            StringBuffer fullName = new StringBuffer(m_parentMd.getFullName());
            fullName.append(FIELD_FULL_NAME_SEPARATOR);
            fullName.append(getSimpleName());
            return fullName.toString();
        }

        public String getFullDisplayName() {
            if (m_parentMd == null)
                return getSimpleDisplayName();

            StringBuffer fullName = new StringBuffer(m_parentMd.getFullDisplayName());
            fullName.append(FIELD_FULL_NAME_SEPARATOR);
            fullName.append(getSimpleDisplayName());
            return fullName.toString();
        }

        String[] getLevelNames() {
            if (m_nameFragments == null)
                m_nameFragments = splitFieldName(getFullName());
            return m_nameFragments;
        }

        private void setParentMetaData(FieldMetaData parentMd) {
            if (parentMd != null)
                m_parentMd = parentMd;
        }

        private FieldMetaData getParentMetaData() {
            return m_parentMd;
        }

        private DocumentsMetaData getContainingMetaData() {
            if (m_parentMd == null)
                return null;
            return m_parentMd.getChildMetaData();
        }

        /**
         * Indicates whether this is a child field, contained by a parent document.
         */
        public boolean isChildField() {
            return m_parentMd != null;
        }

        private void addDataType(Object fieldValue) {
            // add the data type of given fieldValue to existing set, if exists;
            // the same named field set in a different doc may have a different data type 
            byte nativeBSonDataTypeCode = Bytes.getType(fieldValue);
            if (m_nativeDataTypes == null)
                m_nativeDataTypes = new HashSet<Integer>(2);
            m_nativeDataTypes.add(Integer.valueOf(nativeBSonDataTypeCode));

            // check if field value contains a document,
            // iteratively get field document's nested metadata
            if (nativeBSonDataTypeCode == BSON.ARRAY) {
                if (fieldValue instanceof java.util.List) {
                    java.util.List<?> listOfObjects = (java.util.List<?>) fieldValue;
                    if (listOfObjects.size() > 0) {
                        // use first element in array to determine metadata
                        addDataType(listOfObjects.get(0)); // handles nested arrays
                        return;
                    }
                }
            }

            DBObject fieldObjValue = ResultDataHandler.fetchFieldDocument(fieldValue, nativeBSonDataTypeCode);

            if (fieldObjValue != null) // contains nested document
            {
                if (m_childDocMetaData == null)
                    m_childDocMetaData = sm_factory.new DocumentsMetaData();
                m_childDocMetaData.addDocumentMetaData(fieldObjValue, this);
            }
        }

        public Integer getPreferredNativeDataType(boolean isAutoFlattening) {
            Set<Integer> nativeDataTypes = getNativeDataTypes();
            if (nativeDataTypes.isEmpty())
                return NULL_NATIVE_DATA_TYPE; // none available

            // determine the preferred data type of a nested array
            if (hasArrayDataType()) {
                if (isAutoFlattening) {
                    // if field is an array of scalar data elements,
                    // return the scalar data type
                    if (!hasDocumentDataType())
                        return getScalarNativeDataType();
                }
                return ARRAY_NATIVE_DATA_TYPE;
            }

            if (hasDocumentDataType())
                return OBJECT_NATIVE_DATA_TYPE;

            return getPreferredScalarNativeDataType(nativeDataTypes);
        }

        /**
         * Returns all the known data types of the specified field.
         * The same named field could have a different data type in a separate document
         * within a collection.
         * @param ofField   field name
         * @return  a set of known MongoDB native data type codes
         */
        public Set<Integer> getNativeDataTypes() {
            if (m_nativeDataTypes == null)
                return Collections.emptySet();
            return m_nativeDataTypes;
        }

        private Integer getScalarNativeDataType() {
            Set<Integer> nativeDataTypes = getNativeDataTypes();
            Set<Integer> scalarNativeDataTypes = new HashSet<Integer>(nativeDataTypes.size());
            for (Integer nativeDataType : nativeDataTypes) {
                if (nativeDataType == ARRAY_NATIVE_DATA_TYPE || nativeDataType == OBJECT_NATIVE_DATA_TYPE)
                    continue; // skip complex types
                scalarNativeDataTypes.add(nativeDataType);
            }
            return getPreferredScalarNativeDataType(scalarNativeDataTypes);
        }

        public boolean isArrayOfScalarValues() {
            if (!hasArrayDataType())
                return false;
            return !hasDocumentDataType();
        }

        public boolean isArrayOfDocuments() {
            if (!hasArrayDataType())
                return false;
            return hasDocumentDataType();
        }

        public boolean hasDocumentDataType() {
            return getNativeDataTypes().contains(OBJECT_NATIVE_DATA_TYPE);
        }

        public boolean hasArrayDataType() {
            return getNativeDataTypes().contains(ARRAY_NATIVE_DATA_TYPE);
        }

        public boolean isDescendantOfArrayField() {
            if (!isChildField())
                return false;
            return m_parentMd.hasArrayDataType() || m_parentMd.isDescendantOfArrayField();
        }

        public String getArrayAncestorName() {
            if (!isChildField()) // has no parent
                return null;
            if (m_parentMd.hasArrayDataType())
                return m_parentMd.getFullName();
            return m_parentMd.getArrayAncestorName();
        }

        /**
         * Indicates whether this is a parent field, containing nested documents.
         */
        public boolean hasChildDocuments() {
            return m_childDocMetaData != null;
        }

        public DocumentsMetaData getChildMetaData() {
            return m_childDocMetaData != null ? m_childDocMetaData : sm_emptyFields;
        }

        /* (non-Javadoc)
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString() {
            StringBuffer buf = new StringBuffer("name: " + m_simpleName); //$NON-NLS-1$
            buf.append("; full display name: " + getFullDisplayName()); //$NON-NLS-1$
            buf.append("; nativeDataTypes: " + m_nativeDataTypes); //$NON-NLS-1$
            String parentName = m_parentMd != null ? m_parentMd.getFullDisplayName() : "null"; //$NON-NLS-1$
            buf.append("; parent field: " + parentName); //$NON-NLS-1$
            buf.append("; child document metadata: " + m_childDocMetaData); //$NON-NLS-1$
            return buf.toString();
        }
    }

}