org.eclipse.birt.data.oda.mongodb.impl.MongoDBDriver.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.birt.data.oda.mongodb.impl.MongoDBDriver.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.impl;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.eclipse.birt.data.oda.mongodb.internal.impl.DriverUtil;
import org.eclipse.birt.data.oda.mongodb.nls.Messages;
import org.eclipse.datatools.connectivity.oda.IConnection;
import org.eclipse.datatools.connectivity.oda.IDriver;
import org.eclipse.datatools.connectivity.oda.LogConfiguration;
import org.eclipse.datatools.connectivity.oda.OdaException;
import org.eclipse.datatools.connectivity.oda.util.manifest.DataTypeMapping;
import org.eclipse.datatools.connectivity.oda.util.manifest.ExtensionManifest;
import org.eclipse.datatools.connectivity.oda.util.manifest.ManifestExplorer;

import com.mongodb.Mongo;
import com.mongodb.MongoClientOptions;
import com.mongodb.MongoClientOptions.Builder;
import com.mongodb.MongoOptions;
import com.mongodb.MongoURI;
import com.mongodb.ReadPreference;
import com.mongodb.ServerAddress;

/**
 * Implementation class of IDriver for the MongoDB ODA runtime driver.
 */
public class MongoDBDriver implements IDriver {
    public static final String ODA_DATA_SOURCE_ID = "org.eclipse.birt.data.oda.mongodb"; //$NON-NLS-1$

    private static final String MONGO_PROP_PREFIX = ""; //$NON-NLS-1$   // not using a prefix for now

    public static final String IGNORE_URI_PROP = MONGO_PROP_PREFIX.concat("ignoreURI"); //$NON-NLS-1$
    public static final String MONGO_URI_PROP = MONGO_PROP_PREFIX.concat("mongoURI"); //$NON-NLS-1$

    public static final String SERVER_HOST_PROP = MONGO_PROP_PREFIX.concat("serverHost"); //$NON-NLS-1$
    public static final String SERVER_PORT_PROP = MONGO_PROP_PREFIX.concat("serverPort"); //$NON-NLS-1$
    public static final String DBNAME_PROP = MONGO_PROP_PREFIX.concat("databaseName"); //$NON-NLS-1$
    public static final String USERNAME_PROP = MONGO_PROP_PREFIX.concat("userName"); //$NON-NLS-1$
    public static final String PASSWORD_PROP = MONGO_PROP_PREFIX.concat("password"); //$NON-NLS-1$

    // supported MongoOptions that are not covered in MongoURI
    public static final String SOCKET_KEEP_ALIVE_PROP = MONGO_PROP_PREFIX.concat("socketKeepAlive"); //$NON-NLS-1$
    // additional MongoDB ODA connection properties
    public static final String REQUEST_SESSION_PROP = MONGO_PROP_PREFIX.concat("useRequestSession"); //$NON-NLS-1$

    private static final MongoDBDriver sm_factory = new MongoDBDriver();
    private static final MongoOptions sm_defaultClientOptions = createDefaultClientOptions();

    private static ConcurrentMap<ServerNodeKey, Mongo> sm_mongoServerNodes;

    private static ConcurrentMap<ServerNodeKey, Mongo> getMongoServerNodes() {
        // different from Mongo.Holder (which uses MongoURI as key), 
        // this uses cached key based on a
        // MongoURI plus supported options not definable in MongoURI
        if (sm_mongoServerNodes == null) {
            synchronized (MongoDBDriver.class) {
                if (sm_mongoServerNodes == null)
                    sm_mongoServerNodes = new ConcurrentHashMap<ServerNodeKey, Mongo>();
            }
        }
        return sm_mongoServerNodes;
    }

    public static void close() {
        synchronized (MongoDBDriver.class) {
            if (sm_mongoServerNodes == null)
                return;

            for (Mongo node : sm_mongoServerNodes.values()) {
                node.close();
            }
            sm_mongoServerNodes.clear();
            sm_mongoServerNodes = null;
        }
    }

