001: /*
002: * Copyright (C) 1999-2004 <A href="http://www-ist.massey.ac.nz/JBDietrich" target="_top">Jens Dietrich</a>
003: *
004: * This library is free software; you can redistribute it and/or
005: * modify it under the terms of the GNU Lesser General Public
006: * License as published by the Free Software Foundation; either
007: * version 2 of the License, or (at your option) any later version.
008: *
009: * This library is distributed in the hope that it will be useful,
010: * but WITHOUT ANY WARRANTY; without even the implied warranty of
011: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012: * Lesser General Public License for more details.
013: *
014: * You should have received a copy of the GNU Lesser General Public
015: * License along with this library; if not, write to the Free Software
016: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
017: */
018: package org.mandarax.sql;
019:
020: import java.sql.Connection;
021: import java.sql.PreparedStatement;
022: import java.sql.ResultSet;
023: import java.sql.SQLException;
024: import java.util.Map;
025: import java.util.WeakHashMap;
026: import javax.sql.DataSource;
027: import org.mandarax.util.logging.LogCategories;
028: import org.mandarax.kernel.Session;
029:
030: /**
031: * Implementation of a function based on a SQL query.
032: * The query contains variable parameters represented by the VARIABLE_PROXY
033: * constants, just like the query strings used in JDBC prepared statements
034: * (the value of this variable is "?").
035: * The return value is the one row in the result set converted to an object.
036: * If the result set has no rows or more than one row,
037: * an IllegalArgumentException is thrown.
038: * Therefore, queries will typically retrieve one column and have a where
039: * clause with a primary key condition.
040: * <p>
041: * <strong>Example:</strong>The query <code>SELECT NAME FROM CUSTOMER WHERE ID=?</code>
042: * will define a function that takes one value (the id) as input and returns
043: * one value (the name of the customer).
044: * <br>
045: * The (java) return type and the parameter types must be declared.
046: * Defaults (Object/Object[]) will be used otherwise.
047: * <br>
048: * Internally, we are using PreparedStatements. Since these statements can be reused,
049: * they are cached by connection using weak references (so that the function
050: * does not keep the connection alive if nobody else does it).
051: * @see java.sql.PreparedStatement
052: * @see org.mandarax.sql.SQLObjectRelationalMapping
053: * @author <A href="http://www-ist.massey.ac.nz/JBDietrich" target="_top">Jens Dietrich</A>
054: * @version 3.4 <7 March 05>
055: * @since 1.6
056: */
057: public class SQLFunction implements org.mandarax.kernel.Function,
058: LogCategories {
059:
060: public static final String VARIABLE_PROXY = "?";
061: private String name = null;
062: private DataSource dataSource = null;
063: private String query = null;
064: private SQLObjectRelationalMapping objectRelationalMapping = null;
065: private Class[] structure = { Object.class };
066: private transient Map statementCache = new WeakHashMap();
067: private boolean cacheStatements = true;
068: private boolean closeConnection = true;
069:
070: /**
071: * Constructor.
072: */
073: public SQLFunction() {
074: super ();
075: }
076:
077: /**
078: * Set the data source.
079: * @return a data source.
080: */
081: public DataSource getDataSource() {
082: return dataSource;
083: }
084:
085: /**
086: * Get the name of the function.
087: * @return the name of the predicate
088: */
089: public String getName() {
090: return name;
091: }
092:
093: /**
094: * Get the return type.
095: * @return the return type of the function
096: */
097: public Class getReturnType() {
098: return objectRelationalMapping.getTargetType();
099: }
100:
101: /**
102: * Get the query string.
103: * @return a query
104: */
105: public String getQuery() {
106: return query;
107: }
108:
109: /**
110: * Get the type structure of the object, e.g. the types of terms
111: * that can be used with this constructor.
112: * <strong>Warning:</strong>Number and types must be consistent with
113: * the query and the column types in the table(s).
114: * @return an array of classes
115: */
116: public java.lang.Class[] getStructure() {
117: return structure;
118: }
119:
120: /**
121: * Set the type structure of the object, e.g. the types of terms
122: * that can be used with this constructor.
123: * <strong>Warning:</strong>Number and types must be consistent with
124: * the query and the column types in the table(s).
125: * @return struct an array of classes
126: */
127: public void setStructure(Class[] struct) {
128: structure = struct;
129: }
130:
131: /**
132: * Set the object relational mapping used.
133: * @param map a mapping
134: */
135: public void setObjectRelationalMapping(
136: SQLObjectRelationalMapping map) {
137: objectRelationalMapping = map;
138: }
139:
140: /**
141: * Get the object relational mapping.
142: * @return a mapping
143: */
144: public SQLObjectRelationalMapping getObjectRelationalMapping() {
145: return objectRelationalMapping;
146: }
147:
148: /**
149: * Get a prepared statement for a string.
150: * @return a prepared statement
151: * @param con a database connection
152: */
153: private PreparedStatement getStatement(Connection con)
154: throws SQLException {
155: if (con == null) {
156: return null;
157: }
158: PreparedStatement stmt = null;
159: if (cacheStatements) {
160: if (statementCache == null)
161: statementCache = new WeakHashMap();
162: stmt = (PreparedStatement) statementCache.get(con);
163: if (stmt == null) {
164: stmt = con.prepareStatement(query);
165: statementCache.put(con, stmt);
166: } else {
167: LOG_SQL.debug("Using cached SQL for SQLFunction");
168: }
169: } else {
170: stmt = con.prepareStatement(query);
171: }
172: return stmt;
173: }
174:
175: /**
176: * Perform the function or predicate using an array of terms as parameters.
177: * @return the result of the perform operation
178: * @param parameter an array of terms
179: * @param session a session object
180: * @throws java.lang.UnsupportedOperationException
181: * @throws java.lang.IllegalArgumentException thrown if any problem occurs, including connection
182: * problems, or the result set having one or more than one rows
183: */
184: public Object perform(org.mandarax.kernel.Term[] parameter,
185: Session session) throws UnsupportedOperationException,
186: IllegalArgumentException {
187: Connection con = null;
188: try {
189:
190: // get connection and statement
191: // @todo database login
192: con = dataSource.getConnection();
193: PreparedStatement stmt = getStatement(con);
194:
195: LOG_SQL.debug("Preparing SQL " + stmt);
196:
197: // set the parameters
198: for (int i = 0; i < parameter.length; i++) {
199: stmt.setObject(i + 1, parameter[i].resolve(session));
200: }
201:
202: // perform query
203: LOG_SQL.debug("Excecuting SQL " + stmt);
204:
205: ResultSet resultSet = stmt.executeQuery();
206: Object result = null;
207:
208: if (resultSet.next()) {
209: result = objectRelationalMapping.buildObject(resultSet);
210: LOG_SQL.debug("Performing SQL function yields value "
211: + result);
212: } else {
213: LOG_SQL
214: .warn("Performing SQL function yields no value (the result set is empty)");
215: release(con);
216: throw new IllegalArgumentException(
217: "Query yields no results for these values");
218: }
219: if (resultSet.next()) {
220: LOG_SQL
221: .warn("Query yields more than one results for these values");
222: release(con);
223: throw new IllegalArgumentException(
224: "Query yields more than one results for these values");
225: }
226: release(con);
227: return result;
228: } catch (SQLException x) {
229: LOG_SQL.error("Exception executing SQL", x);
230: release(con);
231: throw new IllegalArgumentException(
232: "Problem performing SQL function");
233: }
234: }
235:
236: /**
237: * Release the connection used to invoke the function.
238: * @param con the connection
239: */
240: private void release(Connection con) {
241: try {
242: if (con != null && closeConnection) {
243: con.close();
244: if (LOG_SQL.isDebugEnabled())
245: LOG_SQL
246: .debug("JDBC connection released by SQLFunction "
247: + this );
248: }
249: } catch (SQLException x) {
250: LOG_SQL.error(
251: "Exception closing jdbc connection in SQLFunction "
252: + this , x);
253: }
254: }
255:
256: /**
257: * Set the data source.
258: * @param ds a data source.
259: */
260: public void setDataSource(DataSource ds) {
261: dataSource = ds;
262: }
263:
264: /**
265: * Set the query.
266: * @param q a query string
267: */
268: public void setQuery(String q) {
269: query = q;
270: }
271:
272: /**
273: * Set the name.
274: * @param n a name
275: */
276: public void setName(String n) {
277: name = n;
278: }
279:
280: /**
281: * Indicates whether the object (usually a term or a clause set) can be performed
282: * using the java semantics.
283: * @return true
284: */
285: public boolean isExecutable() {
286: return true;
287: }
288:
289: /**
290: * Convert the object to a string.
291: * @return a string
292: */
293: public String toString() {
294: return (name == null) ? super .toString() : name;
295: }
296:
297: /**
298: * Compare objects.
299: * @param obj another object
300: * @return a boolean
301: */
302: public boolean equals(Object obj) {
303: if ((obj != null) && (obj instanceof SQLFunction)) {
304: SQLFunction f = (SQLFunction) obj;
305: boolean result = true;
306:
307: // compare query, name and data source
308: // bugfix in 1.8 - thanks to chenjb@gsta.com
309: result = (name == null) ? (f.name == null) : name
310: .equals(f.name);
311: result = result
312: && ((dataSource == null) ? (f.dataSource == null)
313: : dataSource.equals(f.dataSource));
314: result = result
315: && ((query == null) ? (f.query == null) : query
316: .equals(f.query));
317: result = result
318: && ((objectRelationalMapping == null) ? (f.objectRelationalMapping == null)
319: : objectRelationalMapping
320: .equals(f.objectRelationalMapping));
321:
322: Class[] c1 = f.structure;
323: Class[] c2 = structure;
324:
325: if (c1 == null) {
326: result = c2 == null;
327: } else {
328: result = result && (c1.length == c2.length);
329:
330: for (int i = 0; i < c1.length; i++) {
331: result = result && (c1[i] == c2[i]);
332: }
333: }
334:
335: return result;
336: } else {
337: return false;
338: }
339: }
340:
341: /**
342: * Get the hashcode of the object.
343: * @return the hash code of the object
344: */
345: public int hashCode() {
346: return ((name == null) ? 0 : name.hashCode())
347: ^ ((query == null) ? 0 : query.hashCode())
348: ^ ((dataSource == null) ? 0 : dataSource.hashCode());
349: }
350:
351: /**
352: * Indicates whether to close the connection at the end.
353: * @return a boolean
354: */
355: public boolean isCloseConnection() {
356: return closeConnection;
357: }
358:
359: /**
360: * Sets whether to close the connection at the end.
361: * @param closeConnection a boolean
362: */
363: public void setCloseConnection(boolean closeConnection) {
364: this.closeConnection = closeConnection;
365: }
366:
367: }
|