/*
 * JBoss, Home of Professional Open Source.
 * See the COPYRIGHT.txt file distributed with this work for information
 * regarding copyright ownership.  Some portions may be licensed
 * to Red Hat, Inc. under one or more contributor license agreements.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 */

package org.teiid.jdbc;

import java.net.MalformedURLException;
import java.sql.Connection;
import java.sql.SQLFeatureNotSupportedException;
import java.util.Properties;
import java.util.logging.Logger;

import javax.sql.XAConnection;

import org.teiid.net.TeiidURL;


/**
 * The Teiid JDBC DataSource implementation class of {@link javax.sql.DataSource} and
 * {@link javax.sql.XADataSource}.
 * <p>
 * The {@link javax.sql.DataSource} interface follows the JavaBean design pattern,
 * meaning the implementation class has <i>properties</i> that are accessed with getter methods
 * and set using setter methods, and where the getter and setter methods follow the JavaBean
 * naming convention (e.g., <code>get</code><i>PropertyName</i><code>() : </code><i>PropertyType</i>
 * and <code>set</code><i>PropertyName</i><code>(</code><i>PropertyType</i><code>) : void</code>).
 * </p>
 * The {@link javax.sql.XADataSource} interface is almost identical to the {@link javax.sql.DataSource}
 * interface, but rather than returning {@link java.sql.Connection} instances, there are methods that
 * return {@link javax.sql.XAConnection} instances that can be used with distributed transactions.
 * <p>
 * The following are the properties for this DataSource:
 * <table cellspacing="0" cellpadding="0" border="1" width="100%">
 *   <tr><td><b>Property Name</b></td><td><b>Type</b></td><td><b>Description</b></td></tr>
 *   <tr><td>portNumber       </td><td><code>int   </code></td><td>The port number where a Teiid Server is listening
 *                                                                 for requests.</td></tr>
 *   <tr><td>serverName       </td><td><code>String</code></td><td>The hostname or IP address of the Teiid Server.</td></tr>
 * <table>
 * If "serverName" property is not set then data source will try to create a embedded connection to the Teiid server.
 * </p>
 */
public class TeiidDataSource extends BaseDataSource {

	private static final long serialVersionUID = -5170316154373144878L;

	/**
     * The port number where a server is listening for requests.
     * This property name is one of the standard property names defined by the JDBC 2.0 specification,
     * and is <i>required</i>.
     */
    private int portNumber;

    /**
     * The name of the host where the sServer is running.
     * This property name is one of the standard property names defined by the JDBC 2.0 specification,
     * and is <i>required</i>.
     */
    private String serverName;
     
    /**
     * Specify whether to make a secure (SSL, mms:) connection or a normal non-SSL mm: connection.
     * the default is to use a non-secure connection.
     * @since 5.0.2
     */
    private boolean secure = false;

    /**
     * Holds a comma delimited list of alternate Server(s):Port(s) that can 
     * be used for connection fail-over.
     * @since 5.5
     */
    private String alternateServers;
    
    /**
     * The auto failover mode for calls made to the query engine.  If true query engine calls that fail will
     * allow the connection to choose another process.
     */
    private String autoFailover;
    
    private String discoveryStrategy;
    
    /**
     * when "true", in the "embedded" scenario, authentication is information is read in pass though manner.
     */
    private boolean passthroughAuthentication = false;
    
    /**
     * Name of the jass configuration to use from the -Djava.security.auth.login.config=login.conf property
     */
    private String jaasName;
    
	/**
     * Name of Kerberos KDC service principle name
     */
    private String kerberosServicePrincipleName;
    /**
     * If not using ssl determines whether requests with the associated command payload should be encrypted
     */
    private boolean encryptRequests;
    
    private final TeiidDriver driver;

	private boolean loadBalance = true;
    
	public TeiidDataSource() {
		this.driver = new TeiidDriver();
    }
	
	TeiidDataSource(TeiidDriver driver) {
		this.driver = driver;
	}

    // --------------------------------------------------------------------------------------------
    //                             H E L P E R   M E T H O D S
    // --------------------------------------------------------------------------------------------

    protected Properties buildProperties(final String userName, final String password) {               
        Properties props = super.buildProperties(userName, password);
        
        if (this.getAutoFailover() != null) {
            props.setProperty(TeiidURL.CONNECTION.AUTO_FAILOVER, this.getAutoFailover());
        }
        
        if (this.getDiscoveryStrategy() != null) {
        	props.setProperty(TeiidURL.CONNECTION.DISCOVERY_STRATEGY, this.getDiscoveryStrategy());
        }
        
        if (this.encryptRequests) {
        	props.setProperty(TeiidURL.CONNECTION.ENCRYPT_REQUESTS, Boolean.TRUE.toString());
        }

        return props;
    }
    
