View Javadoc

1   /*
2    * Licensed to the University Corporation for Advanced Internet Development, 
3    * Inc. (UCAID) under one or more contributor license agreements.  See the 
4    * NOTICE file distributed with this work for additional information regarding
5    * copyright ownership. The UCAID licenses this file to You under the Apache 
6    * License, Version 2.0 (the "License"); you may not use this file except in 
7    * compliance with the License.  You may obtain a copy of the License at
8    *
9    *    http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
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   * A data connector that can retrieve information from a relational database through JDBC, version 3.
45   */
46  public class RDBMSDataConnector extends BaseDataConnector {
47  
48      /** Data types understood by this connector. */
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      /** Class logger. */
54      private final Logger log = LoggerFactory.getLogger(RDBMSDataConnector.class);
55  
56      /** JDBC data source for retrieving connections. */
57      private DataSource dataSource;
58  
59      /** Template engine used to change query template into actual query. */
60      private TemplateEngine queryCreator;
61  
62      /** Name the query template is registered under with the statement creator. */
63      private String queryTemplateName;
64  
65      /** Template that produces the query to use. */
66      private String queryTemplate;
67      
68      /** SQL query timeout in seconds. */
69      private int queryTimeout;
70  
71      /** Whether the JDBC connection is read-only. */
72      private boolean readOnlyConnection;
73  
74      /** Whether queries might use stored procedures. */
75      private boolean usesStoredProcedure;
76  
77      /** Whether an empty result set is an error. */
78      private boolean noResultIsError;
79  
80      /** Set of column descriptors for managing returned data. [columnName => colmentDescriptr] */
81      private Map<String, RDBMSColumnDescriptor> columnDescriptors;
82      
83      /** Query result cache. */
84      private Cache resultsCache;
85      
86      /**
87       * Constructor.
88       * 
89       * @param source data source used to retrieve connections
90       * @param cache cache used to cache results
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      * This sets the underlying template engine and registers the supplied template.
108      * 
109      * @param engine template engine used to generate the query
110      * @param template template used to generate the query
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      * Gets whether this data connector is caching results.
124      * 
125      * @return true if this data connector is caching results, false if not
126      */
127     public boolean isCachingResuts() {
128         return resultsCache != null;
129     }
130 
131     /**
132      * Gets the timeout, in seconds, of the SQL query.
133      * 
134      * @return timeout, in seconds, of the SQL query
135      */
136     public int getQueryTimeout() {
137         return queryTimeout;
138     }
139     
140     /**
141      * Sets the timeout, in seconds, of the SQL query.
142      * 
143      * @param timeout timeout, in seconds, of the SQL query
144      */
145     public void setQueryTimeout(int timeout) {
146         queryTimeout = timeout;
147     }
148     
149     /**
150      * Gets whether this data connector uses read-only connections.
151      * 
152      * @return whether this data connector uses read-only connections
153      */
154     public boolean isConnectionReadOnly() {
155         return readOnlyConnection;
156     }
157 
158     /**
159      * Sets whether this data connector uses read-only connections.
160      * 
161      * @param isReadOnly whether this data connector uses read-only connections
162      */
163     public void setConnectionReadOnly(boolean isReadOnly) {
164         readOnlyConnection = isReadOnly;
165     }
166 
167     /**
168      * Gets whether queries made use stored procedures.
169      * 
170      * @return whether queries made use stored procedures
171      */
172     public boolean getUsesStoredProcedure() {
173         return usesStoredProcedure;
174     }
175 
176     /**
177      * Sets whether queries made use stored procedures.
178      * 
179      * @param storedProcedure whether queries made use stored procedures
180      */
181     public void setUsesStoredProcedure(boolean storedProcedure) {
182         usesStoredProcedure = storedProcedure;
183     }
184 
185     /**
186      * This returns whether this connector will throw an exception if no search results are found. The default is false.
187      * 
188      * @return <code>boolean</code>
189      */
190     public boolean isNoResultIsError() {
191         return noResultIsError;
192     }
193 
194     /**
195      * This sets whether this connector will throw an exception if no search results are found.
196      * 
197      * @param isError <code>boolean</code>
198      */
199     public void setNoResultIsError(boolean isError) {
200         noResultIsError = isError;
201     }
202 
203     /**
204      * Gets the set of column descriptors used to deal with result set data. The name of the database column is the
205      * map's key. This list is unmodifiable.
206      * 
207      * @return column descriptors used to deal with result set data
208      */
209     public Map<String, RDBMSColumnDescriptor> getColumnDescriptor() {
210         return columnDescriptors;
211     }
212 
213     /** {@inheritDoc} */
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     /** {@inheritDoc} */
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      * Attempts to retrieve the attributes from the cache.
279      * 
280      * @param principal the principal name of the user the attributes are for
281      * @param query query used to generate the attributes
282      * 
283      * @return cached attributes
284      * 
285      * @throws AttributeResolutionException thrown if there is a problem retrieving data from the cache
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      * Attempts to retrieve the attribute from the database.
304      * 
305      * @param query query used to get the attributes
306      * 
307      * @return attributes gotten from the database
308      * 
309      * @throws AttributeResolutionException thrown if there is a problem retrieving data from the database or
310      *             transforming that data into {@link BaseAttribute}s
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      * Converts a SQL query results set into a set of {@link BaseAttribute}s.
356      * 
357      * @param resultSet the result set to convert
358      * 
359      * @return the resultant set of attributes
360      * 
361      * @throws AttributeResolutionException thrown if there is a problem converting the result set into attributes
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      * Adds a value extracted from the result set as a specific type into the value set.
414      * 
415      * @param values set to add values into
416      * @param type type the value should be extracted as
417      * @param resultSet result set, on the current row, to extract the value from
418      * @param columnIndex index of the column from which to extract the attribute
419      * 
420      * @throws SQLException thrown if value can not retrieved from the result set
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      * Caches the attributes resulting from a query.
474      * 
475      * @param principal the principal name of the user the attributes are for
476      * @param query the query that generated the attributes
477      * @param attributes the results of the query
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 }