    private static Mongo getMongoNodeInstance(ServerNodeKey serverNodeKey) throws OdaException {
        // first check if a cached node already exists and reuse
        Mongo mongoNode = getMongoServerNodes().get(serverNodeKey);
        if (mongoNode != null)
            return mongoNode;

        // now try get mongo node based on server host/port and supported options
        mongoNode = createMongoNode(serverNodeKey);
        Mongo existingNode = getMongoServerNodes().putIfAbsent(serverNodeKey, mongoNode); // cache the new mongo server node

        if (existingNode == null) // the new one got in
            return mongoNode;

        // there was a race, and the new node lost;
        // close the new node, and return the existing one
        mongoNode.close();
        return existingNode;
    }

    static Mongo getMongoNode(Properties connProperties) throws OdaException {
        ServerNodeKey nodeKey = createServerNodeKey(connProperties);
        return getMongoNodeInstance(nodeKey);
    }

    /*
     * @see org.eclipse.datatools.connectivity.oda.IDriver#getConnection(java.lang.String)
     */
    public IConnection getConnection(String dataSourceType) throws OdaException {
        // driver supports only one type of data source,
        // ignores the specified dataSourceType
        return new MDbConnection();
    }

    /*
     * @see org.eclipse.datatools.connectivity.oda.IDriver#setLogConfiguration(org.eclipse.datatools.connectivity.oda.LogConfiguration)
     */
    public void setLogConfiguration(LogConfiguration logConfig) throws OdaException {
        // not supported
    }

    /*
     * @see org.eclipse.datatools.connectivity.oda.IDriver#getMaxConnections()
     */
    public int getMaxConnections() throws OdaException {
        // use default value defined in MongoClientOptions.connectionsPerHost;
        // this may be called before opening a connection, i.e. no instance of MongoOptions is available
        return 100;
    }

    /*
     * @see org.eclipse.datatools.connectivity.oda.IDriver#setAppContext(java.lang.Object)
     */
    public void setAppContext(Object context) throws OdaException {
        // do nothing; no support for pass-through context
    }

    /**
     * Returns the object that represents this extension's manifest.
     * @throws OdaException
     */
    static ExtensionManifest getManifest() throws OdaException {
        return ManifestExplorer.getInstance().getExtensionManifest(ODA_DATA_SOURCE_ID);
    }

    /**
     * Returns the native data type name of the specified code, as
     * defined in this data source extension's manifest.
     * @param nativeTypeCode    the native data type code
     * @return                  corresponding native data type name
     * @throws OdaException     if lookup fails
     */
    static String getNativeDataTypeName(int nativeDataTypeCode) throws OdaException {
        DataTypeMapping typeMapping = getManifest().getDataSetType(null).getDataTypeMapping(nativeDataTypeCode);
        if (typeMapping != null)
            return typeMapping.getNativeType();
        return Messages.mDbDriver_nonDefinedDataType;
    }

    /**
     * An enum of MongoDB ReadPreference that supports
     * localization of its display names.
     */
    public enum ReadPreferenceChoice {
        PRIMARY, PRIMARY_PREFERRED, SECONDARY, SECONDARY_PREFERRED, NEAREST;

        private ReadPreferenceChoice() {
        }

        public static ReadPreferenceChoice DEFAULT = PRIMARY;
        public static ReadPreference DEFAULT_PREFERENCE = ReadPreference.primary();

        public static ReadPreference getMongoReadPreference(String readPrefChoiceLiteral) {
            if (readPrefChoiceLiteral == null || readPrefChoiceLiteral.trim().isEmpty())
                return null; // use MongoDB default

            try {
                return ReadPreference.valueOf(readPrefChoiceLiteral.trim());
            } catch (IllegalArgumentException ex) {
                // ignore, falls back to return default
            }
            return DEFAULT_PREFERENCE; // default
        }

        public static ReadPreferenceChoice getReadPreferenceChoice(ReadPreference readPref) {
            if (readPref == null)
                return PRIMARY; // default
            String readPrefName = readPref.getName();
            if (readPrefName == ReadPreference.primary().getName())
                return PRIMARY;
            if (readPrefName == ReadPreference.primaryPreferred().getName())
                return PRIMARY_PREFERRED;
            if (readPrefName == ReadPreference.secondary().getName())
                return SECONDARY;
            if (readPrefName == ReadPreference.secondaryPreferred().getName())
                return SECONDARY_PREFERRED;
            if (readPrefName == ReadPreference.nearest().getName())
                return NEAREST;
            return PRIMARY; // default
        }

