Mercurial > hg > ltpda-connection-manager
view LTPDAConnectionManager.m @ 4:c706c10a76bd
Add help comments.
author | Daniele Nicolodi <daniele@science.unitn.it> |
---|---|
date | Mon, 24 May 2010 00:14:37 +0200 |
parents | d5fef23867bb |
children | 35f1cfcaa5a9 |
line wrap: on
line source
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() % RESET Resets the state of the connection manager. % % This static method removes the LTPDAConnectionManager instance data from % the appdata storage. Causes the reset of the credentials cache and the % removal of all the connections from the connection pool. rmappdata(0, LTPDAConnectionManager.appdataKey); end function key = appdataKey() % APPDATAKEY Returns the key used to store instance data in appdata. % % This is defined as static method, and not has an instance constant % property, to to be accessible by the reset static method. key = 'LTPDAConnectionManager'; end end % static methods methods function cm = LTPDAConnectionManager() % LTPDACONNECTIONMANAGER Manages credentials and database connections. % % This constructor returns an handler to a LTPDAConnectionManager class % instance. Database connections can be obtained trough the obtained % object with the connect() method. % % The purpose of this class it to keep track of open database connections % and to cache database credentials. It must be used in all LTPDA toolbox % functions that required to obtain database connections. Its behaviour can % be configured via LTPDA toolbox user preferences. The object status is % persisted trough the appdata matlab facility. % 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) % COUNT Returns the number of open connections in the connections pool. % % This method has the side effect of removing all closed connections from % the connections pool, so that the underlying objects can be garbage % collected. 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) % CLEAR Removes all cached credentials from the connection manager. cm.credentials = {}; end function conn = connect(cm, varargin) % CONNECT Uses provided credential to establish a database connection. % % CONNECT(hostname, database, username, password) Returns an object % implementing the java.sql.Connection interface handing a connection to % the specified database. Any of the parameter is optional. The user will % be queried for the missing information. % % The returned connection are added to a connections pool. When the number % of connections in the pool exceeds a configurable maximum, no more % connection are instantiated. Closed connections are automatically % removed from the pool. % % CONNECT(pl) Works as the above but the parameters are obtained from the % plist object PL. If the 'connection' parameter in the plist contains an % object implementing the java.sql.Connection interface, this object is % returned instead that opening a new connection. In this case the % connection in not added to the connection pool. 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) % CLOSE Forces connections to be closed. % % In the case bugs in other routines working with database connections % produce orphan connections, this method can be used to force the close % of those connections. % % CLOSE(ids) Closes the connections with the corresponding IDs in the % connections pool. If no ID is given all connections in the pool are % closed. if nargin < 2 ids = 1:numel(cm.connections); end cellfun(@close, cm.connections(ids)); end function add(cm, c) % ADD Adds credentials to the credentials cache. % % This method can be used to initialize or add to the cache, credentials % that will be used in subsequent connections attempts. This method accepts % only credentials in the form of utils.jmysql.credentials objects. % check input arguments if nargin < 2 || ~isa(c, 'credentials') error('### invalid call'); end % add to the cache cm.cacheCredentials(c); end end % methods methods(Access=private) function conn = getConnection(cm, varargin) % GETCONNECTION Where the implementation of the connect method really is. import utils.const.* % handle variable number of arguments 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) % FINDCREDENTIALSID Find credentials in the cache and returns their IDs. 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) % FINDCREDENTIALS Find credentials in the cache and returns them in a list. % default cred = []; % search ids = findCredentialsId(cm, varargin{:}); % return an array credentials if ~isempty(ids) cred = [ cm.credentials{ids} ]; end end function cacheCredentials(cm, c) % CACHECREDENTIALS Adds to or updates the credentials cache. 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) % INPUTCREDENTIALS Queries the user for database username and password. % this is a stubb that must be replaced by a graphical interface % 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) % SELECTDATABASE Makes the user choose to which database connect to. % this is a stubb that must be replaced by a graphical interface 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 function conn = connect(hostname, database, username, password) % CONNECT Opens a connection to the given database. % % This function returns a Java object implementing the java.sql.Connection % interface connected to the given database using the provided credentials. % If the connection fails because the given username and password pair is not % accepted by the server an utils:jmysql:connect:AccessDenied error is thrown. % this should become utils.jmysql.connect % 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