    protected String buildServerURL() throws TeiidSQLException {
    	if ( this.alternateServers == null || this.alternateServers.length() == 0) {
    		// Format:  "mm://server:port"
    		return new TeiidURL(this.serverName, this.portNumber, this.secure).getAppServerURL();
    	} 

    	// Format: "mm://server1:port,server2:port,..."
		String serverURL = this.secure ? TeiidURL.SECURE_PROTOCOL : TeiidURL.DEFAULT_PROTOCOL;
		
		if (this.serverName.indexOf(':') != -1 && !this.serverName.startsWith("[")) { //$NON-NLS-1$
			serverURL += "[" + this.serverName + "]"; //$NON-NLS-1$ //$NON-NLS-2$
		} else {
			serverURL += this.serverName; 
		}
		
		serverURL += TeiidURL.COLON_DELIMITER + this.portNumber;
		
		//add in the port number if not specified 
		
    	String[] as = this.alternateServers.split( TeiidURL.COMMA_DELIMITER);
    	
    	for ( int i = 0; i < as.length; i++ ) {
    		String server = as[i].trim();
    		//ipv6 without port
    		if (server.startsWith("[") && server.endsWith("]")) { //$NON-NLS-1$ //$NON-NLS-2$
    			String msg = reasonWhyInvalidServerName(server.substring(1, server.length() - 1));
    			if (msg != null) {
    				throw createConnectionError(JDBCPlugin.Util.getString("MMDataSource.alternateServer_is_invalid", msg)); //$NON-NLS-1$
    			}
    			serverURL += (TeiidURL.COMMA_DELIMITER +as[i] + TeiidURL.COLON_DELIMITER + this.portNumber);
    		} else {
        		String[] serverParts = server.split(TeiidURL.COLON_DELIMITER, 2);
        		String msg = reasonWhyInvalidServerName(serverParts[0]);
    			if (msg != null) {
    				throw createConnectionError(JDBCPlugin.Util.getString("MMDataSource.alternateServer_is_invalid", msg)); //$NON-NLS-1$
    			}
    			serverURL += (TeiidURL.COMMA_DELIMITER + serverParts[0] + TeiidURL.COLON_DELIMITER);
    			if ( serverParts.length > 1 ) {
    				try {
						TeiidURL.validatePort(serverParts[1]);
					} catch (MalformedURLException e) {
						throw createConnectionError(JDBCPlugin.Util.getString("MMDataSource.alternateServer_is_invalid", e.getMessage())); //$NON-NLS-1$
					}
        			
    				serverURL += serverParts[1];
    			} else {
    				serverURL += this.portNumber;
    			}
    		}
    	}
		
		try {
			return new TeiidURL(serverURL).getAppServerURL();
		} catch (MalformedURLException e) {
			throw TeiidSQLException.create(e);
		}
    }

    protected JDBCURL buildURL() throws TeiidSQLException {
        return new JDBCURL(this.getDatabaseName(), buildServerURL(), buildProperties(getUser(), getPassword()));
    }

    protected void validateProperties( final String userName, final String password) throws java.sql.SQLException {
        super.validateProperties(userName, password);
        
        String reason = reasonWhyInvalidPortNumber(this.portNumber);
        if ( reason != null ) {
            throw createConnectionError(reason);
        }

        reason = reasonWhyInvalidServerName(this.serverName);
        if ( reason != null ) {
            throw createConnectionError(reason);
        }
    }
    
    private TeiidSQLException createConnectionError(String reason) {
        String msg = JDBCPlugin.Util.getString("MMDataSource.Err_connecting", reason); //$NON-NLS-1$
        return new TeiidSQLException(msg);        
    }

    // --------------------------------------------------------------------------------------------
    //                        D A T A S O U R C E   M E T H O D S
    // --------------------------------------------------------------------------------------------

    /**
     * Attempt to establish a database connection.
     * @return a Connection to the database
     * @throws java.sql.SQLException if a database-access error occurs
     * @see javax.sql.DataSource#getConnection()
     */
    public Connection getConnection() throws java.sql.SQLException {
        return getConnection(null,null);
    }

    /**
     * Attempt to establish a database connection.
     * @param userName the database user on whose behalf the Connection is being made
     * @param password the user's password
     * @return a Connection to the database
     * @throws java.sql.SQLException if a database-access error occurs
     * @see javax.sql.DataSource#getConnection(java.lang.String, java.lang.String)
     */
    public Connection getConnection(String userName, String password) throws java.sql.SQLException {
    	
    	// check if this is embedded connection 
    	if (getServerName() == null) {
    		super.validateProperties(userName, password);
	        final Properties props = buildEmbeddedProperties(userName, password);	 
	        String url = new JDBCURL(getDatabaseName(), null, null).getJDBCURL();
	        return driver.connect(url, props);    		    		
    	}
    	
    	// if not proceed with socket connection.
        validateProperties(userName,password);
        final Properties props = buildProperties(userName, password);
        return driver.connect(new JDBCURL(this.getDatabaseName(), buildServerURL(), null).getJDBCURL(), props);
    }
    
