Mercurial > hg > ltpda-connection-manager
changeset 0:d5fef23867bb
First workig implementation.
author | Daniele Nicolodi <daniele@science.unitn.it> |
---|---|
date | Sun, 23 May 2010 10:51:35 +0200 |
parents | |
children | c4b57991935a |
files | LTPDAConnectionManager.m README credentials.m test_ltpda_connection_manager.m |
diffstat | 4 files changed, 1055 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/LTPDAConnectionManager.m Sun May 23 10:51:35 2010 +0200 @@ -0,0 +1,441 @@ +classdef LTPDAConnectionManager < handle + + properties(SetAccess=private) + + connections = {}; + credentials = {}; + + end % private properties + + properties(Dependent=true) + + credentialsExpiry; % seconds + cachePassword; % 0=no 1=yes 2=ask + maxConnectionsNumber; + + end % dependent properties + + methods(Static) + + function reset() + setappdata(0, LTPDAConnectionManager.appdataKey, []); + end + + function key = appdataKey() + % defined as static method to be acessible by the reset static method + key = 'LTPDAConnectionManager'; + end + + end % static methods + + methods + + function cm = LTPDAConnectionManager(pl) + + % load state from appdata + acm = getappdata(0, cm.appdataKey()); + + if isempty(acm) + % take those from user preferences + cm.credentials{end+1} = credentials('localhost', 'one'); + cm.credentials{end+1} = credentials('localhost', 'two', 'daniele'); + + % store state in appdata + setappdata(0, cm.appdataKey(), cm); + + import utils.const.* + utils.helper.msg(msg.PROC1, 'new connection manager'); + else + cm = acm; + end + end + + + function val = get.credentialsExpiry(cm) + % obtain from user preferences + p = getappdata(0, 'LTPDApreferences'); + val = p.cm.credentialsExpiry; + end + + + function val = get.cachePassword(cm) + % obtain from user preferences + p = getappdata(0, 'LTPDApreferences'); + val = p.cm.cachePassword; + end + + + function val = get.maxConnectionsNumber(cm) + % obtain from user preferences + p = getappdata(0, 'LTPDApreferences'); + val = p.cm.maxConnectionsNumber; + end + + + function n = count(cm) + import utils.const.* + % find closed connections in the pool + mask = false(numel(cm.connections), 1); + for kk = 1:numel(cm.connections) + if cm.connections{kk}.isClosed() + utils.helper.msg(msg.PROC1, 'connection id=%d closed', kk); + mask(kk) = true; + end + end + + % remove them + cm.connections(mask) = []; + + % count remainig ones + n = numel(cm.connections); + end + + + function clear(cm) + % remove all cached credentials + cm.credentials = {}; + end + + + function conn = connect(cm, varargin) + import utils.const.* + + % save current credentials cache + cache = cm.credentials; + + % count open connections in the pool + count = cm.count(); + + % check parameters + if numel(varargin) == 1 && isa(varargin{1}, 'plist') + + % extract parameters from plist + pl = varargin{1}; + + % check if we have a connection parameter + conn = find(pl, 'connection'); + if ~isempty(conn) + % check that it implements java.sql.Connection interface + if ~isa(conn, 'java.sql.Connection') + error('### connection is not valid database connection'); + end + % return this connection + return; + end + + % otherwise + hostname = find(pl, 'hostname'); + database = find(pl, 'database'); + username = find(pl, 'username'); + password = find(pl, 'password'); + + % if there is no hostname and database ignore other parameters + if ~ischar(hostname) || ~ischar(database) + varargin = {}; + end + % password can not be null but can be an empty string + if ~ischar(password) + varargin = {hostname, database, username}; + else + varargin = {hostname, database, username, password}; + end + end + + % check number of connections + if count > cm.maxConnectionsNumber + error('### too many open connections'); + end + + % connect + try + conn = cm.getConnection(varargin{:}); + catch ex + % restore our copy of the credentials cache + utils.helper.msg(msg.PROC1, 'undo cache changes'); + cm.credentials = cache; + + % hide implementation details + %ex.throwAsCaller(); + ex.rethrow() + end + end + + + function close(cm, ids) + if nargin < 2 + ids = 1:numel(cm.connections); + end + cellfun(@close, cm.connections(ids)); + end + + + function add(cm, c) + if nargin < 2 || ~isa(c, 'credentials') + error('### invalid call'); + end + cm.cacheCredentials(c); + end + + end % methods + + methods(Access=private) + + function conn = getConnection(cm, varargin) + + import utils.const.* + + switch numel(varargin) + case 0 + [hostname, database, username] = cm.selectDatabase(); + conn = cm.getConnection(hostname, database, username); + + case 2 + conn = cm.getConnection(varargin{1}, varargin{2}, []); + + case 3 + % find credentials + cred = cm.findCredentials(varargin{1}, varargin{2}, varargin{3}); + if isempty(cred) + % no credentials found + cred = credentials(varargin{1}, varargin{2}, varargin{3}); + else + utils.helper.msg(msg.PROC1, 'use cached credentials'); + end + + cache = false; + if numel(cred) > 1 || ~cred.complete + % ask for which username and password to use + [username, password, cache] = cm.inputCredentials(cred); + + % cache credentials + cred = credentials(varargin{1}, varargin{2}, username); + cm.cacheCredentials(cred); + + % add password to credentials + cred.password = password; + end + + % try to connect + conn = cm.getConnection(cred.hostname, cred.database, cred.username, cred.password); + + % cache password + if cache + utils.helper.msg(msg.PROC1, 'cache password'); + cm.cacheCredentials(cred); + end + + case 4 + try + % connect + conn = connect(varargin{1}, varargin{2}, varargin{3}, varargin{4}); + + % cache credentials without password + cred = credentials(varargin{1}, varargin{2}, varargin{3}, []); + cm.cacheCredentials(cred); + + catch ex + % look for access denied errors + if strcmp(ex.identifier, 'utils:jmysql:connect:AccessDenied') + % ask for new new credentials + utils.helper.msg(msg.PROC1, ex.message); + conn = cm.getConnection(varargin{1}, varargin{2}, varargin{3}); + else + % error out + throw(MException('', '### connection error').addCause(ex)); + end + end + + % add connection to pool + utils.helper.msg(msg.PROC1, 'add connection to pool'); + cm.connections{end+1} = conn; + + otherwise + error('### invalid call') + end + + end + + + function ids = findCredentialsId(cm, varargin) + import utils.const.* + ids = []; + + for kk = 1:numel(cm.credentials) + % invalidate expired passwords + if expired(cm.credentials{kk}) + utils.helper.msg(msg.PROC1, 'cache entry id=%d expired', kk); + cm.credentials{kk}.password = []; + cm.credentials{kk}.expiry = 0; + end + + % match input with cache + if match(cm.credentials{kk}, varargin{:}) + ids = [ ids kk ]; + end + end + end + + + function cred = findCredentials(cm, varargin) + % default + cred = []; + + % search + ids = findCredentialsId(cm, varargin{:}); + + % return an array credentials + if ~isempty(ids) + cred = [ cm.credentials{ids} ]; + end + end + + + function cacheCredentials(cm, c) + import utils.const.* + + % find entry to update + id = findCredentialsId(cm, c.hostname, c.database, c.username); + + % sanity check + if numel(id) > 1 + error('### more than one cache entry for %s', char(c, 'short')); + end + + % set password expiry time + if ischar(c.password) + c.expiry = double(time()) + cm.credentialsExpiry; + end + + if isempty(id) + % add at the end + utils.helper.msg(msg.PROC1, 'add cache entry %s', char(c)); + cm.credentials{end+1} = c; + else + % update only if the cached informations are less than the one we have + if ~complete(cm.credentials{id}) + utils.helper.msg(msg.PROC1, 'update cache entry id=%d %s', id, char(c)); + cm.credentials{id} = c; + else + % always update expiry time + cm.credentials{id}.expiry = c.expiry; + end + end + end + + + function [username, password, cache] = inputCredentials(cm, cred) + % this is a stubb + + % build a cell array of usernames and passwords + users = { cred(:).username }; + passw = { cred(:).password }; + + % default to the latest used username + [e, ids] = sort([ cred(:).expiry ]); + default = users{ids(1)}; + + username = choose('Username', users, default); + + % pick the corresponding password + ids = find(strcmp(users, username)); + if ~isempty(ids) + default = passw{ids(1)}; + else + default = []; + end + + password = ask('Password', ''); + + if cm.cachePassword == 2 + cache = ask('Store credentials', 'n'); + if ~isempty(cache) && cache(1) == 'y' + cache = true; + else + cache = false; + end + else + cache = logical(cm.cachePassword); + end + end + + + function [hostname, database, username] = selectDatabase(cm) + % this is a stubb + + for kk = 1:numel(cm.credentials) + fprintf('% 2d. %s\n', char(cm.credentials{kk})); + end + fprintf('%d. NEW (default)\n', numel(cm.credentials)+1); + str = input('Select connection: ', 's'); + if isempty(str) + id = numel(cm.credentials)+1; + else + id = eval(str); + end + if id > numel(cm.credentials) + hostname = input('Hostname: ', 's'); + database = input('Database: ', 's'); + username = []; + else + hostname = cm.credentials{kk}.hostname; + database = cm.credentials{kk}.database; + username = cm.credentials{kk}.username; + end + end + + end % private methods + +end % classdef + + +% this should become utils.jmysql.connect +function conn = connect(hostname, database, username, password) + + % informative message + import utils.const.* + utils.helper.msg(msg.PROC1, 'connection to mysql://%s/%s username=%s', hostname, database, username); + + % connection credential + uri = sprintf('jdbc:mysql://%s/%s', hostname, database); + db = javaObject('com.mysql.jdbc.Driver'); + pl = javaObject('java.util.Properties'); + pl.setProperty(db.USER_PROPERTY_KEY, username); + pl.setProperty(db.PASSWORD_PROPERTY_KEY, password); + + try + % connect + conn = db.connect(uri, pl); + catch ex + % haven't decided yet if this code should be here or higher in the stack + if strcmp(ex.identifier, 'MATLAB:Java:GenericException') + % exceptions handling in matlab sucks + if ~isempty(strfind(ex.message, 'java.sql.SQLException: Access denied')) + throw(MException('utils:jmysql:connect:AccessDenied', '### access denied').addCause(ex)); + end + end + rethrow(ex); + end +end + + +function str = ask(msg, default) + str = input(sprintf('%s (default: %s): ', msg, default), 's'); + if isempty(str) + str = default; + end + if ~ischar(str) + str = char(str); + end +end + +function str = choose(msg, choices, default) + options = sprintf('%s, ', choices{:}); + options = options(1:end-2); + str = input(sprintf('%s (options: %s, default: %s): ', msg, options, default), 's'); + if isempty(str) + str = default; + end + if ~ischar(str) + str = char(str); + end +end
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/README Sun May 23 10:51:35 2010 +0200 @@ -0,0 +1,394 @@ +* Requirements + + 1. Provide an interface for the user to insert connection + credentials: server hostname, database name, user name and + password. + + 2. Avoid that the user has to enter those information too often, + like when retrieving many objects from the repository. This is + done caching the credentials for a configurable amount of time + + 3. Provide understandable error messages. + + 4. Track open connections, and avoid excessive proliferation of open + connections to the database, due to miss behaving user functions. + + This is all about managing connections, so I would propose to call + this component LTPDAConnectionManager. + + +* Interface definition + +** LTPDAConnectionManager() + + Isntantiates a singleton class managing credentials and connections. + + Credentials are cached. Credentials, except password are cached + forever, passwords are cached for a configurable amount of time. + + Connections are tracked putting each handed out connection into a + list. When a new connection is requested, or when requested via a + dedicated method, the connection list is walked and closed + connections are removed from it. When the connection number exceeds + a configurable maximum, no more connection are instantiated. + + The singleton clas is implemented as an handle matlab class that + stores an handle to itself in appdata storage. When the class is + instantiates it returns an handle to the copy referenced in + appdata. + +*** appdataKey + + Static mehod that returns the key used to store the handle to + instance data in appdata. + +*** credentialsExpiry + + Property. Taken from user preferences. Time after which the + credentials are not valid anymore. + +*** maxConnectionsNumber + + Property. Taken from user preferences. The maximum number of + connections that can be open at the same time toward the same + host. + +*** cachePassword + + Property. Taken from user preferences. Defines if password should + be cached along with other database access credentials. It may + take the values: + + 0. passwords are not cached, + 1. passwords are always cached, + 2. ask if the users desires to cache the given password. + +*** connect(hostname, database, username, password) + + Try to connect to the database with the given credentials. If the + connection attempt fails with an 'access denied' error, prompt the + user for username using the given username as default, and + password. Present the option to cache password. Cache username and + password accordingly to user choice. Do not cache passwords by + default. + + Return an object implementing the java.sql.Connection interface, + representing a connection to the given database, with the given + credentials. + + Connection errors, other than access denied are reported as + fatal exceptions. User hitting the cancel button on input forms is + reported as an 'user canceled' exception. + +*** connect(hostname, database, username) + + Search the credentials cache for user password. If it is not + found, or the connection attempt fails with an access denied + error, prompt the user for username, using the given username as + default, and password. Present the option to cache password. Try + to connect to the database with the given credentials. When the + connections succeeds, cache username and password accordingly to + user choice. + + Return an object implementing the java.sql.Connection interface, + representing a connection to the given database, with the given + credentials. + + Errors, other than acces denied, are reported as fatal + exceptions. User hitting the cancel button on input forms is + reported as an 'user canceled' exception. Access denied errors are + catched and credentials are asked again. + +*** connect(hostname, database) + + Search the credentials cache looking for usernames and passwords + for accessing the given database. If credentials are not found, + there is more than one set of credentials matching, or the + connection attempt fails with an 'acces denied' error, prompt the + user for username and password. Present the option to cache + password. When the connections succeeds, cache username and + password accordingly to user choice. + + Return an object implementing the java.sql.Connection interface, + representing a connection to the given database, with the given + credentials. + + Errors, other than acces denied, are reported as fatal + exceptions. User hitting the cancel button on input forms is + reported as an 'user canceled' exception. Access denied errors are + catched and credentials are asked again. + +*** connect() + + Make the user choose between a list of hostname-database-username + tuples obtained from the credentials cache, and the possibility of + entering a hostname and a database. Then proceed as in the case of + the call to connect(hostname, database). + + The list of hostname-database pairs can be initialized with data + from the LTPDA toolbox preferences or witj the add() method. + +*** connect(plist) + + Same as the previous calls but taking parameters from PLIST. + + If PLIST has a 'connection' parameter, and if this is a valid java + object, implementing the java.sql.Connection, return it instead of + creating a new connection. If the object does not fulfill the + requirements throw a meaningful error. + +*** count() + + Return the number of open connections in the connection pool. It + has the side effect of removing from the pool any close connection. + +*** close(ids) + + Close connections with the given IDs in the connections pool. If no + ID is given close all connection. + +*** clear() + + Clear credentials cache. + +*** add(credentials) + + Add the given credentials to the cache. + + +** Utilities + +*** utils.jmysql + + Collection of utility functions for interfacing to a database from + a pure matlab environment. Except connect() all the functions take + a java object implementing java.sql.Connection as first parameter. + +*** utils.jmysql.connect(hostname, database, username, password) + + Return an object implementing the java.sql.Connection interface, + representing a connection to the given database, with the given + credentials. + + Throw an error if the connection fails. + + This function can be used when the programmer wants full controll + on the connection, stepping aside of the connection manager. + + Should not be used in toolbox functions or by the toolbox users. + +*** utils.jmysql.execute(conn, query, varargin) + + Execute the query QUERY, with the parameters specified in VARARGIN + through the connection CONN. Returns the results in a 2d cell + array. See the actual implementation. + + QUERY is a string, CONN is a java object implementing + java.sql.Connection, VARARGIN can contain any base matlab type. we + can think about introducing the marshalling for time() objects + into SQL strings. + + See current implementation in CVS. + +*** plist.REPOSITORY_CREDENTIALS_PLIST + + Static method that returns a default plist that specifies the + credentials required to connect to a repository. Should be + combined in the default plist of the ltpda methods that require + a connection to the database. Those parameters are: + + hostname - Server hostname + database - Database name + username - User login name + password - Password + + In addition to those any ltpda method requiring a database + connection should accept a 'connection' parameter in his plist. An + open connection can be provided though this parameter. The caller + is responsible of closing the connection once it is done with it. + + +* Examples + + function out = useless(a, b, plist) + + % obtain a database connection + cm = LTPDAConnectionManager(); + conn = cm.connect(pl) + + % ask the database to sum A and B + out = utils.jmysql.execute(conn, 'SELECT ?+?', a, b); + + % out is a cell array containing A + B + disp out{1}; + + % check who is in charge of the connection + if conn ~= find(pl, 'connection') + % close connection + conn.close() + end + + end + + This function can be used as follows: + + - useless(1, 2) + + Will ask the user for hostname, database, username, password. + + - useless(1, 2, plist('hostname', 'localhost', 'database', 'test')) + + Will ask the user for username and password. + + - useless(1, 2, plist('hostname', 'localhost', 'database', 'test', 'username', 'daniele')) + + Will ask the user for password. + + - useless(1, 2, plist('hostname', 'localhost', 'database', 'test', 'username', 'daniele', 'password', 'passwd')) + + Will not ask the user. + + - conn = utils.jmysql.conn('localhost', 'test', 'daniele', 'passwd') + useless(1, 2, plist('connection', conn)); + useless(3, 4, plist('connection', conn)); + conn.close(); + + Anything is asked to the user. The connection is provided by the + user and should not be closed inside the function. This is + usefull for continuously running automated processes. + + + This code is a stupid example of connections not being closed properly: + + pl = plist('hostname', 'localhost', 'database', 'test', 'username', 'daniele', 'password', 'passwd'); + cm = LTPDAConnectionManager(); + cm.maxConnectionsNumber = 20; + for kk = 1:100 + conn{kk} = cm.connect(pl); + end + + It should fail at iteration 21 with an error similat to + + ### too many connections open to 'localhost' + + +* Use cases + +** Interactive usage + + A connection can be obtained from the connection manager in the + following ways, requiring user interaction: + + 1. Open a connection without providing any credential. + + A. If there are cached credentials show a list of (hostname, + database, username) touple, give the possibiity to chose between + any of those and creating a new one. + + B. If there are no cached credentials for the (hostname, + database, username) touple, or the user choses 'new' in the + previeus step, a dialog box asks the user for hostname and + database. The hostname field is an editable drop down lists + populated with data from the user preferences and cached + credentials. ADVANCED: the database filed is an editable drop + down list, when the user presses the drop down button, a list of + databases is fetch from the server. + + C. If there are cached credentials for the (hostname, database, + username) touple go to E. + + D. A dialog box asks the user for username and password and + gives the option to remember credentials. Username field is an + editable drop down populated with usernames from the credentials + cache, the username last entered username this (hostname, + database) pair is the default. The provided username is cached + into the credential cache. + + E. Connection to the database it attempt. If the connection + fails go back to C. If the connection succeds and the user + selected the 'store credentials' options save the password in + the credentials cache. + + F. Return the connection to the caller. + + 2. Open a connection providing hostname and database. + + A. If there are cached credentials go to 1.E. + + B. If there are no cached credentials go to 1.D. + + 3. Open a connection providing hostname, database, username. + + A. If there are cached credentials for the (hostname, database, + username) touple go to 1.E. + + A. If there are no cached credentials for the (hostname, database, + username) touple go to 1.D. + + 4. Open a connection providing hostname, database, username, password. + + A. Go to 1.E. + +** Non interactive usage + + A connection can be obtained without requiring user interaction in + the following ways: + + 1. Known good credendials can be provided to the connection manager + connect() method. If called with four arguments it will not + require user interaction if it connects successfully to the + database. + + 2. Known good credentials can be added to the cache with the connection + manager add() method. Any call to the connection manager + connect() method specifying at least hostname and database + matching the ones inserted in the cache, will then use the + cached credentials. User interaction is required if the + connection fails. + + 3. A connection can be obtained with the utils.jmysql.connect() + function and given to the ltpda methods requiring a database + connection with the 'connection' plist parameter. In this way + the caller has full control on the connection. + +** Functions chaining + + In the case where a procedure needs to call several ltpda methods + requiring database access, it is recommended that the connection is + created in the outermost function, and passed to called functions + using the 'connection' plist parameter. + + This avoids to query the user for connection credentials more than + once during the executing of the same procedure, even when password + caching is disabled. It also avoids the performance penalty of + connecting multiple times to the server. + +** Multiple users scenario + + Consider the case where two users are sharing the same matlab + instance. The two users must be able to interleave the creation of + database connections, each one with his own credentials. This can + be accomplished in different ways: + + 1. If the interleaving of the operations of two users is frequent, + the connection manager can be configured, via the user + preferences, to never cache passwords. The users will be always + queried for their credentials. + + If procedures are well written, each one should not require to + enter user credentials more than once. + + 2. If the interleaving of the two users is not so frequent, the + connection manager can be configured, via the user preferences, + to cache passwords for a short time. + + 3. The connection manager can be tricked into creating multiple + user profiles for the same database connection. For example: + + cm = LTPDAConnectionManager(); + cm.add(credentials('host', 'db', 'user1'); + cm.add(credentials('host', 'db', 'user2'); + + Password caching can be enabled. The connection manager will + make the user decide which credentials to use. However the use + of the user own credentials is not enforced.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/credentials.m Sun May 23 10:51:35 2010 +0200 @@ -0,0 +1,109 @@ +classdef credentials + properties + + hostname = []; + database = []; + username = []; + password = []; + expiry = 0; + + end % properties + + methods + + % contructor + function obj = credentials(hostname, database, username, password) + switch nargin + case 1 + obj.hostname = hostname; + case 2 + obj.hostname = hostname; + obj.database = database; + case 3 + obj.hostname = hostname; + obj.database = database; + obj.username = username; + case 4 + obj.hostname = hostname; + obj.database = database; + obj.username = username; + obj.password = password; + end + end + + % convert to string representation + function str = char(obj, mode) + if nargin < 2 + mode = ''; + end + switch mode + case 'short' + % do not show password + frm = 'mysql://%s/%s username=%s'; + str = sprintf(frm, obj.hostname, obj.database, obj.username); + case 'full' + % show password + frm = 'mysql://%s/%s username=%s password=%s'; + str = sprintf(frm, obj.hostname, obj.database, obj.username, obj.password); + otherwise + % by default only show if a password is known + passwd = []; + if ischar(obj.password) + passwd = 'YES'; + end + frm = 'mysql://%s/%s username=%s password=%s'; + str = sprintf(frm, obj.hostname, obj.database, obj.username, passwd); + end + end + + % display + function disp(obj) + disp([' ' char(obj) char(10)]); + end + + % check that a credentials object contails all the required informations + function rv = complete(obj) + info = {'hostname', 'database', 'username', 'password'}; + for kk = 1:numel(info) + if isempty(obj.(info{kk})) + rv = false; + return; + end + end + rv = true; + end + + % check if the credentials are expired + function rv = expired(obj) + rv = false; + if obj.expiry > 0 && double(time()) > obj.expiry + rv = true; + end + end + + % check if the credentials object matches the given informations + function rv = match(obj, hostname, database, username) + if nargin < 4 + username = []; + end + + % default value + rv = true; + + if ~strcmp(obj.hostname, hostname) + rv = false; + return; + end + if ~strcmp(obj.database, database) + rv = false; + return; + end + if ischar(username) && ischar(obj.username) && ~strcmp(obj.username, username) + rv = false; + return; + end + end + + end % methods + +end \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test_ltpda_connection_manager.m Sun May 23 10:51:35 2010 +0200 @@ -0,0 +1,111 @@ +function test_ltpda_connection_manager() + + % change with valid credentials + host = '127.0.0.1'; % sometimes using localhost causes problems + db = 'one'; + user = 'daniele'; + passwd = 'daniele'; + + % hack user properties + p = getappdata(0, 'LTPDApreferences'); + p.cm.credentialsExpiry = 120; % seconds + p.cm.cachePassword = 2; % 0=no 1=yes 2=ask + p.cm.maxConnectionsNumber = 10; + setappdata(0, 'LTPDApreferences', p); + + + % reset + setappdata(0, 'LTPDAConnectionManager', []); + % connection manager + cm = LTPDAConnectionManager(); + + + % add credentials + n = numel(cm.credentials); + cm.add(credentials(host, 'db', 'user', 'passwd')); + assert(numel(cm.credentials) == n+1); + + % try again + cm.add(credentials(host, 'db', 'user', 'passwd')); + % just updated previous entry + assert(numel(cm.credentials) == n+1); + + % add other credentials + cm.add(credentials('host', 'db')); + assert(numel(cm.credentials) == n+2); + + % add user + cm.add(credentials('host', 'db', 'user')); + assert(numel(cm.credentials) == n+2); + + % add password + cm.add(credentials('host', 'db', 'user', 'passwd')); + assert(numel(cm.credentials) == n+2); + + % add the oned will be used now on + cm.add(credentials(host, db, user, passwd)); + assert(numel(cm.credentials) == n+3); + + + % use the credentials cache + c = cm.connect(host, db); + % one connection in the pool + assert(cm.count() == 1); + c.close(); + % still there + assert(numel(cm.connections) == 1); + n = cm.count(); + % count has the side effect of removing closed connections from the pool + assert(n == 0); + + % open two connections + c1 = cm.connect(host, db); + c2 = cm.connect(host, db, user); + % one connection in the pool + assert(numel(cm.connections) == 2); + % even when we remove the closed ones + assert(cm.count() == 2); + % clean up + c1.close(); + c2.close(); + % no more connections in the pool + assert(cm.count() == 0); + assert(numel(cm.connections) == 0); + + % plist parameters + c = cm.connect(plist('hostname', host, 'database', db)); + % one more connection in the pool + assert(numel(cm.connections) == 1); + % clen up + c.close(); + + % create a connection + c1 = cm.connect(host, db); + n = cm.count(); + % specify it as connction parameter + c2 = cm.connect(plist('connection', c1)); + % we should get back the same connection + assert(c2 == c1); + % it should not have been added to the connection pool + assert(numel(cm.connections) == n); + % clean up + c1.close(); + + % no connections in the pool + assert(cm.count() == 0); + assert(numel(cm.connections) == 0); + + % open a bunch of connections + c = cm.connect(host, db); + c = cm.connect(host, db); + c = cm.connect(host, db); + c = cm.connect(host, db); + c = cm.connect(host, db); + assert(numel(cm.connections) == 5); + assert(cm.count() == 5); + % close them all + cm.close(); + % check + assert(cm.count() == 0); + +end