        public String displayName() {
            // externalizes name, which is not provided by Mongo Java driver
            if (this == PRIMARY)
                return Messages.mDbDriver_readPrefPrimary; // ReadPreference.primary().getName();
            if (this == PRIMARY_PREFERRED)
                return Messages.mDbDriver_readPrefPrimaryPreferred; // ReadPreference.primaryPreferred().getName();
            if (this == SECONDARY)
                return Messages.mDbDriver_readPrefSecondary; // ReadPreference.secondary().getName();
            if (this == SECONDARY_PREFERRED)
                return Messages.mDbDriver_readPrefSecondaryPreferred; // ReadPreference.secondaryPreferred().getName();
            if (this == NEAREST)
                return Messages.mDbDriver_readPrefNearest; // ReadPreference.nearest().getName();
            return Messages.mDbDriver_readPrefPrimary; // default
        }
    }

    private static Mongo createMongoNode(ServerNodeKey serverNodeKey) throws OdaException {
        /* NOTE: Not able to use the new 2.10 MongoClient classes directly,
         *  as their public methods, as of 2.10.1, do not allow merging options of
         *  MongoClientURL and MongoClientOptions
         */
        List<String> hosts = serverNodeKey.getServerHosts();
        String standaloneHost = hosts.size() == 1 ? hosts.get(0) : null;
        Integer port = serverNodeKey.getServerPort();
        MongoOptions options = serverNodeKey.getOptions();
        if (options == null)
            options = sm_defaultClientOptions;

        try {
            if (standaloneHost != null) {
                ServerAddress serverAddr = port != null ? new ServerAddress(standaloneHost, port)
                        : new ServerAddress(standaloneHost);
                return new Mongo(serverAddr, options);
            } else {
                List<ServerAddress> serverSeeds = new ArrayList<ServerAddress>(hosts.size());
                for (String host : hosts)
                    serverSeeds.add(new ServerAddress(host));
                return new Mongo(serverSeeds, options);
            }
        } catch (Exception ex) {
            throw new OdaException(ex);
        }
    }

    private static ServerNodeKey createServerNodeKey(Properties connProperties) {
        return sm_factory.new ServerNodeKey(connProperties);
    }

    private class ServerNodeKey {
        private List<String> m_serverHosts;
        private Integer m_serverPort;
        private String m_userName;
        private MongoOptions m_options;

        ServerNodeKey(Properties connProperties) {
            // first check if user-defined URL exists, which takes precedence 
            // if not flagged to ignore by the ignoreURI property
            MongoURI mongoUri = getMongoURI(connProperties);
            MongoOptions nodeOptions = getMongoOptions(mongoUri, connProperties);

            if (mongoUri != null) // has user-defined MongoURI
            {
                init(mongoUri, nodeOptions);
                return;
            }

            // none or invalid user-defined URL, initialize from individual properties
            init(connProperties, nodeOptions);
        }

        private void init(MongoURI mongoUri, MongoOptions options) {
            m_serverHosts = new ArrayList<String>(mongoUri.getHosts().size());
            for (String host : mongoUri.getHosts())
                m_serverHosts.add(host); // may contain optional ":<port>"
            m_serverPort = ServerAddress.defaultPort();
            m_userName = mongoUri.getUsername();
            m_options = options;

            // trace logging
            logInitializedValues("ServerNodeKey#init(MongoURI,MongoOptions)"); //$NON-NLS-1$
        }

        private void init(Properties connProperties, MongoOptions options) {
            m_serverHosts = new ArrayList<String>(1);
            m_serverHosts.add(getStringPropValue(connProperties, SERVER_HOST_PROP));
            m_serverPort = getIntegerPropValue(connProperties, SERVER_PORT_PROP);
            m_userName = getUserName(connProperties);
            m_options = options;

            // trace logging
            logInitializedValues("ServerNodeKey#init(Properties,MongoOptions)"); //$NON-NLS-1$
        }

