/* * #! * 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.logic; import java.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import javax.servlet.jsp.JspException; import javax.servlet.jsp.JspTagException; import javax.servlet.jsp.JspWriter; import javax.servlet.jsp.tagext.BodyContent; import javax.servlet.jsp.tagext.BodyTagSupport; import net.ontopia.topicmaps.nav2.core.ContextManagerIF; import net.ontopia.topicmaps.nav2.core.FunctionIF; import net.ontopia.topicmaps.nav2.core.NavigatorApplicationIF; import net.ontopia.topicmaps.nav2.core.NavigatorConfigurationIF; import net.ontopia.topicmaps.nav2.core.NavigatorRuntimeException; import net.ontopia.topicmaps.nav2.utils.FrameworkUtils; import net.ontopia.topicmaps.utils.TopicComparators; import net.ontopia.topicmaps.utils.TopicStringifiers; import net.ontopia.utils.StringifierIF; import org.apache.commons.collections4.comparators.ReverseComparator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * INTERNAL: Logic Tag for iterating over each object in a collection, * creating new content for each iteration. */ public class ForEachTag extends BodyTagSupport { // initialization of logging facility private static Logger log = LoggerFactory.getLogger(ForEachTag.class.getName()); // constants private static final int DEF_MAX_ITER = 100; // fallback-default-value private static final StringifierIF DEF_TOPIC_STRINGIFIER = TopicStringifiers .getDefaultStringifier(); private static final Comparator DEF_TOPIC_COMPARATOR = TopicComparators .getCaseInsensitiveComparator(DEF_TOPIC_STRINGIFIER); private static final String DEF_ORDER_ASCENDING = "ascending"; private static final String DEF_ORDER_DESCENDING = "descending"; // members private ContextManagerIF ctxtMgr; private ContextTag contextTag; private Object[] items; // collection we loop over private int index; // current index we are in the loop // tag attributes private String collVariableName; private String itemVariableName; private Comparator listComparator; private String listComparatorClassName; private int maxNumber; private int startNumber; private String separator; private boolean sortItemsFlag; private String sortOrder; private String functionOnTruncate; /** * Default constructor. */ public ForEachTag() { super(); initializeValues(); } /** * Process the start tag for this instance. */ public int doStartTag() throws JspTagException { this.contextTag = FrameworkUtils.getContextTag(pageContext); this.ctxtMgr = contextTag.getContextManager(); // get Collection to loop over Collection coll = null; if (collVariableName != null) coll = ctxtMgr.getValue(collVariableName); else coll = ctxtMgr.getDefaultValue(); // if not maximum list length set by attribute get from configuration if (maxNumber <= 0) maxNumber = contextTag.getNavigatorConfiguration() .getProperty(NavigatorConfigurationIF.MAX_LIST_LENGTH, NavigatorConfigurationIF.DEF_VAL_MAX_LIST_LENGTH); // establish new lexical scope for this loop ctxtMgr.pushScope(); // do not proceed if no elements in collection at all if (coll.isEmpty()) return SKIP_BODY; // WARN: This might be problematic when collection is of unknown size. this.items = coll.toArray(); // DEFAULT: do *not* sort elements in collection if (sortItemsFlag) { try { listComparator = getComparator(); // TODO: enhance with more comparators? // TODO: Why fallback to another comparator if it is null? if (listComparator == null) listComparator = DEF_TOPIC_COMPARATOR; if (sortOrder != null && sortOrder.equals(DEF_ORDER_DESCENDING)) listComparator = new ReverseComparator(listComparator); Arrays.sort( items, listComparator); } catch (Throwable t) { log.warn("Sort of List (variable '" + (collVariableName != null ? collVariableName : "_default_") + "') had problems: " + t.getMessage()); } } // set first element of collection in beginning if (startNumber >= items.length) startNumber = items.length - 1; index = startNumber; setVariableValues(items[index]); // proceed index = startNumber + 1; //log.debug("doStartTag, itemsSize: " + items.length + ", index: " + index); //log.debug(" coll: " + coll); //log.debug(" first item: " + items[0] + " type: " + items[0].getClass().getName()); return EVAL_BODY_BUFFERED; } /** * Actions after some body has been evaluated. */ public int doAfterBody() throws JspTagException { // put out the evaluated body BodyContent body = getBodyContent(); JspWriter out = body.getEnclosingWriter(); try { out.print(body.getString()); } catch(IOException ioe) { throw new NavigatorRuntimeException("Error in ForEachTag.", ioe); } // Clear for next evaluation body.clearBody(); //log.debug("doAfterBody, itemsSize: " + items.length + ", index: " + index); // test if we have to repeat the body if (index < items.length && index < maxNumber) { // insert separating string if (separator != null) { try { out.print( separator ); } catch(IOException ioe) { throw new NavigatorRuntimeException("Error in ForEachTag.", ioe); } } // set to next value in set setVariableValues(items[index]); index++; return EVAL_BODY_AGAIN; } else { return SKIP_BODY; } } /** * Process the end tag. */ public int doEndTag() throws JspException { // are there still items which have not been displayed yet? if (index >= maxNumber) { // name of function to call when list is truncated String functionName = null; if (functionOnTruncate != null) functionName = functionOnTruncate; else functionName = contextTag.getNavigatorConfiguration() .getProperty(NavigatorConfigurationIF.DEF_FUNC_ONTRUNCATE); // call specified function if list is truncated if (functionName != null && !functionName.equals("")) { // retrieve function object from central managed pool FunctionIF function = contextTag.getFunction(functionName); if (function != null) { // execute function if (log.isDebugEnabled()) log.debug("execute function: " + function.toString()); try { function.call(pageContext, this); } catch (IOException ioe) { throw new NavigatorRuntimeException("Error in ForEachTag: JspWriter not there.", ioe); } } else { log.debug("ForEachTag: no truncate function was specified."); } } } // establish old lexical scope, back to outside of the loop ctxtMgr.popScope(); // reset members ctxtMgr = null; contextTag = null; items = null; // index = 0; return EVAL_PAGE; } /** * Resets the state of the Tag. */ public void release() { // overwrite default behaviour // do not set parent to null!!! } // ----------------------------------------------------------------- // Set methods for tag attributes // ----------------------------------------------------------------- public void setName(String collectionName) { this.collVariableName = collectionName; } public void setSet(String itemName) { this.itemVariableName = itemName; } public void setComparator(String classname) { this.listComparatorClassName = classname; this.sortItemsFlag = (classname != null); } public void setOrder(String order) throws NavigatorRuntimeException { if (!order.equals(DEF_ORDER_DESCENDING) && !order.equals(DEF_ORDER_ASCENDING)) throw new NavigatorRuntimeException("Non-supported value ('" + order + "') given for attribute 'order' in element 'foreach'."); this.sortOrder = order; this.sortItemsFlag = (order != null); } /** * Sets the maximum number of displayed items. * * @param maxString try to convert to valid integer, * otherwise fallback to default value. * If Value is 0 or negative it is set to 1. */ public void setMax(String maxString) { try { this.maxNumber = Integer.parseInt( maxString ); } catch (NumberFormatException e) { this.maxNumber = 0; log.warn("Reset invalid max value '" + maxString + "' " + "to '" + maxNumber + "'."); } } /** * Sets the start number for the object in the collection at this index, * counting from 0. Defaults to 0. Useful for implementing paging * of long lists. * * @param startString try to convert to valid integer, * otherwise fallback to default value. * If Value is 0 or negative it is set to 1. */ public void setStart(String startString) { try { this.startNumber = Integer.parseInt( startString ); } catch (NumberFormatException e) { log.warn("Reset invalid start value to '0' was '" + startString + "'."); this.startNumber = 0; } if (this.startNumber < 0) this.startNumber = 0; } public void setSeparator(String separator) { this.separator = separator; } public void setFunctionOnTruncate(String functionName) { this.functionOnTruncate = functionName; } // ----------------------------------------------------------------- // Helper methods // ----------------------------------------------------------------- /** * INTERNAL: Sets variable values with help of the context manager. */ private void setVariableValues(Object elem) { ctxtMgr.setDefaultValue( elem ); if (itemVariableName != null) ctxtMgr.setValue(itemVariableName, elem); // set variables in current lexical scope // NB: make it 1-based! ctxtMgr.setValue(NavigatorApplicationIF.FOREACH_SEQ_INDEX_KEY, new Integer(index+1)); // figure out if this is the first item in the iteration if (index == startNumber) ctxtMgr.setValue(NavigatorApplicationIF.FOREACH_SEQ_FIRST_KEY, Boolean.TRUE); else ctxtMgr.setValue(NavigatorApplicationIF.FOREACH_SEQ_FIRST_KEY, Collections.EMPTY_LIST); // figure out if this is the last item in the iteration if (index < items.length-1 && index < maxNumber-1) ctxtMgr.setValue(NavigatorApplicationIF.FOREACH_SEQ_LAST_KEY, Collections.EMPTY_LIST); else ctxtMgr.setValue(NavigatorApplicationIF.FOREACH_SEQ_LAST_KEY, Boolean.TRUE); } /** * INTERNAL: Retrieves the comparator class belonging to the * specified name. */ private Comparator getComparator() throws NavigatorRuntimeException { Object obj = contextTag.getNavigatorApplication() .getInstanceOf(listComparatorClassName); if (obj != null && obj instanceof Comparator) return (Comparator) obj; else return null; } /** * INTERNAL: Initialises important member values. */ private void initializeValues() { collVariableName = null; itemVariableName = null; listComparatorClassName = null; sortItemsFlag = false; sortOrder = null; maxNumber = -1; startNumber = 0; separator = null; functionOnTruncate = null; } }