	private Properties buildEmbeddedProperties(final String userName, final String password) {
		Properties props = buildProperties(userName, password);
		props.setProperty(TeiidURL.CONNECTION.PASSTHROUGH_AUTHENTICATION, Boolean.toString(this.passthroughAuthentication));
		
		if (getJaasName() != null) {
			props.setProperty(TeiidURL.CONNECTION.JAAS_NAME, getJaasName());
		}
		if (getKerberosServicePrincipleName() != null) {
			props.setProperty(TeiidURL.CONNECTION.KERBEROS_SERVICE_PRINCIPLE_NAME, getKerberosServicePrincipleName());
		}
		return props;
	}    
	
   /**
     * @see java.lang.Object#toString()
     */
    public String toString() {
        try {
			return buildURL().getJDBCURL();
		} catch (TeiidSQLException e) {
			return e.getMessage();
		} 
    }

    // --------------------------------------------------------------------------------------------
    //                        P R O P E R T Y   M E T H O D S
    // --------------------------------------------------------------------------------------------

    /**
     * Returns the port number.
     * @return the port number
     */
    public int getPortNumber() {
        return portNumber;
    }

    /**
     * Returns the name of the server.
     * @return the name of the server
     */
    public String getServerName() {
        return serverName;
    }
    
    /**
     * Returns a flag indicating whether to create a secure connection or not. 
     * @return True if using secure mms: protocol, false for normal mm: protocol.
     * @since 5.0.2
     */
    public boolean isSecure() {
        return this.secure;
    }
    /**
     * Same as "isSecure". Required by the reflection login in connection pools to identify the type
     * @return
     */
    public boolean getSecure() {
        return this.secure;
    }    

    /**
     * Returns a string containing a comma delimited list of alternate 
     * server(s).  
     * 
     * The list will be in the form of server2[:port2][,server3[:port3]].  If no 
     * alternate servers have been defined <code>null</code> is returned. 
     * @return A comma delimited list of server:port or <code>null</code> If 
     * no alternate servers are defined.
     * @since 5.5
     */
    public String getAlternateServers() {
    	if ( this.alternateServers != null && this.alternateServers.length() < 1 )
    		return null;
        return this.alternateServers;
    }

    /**
     * Sets the portNumber.
     * @param portNumber The portNumber to set
     */
    public void setPortNumber(final int portNumber) {
        this.portNumber = portNumber;
    }

    /**
     * Sets the serverName.
     * @param serverName The serverName to set
     */
    public void setServerName(final String serverName) {
        this.serverName = serverName;
    }
    
    /**
     * Sets the secure flag to use mms: protocol instead of the default mm: protocol. 
     * @param secure True to use mms:
     * @since 5.0.2
     */
    public void setSecure(final boolean secure) {
        this.secure = secure;
    }
    
    /**
     * Sets a list of alternate server(s) that can be used for 
     * connection fail-over.
     * 
     * The form of the list should be server2[:port2][,server3:[port3][,...]].  
     * 
     * If ":port" is omitted, the port defined by <code>portNumber</code> is used.
     * 
     * If <code>servers</code> is empty or <code>null</code>, the value of
     * <code>alternateServers</code> is cleared.
     * @param servers A comma delimited list of alternate 
     * Server(s):Port(s) to use for connection fail-over. If blank or 
     * <code>null</code>, the list is cleared.
     * @since 5.5
     */
    public void setAlternateServers(final String servers) {
    	this.alternateServers = servers;
    	if ( this.alternateServers != null && this.alternateServers.length() < 1 )
    		this.alternateServers = null;
    }
    
    
    // --------------------------------------------------------------------------------------------
    //                  V A L I D A T I O N   M E T H O D S
    // --------------------------------------------------------------------------------------------

    /**
     * Return the reason why the supplied port number may be invalid, or null
     * if it is considered valid.
     * @param portNumber a possible value for the property
     * @return the reason why the property is invalid, or null if it is considered valid
     * @see #setPortNumber(int)
     */
    public static String reasonWhyInvalidPortNumber( final int portNumber) {
    	return TeiidURL.validatePort(portNumber);
    }

    /**
     * Return the reason why the supplied server name may be invalid, or null
     * if it is considered valid.
     * @param serverName a possible value for the property
     * @return the reason why the property is invalid, or null if it is considered valid
     * @see #setServerName(String)
     * */
    public static String reasonWhyInvalidServerName( final String serverName ) {
        if ( serverName == null || serverName.trim().length() == 0 ) {
            return JDBCPlugin.Util.getString("MMDataSource.Server_name_required"); //$NON-NLS-1$
        }
        return null;
    }