        private void logInitializedValues(String methodName) {
            if (getLogger().isLoggable(Level.FINEST))
                getLogger().finest(Messages.bind("{0}: hosts= {1}, port= {2}, user= {3}, options= {4}", //$NON-NLS-1$
                        new Object[] { methodName, m_serverHosts, m_serverPort, m_userName, m_options }));
        }

        @Override
        public boolean equals(Object obj) {
            if (super.equals(obj))
                return true;
            if (!(obj instanceof ServerNodeKey))
                return false;

            // compare the attribute values
            ServerNodeKey thatKey = (ServerNodeKey) obj;
            if (this.m_serverHosts == null && thatKey.m_serverHosts != null)
                return false;
            if (this.m_serverHosts != null && !this.m_serverHosts.equals(thatKey.m_serverHosts))
                return false;

            if (this.m_serverPort == null && thatKey.m_serverPort != null)
                return false;
            if (this.m_serverPort != null && !this.m_serverPort.equals(thatKey.m_serverPort))
                return false;

            if (this.m_options == null && thatKey.m_options != null)
                return false;
            if (this.m_options != null && !this.m_options.toString().equals(thatKey.m_options.toString()))
                return false;

            if (this.m_userName == null && thatKey.m_userName != null)
                return false;
            if (this.m_userName != null && !this.m_userName.equals(thatKey.m_userName))
                return false;

            return true;
        }

        @Override
        public int hashCode() {
            // use its attributes for hashcode if exists
            int hashCode = 0;
            if (m_serverHosts != null)
                hashCode = m_serverHosts.hashCode();

            if (m_serverPort != null)
                hashCode = hashCode ^ m_serverPort.hashCode();

            if (m_options != null)
                hashCode = hashCode ^ m_options.toString().hashCode();

            if (m_userName != null)
                hashCode = hashCode ^ m_userName.hashCode();

            return hashCode == 0 ? super.hashCode() : hashCode;
        }

        private List<String> getServerHosts() {
            if (m_serverHosts == null)
                return Collections.emptyList();
            return m_serverHosts;
        }

        private Integer getServerPort() {
            return m_serverPort;
        }

        private MongoOptions getOptions() {
            return m_options;
        }

        @SuppressWarnings("unused")
        private String getUsername() {
            return m_userName;
        }
    }

    /*
     * Utility methods to handle mongo options, adopting the defaults set by 
     * the MongoClientOptions.
     * NOTE: Not able to use MongoClientOptions directly, 
     * as its public API, as of 2.10.1, does not allow merging option values from a MongoClientURI.
     */

    private static MongoOptions getMongoOptions(MongoURI mongoUri, Properties connProperties) {
        // check if additinal options exists in connection properties 
        // that are not covered by the mongoURI
        MongoOptions nodeOptions = mongoUri != null ? getURIOptions(mongoUri) : null;
        return addSupplementalOptions(nodeOptions, connProperties);
    }

    private static MongoOptions createDefaultClientOptions() {
        Builder optionsBuilder = new MongoClientOptions.Builder();
        return new MongoOptions(optionsBuilder.build());
    }

    private static MongoOptions getURIOptions(MongoURI mongoUri) {
        MongoOptions uriOptions = mongoUri.getOptions();

        // explicitly adopts those default client options, which were overridden
        // by MongoClientOptions.Builder#legacyDefaults()
        uriOptions.setConnectionsPerHost(sm_defaultClientOptions.getConnectionsPerHost());
        uriOptions.setWriteConcern(sm_defaultClientOptions.getWriteConcern());

        return uriOptions;
    }

    private static MongoOptions addSupplementalOptions(MongoOptions mongoOptions, Properties connProperties) {
        if (hasKeepSocketAlive(connProperties)) // need to change setting, as MongoDB default is false
        {
            if (mongoOptions == null)
                mongoOptions = createDefaultClientOptions();
            mongoOptions.setSocketKeepAlive(true);
        }

        return mongoOptions;
    }

    private static Boolean hasKeepSocketAlive(Properties connProperties) {
        String keepSocketAlivePropValue = getStringPropValue(connProperties, SOCKET_KEEP_ALIVE_PROP);
        if (keepSocketAlivePropValue == null) // supported option is not defined
            return Boolean.FALSE;
        boolean keepSocketAlive = Boolean.valueOf(keepSocketAlivePropValue);
        if (keepSocketAlive == false) // mongoDB default
            return Boolean.FALSE; // using default value, no need to return value

        return Boolean.TRUE;
    }

