/* * #! * Ontopia Navigator * #- * Copyright (C) 2001 - 2013 The Ontopia Project * #- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * !# */ package net.ontopia.topicmaps.nav2.taglibs.tolog; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.StringTokenizer; import java.util.TreeSet; import javax.servlet.jsp.JspException; import javax.servlet.jsp.JspTagException; import javax.servlet.jsp.JspWriter; import javax.servlet.jsp.tagext.BodyTagSupport; import net.ontopia.topicmaps.nav2.core.NavigatorRuntimeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * INTERNAL: * Tolog Tag for executing a query, iterating over each object in a result * collection and creating new content for each iteration. * The column names in the query result are accessible as variables in the body * of the tag, and are bound to the query result row of that iteration. * The groupBy attribute can be used to indicate that only some of the * column names should be bound (yet), and that only lines which differ in those * columns should produce new output. * To access those columns, one can use another ForEachTag nested within this * one, which has no query attribute, but uses the result set produced here. * The nested ForEachTag can group by columns not yet grouped. * This can format a tree-result like the following (using a single query): * Asia * China * Beijing * Shanghai * India * Delhi * Europe * Sweden * Gothenburg * */ public class ForEachTag extends BodyTagSupport { // initialization of logging facility private static Logger log = LoggerFactory.getLogger(ForEachTag.class.getName()); // members protected ForEachTag groupingAncestor; // Used for nested grouping. protected QueryWrapper queryWrapper; // Used for holding information common to several queries operating on // the same query. protected boolean groupColumns[]; // Used to indicate which columns to group by. protected Set groupNames; // Holds the names to group by in this grouping ForEachTag. protected List orderBy; // Holds the names to order by in this grouping ForEachTag and // the ones nested within it. protected boolean hasValidated; // Indicates whether the leaf of this ForEachTag has been visited. protected int sequenceNumber; protected boolean neverEvaluatedBody; // Indicates that the body was never evaluated. Used to test for // insufficient grouping. Essentially, if the body is never evaluated, // one can never be absolutely sure if the query is sufficiently // grouped, since it depends on the contents of the body. protected static final String SEQUENCE_FIRST = "sequence-first"; protected static final String SEQUENCE_NUMBER = "sequence-number"; protected static final String SEQUENCE_LAST = "sequence-last"; protected static final Collection FALSE = Collections.EMPTY_LIST; protected static final Collection TRUE = Collections .singletonList(Boolean.TRUE); // tag attributes // (reminder: protected String query inherited from QueryExecutingTag) protected String query; protected String groupBy; protected String separator; /** * Default constructor. */ public ForEachTag() { super(); } /** * Process the start tag for this instance. * Depending on the input, chooses between evaluating an input query or * continuing iteration over a queryResult obtained in an ancestor ForEachTag. * Binds any varibles needed in the body. */ public int doStartTag() throws JspTagException { neverEvaluatedBody = false; // Get a queryWrapper either groupingAncestor or by creating one. if (query == null) { // This is a grouping within an ancestor ForEach - query tag. // Get the nearest ancestor ForEachTag groupingAncestor = (ForEachTag) findAncestorWithClass(this, ForEachTag.class); // It's an error to have neither a query nor an ancestor. if (groupingAncestor == null) throw new JspTagException("<tolog:foreach> missing query attribute"); // Share the queryWrapper in groupingAncestor; queryWrapper = groupingAncestor.getQueryWrapper(); //! // The ancestor(s) must expect this tag as a nested tag. //! if (queryWrapper.fullyGrouped() && !queryWrapper.usedBy(this)) //! throw new JspTagException("<tolog:foreach> missing query attribute" //! + " or grouping ancestor." //! + " A grouping ancestor is another <tolog:foreach> tag" //! + " that is nested around this <tolog:foreach> tag and" //! + " groups by a subset of its columns."); // Allow this ForEachTag to take part in the processing in queryWrapper. queryWrapper.setUsedBy(this); // Get the orderBy list without any variables grouped by in the ancestor. orderBy = new ArrayList(groupingAncestor.getOrderBy()); } else { // This is a self-contained ForEach - query tag. // Create a new queryWrapper for the current pageContext from the query. queryWrapper = new QueryWrapper(pageContext, query); orderBy = queryWrapper.parseQuery().getOrderBy(); // Check if the result set is empty, and if so, skip the body. if (!queryWrapper.hasNext()) { processGroupBy(); queryWrapper.getContextManager().pushScope(); neverEvaluatedBody = true; return SKIP_BODY; } // Move to the first row of the result set queryWrapper.next(); } // Establish new lexical scope for this loop queryWrapper.getContextManager().pushScope(); // Compute which columns to group by, based on groupBy (optional) processGroupBy(); // Move one step ahead in the query and bind all relevant variables. queryWrapper.bindVariables(groupColumns); sequenceNumber = 1; boolean isLast = !queryWrapper.hasNext() || (groupingAncestor != null && (groupingAncestor.needsCurrentRow() || queryWrapper.isOnlyChild(groupingAncestor.groupColumns, groupColumns))); queryWrapper.getContextManager().setValue(SEQUENCE_FIRST, TRUE); queryWrapper.getContextManager().setValue(SEQUENCE_NUMBER, "1"); queryWrapper.getContextManager().setValue(SEQUENCE_LAST, isLast ? TRUE : FALSE); return EVAL_BODY_BUFFERED; } /** * Get the names to order by in this tag (to be used by nested tags for * ordering */ protected List getOrderBy() { return orderBy; } /** * Checks if this ForEachTag needs the current row of the query result. */ public boolean needsCurrentRow() { return queryWrapper.relevantDifferences(groupColumns) || (groupingAncestor != null && groupingAncestor.needsCurrentRow()); } /** * Finds out which columns to group by based on a space separated string. */ protected void processGroupBy() throws JspTagException { groupColumns = new boolean[queryWrapper.getWidth()]; groupNames = new TreeSet(); if (groupBy == null) { // By default group by all columns. // Set all columns to true. for (int i = 0; i < groupColumns.length; i++) groupColumns[i] = true; } else { // Initialize to group by no columns. for (int i = 0; i < groupColumns.length; i++) groupColumns[i] = false; StringTokenizer tok = new StringTokenizer(groupBy); // Must have at least one token in groupBy. if (!tok.hasMoreTokens()) throw new JspTagException("<tolog:foreach> : got an empty groupBy" + " attribute." + "\nPlease group by at least one column" + " or leave the groupBy attribute out alltogether.\n"); // Group by all columns mentioned in the groupBy String. while (tok.hasMoreTokens()) { String currentToken = tok.nextToken(" "); int currentIndex = queryWrapper.getIndex(currentToken); // If token is not recognised as any of the column names if (currentIndex == -1) throw new JspTagException("<tolog:foreach> : The name" + " \"" + currentToken + "\" mentioned in groupBy=\"" + groupBy + "\"," + " is not recognised as a column name in the query:" + "\n\"" + queryWrapper.getQuery() + "\".\n"); groupColumns[currentIndex] = true; groupNames.add(currentToken); } } queryWrapper.updateTotalGroupBy(groupColumns); // Check that the columns in groupBy match those in orderBy. validateGroupByOrderBy(); } /** * Validate the groupNames against the names in orderBy. */ protected void validateGroupByOrderBy() throws JspTagException { if (hasValidated) return; else hasValidated = true; while (!groupNames.isEmpty()) { // Every name in groupNames must occur (at least somewhere in orderBy. if (orderBy.isEmpty()) throw new JspTagException("<tolog:foreach> : A column mentioned in" + " groupBy=\"" + groupBy + "\"" + " did not occur in the \"order by\" part of the query:" + queryWrapper.getQuery() + "." + "\nPlease make sure the query result is ordered in the same" + " way as you wish to group it in the output.\n"); String currentName = (String)orderBy.get(0); // The first few names in orderBy must be in groupBy, until groupBy is // empty. // i.e. the groupBy-s must follow the order of the "order by"-s if (!groupNames.contains(currentName)) throw new JspTagException("A column mentioned in" + " groupBy=\"" + groupBy + "\"" + " did not match the \"order by\" of the query:" + "\n\"" + queryWrapper.getQuery() + "\"." + "\nPlease make sure the query result is ordered in the same" + " way as you wish to group it in the output."); orderBy.remove(0); groupNames.remove(currentName); } } /** * Actions after some body has been evaluated. */ public int doAfterBody() throws JspTagException { JspWriter jspWriter; try { // Get the writer to output the query result. jspWriter = getBodyContent().getEnclosingWriter(); jspWriter.print( getBodyContent().getString() ); } catch (IOException e) { throw new NavigatorRuntimeException("Error in ForEachTag.", e); } // If current row is relevant to the grouping ancestor. if (groupingAncestor != null && groupingAncestor.needsCurrentRow()) return SKIP_BODY; // If the end of the query result has been reached. if (!queryWrapper.hasNext()) return SKIP_BODY; // Move to next row in query result. queryWrapper.next(); queryWrapper.bindVariables(groupColumns); sequenceNumber++; // insert separating string (if any) if (separator != null) { try { jspWriter.print( separator ); } catch(IOException ioe) { throw new NavigatorRuntimeException("Error in ForEachTag.", ioe); } } boolean isLast = !queryWrapper.hasNext() || (groupingAncestor != null && (groupingAncestor.needsCurrentRow() || queryWrapper.isOnlyChild(groupingAncestor.groupColumns, groupColumns))); queryWrapper.getContextManager().setValue(SEQUENCE_FIRST, FALSE); queryWrapper.getContextManager().setValue(SEQUENCE_NUMBER, Integer.toString(sequenceNumber)); queryWrapper.getContextManager().setValue(SEQUENCE_LAST, isLast ? TRUE : FALSE); // Prepare for next iteration. getBodyContent().clearBody(); return EVAL_BODY_AGAIN; } protected QueryWrapper getQueryWrapper() { return queryWrapper; } /** * Process the end tag. */ public int doEndTag() throws JspException { if (!(queryWrapper.fullyGrouped() || neverEvaluatedBody)) throw new JspTagException("<tolog:foreach> - tag insufficiently grouped" + " or missing grouping descendant." + "\nA grouping descendant is another <tolog:foreach> tag" + " that is nested within this <tolog:foreach> tag and" + " groups its result further." + "\nA <tolog:foreach> tag is insufficiently grouped if it" + " has no grouping descendant and uses the \"groupBy\"" + " attribute to group by some but not all its columns.\n"); // establish old lexical scope, back to outside of the loop queryWrapper.getContextManager().popScope(); reset(); return super.doEndTag(); } /** * Reset state so we're ready to run again. */ private void reset() { // reset member variables groupingAncestor = null; queryWrapper = null; groupColumns = null; groupNames = null; orderBy = null; hasValidated = false; sequenceNumber = 0; neverEvaluatedBody = false; } /** * Resets the state of the Tag. */ public void release() { // do *not* reset tag attributes here, as that will cause problems // with tag pooling super.release(); } // ----------------------------------------------------------------- // Set methods for tag attributes // ----------------------------------------------------------------- public void setSeparator(String separator) { this.separator = separator; } public void setGroupBy(String groupBy) { this.groupBy = groupBy; } public void setQuery(String query) { this.query = query; } }