001: /*
002: * Copyright (c) 1998 - 2005 Versant Corporation
003: * All rights reserved. This program and the accompanying materials
004: * are made available under the terms of the Eclipse Public License v1.0
005: * which accompanies this distribution, and is available at
006: * http://www.eclipse.org/legal/epl-v10.html
007: *
008: * Contributors:
009: * Versant Corporation - initial API and implementation
010: */
011: package com.versant.core.jdbc.ejbql;
012:
013: import com.versant.core.jdbc.JdbcStorageManager;
014: import com.versant.core.jdbc.metadata.JdbcField;
015: import com.versant.core.jdbc.fetch.FetchSpec;
016: import com.versant.core.jdbc.fetch.SqlBuffer;
017: import com.versant.core.jdbc.sql.exp.SqlExp;
018: import com.versant.core.jdbc.sql.exp.SelectExp;
019: import com.versant.core.jdbc.sql.exp.SqlParamUsage;
020: import com.versant.core.jdbc.sql.exp.ColumnExp;
021: import com.versant.core.jdbc.query.JdbcCompiledQuery;
022: import com.versant.core.metadata.ModelMetaData;
023: import com.versant.core.metadata.ClassMetaData;
024: import com.versant.core.jdo.QueryDetails;
025: import com.versant.core.ejb.query.*;
026: import com.versant.core.common.BindingSupportImpl;
027: import com.versant.core.common.Debug;
028: import com.versant.core.common.CmdBitSet;
029:
030: import java.io.StringReader;
031:
032: /**
033: * This will compile an EJBQL query.
034: */
035: public class JdbcEJBQLCompiler {
036:
037: private final JdbcStorageManager sm;
038: private final ModelMetaData jmd;
039:
040: public JdbcEJBQLCompiler(JdbcStorageManager sm) {
041: this .sm = sm;
042: this .jmd = sm.getJmd();
043: }
044:
045: /**
046: * Compile a QueryDetails into a JdbcCompiledQuery ready to run.
047: */
048: public JdbcCompiledQuery compile(QueryDetails q) {
049: Node tree = parse(q);
050: if (Debug.DEBUG) {
051: System.out.println("\n%%% parsed:\n" + tree + "\n");
052: }
053:
054: ResolveContext rc = new ResolveContext(jmd);
055: tree.resolve(rc);
056: if (Debug.DEBUG) {
057: System.out.println("%%% resolved:\n" + tree + "\n");
058: rc.dump(System.out);
059: System.out.println();
060: }
061:
062: EJBQLNodeToSqlExp converter = new EJBQLNodeToSqlExp(rc, sm
063: .getSqlDriver());
064: SqlExp sqlExp = converter.toSqlExp(tree, null);
065: if (!(sqlExp instanceof SelectExp)) {
066: throw BindingSupportImpl.getInstance().internal(
067: "not supported");
068: }
069: SelectExp root = (SelectExp) sqlExp;
070: FetchSpec spec = root.fetchSpec;
071: compileParams(rc, spec);
072: if (Debug.DEBUG) {
073: System.out.println("%%% root:");
074: root.dump(" ");
075: System.out.println("\n%%% root.fetchSpec");
076: spec.printPlan(System.out, " ");
077: }
078:
079: ClassMetaData candidateCMD = rc.getRoot(0)
080: .getNavClassMetaData();
081: JdbcCompiledQueryEJBQL cq = new JdbcCompiledQueryEJBQL(
082: candidateCMD, q, spec);
083:
084: CmdBitSet bits = new CmdBitSet(jmd);
085: for (int i = rc.getRootCount() - 1; i >= 0; i--) {
086: rc.getRoot(i).addInvolvedClasses(bits);
087: }
088: int[] a = q.getExtraEvictClasses();
089: if (a != null) {
090: for (int i = a.length - 1; i >= 0; i--) {
091: bits.add(jmd.classes[a[i]]);
092: }
093: }
094: cq.setFilterClsIndexs(bits.toArray());
095: cq.setEvictionClassBits(bits.getBits());
096: cq.setEvictionClassIndexes(bits.getIndexes());
097:
098: return cq;
099: }
100:
101: private Node parse(QueryDetails q) {
102: String filter = q.getFilter();
103: if (filter == null) {
104: throw BindingSupportImpl.getInstance().invalidOperation(
105: "EJBQL query string (filter) is null");
106: }
107: StringReader r = new StringReader(filter);
108: EJBQLParser p = new EJBQLParser(r);
109: try {
110: return p.ejbqlQuery();
111: } catch (ParseException e) {
112: throw BindingSupportImpl.getInstance().invalidOperation(
113: e.getMessage());
114: }
115: }
116:
117: /**
118: * This nasty code has to find the bits of SQL occupied by parameter
119: * expressions that could be null. They may need to be converted into
120: * 'is null' or 'is not null' or removed completely (for shared columns)
121: * if the corresponding parameter is null. This is not required by the
122: * JSR 220 spec but we already have it for JDOQL so might as well have
123: * it for EJBQL as well.
124: *
125: * The parameter code is way too complex and needs to be refactored. A
126: * better solution would be to create a new 'fake parameter' for each time
127: * a given input parameter is used in the query. That would simplify
128: * things a lot. The real parameters to the query can be easily expanded
129: * to match the list of 'fake parameters'. This would simplify things by
130: * getting rid of the first layer of usage.
131: */
132: private void compileParams(ResolveContext rc, FetchSpec spec) {
133: ResolveContext.ParamUsage[] params = rc.getParameters();
134: if (params == null) {
135: return;
136: }
137: SqlBuffer.Param list = null;
138: SqlBuffer.Param pos = null;
139: int np = params.length;
140: for (int i = 0; i < np; i++) {
141: for (ResolveContext.ParamUsage rcUsage = params[i]; rcUsage != null; rcUsage = rcUsage
142: .getNext()) {
143: SqlParamUsage usage = (SqlParamUsage) rcUsage.storeObject;
144:
145: // create new param and add it to the list
146: SqlBuffer.Param param = new SqlBuffer.Param(rcUsage
147: .getParamNode().getName());
148: if (pos == null) {
149: pos = list = param;
150: } else {
151: pos = pos.next = param;
152: }
153:
154: // fill in the param
155: param.declaredParamIndex = rcUsage.getIndex();
156: JdbcField jdbcField = usage.jdbcField;
157: if (jdbcField == null) {
158: param.classIndex = usage.classIndex;
159: param.fieldNo = -1;
160: param.javaTypeCode = usage.javaTypeCode;
161: // todo param.javaTypeCode == 0: get type from value?
162: param.jdbcType = usage.jdbcType;
163: param.col = usage.col;
164: } else {
165: param.classIndex = jdbcField.fmd.classMetaData.index;
166: param.fieldNo = jdbcField.stateFieldNo;
167: param.col = usage.col;
168: }
169: param.mod = usage.mod;
170:
171: // make a CharSpan for each ? in the SQL
172: if (usage.expCount > 0) {
173: SqlBuffer.CharSpan cspos = null;
174: SqlBuffer.CharSpan[] a = new SqlBuffer.CharSpan[usage.expCount];
175: boolean multicol = usage.expCount > 1;
176: int j = 0;
177: int removeCount = 0;
178: for (SqlExp e = usage.expList; j < a.length; j++) {
179: SqlBuffer.CharSpan cs = a[j] = new SqlBuffer.CharSpan();
180: if (multicol && mustBeRemovedIfNull(e)) {
181: if (++removeCount == a.length) {
182: // all expressions are to be removed so restart
183: // the loop making them all 'is null' instead
184: multicol = false;
185: e = usage.expList;
186: j = -1;
187: cspos = null;
188: continue;
189: }
190: cs.firstCharIndex = e
191: .getPreFirstCharIndex();
192: if (e.next == null) { // last span
193: cs.lastCharIndex = e.getLastCharIndex();
194: // work back and remove trailing 'and' if any
195: for (int k = j - 1; k >= 0; k--) {
196: if (a[k].type != SqlBuffer.CharSpan.TYPE_REMOVE) {
197: a[k + 1].firstCharIndex -= 4; // 'and '
198: break;
199: }
200: }
201: } else { // first or middle span
202: cs.lastCharIndex = e.next
203: .getPreFirstCharIndex();
204: }
205: cs.type = SqlBuffer.CharSpan.TYPE_REMOVE;
206: } else {
207: cs.firstCharIndex = e.getFirstCharIndex();
208: cs.lastCharIndex = e.getLastCharIndex();
209: cs.type = e.isNegative() ? SqlBuffer.CharSpan.TYPE_NOT_NULL
210: : SqlBuffer.CharSpan.TYPE_NULL;
211: }
212:
213: if (cspos == null) {
214: cspos = param.charSpanList = cs;
215: param.firstCharIndex = cs.firstCharIndex;
216: } else {
217: cspos = cspos.next = cs;
218: }
219:
220: e = e.next;
221: }
222: } else {
223: param.firstCharIndex = usage.expList
224: .getFirstCharIndex();
225: }
226: }
227: }
228: if (list != null) {
229: spec.setParamList(sortParams(list));
230: }
231: }
232:
233: /**
234: * Columns that are not updated (i.e. are shared) must be removed
235: * completely if the matching parameter is null.
236: */
237: private static boolean mustBeRemovedIfNull(SqlExp e) {
238: if (!e.isNegative() && e.childList instanceof ColumnExp) {
239: ColumnExp ce = (ColumnExp) e.childList;
240: return !ce.col.isForUpdate();
241: }
242: return false;
243: }
244:
245: /**
246: * Sort the params in the order that they appear in the query.
247: */
248: private static SqlBuffer.Param sortParams(SqlBuffer.Param list) {
249: if (list.next == null)
250: return list;
251: // stone sort the list (bubble sort except elements sink to the bottom)
252: for (;;) {
253: boolean changed = false;
254: SqlBuffer.Param p0 = null;
255: for (SqlBuffer.Param p1 = list;;) {
256: SqlBuffer.Param p2 = p1.next;
257: if (p2 == null)
258: break;
259: if (p1.firstCharIndex > p2.firstCharIndex) {
260: // exchange p and p2
261: p1.next = p2.next;
262: p2.next = p1;
263: if (p0 == null) {
264: list = p2;
265: } else {
266: p0.next = p2;
267: }
268: p0 = p2;
269: changed = true;
270: } else {
271: p0 = p1;
272: p1 = p2;
273: }
274: }
275: if (!changed)
276: return list;
277: }
278: }
279:
280: /*
281: private void dumpParams(JdbcCompiledQuery cq, SqlBuffer.Param p) {
282: for (; p != null; p = p.next) {
283: if (Debug.DEBUG) {
284: Debug.OUT.println("Param " + p.declaredParamIndex +
285: " firstCharIndex " + p.firstCharIndex);
286: }
287: for (SqlBuffer.CharSpan s = p.charSpanList; s != null; s = s.next) {
288: if (Debug.DEBUG) {
289: String ts;
290: switch (s.type) {
291: case SqlBuffer.CharSpan.TYPE_NULL:
292: ts = "NULL";
293: break;
294: case SqlBuffer.CharSpan.TYPE_NOT_NULL:
295: ts = "NOT_NULL";
296: break;
297: case SqlBuffer.CharSpan.TYPE_REMOVE:
298: ts = "REMOVE";
299: break;
300: default:
301: ts = "Unknown(" + s.type + ")";
302: }
303: Debug.OUT.println(" CharSpan " + s.firstCharIndex + " to " +
304: s.lastCharIndex + " " + ts + " = '" +
305: cq.getSqlbuf().toString(s.firstCharIndex,
306: s.lastCharIndex - s.firstCharIndex) + "'");
307: }
308: }
309: }
310: }
311: */
312:
313: }
|