1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package edu.internet2.middleware.shibboleth.common.attribute.resolver.provider.dataConnector;
18
19 import java.lang.ref.SoftReference;
20 import java.sql.Connection;
21 import java.sql.DatabaseMetaData;
22 import java.sql.ResultSet;
23 import java.sql.ResultSetMetaData;
24 import java.sql.SQLException;
25 import java.util.Collection;
26 import java.util.HashMap;
27 import java.util.Map;
28
29 import javax.sql.DataSource;
30
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
33 import org.springframework.context.ApplicationEvent;
34 import org.springframework.context.ApplicationListener;
35
36 import edu.internet2.middleware.shibboleth.common.attribute.BaseAttribute;
37 import edu.internet2.middleware.shibboleth.common.attribute.provider.BasicAttribute;
38 import edu.internet2.middleware.shibboleth.common.attribute.resolver.AttributeResolutionException;
39 import edu.internet2.middleware.shibboleth.common.attribute.resolver.provider.ShibbolethResolutionContext;
40 import edu.internet2.middleware.shibboleth.common.session.LogoutEvent;
41
42
43
44
45 public class RDBMSDataConnector extends BaseDataConnector implements ApplicationListener {
46
47
48 public static enum DATA_TYPES {
49 BigDecimal, Boolean, Byte, ByteArray, Date, Double, Float, Integer, Long, Object, Short, String, Time, Timestamp, URL
50 };
51
52
53 private final Logger log = LoggerFactory.getLogger(RDBMSDataConnector.class);
54
55
56 private boolean initialized;
57
58
59 private DataSource dataSource;
60
61
62 private boolean readOnlyConnection;
63
64
65 private boolean usesStoredProcedure;
66
67
68 private boolean noResultIsError;
69
70
71 private boolean cacheResults;
72
73
74 private String queryTemplateName;
75
76
77 private String queryTemplate;
78
79
80 private Map<String, RDBMSColumnDescriptor> columnDescriptors;
81
82
83 private Map<String, Map<String, SoftReference<Map<String, BaseAttribute>>>> resultsCache;
84
85
86 private TemplateEngine queryCreator;
87
88
89
90
91
92
93
94 public RDBMSDataConnector(DataSource source, boolean resultCaching) {
95 super();
96 initialized = false;
97 dataSource = source;
98 cacheResults = resultCaching;
99 usesStoredProcedure = false;
100 columnDescriptors = new HashMap<String, RDBMSColumnDescriptor>();
101 }
102
103
104 public void initialize() {
105 registerTemplate();
106 if (cacheResults) {
107 resultsCache = new HashMap<String, Map<String, SoftReference<Map<String, BaseAttribute>>>>();
108 }
109 initialized = true;
110 }
111
112
113
114
115
116
117 public boolean isConnectionReadOnly() {
118 return readOnlyConnection;
119 }
120
121
122
123
124
125
126 public void setConnectionReadOnly(boolean isReadOnly) {
127 readOnlyConnection = isReadOnly;
128 }
129
130
131
132
133
134
135 public boolean getUsesStoredProcedure() {
136 return usesStoredProcedure;
137 }
138
139
140
141
142
143
144 public void setUsesStoredProcedure(boolean storedProcedure) {
145 usesStoredProcedure = storedProcedure;
146 }
147
148
149
150
151
152
153 public boolean isNoResultIsError() {
154 return noResultIsError;
155 }
156
157
158
159
160
161
162 public void setNoResultIsError(boolean b) {
163 noResultIsError = b;
164 }
165
166
167
168
169
170
171 public boolean getCacheResults() {
172 return cacheResults;
173 }
174
175
176
177
178
179
180 public TemplateEngine getTemplateEngine() {
181 return queryCreator;
182 }
183
184
185
186
187
188
189 public void setTemplateEngine(TemplateEngine engine) {
190 queryCreator = engine;
191 registerTemplate();
192 }
193
194
195
196
197
198
199 public String getQueryTemplate() {
200 return queryTemplate;
201 }
202
203
204
205
206
207
208 public void setQueryTemplate(String template) {
209 queryTemplate = template;
210 }
211
212
213
214
215
216
217
218 public Map<String, RDBMSColumnDescriptor> getColumnDescriptor() {
219 return columnDescriptors;
220 }
221
222
223 public void onApplicationEvent(ApplicationEvent event) {
224 if (event instanceof LogoutEvent) {
225 LogoutEvent logoutEvent = (LogoutEvent) event;
226 if (cacheResults) {
227 resultsCache.remove(logoutEvent.getUserSession().getPrincipalName());
228 }
229 }
230 }
231
232
233 public Map<String, BaseAttribute> resolve(ShibbolethResolutionContext resolutionContext)
234 throws AttributeResolutionException {
235 String query = queryCreator.createStatement(queryTemplateName, resolutionContext, getDependencyIds(), null);
236 log.debug("RDBMS data connector {} - Search Query: {}", getId(), query);
237
238 Map<String, BaseAttribute> resolvedAttributes = null;
239 resolvedAttributes = retrieveAttributesFromCache(resolutionContext.getAttributeRequestContext()
240 .getPrincipalName(), query);
241
242 if (resolvedAttributes == null) {
243 resolvedAttributes = retrieveAttributesFromDatabase(query);
244 }
245
246 if (cacheResults) {
247 Map<String, SoftReference<Map<String, BaseAttribute>>> individualCache = resultsCache.get(resolutionContext
248 .getAttributeRequestContext().getPrincipalName());
249 if (individualCache == null) {
250 individualCache = new HashMap<String, SoftReference<Map<String, BaseAttribute>>>();
251 resultsCache.put(resolutionContext.getAttributeRequestContext().getPrincipalName(), individualCache);
252 }
253 individualCache.put(query, new SoftReference<Map<String, BaseAttribute>>(resolvedAttributes));
254 }
255
256 return resolvedAttributes;
257 }
258
259
260 public void validate() throws AttributeResolutionException {
261 log.debug("RDBMS data connector {} - Validating configuration.", getId());
262
263 if(dataSource == null){
264 log.error("RDBMS data connector {} - Datasource is null", getId());
265 throw new AttributeResolutionException("Datasource is null");
266 }
267
268 Connection connection = null;
269 try {
270 connection = dataSource.getConnection();
271 if (connection == null) {
272 log.error("RDBMS data connector {} - Unable to create connections", getId());
273 throw new AttributeResolutionException("Unable to create connections for RDBMS data connector "
274 + getId());
275 }
276
277 DatabaseMetaData dbmd = connection.getMetaData();
278 if (!dbmd.supportsStoredProcedures() && usesStoredProcedure) {
279 log.error("RDBMS data connector {} - Database does not support stored procedures.", getId());
280 throw new AttributeResolutionException("Database does not support stored procedures.");
281 }
282
283 log.debug("RDBMS data connector {} - Connector configuration is valid.", getId());
284 } catch (SQLException e) {
285 if (e.getSQLState() != null) {
286 log.error("RDBMS data connector {} - Invalid connector configuration; SQL state: {}, SQL Code: {}",
287 new Object[] { getId(), e.getSQLState(), e.getErrorCode() }, e);
288 } else {
289 log.error("RDBMS data connector {} - Invalid connector configuration", new Object[] { getId() }, e);
290 }
291 throw new AttributeResolutionException("Invalid connector configuration", e);
292 } finally {
293 try {
294 if (connection != null && !connection.isClosed()) {
295 connection.close();
296 }
297 } catch (SQLException e) {
298 if (e.getSQLState() != null) {
299 log.error(
300 "RDBMS data connector {} - Error closing database connection; SQL State: {}, SQL Code: {}",
301 new Object[] { getId(), e.getSQLState(), e.getErrorCode() }, e);
302 } else {
303 log.error("RDBMS data connector {} - Error closing database connection", new Object[] { getId() },
304 e);
305 }
306 }
307 }
308 }
309
310
311 public void clearCache() {
312 if (initialized && cacheResults) {
313 resultsCache.clear();
314 }
315 }
316
317
318 protected void registerTemplate() {
319 queryTemplateName = "shibboleth.resolver.dc." + getId();
320 queryCreator.registerTemplate(queryTemplateName, queryTemplate);
321 }
322
323
324
325
326
327
328
329
330
331
332
333 protected Map<String, BaseAttribute> retrieveAttributesFromCache(String princpal, String query)
334 throws AttributeResolutionException {
335 if (!cacheResults) {
336 return null;
337 }
338
339 Map<String, SoftReference<Map<String, BaseAttribute>>> queryCache = resultsCache.get(princpal);
340 if (queryCache != null) {
341 SoftReference<Map<String, BaseAttribute>> cachedAttributes = queryCache.get(query);
342 if (cachedAttributes != null) {
343 log.debug("RDBMS data connector {} - Fetched attributes from cache for principal {}", getId(),
344 princpal);
345 return cachedAttributes.get();
346 }
347 }
348
349 return null;
350 }
351
352
353
354
355
356
357
358
359
360
361
362 protected Map<String, BaseAttribute> retrieveAttributesFromDatabase(String query)
363 throws AttributeResolutionException {
364 Map<String, BaseAttribute> resolvedAttributes;
365 Connection connection = null;
366 ResultSet queryResult = null;
367
368 try {
369 connection = dataSource.getConnection();
370 if (readOnlyConnection) {
371 connection.setReadOnly(true);
372 }
373 log.debug("RDBMS data connector {} - Querying database for attributes with query {}", getId(), query);
374 queryResult = connection.createStatement().executeQuery(query);
375 resolvedAttributes = processResultSet(queryResult);
376 if (resolvedAttributes.isEmpty() && noResultIsError) {
377 log.error("RDBMS data connector {} - No attribtues from query", getId());
378 throw new AttributeResolutionException("No attributes returned from query");
379 }
380 log.debug("RDBMS data connector {} - Retrieved attributes: {}", getId(), resolvedAttributes.keySet());
381 return resolvedAttributes;
382 } catch (SQLException e) {
383 if (e.getSQLState() != null) {
384 log.error("RDBMS data connector {} - Unable to execute SQL query {}; SQL State: {}, SQL Code: {}",
385 new Object[] { getId(), query, e.getSQLState(), e.getErrorCode(), }, e);
386 } else {
387 log.error("RDBMS data connector {} - Unable to execute SQL query {}", new Object[] { getId(), query },
388 e);
389 }
390 throw new AttributeResolutionException("Unable to execute SQL query", e);
391 } finally {
392 try {
393 if (queryResult != null) {
394 queryResult.close();
395 }
396
397 if (connection != null && !connection.isClosed()) {
398 connection.close();
399 }
400
401 } catch (SQLException e) {
402 if (e.getSQLState() != null) {
403 log.error(
404 "RDBMS data connector {} - Unable to close connection to database; SQL State: {}, SQL Code: {}",
405 new Object[] { getId(), e.getSQLState(), e.getErrorCode() }, e);
406 } else {
407 log.error("RDBMS data connector {} - Unable to close connection to database",
408 new Object[] { getId() }, e);
409 }
410 }
411 }
412 }
413
414
415
416
417
418
419
420
421
422
423 protected Map<String, BaseAttribute> processResultSet(ResultSet resultSet) throws AttributeResolutionException {
424 Map<String, BaseAttribute> attributes = new HashMap<String, BaseAttribute>();
425
426 try {
427 if (!resultSet.next()) {
428 return attributes;
429 }
430
431 ResultSetMetaData resultMD = resultSet.getMetaData();
432 int numOfCols = resultMD.getColumnCount();
433 String columnName;
434 RDBMSColumnDescriptor columnDescriptor;
435 BaseAttribute attribute;
436 Collection attributeValues;
437 do {
438 for (int i = 1; i <= numOfCols; i++) {
439 columnName = resultMD.getColumnName(i);
440 columnDescriptor = columnDescriptors.get(columnName);
441
442 if (columnDescriptor == null || columnDescriptor.getAttributeID() == null) {
443 attribute = attributes.get(columnName);
444 if (attribute == null) {
445 attribute = new BasicAttribute(columnName);
446 }
447 } else {
448 attribute = attributes.get(columnDescriptor.getAttributeID());
449 if (attribute == null) {
450 attribute = new BasicAttribute(columnDescriptor.getAttributeID());
451 }
452 }
453
454 attributes.put(attribute.getId(), attribute);
455 attributeValues = attribute.getValues();
456 if (columnDescriptor == null || columnDescriptor.getDataType() == null) {
457 attributeValues.add(resultSet.getObject(i));
458 } else {
459 addValueByType(attributeValues, columnDescriptor.getDataType(), resultSet, i);
460 }
461 }
462 } while (resultSet.next());
463 } catch (SQLException e) {
464 if (e.getSQLState() != null) {
465 log.error(
466 "RDBMS data connector {} - Unable to read data from query result; SQL State: {}, SQL Code: {}",
467 new Object[] { getId(), e.getSQLState(), e.getErrorCode() }, e);
468 } else {
469 log.error("RDBMS data connector {} - Unable to read data from query result set",
470 new Object[] { getId() }, e);
471 }
472 }
473
474 return attributes;
475 }
476
477
478
479
480
481
482
483
484
485
486
487 protected void addValueByType(Collection values, DATA_TYPES type, ResultSet resultSet, int columnIndex)
488 throws SQLException {
489 switch (type) {
490 case BigDecimal:
491 values.add(resultSet.getBigDecimal(columnIndex));
492 break;
493 case Boolean:
494 values.add(resultSet.getBoolean(columnIndex));
495 break;
496 case Byte:
497 values.add(resultSet.getByte(columnIndex));
498 break;
499 case ByteArray:
500 values.add(resultSet.getBytes(columnIndex));
501 break;
502 case Date:
503 values.add(resultSet.getDate(columnIndex));
504 break;
505 case Double:
506 values.add(resultSet.getDouble(columnIndex));
507 break;
508 case Float:
509 values.add(resultSet.getFloat(columnIndex));
510 break;
511 case Integer:
512 values.add(resultSet.getInt(columnIndex));
513 break;
514 case Long:
515 values.add(resultSet.getLong(columnIndex));
516 break;
517 case Object:
518 values.add(resultSet.getObject(columnIndex));
519 break;
520 case Short:
521 values.add(resultSet.getShort(columnIndex));
522 break;
523 case Time:
524 values.add(resultSet.getTime(columnIndex));
525 break;
526 case Timestamp:
527 values.add(resultSet.getTimestamp(columnIndex));
528 break;
529 case URL:
530 values.add(resultSet.getURL(columnIndex));
531 break;
532 default:
533 values.add(resultSet.getString(columnIndex));
534 }
535 }
536 }