001: /*
002: * Copyright 2004 (C) TJDO.
003: * All rights reserved.
004: *
005: * This software is distributed under the terms of the TJDO License version 1.0.
006: * See the terms of the TJDO License in the documentation provided with this software.
007: *
008: * $Id: TJDOSQLQuery.java,v 1.11 2004/01/25 22:31:27 jackknifebarber Exp $
009: */
010:
011: package com.triactive.jdo.store;
012:
013: import com.triactive.jdo.ClassNotPersistenceCapableException;
014: import com.triactive.jdo.PersistenceManager;
015: import com.triactive.jdo.model.ClassMetaData;
016: import com.triactive.jdo.model.FieldMetaData;
017: import com.triactive.jdo.util.IntArrayList;
018: import com.triactive.jdo.util.MacroString;
019: import java.sql.Connection;
020: import java.sql.PreparedStatement;
021: import java.sql.ResultSet;
022: import java.sql.ResultSetMetaData;
023: import java.sql.SQLException;
024: import java.util.ArrayList;
025: import java.util.Collection;
026: import java.util.HashSet;
027: import java.util.Iterator;
028: import java.util.Map;
029: import java.util.List;
030: import javax.jdo.Extent;
031: import javax.jdo.JDODataStoreException;
032: import javax.jdo.JDOFatalInternalException;
033: import javax.jdo.JDOFatalUserException;
034: import javax.jdo.JDOUserException;
035: import org.apache.log4j.Category;
036:
037: /**
038: * A JDO query that uses the default JQOQL language.
039: *
040: * @author <a href="mailto:mmartin5@austin.rr.com">Mike Martin</a>
041: * @version $Revision: 1.11 $
042: *
043: * @see Query
044: */
045:
046: public class TJDOSQLQuery extends Query {
047: private static final Category LOG = Category
048: .getInstance(TJDOSQLQuery.class);
049:
050: private transient final String tjdoSqlText;
051:
052: private transient String jdbcSqlText = null;
053: private transient int[] fieldNumbers = null;
054: private transient ColumnMapping[] fieldMappings = null;
055: private transient List fieldColumnNames = null;
056: private transient List parameterOccurrences = null;
057:
058: /**
059: * Constructs a new query instance having the same criteria as the given
060: * query.
061: *
062: * @param pm The persistence manager.
063: * @param storeMgr The store manager.
064: * @param tjdoSqlText The SQL text of the query.
065: */
066:
067: public TJDOSQLQuery(PersistenceManager pm, StoreManager storeMgr,
068: String tjdoSqlText) {
069: super (pm, storeMgr);
070:
071: candidateClass = null;
072: filter = null;
073: imports = null;
074: variables = null;
075: parameters = null;
076: ordering = null;
077:
078: this .tjdoSqlText = tjdoSqlText;
079: }
080:
081: protected void discardCompiled() {
082: super .discardCompiled();
083:
084: jdbcSqlText = null;
085: fieldNumbers = null;
086: fieldMappings = null;
087: fieldColumnNames = null;
088: parameterOccurrences = null;
089: }
090:
091: public boolean equals(Object obj) {
092: if (obj == this )
093: return true;
094:
095: if (!(obj instanceof TJDOSQLQuery) || !super .equals(obj))
096: return false;
097:
098: return tjdoSqlText.equals(((TJDOSQLQuery) obj).tjdoSqlText);
099: }
100:
101: /**
102: * Set the candidate Extent to query.
103: *
104: * <p>This implementation always throws a JDOUserException since this
105: * concept doesn't apply to TJDOSQL queries.
106: *
107: * @param pcs the Candidate Extent.
108: *
109: * @exception JDOUserException Always thrown.
110: *
111: * @see javax.jdo.Query#setCandidates(javax.jdo.Extent)
112: */
113:
114: public void setCandidates(Extent pcs) {
115: throw new JDOUserException(
116: "Candidate extents not applicable to TJDOSQL queries");
117: }
118:
119: /**
120: * Set the candidate Collection to query.
121: *
122: * <p>This implementation always throws a JDOUserException since this
123: * concept doesn't apply to TJDOSQL queries.
124: *
125: * @param pcs the Candidate collection.
126: *
127: * @exception JDOUserException Always thrown.
128: *
129: * @see javax.jdo.Query#setCandidates(java.util.Collection)
130: */
131:
132: public void setCandidates(Collection pcs) {
133: throw new JDOUserException(
134: "Candidate collections not applicable to TJDOSQL queries");
135: }
136:
137: /**
138: * Set the filter for the query.
139: *
140: * <p>This implementation always throws a JDOUserException since this
141: * concept doesn't apply to TJDOSQL queries.
142: *
143: * @param filter the query filter.
144: *
145: * @exception JDOUserException Always thrown.
146: *
147: * @see javax.jdo.Query#setFilter
148: */
149:
150: public void setFilter(String filter) {
151: throw new JDOUserException(
152: "Filter strings not applicable to TJDOSQL queries");
153: }
154:
155: /**
156: * Declare the unbound variables to be used in the query.
157: *
158: * <p>This implementation always throws a JDOUserException since this
159: * concept doesn't apply to TJDOSQL queries.
160: *
161: * @param variables the variables separated by semicolons.
162: *
163: * @exception JDOUserException Always thrown.
164: *
165: * @see javax.jdo.Query#declareVariables
166: */
167:
168: public void declareVariables(String variables) {
169: throw new JDOUserException(
170: "Variables are not applicable to TJDOSQL queries");
171: }
172:
173: /**
174: * Set the ordering specification for the result Collection.
175: *
176: * <p>This implementation always throws a JDOUserException since this
177: * concept doesn't apply to TJDOSQL queries.
178: *
179: * @param ordering the ordering specification.
180: *
181: * @exception JDOUserException Always thrown.
182: *
183: * @see javax.jdo.Query#setOrdering
184: */
185:
186: public void setOrdering(String ordering) {
187: throw new JDOUserException(
188: "Ordering must be set via explicit ORDER BY in the SQL text for TJDOSQL queries");
189: }
190:
191: /**
192: * Verify the elements of the query and provide a hint to the query to
193: * prepare and optimize an execution plan.
194: *
195: * @see javax.jdo.Query#compile
196: */
197:
198: public void compile() {
199: if (!isCompiled) {
200: super .compile();
201:
202: generateQueryStatement();
203:
204: isCompiled = true;
205: }
206: }
207:
208: private void generateQueryStatement() {
209: if (candidateClass == null)
210: throw new JDOUserException("No candidate class provided");
211:
212: final ClassMetaData cmd = ClassMetaData
213: .forClass(candidateClass);
214:
215: if (cmd == null)
216: throw new ClassNotPersistenceCapableException(
217: candidateClass);
218:
219: if (cmd.requiresExtent())
220: throw new JDOUserException(
221: "Invalid candidate class for TJDOSQL, must not have an extent (use requires-extent=\"false\" in the JDO metadata): "
222: + candidateClass.getName());
223:
224: if (cmd.getIdentityType() != ClassMetaData.NO_IDENTITY)
225: throw new JDOUserException(
226: "Invalid candidate class for TJDOSQL, must use non-durable identity (use identity-type=\"nondurable\" in the JDO metadata): "
227: + candidateClass.getName());
228:
229: if (cmd.getPCSuperclass() != null)
230: throw new PersistentSuperclassNotAllowedException(
231: candidateClass);
232:
233: int fieldCount = cmd.getFieldCount();
234: IntArrayList fn = new IntArrayList(fieldCount);
235: fieldMappings = new ColumnMapping[fieldCount];
236: fieldColumnNames = new ArrayList(fieldCount);
237:
238: for (int fieldNumber = 0; fieldNumber < fieldCount; ++fieldNumber) {
239: FieldMetaData fmd = cmd.getFieldRelative(fieldNumber);
240: String fieldName = fmd.getName();
241: Class fieldType = fmd.getType();
242:
243: switch (fmd.getPersistenceModifier()) {
244: case FieldMetaData.PERSISTENCE_MODIFIER_NONE:
245: default:
246: throw new JDOFatalInternalException(
247: "Invalid persistence modifier on field "
248: + fieldName);
249:
250: case FieldMetaData.PERSISTENCE_MODIFIER_TRANSACTIONAL:
251: break;
252:
253: case FieldMetaData.PERSISTENCE_MODIFIER_PERSISTENT:
254: Mapping m = dba.getMapping(fieldType);
255:
256: if (!(m instanceof ColumnMapping))
257: throw new JDOFatalUserException(
258: "Mapping "
259: + m
260: + " not suitable for a TJDOSQL result column, field = "
261: + fieldName);
262:
263: fieldMappings[fieldNumber] = (ColumnMapping) m;
264: fn.add(fieldNumber);
265: fieldColumnNames.add(new ColumnIdentifier(dba,
266: fieldName, fieldType, Role.NONE)
267: .getSQLIdentifier());
268: break;
269: }
270:
271: }
272:
273: if (fn.isEmpty())
274: throw new JDOFatalUserException(
275: "View class has no persistent fields: "
276: + candidateClass.getName());
277:
278: fieldNumbers = fn.toArray();
279:
280: /*
281: * Generate the actual JDBC SQL text by processing the embedded macros
282: * in the given TJDO SQL text. The parameterOccurrences list collects
283: * the names of all of the parameters in the order in which they occur
284: * in the statement.
285: */
286: parameterOccurrences = new ArrayList();
287:
288: MacroString ms = new MacroString(candidateClass, imports,
289: tjdoSqlText);
290:
291: jdbcSqlText = ms
292: .substituteMacros(new MacroString.MacroHandler() {
293: public void onIdentifierMacro(
294: MacroString.IdentifierMacro im) {
295: if (im.clazz.equals(candidateClass)) {
296: if (im.fieldName == null)
297: throw new JDOUserException(
298: "Invalid macro, query result classes have no table: "
299: + im);
300: if (im.subfieldName != null)
301: throw new JDOUserException(
302: "No such field in query result class "
303: + im.clazz.getName()
304: + ": " + im);
305: int fieldNumber = cmd
306: .getRelativeFieldNumber(im.fieldName);
307:
308: if (fieldNumber < 0)
309: throw new JDOUserException(
310: "No such field in query result class "
311: + im.clazz.getName()
312: + ": " + im);
313: im.value = (String) fieldColumnNames
314: .get(fieldNumber);
315: } else
316: storeMgr.resolveIdentifierMacro(im);
317: }
318:
319: public void onParameterMacro(
320: MacroString.ParameterMacro pm) {
321: parameterOccurrences.add(pm.parameterName);
322: }
323: });
324: }
325:
326: /**
327: * Execute the query and return the filtered Collection.
328: *
329: * @param parameters the Map containing all of the parameters.
330: *
331: * @return the filtered Collection.
332: *
333: * @see javax.jdo.Query#executeWithMap(Map)
334: * @see #executeWithArray(Object[] parameters)
335: */
336:
337: public Object executeWithMap(Map parameters) {
338: compile();
339:
340: if (parameters.size() != parameterNames.size())
341: throw new JDOUserException(
342: "Incorrect number of parameters: "
343: + parameters.size() + ", s/b "
344: + parameterNames.size());
345:
346: QueryResult qr = null;
347:
348: try {
349: Connection conn = pm.getConnection(false);
350:
351: try {
352: PreparedStatement ps = conn
353: .prepareStatement(jdbcSqlText);
354:
355: try {
356:
357: Iterator i = parameterOccurrences.iterator();
358: int stmtParamNum = 1;
359:
360: while (i.hasNext()) {
361: String paramName = (String) i.next();
362: Class paramType = (Class) parameterTypesByName
363: .get(paramName);
364:
365: if (!parameters.containsKey(paramName))
366: throw new JDOUserException(
367: "Required parameter " + paramName
368: + " not provided");
369: if (paramType == null)
370: throw new JDOUserException(
371: "Undeclared parameter " + paramName
372: + " occurred in SQL text");
373:
374: Mapping m = dba.getMapping(paramType);
375: Object paramValue = parameters.get(paramName);
376:
377: if (!(m instanceof ColumnMapping))
378: throw new JDOUserException(
379: "Illegal parameter type, param = "
380: + paramName + " type = "
381: + paramType.getName());
382:
383: ((ColumnMapping) m).setObject(pm, ps,
384: stmtParamNum++, paramValue);
385: }
386:
387: long startTime = System.currentTimeMillis();
388:
389: ResultSet rs = ps.executeQuery();
390:
391: if (LOG.isDebugEnabled())
392: LOG
393: .debug("Time = "
394: + (System.currentTimeMillis() - startTime)
395: + " ms: " + jdbcSqlText);
396:
397: try {
398: int[] columnNumbersByField = new int[fieldMappings.length];
399: ResultSetMetaData rsmd = rs.getMetaData();
400: HashSet remainingColumnNames = new HashSet(
401: fieldColumnNames);
402:
403: int colCount = rsmd.getColumnCount();
404:
405: for (int colNum = 1; colNum <= colCount; ++colNum) {
406: String colName = rsmd.getColumnName(colNum);
407: int fieldNumber = fieldColumnNames
408: .indexOf(colName);
409:
410: if (fieldNumber >= 0) {
411: columnNumbersByField[fieldNumber] = colNum;
412: remainingColumnNames.remove(colName);
413: }
414: }
415:
416: if (!remainingColumnNames.isEmpty())
417: throw new JDOUserException(
418: "Expected columns missing from result set: "
419: + remainingColumnNames);
420:
421: qr = new QueryResult(this , new TransientIDROF(
422: pm, candidateClass, fieldNumbers,
423: fieldMappings, columnNumbersByField),
424: rs);
425: } finally {
426: if (qr == null)
427: rs.close();
428: }
429: } finally {
430: if (qr == null)
431: ps.close();
432: }
433: } finally {
434: pm.releaseConnection(conn);
435: }
436: } catch (SQLException e) {
437: throw dba.newDataStoreException("Error executing query: "
438: + jdbcSqlText, e);
439: }
440:
441: queryResults.add(qr);
442:
443: return qr;
444: }
445: }
|