001: ///////////////////////////////
002: // Makumba, Makumba tag library
003: // Copyright (C) 2000-2003 http://www.makumba.org
004: //
005: // This library is free software; you can redistribute it and/or
006: // modify it under the terms of the GNU Lesser General Public
007: // License as published by the Free Software Foundation; either
008: // version 2.1 of the License, or (at your option) any later version.
009: //
010: // This library is distributed in the hope that it will be useful,
011: // but WITHOUT ANY WARRANTY; without even the implied warranty of
012: // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
013: // Lesser General Public License for more details.
014: //
015: // You should have received a copy of the GNU Lesser General Public
016: // License along with this library; if not, write to the Free Software
017: // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
018: //
019: // -------------
020: // $Id: ComposedQuery.java 2150 2007-11-22 17:16:18Z cristian_bogdan $
021: // $Name$
022: /////////////////////////////////////
023:
024: package org.makumba.list.engine;
025:
026: import java.util.Enumeration;
027: import java.util.HashMap;
028: import java.util.Hashtable;
029: import java.util.Iterator;
030: import java.util.Map;
031: import java.util.StringTokenizer;
032: import java.util.Vector;
033:
034: import org.makumba.DataDefinition;
035: import org.makumba.FieldDefinition;
036: import org.makumba.InvalidFieldTypeException;
037: import org.makumba.LogicException;
038: import org.makumba.commons.ArgumentReplacer;
039: import org.makumba.list.tags.QueryTag;
040: import org.makumba.providers.QueryProvider;
041:
042: /**
043: * An OQL query composed from various elements found in script pages. It can be enriched when a new element is found. It
044: * has a prepared Qyuery correspondent in a makumba database It may be based on a super query.
045: *
046: * @author Cristian Bogdan
047: * @version $Id: ComposedQuery.java 2150 2007-11-22 17:16:18Z cristian_bogdan $
048: */
049: public class ComposedQuery {
050:
051: /**
052: * Interface for an Evaluator which can evaluate expressions
053: *
054: * @author Cristian Bogdan
055: */
056: public static interface Evaluator {
057:
058: /**
059: * Evaluates the expression
060: *
061: * @param s
062: * the expression to evaluate
063: * @return The transformed expression after evaluation
064: */
065: String evaluate(String s);
066: }
067:
068: public QueryProvider qep = null;
069:
070: /**
071: * Default constructor
072: *
073: * @param sections
074: * @param usesHQL
075: */
076: public ComposedQuery(String[] sections, String queryLanguage) {
077: this .sections = sections;
078: this .derivedSections = sections;
079: this .qep = QueryProvider.makeQueryAnalzyer(queryLanguage);
080: }
081:
082: /** The subqueries of this query */
083: Vector<ComposedSubquery> subqueries = new Vector<ComposedSubquery>();
084:
085: /** The projections made in this query */
086: Vector<Object> projections = new Vector<Object>();
087:
088: /** The expression associated to each projection */
089: Hashtable<String, Integer> projectionExpr = new Hashtable<String, Integer>();
090:
091: /** Standard index for the FROM query section */
092: public static final int FROM = 0;
093:
094: /** Standard index for the WHERE query section */
095: public static final int WHERE = 1;
096:
097: /** Standard index for the GROUPBY query section */
098: public static final int GROUPBY = 2;
099:
100: /** Standard index for the ORDERBY query section */
101: public static final int ORDERBY = 3;
102:
103: /** Standard index for the VARFROM query section */
104: public static final int VARFROM = 4;
105:
106: /** Section texts, encoded with the standard indexes */
107: String[] sections;
108:
109: /** Derived section texts, made from the sections of this query and the sections of its superqueries */
110: String[] derivedSections;
111:
112: String typeAnalyzerOQL;
113:
114: String fromAnalyzerOQL;
115:
116: /**
117: * The keyset defining the primary key for this query. Normally the primary key is made of the keys declared in
118: * FROM, in this query and all the parent queries. Keys are kept as integers (indexes)
119: */
120: Vector<Integer> keyset;
121:
122: /** The keyset of all the parent queries */
123: Vector previousKeyset;
124:
125: /** The labels of the keyset */
126: Vector<String> keysetLabels;
127:
128: /** A Vector containing and empty vector. Used for empty keysets */
129: static Vector empty;
130: static {
131: empty = new Vector();
132: empty.addElement(new Vector());
133: }
134:
135: /**
136: * Gets the type of the result
137: *
138: * @return The DataDefinition corresponding to the type of the result
139: */
140: public DataDefinition getResultType() {
141: if (typeAnalyzerOQL == null) {
142: return null;
143: } else {
144: return qep.getQueryAnalysis(typeAnalyzerOQL)
145: .getProjectionType();
146: }
147: }
148:
149: /**
150: * Gets the type of a given label
151: *
152: * @param s
153: * the name of the label
154: * @return A DataDefinition corresponding to the type of the label
155: */
156: public DataDefinition getLabelType(String s) {
157: if (typeAnalyzerOQL == null) {
158: return null;
159: } else {
160: return qep.getQueryAnalysis(typeAnalyzerOQL)
161: .getLabelType(s);
162: }
163: }
164:
165: /**
166: * Initializes the object. This is a template method
167: */
168: public void init() {
169: initKeysets();
170: fromAnalyzerOQL = "SELECT 1 ";
171: if (getFromSection() != null)
172: fromAnalyzerOQL += "FROM " + getFromSection();
173: }
174:
175: /**
176: * Gets the FROM section
177: *
178: * @return A String containing the FROM section of the query
179: */
180: public String getFromSection() {
181: return derivedSections[FROM];
182: }
183:
184: /**
185: * Initializes the keysets. previousKeyset is "empty"
186: */
187: protected void initKeysets() {
188: previousKeyset = empty;
189: keyset = new Vector<Integer>();
190: keysetLabels = new Vector<String>();
191: }
192:
193: /**
194: * Adds a subquery to this query. Makes it aware that it has subqueries at all. Makes it be able to announce its
195: * subqueries about changes (this will be needed when unique=true will be possible)
196: *
197: * @param q
198: * the subquery
199: */
200: protected void addSubquery(ComposedSubquery q) {
201: if (subqueries.size() == 0)
202: prependFromToKeyset();
203: subqueries.addElement(q);
204: }
205:
206: /**
207: * Adds all keys from the FROM section to the keyset, and their labels to the keyLabels. They are all added as
208: * projections (this has to change)
209: */
210: protected void prependFromToKeyset() {
211: projectionExpr.clear();
212: Enumeration e = ((Vector) projections.clone()).elements();
213: projections.removeAllElements();
214:
215: // add the previous keyset
216: for (int i = 0; i < keyset.size(); i++)
217: checkProjectionInteger((String) e.nextElement());
218:
219: for (StringTokenizer st = new StringTokenizer(
220: sections[FROM] == null ? "" : sections[FROM], ","); st
221: .hasMoreTokens();) {
222: String label = st.nextToken().trim();
223: int j = label.lastIndexOf(" ");
224: if (j == -1)
225: throw new RuntimeException("invalid FROM");
226: label = label.substring(j + 1).trim();
227:
228: label = qep.getPrimaryKeyNotation(label);
229:
230: keysetLabels.addElement(label);
231:
232: keyset.addElement(addProjection(label));
233: }
234:
235: while (e.hasMoreElements())
236: checkProjectionInteger((String) e.nextElement());
237: }
238:
239: /**
240: * Gets a given projection
241: *
242: * @param n
243: * the index of the projection
244: * @return A String containing the projection
245: */
246: public String getProjectionAt(int n) {
247: return (String) projections.elementAt(n);
248: }
249:
250: /**
251: * Adds a projection with the given expression
252: *
253: * @param expr
254: * the expression to add
255: * @return The index at which the expression was added
256: */
257: Integer addProjection(String expr) {
258: Integer index = new Integer(projections.size());
259: projections.addElement(expr);
260: projectionExpr.put(expr, index);
261: return index;
262: }
263:
264: /**
265: * Checks if a projection exists, and if not, adds it.
266: *
267: * @param expr
268: * the expression to add
269: * @return The index of the added projection
270: */
271: public Integer checkProjectionInteger(String expr) {
272: Integer index = (Integer) projectionExpr.get(expr);
273: if (index == null) {
274: addProjection(expr);
275: // FIXME: if DISTINCT is true, need to recompute the keyset and notify the subqueries to recompute their
276: // previous keyset
277: return null;
278: }
279: return index;
280: }
281:
282: /**
283: * Checks if a projection exists, and if not, adds it.
284: *
285: * @param expr
286: * the expression to add
287: * @return The column name of the projection
288: */
289: String checkProjection(String expr) {
290: Integer i = checkProjectionInteger(expr);
291: if (i == null)
292: return null;
293: return columnName(i);
294: }
295:
296: /**
297: * Gets the name of a column indicated by index
298: *
299: * @param n
300: * the index of the column
301: * @return A String containing the name of the column, of the kind "colN"
302: */
303: public static String columnName(Integer n) {
304: return "col" + (n.intValue() + 1);
305: }
306:
307: /**
308: * Checks the orderBy or groupBy expressions to see if they are already selected, if not adds a projection. Only
309: * group by and order by labels.
310: *
311: * @param str
312: * an orderBy or groupBy expression
313: * @return The checked expression, transformed according to the projections
314: */
315: String checkExpr(String str) {
316: if (!qep.selectGroupOrOrderAsLabels())
317: return str;
318: if (str == null)
319: return null;
320: if (str.trim().length() == 0)
321: return null;
322: // if(projections.size()==1)
323: // new Throwable().printStackTrace();
324:
325: StringBuffer ret = new StringBuffer();
326: String sep = "";
327: for (StringTokenizer st = new StringTokenizer(str, ","); st
328: .hasMoreTokens();) {
329: ret.append(sep);
330: sep = ",";
331: String s = st.nextToken().trim();
332: String rest = "";
333: int i = s.indexOf(" ");
334: if (i != -1) {
335: rest = s.substring(i);
336: s = s.substring(0, i);
337: }
338: // if the projection doesnt exist, this returns null, but it adds a new projection
339: String p = checkProjection(s);
340: if (p == null)
341: // and the second time this doesn#t return null, but the projection name
342: p = checkProjection(s);
343: ret.append(p).append(rest);
344: }
345: return ret.toString();
346: }
347:
348: /**
349: * Computes the query from its sections
350: *
351: * @param derivedSections
352: * the sections of this query
353: * @param typeAnalysisOnly
354: * indicates whether this is only a type analysis
355: * @return The computed OQL query
356: */
357: protected String computeQuery(String derivedSections[],
358: boolean typeAnalysisOnly) {
359: String groups = null;
360: String orders = null;
361: if (!typeAnalysisOnly) {
362: groups = checkExpr((String) derivedSections[GROUPBY]);
363: orders = checkExpr((String) derivedSections[ORDERBY]);
364: }
365:
366: StringBuffer sb = new StringBuffer();
367: sb.append("SELECT ");
368: String sep = "";
369:
370: int i = 0;
371:
372: for (Enumeration e = projections.elements(); e
373: .hasMoreElements();) {
374: sb.append(sep);
375: sep = ",";
376: sb.append(e.nextElement()).append(" AS ").append(
377: columnName(new Integer(i++)));
378: }
379: Object o;
380:
381: if ((o = derivedSections[FROM]) != null) {
382: sb.append(" FROM ");
383: sb.append(o);
384:
385: // there can be no VARFROM without FROM
386: // VARFROM is not part of type analysis
387: // (i.e. projections don't know about it)
388: if (!typeAnalysisOnly && derivedSections.length == 5
389: && derivedSections[VARFROM] != null
390: && derivedSections[VARFROM].trim().length() > 0)
391: sb.append(",").append(derivedSections[VARFROM]);
392: }
393: if (!typeAnalysisOnly) {
394: if ((o = derivedSections[WHERE]) != null
395: && derivedSections[WHERE].trim().length() > 0) {
396: sb.append(" WHERE ");
397: sb.append(o);
398: }
399: if (groups != null) {
400: sb.append(" GROUP BY ");
401: sb.append(groups);
402: }
403: if (orders != null) {
404: sb.append(" ORDER BY ");
405: sb.append(orders);
406: }
407: }
408: String ret = sb.toString();
409: if (!typeAnalysisOnly)
410: return ret;
411:
412: // replace names with numbers
413: ArgumentReplacer ar = new ArgumentReplacer(ret);
414: Map<String, Object> d = new HashMap<String, Object>();
415: int j = 1;
416: for (Iterator<String> e = ar.getArgumentNames(); e.hasNext();)
417: d.put(e.next(), "$" + (j++));
418: return ar.replaceValues(d);
419: }
420:
421: // ------------
422: /**
423: * Executes the contained query in the given database
424: *
425: * @param qep
426: * the database where the query should be ran
427: * @param args
428: * the arguments we may need during the execution
429: * @param v
430: * the evaluator evaluating the expressions
431: * @param offset
432: * at which iteration this query should start
433: * @param limit
434: * how many times should this query be ran
435: * @throws LogicException
436: */
437: public Grouper execute(QueryProvider qep, Map args, Evaluator v,
438: int offset, int limit) throws LogicException {
439: analyze();
440: String[] vars = new String[5];
441: vars[0] = getFromSection();
442: for (int i = 1; i < 5; i++)
443: vars[i] = derivedSections[i] == null ? null : v
444: .evaluate(derivedSections[i]);
445:
446: return new Grouper(previousKeyset, qep.execute(
447: computeQuery(vars, false), args, offset, limit)
448: .elements());
449: }
450:
451: public synchronized void analyze() {
452: if (projections.isEmpty())
453: prependFromToKeyset();
454: if (typeAnalyzerOQL == null)
455: typeAnalyzerOQL = computeQuery(derivedSections, true);
456: }
457:
458: /**
459: * Checks if an expression is valid, nullable or set
460: *
461: * @param expr
462: * the expression
463: * @return The path to the null pointer (if the object is nullable), <code>null</code> otherwise
464: */
465: public Object checkExprSetOrNullable(String expr) {
466: if (expr.toLowerCase().indexOf(" from ") != -1)
467: // subqueries do not need separate queries
468: return null;
469: int n = 0;
470: int m = 0;
471: while (true) {
472: // FIXME: this is a not that good algorithm for finding label.field1.fiel2.field3
473: while (n < expr.length() && !isMakId(expr.charAt(n)))
474: n++;
475:
476: if (n == expr.length())
477: return null;
478: m = n;
479: while (n < expr.length() && isMakId(expr.charAt(n)))
480: n++;
481: Object nl = checkLabelSetOrNullable(expr.substring(m, n));
482: if (nl != null)
483: return nl;
484: if (n == expr.length())
485: return null;
486: }
487: }
488:
489: /**
490: * Checks if a character can be part of a makumba identifier
491: *
492: * @param c
493: * the character to check
494: * @return <code>true</code> if the character can be part of a makumba identifier, <code>false</code> otherwise
495: */
496: static boolean isMakId(char c) {
497: return Character.isJavaIdentifierPart(c) || c == '.';
498: }
499:
500: /**
501: * Checks if an id is nullable, and if so, return the path to the null pointer
502: *
503: * @param referenceSequence
504: * a sequence like field1.field2.field3
505: * @return The path to the null pointer (if the object is nullable), <code>null</code> otherwise
506: */
507: public Object checkLabelSetOrNullable(String referenceSequence) {
508: int dot = referenceSequence.indexOf(".");
509: if (dot == -1)
510: return null;
511: String substring = referenceSequence.substring(0, dot);
512: try { // if the "label" is actually a real number as 3.0
513: Integer.parseInt(substring);
514: return null; // if so, just return
515: } catch (NumberFormatException e) {
516: }
517: DataDefinition dd = qep.getQueryAnalysis(fromAnalyzerOQL)
518: .getLabelType(substring);
519: if (dd == null)
520: throw new org.makumba.InvalidValueException(
521: "no such label " + substring);
522: while (true) {
523: int dot1 = referenceSequence.indexOf(".", dot + 1);
524: if (dot1 == -1) {
525: String fn = referenceSequence.substring(dot + 1);
526: FieldDefinition fd = dd.getFieldDefinition(fn);
527: if (fd == null
528: && (fd = qep.getAlternativeField(dd, fn)) == null)
529: throw new org.makumba.NoSuchFieldException(dd, fn);
530:
531: if (fd.getType().equals("set"))
532: return fd;
533: return null;
534: }
535: FieldDefinition fd = dd
536: .getFieldDefinition(referenceSequence.substring(
537: dot + 1, dot1));
538: if (fd == null)
539: throw new org.makumba.NoSuchFieldException(dd,
540: referenceSequence.substring(dot + 1, dot1));
541: if (!fd.getType().startsWith("ptr"))
542: throw new InvalidFieldTypeException(fd, "pointer");
543: if (!fd.isNotNull())
544: return referenceSequence.substring(0, dot1);
545: dd = fd.getPointedType();
546: dot = dot1;
547: }
548: }
549:
550: /**
551: * allows to directly set a projection. Used for totalCount in
552: * {@link QueryTag#doAnalyzedStartTag(org.makumba.analyser.PageCache)} to compose a query with 'count(*)' as the
553: * only projection.
554: */
555: public void addProjection(Object o) {
556: projections.add(o);
557: }
558: }
|