/*
* ====================
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2008-2009 Sun Microsystems, Inc. All rights reserved.
*
* The contents of this file are subject to the terms of the Common Development
* and Distribution License("CDDL") (the "License"). You may not use this file
* except in compliance with the License.
*
* You can obtain a copy of the License at
* http://IdentityConnectors.dev.java.net/legal/license.txt
* See the License for the specific language governing permissions and limitations
* under the License.
*
* When distributing the Covered Code, include this CDDL Header Notice in each file
* and include the License file at identityconnectors/legal/license.txt.
* If applicable, add the following below this CDDL Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
* ====================
*/
package org.identityconnectors.racf;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.Semaphore;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.identityconnectors.framework.common.exceptions.ConnectorException;
import org.identityconnectors.rw3270.RW3270BaseConnection;
import org.identityconnectors.rw3270.RW3270Connection;
import org.identityconnectors.rw3270.RW3270ConnectionFactory;
/**
* This class represents a pool of connections to a Racf host.
* <p>
* While it's theoretically possible to have multiple pools associated with a single
* host, we are not going to try to support that here; the application could distinguish
* between pools, but we can't at this low level.
*
* @author hetrick
*/
public class ConnectionPool {
private boolean _reapable;
/**
* Use a java.util.logging Logger, since we need a way for tests to know what
* the ConnectionPool has been doing, but the framework Log provides no access
* to the log contents.
*/
private Logger _log = Logger.getLogger(ConnectionPool.class.getName());
/**
* The ActiveConnectionReaper runs periodically to log out connections before they
* are shut down by a terminal server or the racf mainframe. The ConnectionPool will
* log the connection back in before handing it back out to the RacfConnector.
*/
private static class ActiveConnectionReaper extends Thread {
private Logger _log = Logger.getLogger(ConnectionPool.class.getName());
public void run() {
_log.info("Starting ConnectionPool reaper");
while (true) {
try {
// Once/minute
//
Thread.sleep(60000);
} catch (InterruptedException e) { }
// Reap the connections for each of the currently existing pools
//
for (Map.Entry<String, ConnectionPool> pool : ConnectionPool._pools.entrySet().toArray(new Map.Entry[0]))
reapConnections(pool);
}
}
private void reapConnections(Map.Entry<String, ConnectionPool> poolEntry) {
long expire = System.nanoTime()-poolEntry.getValue()._reaperMaximumIdle*1000000;
ConnectionPool pool = poolEntry.getValue();
// Go through the current set of active connections
//
for (QueueEntry entry : pool._activeConnections.toArray(new QueueEntry[0])) {
if (entry._timeReturned<expire) {
if (pool._activeConnections.remove(entry)) {
// By using remove, we only get here if no other thread took the
// connection for real use
//
try {
_log.info("reaping connection for "+entry._connection.getConfiguration().getUserName()+" on "+pool._currentConfiguration.getHostNameOrIpAddr());
entry._connection.logoutUser();
} catch (Exception e) {
_log.severe("failed to logout connection in reaper: userName '"+entry._connection.getConfiguration().getUserName()+"':"+e);
}
entry._connection.dispose();
entry._connection = null;
pool._inactiveConnections.add(entry);
}
}
}
// Andrei suggests this as a good point to reap the entire pool
//
synchronized (_pools) {
// First, make sure the pool hasn't been replaced while we were looking.
// We want to be sure we remove *this* pool entry, not a replacement
// that snuck in.
//
if (_pools.get(poolEntry.getKey())==pool && pool._reapable) {
if (pool._count==pool._inactiveConnections.size()) {
// All connections are in the inactive pool (or bad connection pool)
//
_pools.remove(poolEntry.getKey());
_log.info("reaping pool for "+pool._currentConfiguration.getHostNameOrIpAddr());
}
}
}
}
};
private static class QueueEntry {
public QueueEntry(int index) {
_index = index;
}
public long _timeReturned;
public int _index;
public RW3270BaseConnection _connection;
};
/**
* The _semaphore is used to enable/disable getConnection()
*/
private Semaphore _semaphore;
private RacfConfiguration _currentConfiguration;
private long _reaperMaximumIdle;
/**
* The number of connections in the pool
*/
private int _count;
private BlockingQueue<QueueEntry> _activeConnections;
private BlockingQueue<QueueEntry> _badConnections;
private BlockingQueue<QueueEntry> _inactiveConnections;
private Map<RW3270Connection, QueueEntry> _inUseConnections;
private static Map<String, ConnectionPool> _pools = new HashMap<String, ConnectionPool>();
private static ActiveConnectionReaper _reaper = new ActiveConnectionReaper();
static {
_reaper.setDaemon(true);
_reaper.start();
}
/**
* Get the ConnectionPool associated with the specified configuration.
* <p>
* If there is no pool associated with the host in the configuration,
* create one, with all connections logged out.
* <p>
* If there is a pool associated with the host in the configuration,
* but any of the pertinent parameters have changed (port, usernames, passwords),
* shut down the pool, logging out all connections, and create a new pool.
* <p>
* If the pertinent parameters of the configuration associated with the
* hostname are unchanged, return the existing pool.
*
* @param configuration -- the RacfConfiguration with which the pool is associated
* @return
*/
public static ConnectionPool getConnectionPool(RacfConfiguration configuration) {
return getConnectionPool(configuration, true);
}
static ConnectionPool getConnectionPool(RacfConfiguration configuration, boolean reapable) {
synchronized (_pools) {
if (configuration.getUserNames()==null || configuration.getUserNames().length==0)
throw new IllegalArgumentException(configuration.getMessage(RacfMessages.USERNAMES_NULL));
Logger log = Logger.getLogger(ConnectionPool.class.getName());
ConnectionPool pool = _pools.get(configuration.getHostNameOrIpAddr());
if (pool==null) {
log.info("creating new pool for "+configuration.getHostNameOrIpAddr());
pool = new ConnectionPool(configuration);
_pools.put(configuration.getHostNameOrIpAddr(), pool);
} else if (changed(pool._currentConfiguration, configuration)) {
log.info("replacing pool for "+configuration.getHostNameOrIpAddr());
// Shut down the existing pool for this host
//
pool.closeAllConnections();
pool = new ConnectionPool(configuration);
_pools.put(configuration.getHostNameOrIpAddr(), pool);
}
pool._reapable = reapable;
return pool;
}
}
/**
* Get a connection from the pool
* @param configuration
* @return
*/
public static RW3270Connection getConnectionFromPool(RacfConfiguration configuration) {
ConnectionPool pool = getConnectionPool(configuration, false);
RW3270Connection connection = pool.getConnection();
pool._reapable = true;
return connection;
}
/**
* Check if any of the RW3270 parameters have changed:
* <ul>
* <li>host name</li>
* <li>host port</li>
* <li>user names</li>
* <li>passwords</li>
* <li>login script</li>
* <li>logoff script</li>
* </ul>
* @param configuration
* @return
*/
private static boolean changed(RacfConfiguration oldConfiguration, RacfConfiguration newConfiguration) {
if (!oldConfiguration.getHostNameOrIpAddr().equals(newConfiguration.getHostNameOrIpAddr()))
return true;
if (!oldConfiguration.getHostTelnetPortNumber().equals(newConfiguration.getHostTelnetPortNumber()))
return true;
if (oldConfiguration.getUserNames().length!=newConfiguration.getUserNames().length)
return true;
if (oldConfiguration.getPasswords().length!=newConfiguration.getPasswords().length)
return true;
if (!Arrays.asList(oldConfiguration.getUserNames()).equals(Arrays.asList(newConfiguration.getUserNames())))
return true;
if (!Arrays.asList(oldConfiguration.getPasswords()).equals(Arrays.asList(newConfiguration.getPasswords())))
return true;
if (!oldConfiguration.getConnectScript().equals(newConfiguration.getConnectScript()))
return true;
if (!oldConfiguration.getDisconnectScript().equals(newConfiguration.getDisconnectScript()))
return true;
//The pool must be re-initialized, when the other connection implementation is selected
if (!oldConfiguration.getConnectionClassName().equals(newConfiguration.getConnectionClassName()))
return true;
return false;
}
private ConnectionPool(RacfConfiguration configuration) {
_currentConfiguration = configuration;
_semaphore = new Semaphore(1);
// Initialize the queues
//
_activeConnections = new LinkedBlockingQueue<QueueEntry>();
_badConnections = new LinkedBlockingQueue<QueueEntry>();
_inactiveConnections = new LinkedBlockingQueue<QueueEntry>();
_inUseConnections = new ConcurrentHashMap<RW3270Connection, QueueEntry>();
_count = 0;
_reaperMaximumIdle = _currentConfiguration.getReaperMaximumIdleTime()*1000;
// Create an inactiveConnection for each entry in _currentConfiguration
//
String[] userNames = _currentConfiguration.getUserNames();
for (int i=0; i<userNames.length; i++) {
QueueEntry entry = new QueueEntry(i);
_log.info("creating inactive connection for "+_currentConfiguration.getUserNames()[i]+" on "+_currentConfiguration.getHostNameOrIpAddr());
_inactiveConnections.add(entry);
_count++;
}
}
public synchronized boolean isEmpty() {
return _count==0;
}
boolean isReapable() {
return _reapable;
}
void setReapable(boolean reapable) {
_reapable = reapable;
}
private RW3270Connection getConnection(boolean wait) {
try {
QueueEntry entry = findUnusedConnection(wait);
if (entry!=null) {
_inUseConnections.put(entry._connection, entry);
return entry._connection;
} else {
return null;
}
} catch (Exception ie) {
throw ConnectorException.wrap(ie);
}
}
private RW3270Connection getConnection() {
return getConnection(false);
}
private QueueEntry findUnusedConnection(boolean inTest) throws InterruptedException {
// Wait for pool to be available to getConnection().
//
acquireSemaphore(inTest);
// Are there any connections in the pool?
//
if (isEmpty()) {
releaseSemaphore(inTest);
throw new ConnectorException(_currentConfiguration.getMessage(RacfMessages.EMPTY_POOL));
}
// If there is an available activeConnection, use it
//
QueueEntry entry = _activeConnections.poll();
if (entry!=null) {
releaseSemaphore(inTest);
// Fall through into return code
//
_log.info("retrieved active connection for "+entry._connection.getConfiguration().getUserName()+" on "+_currentConfiguration.getHostNameOrIpAddr());
} else {
// If there is an available inactiveConnection, use it
//
entry = _inactiveConnections.poll();
if (entry!=null) {
releaseSemaphore(inTest);
try {
RW3270ConnectionFactory factory = new RW3270ConnectionFactory(_currentConfiguration.getRW3270Configuration(entry._index));
entry._connection = (RW3270BaseConnection)factory.newConnection();
entry._connection.loginUser();
_log.info("activated connection for "+entry._connection.getConfiguration().getUserName()+" on "+_currentConfiguration.getHostNameOrIpAddr());
} catch (Exception e) {
if (entry._connection!=null)
entry._connection.dispose();
_log.log(Level.SEVERE, "failed to create connection: userName '"+_currentConfiguration.getRW3270Configuration(entry._index).getUserName()+"'", e);
// If we got an error activating the connection
// ignore it, and try again
//
returnBrokenEntry(entry);
if (inTest)
throw ConnectorException.wrap(e);
else
entry = findUnusedConnection(true);
}
} else {
// Wait for an activeConnection to become available
//
if (!inTest) {
_log.info("waiting for active connection on "+_currentConfiguration.getHostNameOrIpAddr());
entry = _activeConnections.take();
_log.info("waited for active connection for "+entry._connection.getConfiguration().getUserName()+" on "+_currentConfiguration.getHostNameOrIpAddr());
}
releaseSemaphore(inTest);
}
}
return entry;
}
private void releaseSemaphore(boolean inTest) {
if (!inTest)
_semaphore.release();
}
private void acquireSemaphore(boolean inTest) throws InterruptedException {
if (!inTest)
_semaphore.acquire();
}
/**
* Return a RacfConnection to the ConnectionPool.
*
* @param connection
*/
public void returnConnection(RW3270Connection connection) {
QueueEntry entry = _inUseConnections.remove(connection);
if (entry==null) {
throw new ConnectorException(_currentConfiguration.getMessage(RacfMessages.BAD_CONN_ENTRY));
} else {
entry._timeReturned = System.nanoTime();
_log.info("returned connection for "+entry._connection.getConfiguration().getUserName()+" on "+_currentConfiguration.getHostNameOrIpAddr());
_activeConnections.add(entry);
}
}
/**
* Return a RacfConnection that is not working to the ConnectionPool.
*
* @param connection
*/
public void returnBrokenConnection(RW3270Connection connection) {
QueueEntry entry = _inUseConnections.remove(connection);
if (entry==null) {
throw new ConnectorException(_currentConfiguration.getMessage(RacfMessages.BAD_CONN_ENTRY));
} else {
entry._timeReturned = System.nanoTime();
entry._connection.dispose();
entry._connection = null;
//TODO: we may eventually want to see if we can restart bad connections
// While IDM doesn't do this, we can try it.
returnBrokenEntry(entry);
}
}
private void returnBrokenEntry(QueueEntry entry) {
_badConnections.add(entry);
_log.info("returned bad connection for "+_currentConfiguration.getRW3270Configuration(entry._index).getUserName()+" on "+_currentConfiguration.getHostNameOrIpAddr());
synchronized (this) {
_count--;
}
}
/**
* Close all connections in ConnectionPool
*/
public void closeAllConnections() {
closeAllConnections(false);
}
/**
* Close all connections in ConnectionPool
*/
public void closeAllConnections(boolean leaveLocked) {
try {
_semaphore.acquire();
// Need to wait for any inUse connections to be returned
//
int i = 0;
while (!_inUseConnections.isEmpty()) {
// Just to set a limit, make it two tries per connection
//
if (i++>_inUseConnections.size()*2)
throw new ConnectorException(_currentConfiguration.getMessage(RacfMessages.EMPTY_POOL));
try {
// wait for the allowed timeout of a command
//
Thread.sleep(_currentConfiguration.getCommandTimeout());
} catch (InterruptedException ie) {
// Just ignore this
}
}
for (QueueEntry entry : _activeConnections.toArray(new QueueEntry[0])) {
try {
_log.info("closing active connection for "+entry._connection.getConfiguration().getUserName()+" on "+_currentConfiguration.getHostNameOrIpAddr());
entry._connection.dispose(); //dispose will do an logout entry._connection.logoutUser();
_activeConnections.remove(entry);
_inactiveConnections.add(entry);
} catch (Exception e) {
_activeConnections.remove(entry);
_badConnections.add(entry);
_log.severe("failed to logout connection in closeAllConnections: userName '"+entry._connection.getConfiguration().getUserName()+"':"+e);
}
entry._connection = null;
}
} catch (InterruptedException ie) {
throw ConnectorException.wrap(ie);
} finally {
releaseSemaphore(leaveLocked);
}
}
/**
* Test all connections using _currentConfiguration
*/
public void testAllConnections() {
testAllConnections(_currentConfiguration);
}
/**
* Test all connections using specified RacfConfiguration
* @param configuration
*/
public void testAllConnections(RacfConfiguration configuration) {
try {
closeAllConnections(true);
// We want to use a Vector here, since it is thread-safe
//
final Vector<String> errors = new Vector<String>();
final String[] userNames = configuration.getUserNames();
Thread[] threads = new Thread[userNames.length];
for (int i=0; i<userNames.length; i++) {
final int index = i;
threads[i] = new Thread() {
RW3270Connection connection = null;
public void run() {
//TODO: test only connections on this machine, or all?
try {
connection = getConnection(true);
_log.info("activated test connection for "+_currentConfiguration.getRW3270Configuration(index).getUserName()+" on "+_currentConfiguration.getHostNameOrIpAddr());
if (connection!=null) {
_log.info("shut down test connection for "+_currentConfiguration.getRW3270Configuration(index).getUserName()+" on "+_currentConfiguration.getHostNameOrIpAddr());
returnConnection(connection);
}
} catch (Exception e) {
if (connection!=null)
returnBrokenConnection(connection);
_log.info("bad test connection for "+_currentConfiguration.getRW3270Configuration(index).getUserName()+" on "+_currentConfiguration.getHostNameOrIpAddr());
errors.add(_currentConfiguration.getMessage(RacfMessages.ERROR_CONNECTING, userNames[index], e.toString()));
}
}
};
}
for (Thread thread : threads)
thread.start();
for (Thread thread : threads)
thread.join();
if (errors.size()>0) {
StringBuffer buffer = new StringBuffer();
for (String error : errors) {
buffer.append("\n"+error.toString());
}
throw new ConnectorException(buffer.substring(1));
}
} catch (Exception ie) {
throw ConnectorException.wrap(ie);
} finally {
_semaphore.release();
_reapable = true;
}
}
}