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: QueryTag.java 1950 2007-10-26 19:50:01Z rosso_nero $
021: // $Name$
022: /////////////////////////////////////
023:
024: package org.makumba.list.tags;
025:
026: import javax.servlet.ServletRequest;
027: import javax.servlet.jsp.JspException;
028: import javax.servlet.jsp.PageContext;
029: import javax.servlet.jsp.tagext.IterationTag;
030:
031: import org.makumba.LogicException;
032: import org.makumba.MakumbaError;
033: import org.makumba.MakumbaSystem;
034: import org.makumba.ProgrammerError;
035: import org.makumba.analyser.AnalysableTag;
036: import org.makumba.analyser.PageCache;
037: import org.makumba.commons.MakumbaJspAnalyzer;
038: import org.makumba.commons.MultipleKey;
039: import org.makumba.commons.RuntimeWrappedException;
040: import org.makumba.commons.formatters.RecordFormatter;
041: import org.makumba.list.engine.ComposedQuery;
042: import org.makumba.list.engine.ComposedSubquery;
043: import org.makumba.list.engine.QueryExecution;
044: import org.makumba.list.engine.valuecomputer.ValueComputer;
045: import org.makumba.list.html.RecordViewer;
046:
047: /**
048: * Display of OQL query results in nested loops. The Query FROM, WHERE, GROUPBY and ORDERBY are indicated in the head of
049: * the tag. The query projections are indicated by Value tags in the body of the tag. The sub-tags will generate
050: * subqueries of their enclosing tag queries (i.e. their WHERE, GROUPBY and ORDERBY are concatenated). Attributes of the
051: * environment can be passed as $attrName to the query
052: *
053: * @author Cristian Bogdan
054: * @version $Id: QueryTag.java 1950 2007-10-26 19:50:01Z rosso_nero $
055: */
056: public class QueryTag extends GenericListTag implements IterationTag {
057:
058: private static final long serialVersionUID = 1L;
059:
060: String[] queryProps = new String[5];
061:
062: String separator = "";
063:
064: String countVar;
065:
066: String maxCountVar;
067:
068: String offset, limit;
069:
070: static String standardCountVar = "org_makumba_view_jsptaglib_countVar";
071:
072: static String standardMaxCountVar = "org_makumba_view_jsptaglib_maxCountVar";
073:
074: static String standardLastCountVar = "org_makumba_view_jsptaglib_lastCountVar";
075:
076: static String standardMaxResultsVar = "org_makumba_view_jsptaglib_MaxResultsVar";
077:
078: static String standardMaxResultsContext = "org_makumba_view_jsptaglib_MaxResultsContext";
079:
080: static String standardMaxResultsKey = "org_makumba_view_jsptaglib_MaxResultsKey";
081:
082: public void setFrom(String s) {
083: queryProps[ComposedQuery.FROM] = s;
084: }
085:
086: public void setVariableFrom(String s) {
087: queryProps[ComposedQuery.VARFROM] = s;
088: }
089:
090: public void setWhere(String s) {
091: queryProps[ComposedQuery.WHERE] = s;
092: }
093:
094: public void setOrderBy(String s) {
095: queryProps[ComposedQuery.ORDERBY] = s;
096: }
097:
098: public void setGroupBy(String s) {
099: queryProps[ComposedQuery.GROUPBY] = s;
100: }
101:
102: public void setSeparator(String s) {
103: separator = s;
104: }
105:
106: public void setCountVar(String s) {
107: countVar = s;
108: }
109:
110: public void setMaxCountVar(String s) {
111: maxCountVar = s;
112: }
113:
114: public void setOffset(String s) throws JspException {
115: onlyOuterListArgument("offset");
116: onlyInt("offset", s);
117: offset = s.trim();
118: }
119:
120: public void setLimit(String s) throws JspException {
121: onlyOuterListArgument("limit");
122: onlyInt("limit", s);
123: limit = s.trim();
124: }
125:
126: protected void onlyOuterListArgument(String s) throws JspException {
127: QueryTag t = (QueryTag) findAncestorWithClass(this ,
128: QueryTag.class);
129: while (t != null && t instanceof ObjectTag)
130: t = (QueryTag) findAncestorWithClass(t, QueryTag.class);
131: if (t instanceof QueryTag)
132: throw new RuntimeWrappedException(
133: new MakumbaJspException(
134: this ,
135: "the "
136: + s
137: + " parameter can only be set for the outermost mak:list tag"));
138: }
139:
140: protected void onlyInt(String s, String value) throws JspException {
141: value = value.trim();
142: if (value.startsWith("$"))
143: return;
144: try {
145: Integer.parseInt(value);
146: } catch (NumberFormatException nfe) {
147: throw new RuntimeWrappedException(
148: new MakumbaJspException(
149: this ,
150: "the "
151: + s
152: + " parameter can only be an $attribute or an int"));
153:
154: }
155: }
156:
157: // runtime stuff
158: QueryExecution execution;
159:
160: /**
161: * Computes and set the tagKey. At analisys time, the listQuery is associated with the tagKey, and retrieved at
162: * runtime. At runtime, the QueryExecution is discovered by the tag based on the tagKey.
163: *
164: * @param pageCache
165: * The page cache for the current page
166: */
167: public void setTagKey(PageCache pageCache) {
168: tagKey = new MultipleKey(queryProps.length + 2);
169: for (int i = 0; i < queryProps.length; i++)
170: tagKey.setAt(queryProps[i], i);
171:
172: // if we have a parent, we append the key of the parent
173: tagKey.setAt(getParentListKey(this , pageCache),
174: queryProps.length);
175: tagKey.setAt(id, queryProps.length + 1);
176: }
177:
178: /**
179: * Determines whether the tag can have the same key as others in the page
180: *
181: * @return <code>true</code> if the tag is allowed to have the same key as others in the page, <code>false</code>
182: * otherwise
183: */
184: public boolean allowsIdenticalKey() {
185: return false;
186: }
187:
188: /**
189: * Starts the analysis of the tag, without knowing what tags follow it in the page. Defines a query, sets the types
190: * of variables to "int".
191: *
192: * @param pageCache
193: * The page cache for the current page
194: */
195: public void doStartAnalyze(PageCache pageCache) {
196: // check whether we have an $.. in the order by (not supported, only #{..} is allowed
197: String orderBy = queryProps[ComposedQuery.ORDERBY];
198: if (orderBy != null && orderBy.indexOf("$") != -1) {
199: throw new ProgrammerError(
200: "Illegal use of an $attribute orderBy: '"
201: + orderBy
202: + "' ==> only JSP Expression Language using #{..} is allowed!");
203: }
204:
205: // we make ComposedQuery cache our query
206: QueryTag.cacheQuery(pageCache, tagKey, queryProps,
207: getParentListKey(this , pageCache));
208:
209: if (countVar != null)
210: setType(pageCache, countVar, MakumbaSystem.makeFieldOfType(
211: countVar, "int"));
212:
213: if (maxCountVar != null)
214: setType(pageCache, maxCountVar, MakumbaSystem
215: .makeFieldOfType(maxCountVar, "int"));
216: }
217:
218: /**
219: * Ends the analysis of the tag, after all tags in the page were visited. As all the query projections are known, a
220: * RecordViewer is cached as formatter for the mak:values nested in this tag.
221: *
222: * @param pageCache
223: * The page cache for the current page
224: */
225: public void doEndAnalyze(PageCache pageCache) {
226: ComposedQuery cq = QueryTag.getQuery(pageCache, tagKey);
227: cq.analyze();
228: pageCache.cache(RecordFormatter.FORMATTERS, tagKey,
229: new RecordViewer(cq));
230: }
231:
232: static final Integer zero = new Integer(0);
233:
234: static final Integer one = new Integer(1);
235:
236: Object upperCount = null;
237:
238: Object upperMaxCount = null;
239:
240: ValueComputer choiceComputer;
241:
242: private static ThreadLocal<ServletRequest> servletRequestThreadLocal = new ThreadLocal<ServletRequest>();
243:
244: /**
245: * Decides if there will be any tag iteration. The QueryExecution is found (and made if needed), and we check if
246: * there are any results in the iterationGroup.
247: *
248: * @param pageCache
249: * The page cache for the current page
250: * @return The tag return state as defined in the {@link javax.servlet.jsp.tagext.Tag} interface
251: * @see QueryExecution
252: */
253: public int doAnalyzedStartTag(PageCache pageCache)
254: throws LogicException, JspException {
255: servletRequestThreadLocal.set(pageContext.getRequest());
256: if (getParentList(this ) == null)
257: QueryExecution.startListGroup(pageContext);
258: else {
259: upperCount = pageContext.getRequest().getAttribute(
260: standardCountVar);
261: upperMaxCount = pageContext.getRequest().getAttribute(
262: standardMaxCountVar);
263: }
264:
265: execution = QueryExecution.getFor(tagKey, pageContext, offset,
266: limit);
267:
268: int n = execution.onParentIteration();
269:
270: setNumberOfIterations(n);
271:
272: // set the total result count, i.e. the count this list would have w/o limit & offset
273: int maxResults = Integer.MIN_VALUE;
274: int limitEval = QueryExecution.computeLimit(pageContext, limit,
275: -1);
276: int offsetEval = QueryExecution.computeLimit(pageContext,
277: offset, 0);
278: if ((offsetEval == 0 && limitEval == -1)
279: || (offsetEval == 0 && (limitEval > 00 && limitEval < n))) {
280: // we can set the total count if there is no limit / offset in the page
281: maxResults = n;
282: } else {
283: // otherwise we need to make a new query
284: // we only prepare the query, but do not run it, that will happen on demand in the method getting the result
285: ComposedQuery query = null;
286: String[] simpleQueryProps = queryProps.clone();
287: simpleQueryProps[ComposedQuery.ORDERBY] = "";
288: MultipleKey maxResultsKey = getMaxResultsKey(tagKey);
289: MultipleKey parentKey = getParentListKey(this , pageCache);
290: String ql = (String) pageCache.retrieve(
291: MakumbaJspAnalyzer.QUERY_LANGUAGE,
292: MakumbaJspAnalyzer.QUERY_LANGUAGE);
293: query = parentKey == null ? new ComposedQuery(
294: simpleQueryProps, ql) : new ComposedSubquery(
295: simpleQueryProps, QueryTag.getQuery(pageCache,
296: parentKey), ql);
297: query.addProjection("count(*)");
298: query.init();
299: pageCache.cache(GenericListTag.QUERY, maxResultsKey, query);
300: // we need to pass these variables in request to the method doing the query
301: // TODO: this looks like a hack, and might not be safe if there are more lists in the same page
302: pageContext.getRequest().setAttribute(
303: standardMaxResultsContext, pageContext);
304: pageContext.getRequest().setAttribute(
305: standardMaxResultsKey, maxResultsKey);
306: }
307: pageContext.getRequest().setAttribute(standardMaxResultsVar,
308: maxResults);
309:
310: if (n > 0) {
311: if (countVar != null)
312: pageContext.setAttribute(countVar, one);
313: pageContext.getRequest()
314: .setAttribute(standardCountVar, one);
315: return EVAL_BODY_INCLUDE;
316: }
317: if (countVar != null)
318: pageContext.setAttribute(countVar, zero);
319: pageContext.getRequest().setAttribute(standardCountVar, zero);
320: return SKIP_BODY;
321: }
322:
323: private MultipleKey getMaxResultsKey(MultipleKey tagKey) {
324: MultipleKey totalKey = (MultipleKey) tagKey.clone();
325: totalKey.add(standardMaxResultsVar);
326: return totalKey;
327: }
328:
329: /**
330: * Sets the number of iterations in the iterationGroup. ObjectTag will redefine this and throw an exception if n>1
331: *
332: * @param n
333: * The number of iterations in the iterationGroup
334: * @throws JspException
335: * @see ObjectTag
336: */
337: protected void setNumberOfIterations(int n) throws JspException {
338: Integer cnt = new Integer(n);
339: if (maxCountVar != null)
340: pageContext.setAttribute(maxCountVar, cnt);
341: pageContext.getRequest().setAttribute(standardMaxCountVar, cnt);
342: }
343:
344: /**
345: * Decides whether to do further iterations. Checks if we got to the end of the iterationGroup.
346: *
347: * @return The tag return state as defined in the {@link javax.servlet.jsp.tagext.Tag} interface
348: * @throws JspException
349: */
350: public int doAfterBody() throws JspException {
351: runningTag.set(tagData);
352: try {
353:
354: int n = execution.nextGroupIteration();
355:
356: if (n != -1) {
357: // print the separator
358: try {
359: pageContext.getOut().print(separator);
360: } catch (Exception e) {
361: throw new MakumbaJspException(e);
362: }
363:
364: Integer cnt = new Integer(n + 1);
365: if (countVar != null)
366: pageContext.setAttribute(countVar, cnt);
367: pageContext.getRequest().setAttribute(standardCountVar,
368: cnt);
369: return EVAL_BODY_AGAIN;
370: }
371: return SKIP_BODY;
372: } finally {
373: runningTag.set(null);
374: }
375: }
376:
377: /**
378: * Cleans up variables, especially for the rootList.
379: *
380: * @param pageCache
381: * The page cache for the current page
382: * @return The tag return state as defined in the {@link javax.servlet.jsp.tagext.Tag} interface in order to
383: * continue evaluating the page.
384: * @throws JspException
385: */
386: public int doAnalyzedEndTag(PageCache pageCache)
387: throws JspException {
388: pageContext.getRequest().setAttribute(
389: standardLastCountVar,
390: pageContext.getRequest().getAttribute(
391: standardMaxCountVar));
392:
393: pageContext.getRequest().setAttribute(standardCountVar,
394: upperCount);
395: pageContext.getRequest().setAttribute(standardMaxCountVar,
396: upperMaxCount);
397: execution.endIterationGroup();
398:
399: if (getParentList(this ) == null)
400: QueryExecution.endListGroup(pageContext);
401:
402: execution = null;
403: queryProps[0] = queryProps[1] = queryProps[2] = queryProps[3] = null;
404: countVar = maxCountVar = null;
405: separator = "";
406: return EVAL_PAGE;
407: }
408:
409: /**
410: * Finds the parentList of a list
411: *
412: * @param tag
413: * the tag we want to discover the parent of
414: * @return the parent QueryTag of the Tag
415: */
416: public static AnalysableTag getParentList(AnalysableTag tag) {
417: return (AnalysableTag) findAncestorWithClass(tag,
418: QueryTag.class);
419: }
420:
421: /**
422: * Finds the key of the parentList of the Tag
423: *
424: * @param tag
425: * the tag we want to discover the parent of
426: * @param pageCache
427: * the pageCache of the current page
428: * @return The MultipleKey identifying the parentList
429: */
430: public static MultipleKey getParentListKey(AnalysableTag tag,
431: PageCache pageCache) {
432: AnalysableTag parentList = getParentList(tag);
433: return parentList == null ? null : parentList.getTagKey();
434: }
435:
436: /**
437: * Gets the query for a given key
438: *
439: * @param key
440: * the key of the tag for which we want to get a query
441: * @return The OQL query corresponding to this tag
442: */
443: public static ComposedQuery getQuery(PageCache pc, MultipleKey key) {
444: ComposedQuery ret = (ComposedQuery) pc.retrieve(
445: GenericListTag.QUERY, key);
446: if (ret == null)
447: throw new MakumbaError("unknown query for key " + key);
448: return ret;
449: }
450:
451: /**
452: * Gets a composed query from the cache, and if none is found, creates one and caches it.
453: *
454: * @param key
455: * the key of the tag
456: * @param sections
457: * the sections needed to compose a query
458: * @param parentKey
459: * the key of the parent tag, if any
460: */
461: public static ComposedQuery cacheQuery(PageCache pc,
462: MultipleKey key, String[] sections, MultipleKey parentKey) {
463: ComposedQuery ret = (ComposedQuery) pc.retrieve(
464: GenericListTag.QUERY, key);
465: if (ret != null)
466: return ret;
467: String ql = (String) pc.retrieve(
468: MakumbaJspAnalyzer.QUERY_LANGUAGE,
469: MakumbaJspAnalyzer.QUERY_LANGUAGE);
470: ret = parentKey == null ? new ComposedQuery(sections, ql)
471: : new ComposedSubquery(sections, QueryTag.getQuery(pc,
472: parentKey), ql);
473:
474: ret.init();
475: pc.cache(GenericListTag.QUERY, key, ret);
476: return ret;
477: }
478:
479: /**
480: * Gives the value of the iteration in progress
481: *
482: * @return The current count of iterations
483: */
484: public static int count() {
485: Object countAttr = servletRequestThreadLocal.get()
486: .getAttribute(standardCountVar);
487: if (countAttr == null) {
488: // throw new ProgrammerError("mak:count() can only be used inside a <mak:list> tag");
489: return -1;
490: }
491: return ((Integer) countAttr).intValue();
492: }
493:
494: /**
495: * Gives the maximum number of iteration of the iterationGroup
496: *
497: * @return The maximum number of iterations within the current iterationGroup
498: */
499: public static int maxCount() {
500: Object maxAttr = servletRequestThreadLocal.get().getAttribute(
501: standardMaxCountVar);
502: if (maxAttr == null) {
503: // throw new ProgrammerError("mak:maxCount() can only be used inside a <mak:list> tag");
504: return -1;
505: }
506: return ((Integer) maxAttr).intValue();
507: }
508:
509: /**
510: * Gives the maximum number of results returned as if the query would not contain any limit / offset. <br>
511: * TODO: we need to pass quite some information in the request attributes, as this method has to be static. Not sure
512: * what happens if there are more lists in the same page, if that would overlap or not.
513: *
514: * @return The maximum number of results returned as if the query would not contain any limit / offset.
515: */
516: public static int maxResults() {
517: ServletRequest servletRequest = servletRequestThreadLocal.get();
518: Object totalAttr = servletRequest
519: .getAttribute(standardMaxResultsVar);
520: if (totalAttr == null) {
521: // throw new ProgrammerError("mak:maxResults() can only be used inside a <mak:list> tag");
522: return -1;
523: }
524:
525: Integer total = ((Integer) totalAttr);
526: if (total == Integer.MIN_VALUE) { // we still need to evaluate this total count
527: PageContext pageContext = (PageContext) servletRequest
528: .getAttribute(standardMaxResultsContext);
529: MultipleKey keyMaxResults = (MultipleKey) servletRequest
530: .getAttribute(standardMaxResultsKey);
531: try {
532: QueryExecution exec = QueryExecution.getFor(
533: keyMaxResults, pageContext, null, null);
534: exec.getIterationGroupData();
535: total = ((Integer) exec.currentListData().get("col1"));
536: servletRequest.setAttribute(standardMaxResultsVar,
537: total);
538: } catch (LogicException e) {
539: e.printStackTrace();
540: throw new RuntimeWrappedException(e);
541: }
542: }
543: return total;
544: }
545:
546: /**
547: * Gives the total number of iterations of the previous iterationGroup
548: *
549: * @return The total number of iterations performed within the previous iterationGroup
550: */
551: public static int lastCount() {
552: if (servletRequestThreadLocal.get() == null)
553: return -1;
554: return ((Integer) servletRequestThreadLocal.get().getAttribute(
555: standardLastCountVar)).intValue();
556: }
557:
558: @Override
559: public boolean canHaveBody() {
560: return true;
561: }
562: }
|