comparison m-toolbox/classes/@LTPDADatabaseConnectionManager/LTPDADatabaseConnectionManager.m @ 2:18e956c96a1b database-connection-manager

Add LTPDADatabaseConnectionManager implementation. Matlab code
author Daniele Nicolodi <nicolodi@science.unitn.it>
date Sun, 04 Dec 2011 21:23:09 +0100
parents
children e3c5468b1bfe
comparison
equal deleted inserted replaced
1:2014ba5b353a 2:18e956c96a1b
1 classdef LTPDADatabaseConnectionManager < handle
2
3 properties(SetAccess=private)
4
5 connections = {};
6 credentials = {};
7
8 p_credentialsExpiry = 3600;
9 p_cachePassword = 2;
10 p_maxConnectionsNumber = 10;
11
12 end % private properties
13
14 properties(Dependent=true)
15
16 credentialsExpiry; % seconds
17 cachePassword; % 0=no 1=yes 2=ask
18 maxConnectionsNumber;
19
20 end % dependent properties
21
22 methods(Static)
23
24 function reset()
25 % RESET Resets the state of the connection manager.
26 %
27 % This static method removes the LTPDADatabaseConnectionManager
28 % instance data from the appdata storage. Causes the reset of the
29 % credentials cache and the removal of all the connections from
30 % the connection pool.
31
32 rmappdata(0, LTPDADatabaseConnectionManager.appdataKey);
33 end
34
35
36 function key = appdataKey()
37 % APPDATAKEY Returns the key used to store instance data in appdata.
38 %
39 % This is defined as static method, and not has an instance constant
40 % property, to to be accessible by the reset static method.
41
42 key = 'LTPDADatabaseConnectionManager';
43 end
44
45 end % static methods
46
47 methods
48
49 function cm = LTPDADatabaseConnectionManager()
50 % LTPDACONNECTIONMANAGER Manages credentials and database connections.
51 %
52 % This constructor returns an handler to a LTPDADatabaseConnectionManager
53 % class instance. Database connections can be obtained trough the
54 % obtained object with the connect() method.
55 %
56 % The purpose of this class it to keep track of open database connections
57 % and to cache database credentials. It must be used in all LTPDA toolbox
58 % functions that required to obtain database connections. Its behaviour can
59 % be configured via LTPDA toolbox user preferences. The object status is
60 % persisted trough the appdata matlab facility.
61
62 % import credentials class
63 import utils.credentials
64
65 % load state from appdata
66 acm = getappdata(0, cm.appdataKey());
67
68 if isempty(acm)
69 % store state in appdata
70 setappdata(0, cm.appdataKey(), cm);
71
72 import utils.const.*
73 utils.helper.msg(msg.PROC1, 'new connection manager');
74 else
75 cm = acm;
76 end
77 end
78
79 function str = disp(cm)
80 disp(sprintf('%s()\n', class(cm)));
81 end
82
83
84 function val = get.credentialsExpiry(cm)
85 % obtain from user preferences
86 %p = getappdata(0, 'LTPDApreferences');
87 val = cm.p_credentialsExpiry;
88 end
89
90
91 function val = get.cachePassword(cm)
92 % obtain from user preferences
93 %p = getappdata(0, 'LTPDApreferences');
94 val = cm.p_cachePassword;
95 end
96
97
98 function val = get.maxConnectionsNumber(cm)
99 % obtain from user preferences
100 %p = getappdata(0, 'LTPDApreferences');
101 val = cm.p_maxConnectionsNumber;
102 end
103
104
105 function n = count(cm)
106 % COUNT Returns the number of open connections in the connections pool.
107 %
108 % This method has the side effect of removing all closed connections from
109 % the connections pool, so that the underlying objects can be garbage
110 % collected.
111
112 import utils.const.*
113
114 % find closed connections in the pool
115 mask = false(numel(cm.connections), 1);
116 for kk = 1:numel(cm.connections)
117 if cm.connections{kk}.isClosed()
118 utils.helper.msg(msg.PROC1, 'connection id=%d closed', kk);
119 mask(kk) = true;
120 end
121 end
122
123 % remove them
124 cm.connections(mask) = [];
125
126 % count remainig ones
127 n = numel(cm.connections);
128 end
129
130 function clear(cm)
131 % CLEAR Removes all cached credentials from the connection manager.
132 cm.credentials = {};
133 end
134
135
136 function conn = connect(cm, varargin)
137 % CONNECT Uses provided credential to establish a database connection.
138 %
139 % CONNECT(hostname, database, username, password) Returns an object
140 % implementing the java.sql.Connection interface handing a connection to
141 % the specified database. Any of the parameter is optional. The user will
142 % be queried for the missing information.
143 %
144 % The returned connection are added to a connections pool. When the number
145 % of connections in the pool exceeds a configurable maximum, no more
146 % connection are instantiated. Closed connections are automatically
147 % removed from the pool.
148 %
149 % CONNECT(pl) Works as the above but the parameters are obtained from the
150 % plist object PL. If the 'connection' parameter in the plist contains an
151 % object implementing the java.sql.Connection interface, this object is
152 % returned instead that opening a new connection. In this case the
153 % connection in not added to the connection pool.
154
155 import utils.const.*
156
157 % save current credentials cache
158 cache = cm.credentials;
159
160 % count open connections in the pool
161 count = cm.count();
162
163 % check parameters
164 if numel(varargin) == 1 && isa(varargin{1}, 'plist')
165
166 % extract parameters from plist
167 pl = varargin{1};
168
169 % check if we have a connection parameter
170 conn = find(pl, 'conn');
171 if ~isempty(conn)
172 % check that it implements java.sql.Connection interface
173 if ~isa(conn, 'java.sql.Connection')
174 error('### connection is not valid database connection');
175 end
176 % return this connection
177 return;
178 end
179
180 % otherwise
181 hostname = find(pl, 'hostname');
182 database = find(pl, 'database');
183 username = find(pl, 'username');
184 password = find(pl, 'password');
185
186 % if there is no hostname ignore other parameters
187 if ~ischar(hostname) || isempty(hostname)
188 varargin = {};
189 % if there is no database ignore other parameters
190 elseif ~ischar(database) || isempty(database)
191 varargin = {hostname};
192 % if there is no username ignore other parameters
193 elseif ~ischar(username) || isempty(username)
194 varargin = {hostname, database};
195 % password can not be null but can be an empty string
196 elseif ~ischar(password)
197 varargin = {hostname, database, username};
198 else
199 varargin = {hostname, database, username, password};
200 end
201 end
202
203 % check number of connections
204 if count > cm.maxConnectionsNumber
205 error('### too many open connections');
206 end
207
208 % connect
209 try
210 conn = cm.getConnection(varargin{:});
211
212 % add connection to pool
213 utils.helper.msg(msg.PROC1, 'add connection to pool');
214 cm.connections{end+1} = conn;
215
216 catch ex
217 % restore our copy of the credentials cache
218 utils.helper.msg(msg.PROC1, 'undo cache changes');
219 cm.credentials = cache;
220
221 % hide implementation details
222 ex.throw();
223 end
224 end
225
226
227 function close(cm, ids)
228 % CLOSE Forces connections to be closed.
229 %
230 % In the case bugs in other routines working with database connections
231 % produce orphan connections, this method can be used to force the close
232 % of those connections.
233 %
234 % CLOSE(ids) Closes the connections with the corresponding IDs in the
235 % connections pool. If no ID is given all connections in the pool are
236 % closed.
237
238 if nargin < 2
239 ids = 1:numel(cm.connections);
240 end
241 cellfun(@close, cm.connections(ids));
242
243 % remove closed connections from pool
244 cm.count();
245 end
246
247
248 function add(cm, c)
249 % ADD Adds credentials to the credentials cache.
250 %
251 % This method can be used to initialize or add to the cache, credentials
252 % that will be used in subsequent connections attempts. This method accepts
253 % only credentials in the form of utils.credentials objects.
254
255 % check input arguments
256 if nargin < 2 || ~isa(c, 'utils.credentials')
257 error('### invalid call');
258 end
259
260 % add to the cache
261 cm.cacheCredentials(c);
262 end
263
264 end % methods
265
266 methods(Access=private)
267
268 function conn = getConnection(cm, varargin)
269 % GETCONNECTION Where the implementation of the connect method really is.
270
271 import utils.const.*
272
273 % handle variable number of arguments
274 switch numel(varargin)
275 case 0
276 % find credentials
277 [hostname, database, username] = cm.selectDatabase([cm.credentials{:}]);
278 conn = cm.getConnection(hostname, database, username);
279
280 case 1
281 % find credentials
282 cred = cm.findCredentials(varargin{:});
283 if numel(cred) == 0
284 cred = utils.credentials(varargin{:});
285 end
286 [hostname, database, username] = cm.selectDatabase(cred);
287 conn = cm.getConnection(hostname, database, username);
288
289 case 2
290 % find credentials
291 cred = cm.findCredentials(varargin{:});
292 switch numel(cred)
293 case 0
294 conn = cm.getConnection(varargin{1}, varargin{2}, []);
295 case 1
296 conn = cm.getConnection(cred.hostname, cred.database, cred.username);
297 otherwise
298 [hostname, database, username] = cm.selectDatabase(cred);
299 conn = cm.getConnection(hostname, database, username);
300 end
301
302 case 3
303 % find credentials
304 cred = cm.findCredentials(varargin{1}, varargin{2}, varargin{3});
305 switch numel(cred)
306 case 0
307 % no credentials found
308 usernames = { varargin{3} };
309 if isempty(varargin{3})
310 % use usernames for same hostname
311 tmp = cm.findCredentials(varargin{1});
312 if ~isempty(tmp)
313 usernames = { tmp(:).username };
314 end
315 end
316 % build credentials objects
317 tmp = {};
318 for kk = 1:numel(usernames)
319 tmp{kk} = utils.credentials(varargin{1}, varargin{2}, usernames{kk});
320 end
321 % convert from cell array to array
322 cred = [tmp{:}];
323 case 1
324 % credentials in cache
325 utils.helper.msg(msg.PROC1, 'use cached credentials');
326 otherwise
327 % we should not have more than one credentials set
328 error('### more than one credentials set matching');
329 end
330
331 cache = false;
332 if (numel(cred) > 1) || ~cred.complete
333 % ask for password
334 [username, password, cache] = cm.inputCredentials(cred);
335
336 % cache credentials
337 cred = utils.credentials(varargin{1}, varargin{2}, username);
338 cm.cacheCredentials(cred);
339
340 % add password to credentials
341 cred.password = password;
342 end
343
344 % try to connect
345 try
346 conn = utils.mysql.connect(cred.hostname, cred.database, cred.username, cred.password);
347 catch ex
348 % look for access denied errors
349 if strcmp(ex.identifier, 'utils:mysql:connect:AccessDenied')
350 % ask for new new credentials
351 utils.helper.msg(msg.IMPORTANT, ex.message);
352 conn = cm.getConnection(varargin{1}, varargin{2}, varargin{3});
353 else
354 % error out
355 throw(ex);
356 end
357 end
358
359 % cache password
360 if cache
361 utils.helper.msg(msg.PROC1, 'cache password');
362 cm.cacheCredentials(cred);
363 end
364
365 case 4
366 % connect
367 conn = utils.mysql.connect(varargin{1}, varargin{2}, varargin{3}, varargin{4});
368
369 if cm.cachePassword == 1
370 % cache credentials with password
371 cred = utils.credentials(varargin{1}, varargin{2}, varargin{3}, varargin{4});
372 else
373 % cache credentials without password
374 cred = utils.credentials(varargin{1}, varargin{2}, varargin{3});
375 end
376 cm.cacheCredentials(cred);
377
378 otherwise
379 error('### invalid call')
380 end
381
382 end
383
384
385 function ids = findCredentialsId(cm, varargin)
386 % FINDCREDENTIALSID Find credentials in the cache and returns their IDs.
387
388 import utils.const.*
389 ids = [];
390
391 for kk = 1:numel(cm.credentials)
392 % invalidate expired passwords
393 if expired(cm.credentials{kk})
394 utils.helper.msg(msg.PROC1, 'cache entry id=%d expired', kk);
395 cm.credentials{kk}.password = [];
396 cm.credentials{kk}.expiry = 0;
397 end
398
399 % match input with cache
400 if match(cm.credentials{kk}, varargin{:})
401 ids = [ ids kk ];
402 end
403 end
404 end
405
406
407 function cred = findCredentials(cm, varargin)
408 % FINDCREDENTIALS Find credentials in the cache and returns them in a list.
409
410 % default
411 cred = [];
412
413 % search
414 ids = findCredentialsId(cm, varargin{:});
415
416 % return a credentials objects array
417 if ~isempty(ids)
418 cred = [cm.credentials{ids}];
419 end
420 end
421
422
423 function cacheCredentials(cm, c)
424 % CACHECREDENTIALS Adds to or updates the credentials cache.
425
426 import utils.const.*
427
428 % find entry to update
429 ids = findCredentialsId(cm, c.hostname, c.database, c.username);
430
431 % set password expiry time
432 if ischar(c.password)
433 c.expiry = double(time()) + cm.credentialsExpiry;
434 end
435
436 if isempty(ids)
437 % add at the end
438 utils.helper.msg(msg.PROC1, 'add cache entry %s', char(c));
439 cm.credentials{end+1} = c;
440 else
441 for id = ids
442 % update only if the cached informations are less than the one we have
443 if length(c) > length(cm.credentials{id})
444 utils.helper.msg(msg.PROC1, 'update cache entry id=%d %s', id, char(c));
445 cm.credentials{id} = c;
446 else
447 % always update expiry time
448 cm.credentials{id}.expiry = c.expiry;
449 end
450 end
451 end
452 end
453
454
455 function [username, password, cache] = inputCredentials(cm, cred)
456 % INPUTCREDENTIALS Queries the user for database username and password.
457
458 % build a cell array of usernames
459 users = {};
460 for id = 1:numel(cred)
461 if ~isempty(cred(id).username)
462 users = [ users { cred(id).username } ];
463 end
464 end
465 users = sort(unique(users));
466
467 parent = com.mathworks.mde.desk.MLDesktop.getInstance().getMainFrame();
468 dialog = javaObjectEDT('connectionmanager.CredentialsDialog', ...
469 parent, cred(1).hostname, cred(1).database, users, cm.cachePassword);
470 dialog.show();
471 if dialog.cancelled
472 throw(MException('utils:mysql:connect:UserCancelled', '### user cancelled'));
473 end
474 username = char(dialog.username);
475 password = char(dialog.password);
476 cache = logical(dialog.cache);
477 end
478
479
480 function [hostname, database, username] = selectDatabase(cm, credentials)
481 % SELECTDATABASE Makes the user choose to which database connect to.
482
483 parent = com.mathworks.mde.desk.MLDesktop.getInstance().getMainFrame();
484 dialog = javaObjectEDT('connectionmanager.DatabaseSelectorDialog', parent);
485 for c = credentials
486 dialog.add(c.hostname, c.database, c.username);
487 end
488 dialog.show();
489 if dialog.cancelled
490 throw(MException('utils:mysql:connect:UserCancelled', '### user cancelled'));
491 end
492 hostname = char(dialog.hostname);
493 database = char(dialog.database);
494 username = char(dialog.username);
495 if isempty(username)
496 username = [];
497 end
498 end
499
500 end % private methods
501
502 end % classdef