    /**
     * The reason why "socketsPerVM" is invalid.
     * @param value of "socketsPerVM" property
     * @return reason
     */
    public static String reasonWhyInvalidSocketsPerVM(final String socketsPerVM) {
        if (socketsPerVM != null) {
            int value = -1;
            try {
                value = Integer.parseInt(socketsPerVM);
            } catch (Exception e) {                
            }
            
            if (value <= 0) {
                return JDBCPlugin.Util.getString("MMDataSource.Sockets_per_vm_invalid"); //$NON-NLS-1$
            }
        }
        return null;
    }
    
    
    /**
     * The reason why "stickyConnections" is invalid.
     * @param value of "stickyConnections" property
     * @return reason
     */
    public static String reasonWhyInvalidStickyConnections(final String stickyConnections) {
        if (stickyConnections != null) {
            if ((! stickyConnections.equalsIgnoreCase("true")) &&    //$NON-NLS-1$ 
                (! stickyConnections.equalsIgnoreCase("false"))) {   //$NON-NLS-1$          
                return JDBCPlugin.Util.getString("MMDataSource.Sticky_connections_invalid"); //$NON-NLS-1$
            }
        }
        return null;
    }
 
    /** 
     * @return Returns the transparentFailover.
     */
    public String getAutoFailover() {
        return this.autoFailover;
    }

    /** 
     * @param transparentFailover The transparentFailover to set.
     */
    public void setAutoFailover(String autoFailover) {
        this.autoFailover = autoFailover;
    }

	public String getDiscoveryStrategy() {
		return discoveryStrategy;
	}

	public void setDiscoveryStrategy(String discoveryStrategy) {
		this.discoveryStrategy = discoveryStrategy;
	}
	
	/**
	 * When true, this connection uses the passed in security domain to do the authentication. 
	 * @return
	 */
    public boolean isPassthroughAuthentication() {
		return passthroughAuthentication;
	}
    
    /**
     * Same as "isPassthroughAuthentication". Required by the reflection login in connection pools to identify the type
     * @return
     */
    public boolean getPassthroughAuthentication() {
		return passthroughAuthentication;
	}
    
	/**
	 * When set to true, the connection uses the passed in security domain to do the authentication.
	 * @since 7.1 
	 * @return
	 */
	public void setPassthroughAuthentication(final boolean passthroughAuthentication) {
		this.passthroughAuthentication = passthroughAuthentication;
	}	
	
	/**
	 * Application name from JAAS Login Config file
	 * @since 7.6
	 * @return
	 */
    public String getJaasName() {
		return jaasName;
	}

	/**
	 * Application name from JAAS Login Config file
	 * @since 7.6
	 */    
	public void setJaasName(String jaasApplicationName) {
		this.jaasName = jaasApplicationName;
	}

	/**
	 * Kerberos KDC service principle name
	 * @since 7.6
	 * @return
	 */	
	public String getKerberosServicePrincipleName() {
		return kerberosServicePrincipleName;
	}

	/**
	 * Kerberos KDC service principle name
	 * @since 7.6
	 */	
	public void setKerberosServicePrincipleName(String kerberosServerName) {
		this.kerberosServicePrincipleName = kerberosServerName;
	}

	public Logger getParentLogger() throws SQLFeatureNotSupportedException {
		return TeiidDriver.logger;
	}
	
	public void setEncryptRequests(boolean encryptRequests) {
		this.encryptRequests = encryptRequests;
	}
	
	public boolean isEncryptRequests() {
		return encryptRequests;
	}
	
	public boolean getEncryptRequests() {
		return encryptRequests;
	}
	
	public boolean isLoadBalance() {
		return loadBalance;
	}
	
	public boolean getLoadBalance() {
		return loadBalance;
	}
	
	public void setLoadBalance(boolean loadBalance) {
		this.loadBalance = loadBalance;
	}
	
	/**
     * Attempt to establish a database connection that can be used with distributed transactions.
     * @param userName the database user on whose behalf the XAConnection is being made
     * @param password the user's password
     * @return an XAConnection to the database
     * @throws java.sql.SQLException if a database-access error occurs
     * @see javax.sql.XADataSource#getXAConnection(java.lang.String, java.lang.String)
     */
    public XAConnection getXAConnection(final String userName, final String password) throws java.sql.SQLException {
    	XAConnectionImpl result = new XAConnectionImpl((ConnectionImpl) getConnection(userName, password));
    	result.setLoadBalance(loadBalance);
    	return result;
    }
    
}

