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