    static String getDatabaseName(Properties connProps) {
        MongoURI mongoURI = getMongoURI(connProps);
        if (mongoURI != null)
            return mongoURI.getDatabase();

        // no mongoURI specified, get from the individual property
        return getStringPropValue(connProps, DBNAME_PROP);
    }

    static String getUserName(Properties connProps) {
        return getStringPropValue(connProps, USERNAME_PROP);
    }

    static String getPassword(Properties connProps) {
        return getStringPropValue(connProps, PASSWORD_PROP);
    }

    private static MongoURI getMongoURI(Properties connProps) {
        // check if explicitly indicated not to use URI, even if URI value exists
        Boolean ignoreURI = getBooleanPropValue(connProps, IGNORE_URI_PROP);
        if (ignoreURI != null && ignoreURI)
            return null;

        String uri = getStringPropValue(connProps, MONGO_URI_PROP);
        if (uri == null || uri.isEmpty())
            return null;

        try {
            return new MongoURI(uri);
        } catch (Exception ex) {
            // log and ignore
            getLogger().log(Level.INFO, Messages.bind("Invalid Mongo Database URI: {0}", uri), ex); //$NON-NLS-1$
        }
        return null;
    }

    /*
     * Not currently used.
     */
    @SuppressWarnings("unused")
    private static String formatMongoURI(Properties connProps) {
        // format a Mongo URI text from supported connection properties
        // Mongo URI syntax:
        // mongodb://[username:password@]host1[:port1]...[,hostN[:portN]][/[database][?options]]

        // validate that the mininum required URI part exists
        String serverHost = getStringPropValue(connProps, SERVER_HOST_PROP);
        if (serverHost == null || serverHost.isEmpty())
            throw new IllegalArgumentException(Messages.mDbDriver_missingValueServerHost);

        StringBuffer buf = new StringBuffer(MongoURI.MONGODB_PREFIX);

        String username = getUserName(connProps);
        if (username != null && !username.isEmpty()) {
            String passwd = getPassword(connProps);
            buf.append(username);
            buf.append(':');
            buf.append(passwd);
            buf.append('@');
        }

        buf.append(serverHost);

        Integer serverPort = getIntegerPropValue(connProps, SERVER_PORT_PROP);
        if (serverPort != null) {
            buf.append(':');
            buf.append(serverPort);
        }

        String dbName = getStringPropValue(connProps, DBNAME_PROP);
        if (dbName != null) {
            buf.append('/');
            buf.append(dbName);
        }

        //  comment out inclusion of all options in the generated URI text, cuz
        //  the MongoURI parser does not yet support all the options allowed in MongoOptions
        /*
                MongoOptions options = createMongoOptions( connProps );
                if( options != null )
                {
        // if database is absent, a '/' is still required
        if( dbName == null )
            buf.append( '/' );
        buf.append( '?' );
            
        String optionsText = options.toString();
        // replace the option separator ',' with ';' as required in MongoURI
        optionsText = optionsText.replace( ',', ';' );
        buf.append( optionsText );
                }
        */
        return buf.toString();
    }

    static String getStringPropValue(Properties props, String propName) {
        String propValue = props.getProperty(propName);
        return propValue != null ? propValue.trim() : null;
    }

    static Boolean getBooleanPropValue(Properties props, String propName) {
        String propValue = getStringPropValue(props, propName);
        if (propValue == null || propValue.isEmpty())
            return null;
        return Boolean.valueOf(propValue);
    }

    static Integer getIntegerPropValue(Properties props, String propName) {
        String propValue = getStringPropValue(props, propName);
        if (propValue == null || propValue.isEmpty())
            return null;
        try {
            return Integer.valueOf(propValue);
        } catch (NumberFormatException ex) {
            // log and ignore
            getLogger().log(Level.INFO, "MongoDBDriver#getIntegerPropValue ignoring exception: " + ex); //$NON-NLS-1$
        }
        return null;
    }

    static Logger getLogger() {
        return DriverUtil.getLogger();
    }

}