2 * Copyright (c) 2003 National Research Council of Canada
4 * Permission is hereby granted, free of charge, to any person
5 * obtaining a copy of this software and associated documentation
6 * files (the "Software"), to deal in the Software without
7 * restriction, including without limitation the rights to use,
8 * copy, modify, merge, publish, distribute, sublicense, and/or
9 * sell copies of the Software, and to permit persons to whom the
10 * Software is furnished to do so, subject to the following conditions:
12 * The above copyright notice and this permission notice shall be
13 * included in all copies or substantial portions of the Software.
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 * OTHER DEALINGS IN THE SOFTWARE.
26 package edu.internet2.middleware.shibboleth.aa.attrresolv.provider;
28 import java.lang.reflect.InvocationTargetException;
29 import java.lang.reflect.Method;
30 import java.security.Principal;
31 import java.sql.Connection;
32 import java.sql.DriverManager;
33 import java.sql.ResultSet;
34 import java.sql.ResultSetMetaData;
35 import java.sql.SQLException;
36 import java.sql.Statement;
37 import java.util.Iterator;
38 import java.util.Properties;
39 import java.util.regex.Matcher;
40 import java.util.regex.Pattern;
42 import javax.naming.NamingEnumeration;
43 import javax.naming.NamingException;
44 import javax.naming.directory.Attribute;
45 import javax.naming.directory.Attributes;
46 import javax.naming.directory.BasicAttribute;
47 import javax.naming.directory.BasicAttributes;
49 import org.apache.log4j.Logger;
50 import org.w3c.dom.Element;
51 import org.w3c.dom.Node;
52 import org.w3c.dom.NodeList;
54 import edu.internet2.middleware.shibboleth.aa.attrresolv.AttributeResolver;
55 import edu.internet2.middleware.shibboleth.aa.attrresolv.DataConnectorPlugIn;
56 import edu.internet2.middleware.shibboleth.aa.attrresolv.Dependencies;
57 import edu.internet2.middleware.shibboleth.aa.attrresolv.ResolutionPlugInException;
58 import edu.internet2.middleware.shibboleth.aa.attrresolv.ResolverAttribute;
61 * Built at the Canada Institute for Scientific and Technical Information (CISTI
62 * <ahref="http://www.cisti-icist.nrc-cnrc.gc.ca/">http://www.cisti-icist.nrc-cnrc.gc.ca/</a>,
63 * the National Research Council Canada
64 * (NRC <a href="http://www.nrc-cnrc.gc.ca/">http://www.nrc-cnrc.gc.ca/</a>)
65 * by David Dearman, COOP student from Dalhousie University,
66 * under the direction of Glen Newton, Head research (IT)
67 * <ahref="mailto:glen.newton@nrc-cnrc.gc.ca">glen.newton@nrc-cnrc.gc.ca</a>.
71 * Data Connector that uses JDBC to access user attributes stored in databases.
73 * @author David Dearman (dearman@cs.dal.ca)
76 public class JDBCDataConnector extends BaseResolutionPlugIn implements DataConnectorPlugIn {
78 private static Logger log = Logger.getLogger(JDBCDataConnector.class.getName());
79 private Properties props = new Properties();
80 private String searchVal = null;
81 private String aeClassName = null;
83 final private static String QueryAtt = "query";
84 final private static String AttributeExtractorAtt = "attributeExtractor";
85 final private static String DBDriverAtt = "dbDriver";
86 final private static String AEInstanceMethodAtt = "instance";
87 final private static String URLAtt = "dbURL";
89 public JDBCDataConnector(Element e) throws ResolutionPlugInException {
93 NodeList propertiesNode = e.getElementsByTagNameNS(AttributeResolver.resolverNamespace, "Property");
94 NodeList searchNode = e.getElementsByTagNameNS(AttributeResolver.resolverNamespace, "Search");
97 * Gets and sets the search parameter and the attribute extractor
99 searchVal = ((Element) searchNode.item(0)).getAttribute(QueryAtt);
100 aeClassName = ((Element) searchNode.item(0)).getAttribute(AttributeExtractorAtt);
102 if (searchVal == null || searchVal.equals("")) {
103 Node tnode = searchNode.item(0).getFirstChild();
104 if (tnode != null && tnode.getNodeType() == Node.TEXT_NODE) {
105 searchVal = tnode.getNodeValue();
107 if (searchVal == null || searchVal.equals("")) {
108 log.error("Search requires a specified query field");
109 throw new ResolutionPlugInException("mySQLDataConnection requires a \"Search\" specification");
113 log.debug("Search Query: (" + searchVal + ")");
117 * Assigns the property attribute name/value pairs to a hashtable
119 for (int i = 0; propertiesNode.getLength() > i; i++) {
120 Element property = (Element) propertiesNode.item(i);
121 String propertiesName = property.getAttribute("name");
122 String propertiesValue = property.getAttribute("value");
124 if (propertiesName != null
125 && !propertiesName.equals("")
126 && propertiesValue != null
127 && !propertiesValue.equals("")) {
128 props.setProperty(propertiesName, propertiesValue);
129 log.debug("Property: (" + propertiesName + ")");
130 log.debug(" Value: (" + propertiesValue + ")");
132 log.error("Property is malformed.");
133 throw new ResolutionPlugInException("Property is malformed.");
137 if (props.getProperty(URLAtt) == null) {
138 log.error("JDBC connection requires a dbURL property");
139 throw new ResolutionPlugInException("JDBCDataConnection requires a \"dbURL\" property");
143 protected String substitute(String source, String pattern, boolean quote, Dependencies depends) {
144 Matcher m=Pattern.compile(pattern).matcher(source);
146 String field = source.substring(m.start()+1, m.end()-1);
147 if (field != null && field.length() > 0) {
148 StringBuffer buf = new StringBuffer();
150 //Look for an attribute dependency.
151 ResolverAttribute dep = depends.getAttributeResolution(field);
153 Iterator iter=dep.getValues();
154 while (iter.hasNext()) {
155 if (buf.length() > 0)
156 buf = buf.append(',');
158 buf = buf.append("'");
159 buf = buf.append(iter.next());
161 buf = buf.append("'");
165 //If no values found, cycle over the connectors.
166 Iterator connDeps = connectorDependencyIds.iterator();
167 while (buf.length() == 0 && connDeps.hasNext()) {
168 Attributes attrs = depends.getConnectorResolution((String)connDeps.next());
170 Attribute attr = attrs.get(field);
173 NamingEnumeration vals = attr.getAll();
174 while (vals.hasMore()) {
175 if (buf.length() > 0)
176 buf = buf.append(',');
178 buf = buf.append("'");
179 buf = buf.append(vals.next());
181 buf = buf.append("'");
184 catch (NamingException e) {
185 // Auto-generated catch block
191 if (buf.length() == 0) {
192 log.warn("Unable to find any values to substitute in query for " + field + ", so using the empty string");
194 source = source.replaceAll(m.group(), buf.toString());
201 public Attributes resolve(Principal principal, String requester, Dependencies depends) throws ResolutionPlugInException {
202 Connection conn = null;
204 JDBCAttributeExtractor aeClassObj = null;
206 log.debug("Resolving connector: (" + getId() + ")");
207 log.debug(getId() + " resolving for principal: (" + principal.getName() + ")");
208 log.debug("The query string before inserting substitutions: " + searchVal);
210 //Replaces %PRINCIPAL% in the query string with its value
211 String convertedSearchVal = searchVal.replaceAll("%PRINCIPAL%", principal.getName());
212 convertedSearchVal = convertedSearchVal.replaceAll("@PRINCIPAL@", "'" + principal.getName() + "'");
214 //Find all delimited substitutions and replace with the named attribute value(s).
215 convertedSearchVal = substitute(convertedSearchVal,"%.+%",false,depends);
216 convertedSearchVal = substitute(convertedSearchVal,"@.+@",true,depends);
218 //Replace any escaped substitution delimiters.
219 convertedSearchVal = convertedSearchVal.replaceAll("\\%","%");
220 convertedSearchVal = convertedSearchVal.replaceAll("\\@","@");
222 log.debug("The query string after inserting substitutions: " + convertedSearchVal);
225 //Loads the database driver
226 loadDriver((String) props.get(DBDriverAtt));
227 } catch (ClassNotFoundException e) {
228 log.error("An ClassNotFoundException occured while loading database driver");
229 throw new ResolutionPlugInException(
230 "An ClassNotFoundException occured while loading database driver: " + e.getMessage());
231 } catch (IllegalAccessException e) {
232 log.error("An IllegalAccessException occured while loading database driver");
233 throw new ResolutionPlugInException(
234 "An IllegalAccessException occured while loading database driver: " + e.getMessage());
235 } catch (InstantiationException e) {
236 log.error("An InstantionException occured while loading database driver");
237 throw new ResolutionPlugInException(
238 "An InstantiationException occured while loading database driver: " + e.getMessage());
242 //Makes the connection to the database
244 } catch (SQLException e) {
245 log.error("An ERROR occured while connecting to database");
246 throw new ResolutionPlugInException("An ERROR occured while connecting to the database: " + e.getMessage());
250 //Gets the results set for the query
251 rs = executeQuery(conn, convertedSearchVal);
253 return new BasicAttributes();
255 } catch (SQLException e) {
256 log.error("An ERROR occured while executing the query");
257 throw new ResolutionPlugInException("An ERROR occured while executing the query: " + e.getMessage());
261 * If the user has supplied their own class for extracting the attributes from the
262 * result set, then their class will be run. A BasicAttributes object is expected as
263 * the result of the extraction.
265 * If the user has no supplied their own class for extracting the attributes then
266 * the default extraction is run, which is specified in DefaultAEAtt.
268 if (aeClassName == null || aeClassName.equals("")) {
269 aeClassName = DefaultAE.class.getName();
273 Class aeClass = Class.forName(aeClassName);
274 Method aeMethod = aeClass.getMethod(AEInstanceMethodAtt, null);
276 //runs the "instance" method returning and instance of the object
277 aeClassObj = (JDBCAttributeExtractor) (aeMethod.invoke(null, null));
278 log.debug("Supplied attributeExtractor class loaded.");
280 } catch (ClassNotFoundException e) {
281 log.error("The supplied attribute extractor class could not be found");
282 throw new ResolutionPlugInException(
283 "The supplied attribute extractor class could not be found: " + e.getMessage());
284 } catch (NoSuchMethodException e) {
285 log.error("The requested method does not exist");
286 throw new ResolutionPlugInException("The requested method does not exist: " + e.getMessage());
287 } catch (IllegalAccessException e) {
288 log.error("Access is not permitted for invoking requested method");
289 throw new ResolutionPlugInException(
290 "Access is not permitted for invoking requested method: " + e.getMessage());
291 } catch (InvocationTargetException e) {
292 log.error("An ERROR occured invoking requested method");
293 throw new ResolutionPlugInException("An ERROR occured involking requested method: " + e.getMessage());
297 return aeClassObj.extractAttributes(rs);
299 } catch (JDBCAttributeExtractorException e) {
300 log.error("An ERROR occured while extracting attributes from result set");
301 throw new ResolutionPlugInException(
302 "An ERROR occured while extracting attributes from result set: " + e.getMessage());
307 log.debug("Result set released");
308 } catch (SQLException e) {
309 log.error("An error occured while closing the result set: " + e);
310 throw new ResolutionPlugInException("An error occured while closing the result set: " + e);
314 //close the connection
316 log.debug("Connection to database closed");
317 } catch (SQLException e) {
318 log.error("An error occured while closing the database connection: " + e);
319 throw new ResolutionPlugInException("An error occured while closing the database connection: " + e);
325 * Loads the driver used to access the database
326 * @param driver The driver used to access the database
327 * @throws ResolutionPlugInException If there is a failure to load the driver
329 public void loadDriver(String driver)
330 throws ClassNotFoundException, IllegalAccessException, InstantiationException {
331 Class.forName(driver).newInstance();
332 log.debug("Loading driver: " + driver);
336 * Makes a connection to the database using the property set.
337 * @return Connection object
338 * @throws SQLException If there is a failure to make a database connection
340 public Connection connect()
341 throws SQLException {
342 String url = props.getProperty(URLAtt);
344 Connection conn = DriverManager.getConnection(url,props);
345 log.debug("Connection with database established");
351 * Execute the users query
352 * @param query The query the user wishes to execute
353 * @return The result of the users <code>query</code>
354 * @return null if an error occurs during execution
355 * @throws SQLException If an error occurs while executing the query
357 public ResultSet executeQuery(Connection conn, String query) throws SQLException {
358 log.debug("Users Query: " + query);
359 Statement stmt = conn.createStatement();
360 return stmt.executeQuery(query);
365 * The default attribute extractor.
368 class DefaultAE implements JDBCAttributeExtractor {
369 private static DefaultAE _instance = null;
370 private static Logger log = Logger.getLogger(DefaultAE.class.getName());
373 protected DefaultAE() {
376 // Ensures that only one istance of the class at a time
377 public static DefaultAE instance() {
378 if (_instance == null)
379 return new DefaultAE();
385 * Method of extracting the attributes from the supplied result set.
387 * @param ResultSet The result set from the query which contains the attributes
388 * @return BasicAttributes as objects containing all the attributes
389 * @throws JDBCAttributeExtractorException If there is a complication in retrieving the attributes
391 public BasicAttributes extractAttributes(ResultSet rs) throws JDBCAttributeExtractorException {
392 BasicAttributes attributes = new BasicAttributes();
394 log.debug("Using default Attribute Extractor");
397 ResultSetMetaData rsmd = rs.getMetaData();
398 int numColumns = rsmd.getColumnCount();
399 log.debug("Number of returned columns: " + numColumns);
401 for (int i = 1; i <= numColumns; i++) {
402 String columnName = rsmd.getColumnName(i);
403 String columnType = rsmd.getColumnTypeName(i);
404 Object columnValue = rs.getObject(columnName);
406 "(" + i + ". ColumnType = " + columnType + ") " + columnName + " -> " + (columnValue!=null ? columnValue.toString() : "(null)"));
407 attributes.put(new BasicAttribute(columnName, columnValue));
410 catch (SQLException e) {
411 log.error("An ERROR occured while retrieving result set meta data");
412 throw new JDBCAttributeExtractorException(
413 "An ERROR occured while retrieving result set meta data: " + e.getMessage());
416 // Check for multiple rows.
419 throw new JDBCAttributeExtractorException("Query returned more than one row.");
421 catch (SQLException e) {