/*==========================================================================*\ | $Id: ReportUtilityEnvironment.java,v 1.1 2010/05/11 14:51:48 aallowat Exp $ |*-------------------------------------------------------------------------*| | Copyright (C) 2006-2008 Virginia Tech | | This file is part of Web-CAT. | | Web-CAT is free software; you can redistribute it and/or modify | it under the terms of the GNU Affero General Public License as published | by the Free Software Foundation; either version 3 of the License, or | (at your option) any later version. | | Web-CAT is distributed in the hope that it will be useful, | but WITHOUT ANY WARRANTY; without even the implied warranty of | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | GNU General Public License for more details. | | You should have received a copy of the GNU Affero General Public License | along with Web-CAT; if not, see <http://www.gnu.org/licenses/>. \*==========================================================================*/ package org.webcat.reporter; import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import ognl.Node; import ognl.Ognl; import ognl.OgnlContext; import ognl.OgnlException; import ognl.OgnlOps; import ognl.OgnlRuntime; //------------------------------------------------------------------------- /** * <p> * A class that contains various handy utility functions that can be used from * OGNL expressions in a report. Many of these methods are functional * iteration-based algorithms, given the inability to easily do loops in OGNL. * </p><p> * If you use the <tt>newOgnlContext()</tt> static method to create an OGNL * context, then an instance of this class is stored in the context as the * variable <tt>#ENV</tt>, so each of the public methods below can be called * by invoking them on that variable; for example, * <tt>#ENV.accumulate(...)</tt>. * </p><p> * Many of the methods in this class take OGNL lambda expressions as an * argument; to simplify the documentation below, these lambda expressions will * be categorized as one of the following: * <ul> * <li><b>UnaryOp:</b> an expression that takes a single argument, denoted by * <tt>#this</tt>. The return value is arbitrary.</li> * <li><b>BinaryOp:</b> an expression that takes two arguments, denoted by * <tt>#this.l</tt> and <tt>#this.r</tt> (that is, left-hand side and * right-hand side). The return type is arbitrary.</li> * <li><b>UnaryPredicate:</b> an expression that takes a single argument, * denoted by <tt>#this</tt>, and has a Boolean return type.</li> * <li><b>BinaryPredicate:</b> an expression that takes two arguments, denoted * by <tt>#this.l</tt> and <tt>#this.r</tt>, and has a Boolean return type.</li> * <li><b>Generator:</b> an expression that takes no arguments (<tt>#this</tt> * is undefined inside it) and returns an arbitrary value. * </ul> * </p><p> * Due to the way OGNL parses expressions, a lambda expression cannot be * included directly as the argument to one of these methods; it must be stored * in a variable first. In other words, * <pre> * #ENV.accumulate({1,2,3}, :[ #this.l + #this.r ])</pre> * * is incorrect. The above expression must instead be written as * <pre> * #f = :[ #this.l + #this.r ], #ENV.accumulate({1,2,3}, #f)</pre> * </p> * * @author Tony Allevato * @version $Id: ReportUtilityEnvironment.java,v 1.1 2010/05/11 14:51:48 aallowat Exp $ */ public class ReportUtilityEnvironment { //~ Static methods ........................................................ // ---------------------------------------------------------- /** * Creates a new <tt>OgnlContext</tt> that is prepped with the utility * functions in the <tt>#ENV</tt> variable. * * @return a new <tt>OgnlContext</tt> */ public static OgnlContext newOgnlContext() { OgnlContext context = new OgnlContext(); context.put("ENV", new ReportUtilityEnvironment(context)); return context; } //~ Constructors .......................................................... // ---------------------------------------------------------- /** * <p> * Creates a new instance of the ReportUtilityEnvironment class. * </p><p> * This method takes as a parameter the OGNl context that will be used to * execute the expressions in the report. This is required by the methods * that take a lambda expression as an argument so that such expressions * can refer to variables and names outside the lambda. * </p> * * @param globalContext the global OGNL context that will be used to * execute the expressions in the report */ public ReportUtilityEnvironment(OgnlContext globalContext) { this.context = globalContext; } //~ Methods ............................................................... // ---------------------------------------------------------- /** * <p> * Computes the sum of all the objects in the specified list. * </p><p> * If the list contains elements of different types, then the type of the * result is determined by OGNL type conversions, after performing a * first-to-last addition of each item in the list. * </p> * * @param list the list of items to add * @return the sum of the objects in the list * * @throws OgnlException if a nested OGNL expression has an error */ public Object accumulate(List<Object> list) throws OgnlException { Object initial = null; if (list != null && !list.isEmpty()) { initial = defaultValueForType(list.get(0).getClass()); } return accumulate(list, initial, (Node) Ognl.parseExpression("#this.l + #this.r")); } // ---------------------------------------------------------- /** * Computes the generalized sum of all the objects in the specified list * by using a lambda expression as the "add" operation. * * @param list the list of items to "add" * @param initial the initial value from which to begin the accumulation * @param binaryOp a <b>BinaryOp</b> OGNL lambda expression used to sum the * items in the list. <tt>#this.l</tt> is the currently accumulated * "sum" so far, and <tt>#this.r</tt> is the next item to be "added". * The result of this expression should be the "sum" of those two * values. * @return the generalized sum of the objects in the list * * @throws OgnlException if a nested OGNL expression has an error */ public Object accumulate(List<Object> list, Object initial, Node binaryOp) throws OgnlException { if (list == null || list.isEmpty()) { return null; } HashMap<String, Object> args = new HashMap<String, Object>(); Object oldThis = replaceThisInContext(args); for(Object item : list) { args.put("l", initial); args.put("r", item); initial = binaryOp.getValue(context, args); } replaceThisInContext(oldThis); return initial; } // ---------------------------------------------------------- /** * Returns a list of values where the (<i>i</i>+1)st element in the result * is the difference between the (<i>i</i>+1)st element and the <i>i</i>th * element in the specified list. * * @param list the list of items from which to compute the * adjacent differences * @return the list of adjacent differences * * @throws OgnlException if a nested OGNL expression has an error */ public List<Object> adjacentDifference(List<Object> list) throws OgnlException { return adjacentDifference(list, (Node) Ognl.parseExpression("#this.r - #this.l")); } // ---------------------------------------------------------- /** * <p> * Returns a list of values where the (<i>i</i>+1)st element in the result * is the difference between the (<i>i</i>+1)st element and the <i>i</i>th * element in the specified list. The 0th element in the resulting list is * equal to the 0th element in the specified list. * </p><p> * This version of the method is a generalized operation where the * specified expression is used to compute the "difference", rather than * standard subtraction. * </p> * * @param list the list of items from which to compute the adjacent * differences * @param binaryOp a <b>BinaryOp</b> OGNL lambda expression representing * the operation used to "subtract" the items in the list; * <tt>#this.l</tt> is the <i>i</i>th element, and <tt>#this.r</tt> is * the (<i>i</i>+1)st element. The result of this expression should be * the "difference" of <tt>#this.l</tt> from <tt>#this.r</tt>. * @return the list of generalized adjacent differences * * @throws OgnlException if a nested OGNL expression has an error */ public List<Object> adjacentDifference(List<Object> list, Node binaryOp) throws OgnlException { if (list == null) { return null; } ArrayList<Object> newList = new ArrayList<Object>(); if (!list.isEmpty()) { HashMap<String, Object> args = new HashMap<String, Object>(); Object oldThis = replaceThisInContext(args); Object value = list.get(0); newList.add(value); for (int i = 0; i < list.size() - 1; i++) { args.put("l", list.get(i)); args.put("r", list.get(i + 1)); value = binaryOp.getValue(context, args); newList.add(value); } replaceThisInContext(oldThis); } return newList; } // ---------------------------------------------------------- /** * Returns the number of items in the specified list that satisfy a * predicate. This is essentially the same as the OGNL expression * <tt>list.{? predicate }.size</tt>, but this method does not require that * the sub-list be computed in memory before getting its size. * * @param list the list containing the items to count * @param predicate a <b>UnaryPredicate</b> OGNL lambda expression that is * called to determine if an element in the list should be counted. * <tt>#this</tt> refers to the current item in the list. * @return the number of items in the list that satisfy the predicate * * @throws OgnlException if a nested OGNL expression has an error */ public int countIf(List<Object> list, Node predicate) throws OgnlException { if (list == null || list.isEmpty()) { return 0; } int count = 0; for (Object item : list) { Object oldThis = replaceThisInContext(item); if (predicate.getValue(context, item).equals(Boolean.TRUE)) count++; replaceThisInContext(oldThis); } return count; } // ---------------------------------------------------------- /** * Evaluates an OGNL lambda expression for each element in the specified * list. * * @param list the list of items to iterate over * @param unaryOp a <b>UnaryOp</b> OGNL lambda expression that will be * called for each item in the list. Inside this expression, * <tt>#this</tt> refers to the current item in the list; the result of * the expression is ignored. * * @throws OgnlException if a nested OGNL expression has an error */ public void forEach(List<Object> list, Node unaryOp) throws OgnlException { for (Object item : list) { Object oldThis = replaceThisInContext(item); unaryOp.getValue(context, item); replaceThisInContext(oldThis); } } // ---------------------------------------------------------- /** * Returns the inner product of the two lists. The lists are assumed to be * numeric. * * @param list1 the first list of values * @param list2 the second list of values * @return the scalar inner product of the two lists * * @throws OgnlException if a nested OGNL expression has an error */ public Object innerProduct(List<Object> list1, List<Object> list2) throws OgnlException { return innerProduct(list1, list2, Integer.valueOf(0)); } // ---------------------------------------------------------- /** * Returns the inner product of the two lists, using the specified initial * sum. The lists are assumed to be numeric. * * @param list1 the first list of values * @param list2 the second list of values * @param initial the initial value to which the pairwise products from the * lists are added * @return the scalar inner product of the two lists * * @throws OgnlException if a nested OGNL expression has an error */ public Object innerProduct(List<Object> list1, List<Object> list2, Object initial) throws OgnlException { return innerProduct(list1, list2, initial, (Node) Ognl.parseExpression("#this.l + #this.r"), (Node) Ognl.parseExpression("#this.l * #this.r")); } // ---------------------------------------------------------- /** * Returns the generalized inner product of the two lists, using the * specified initial sum, additive operation, and multiplicative operation. * * @param list1 the first list of values * @param list2 the second list of values * @param initial the initial value to which the pairwise products from the * lists are added * @param addBinaryOp a <b>BinaryOp</b> OGNL lambda expression used to * accumulate the pairwise products. <tt>#this.l</tt> is the * accumulated value thus far, and <tt>#this.r</tt> is the next item to * accumulate. The result of this expression should be the "sum" of * those two values. * @param multBinaryOp a <b>BinaryOp</b> OGNL lambda expression used to * calculate the pairwise products of values in the lists. * <tt>#this.l</tt> is the <i>i</i>th value in <tt>list1</tt>, and * <tt>#this.r</tt> is the <i>i</i>th item in <tt>list2</tt>. The * result of this expression should be the "product" of those two * values. * @return the generalized inner product of the two lists * * @throws OgnlException if a nested OGNL expression has an error */ public Object innerProduct(List<Object> list1, List<Object> list2, Object initial, Node addBinaryOp, Node multBinaryOp) throws OgnlException { if (list1 == null || list2 == null) { return null; } if (!list1.isEmpty() && !list2.isEmpty()) { HashMap<String, Object> args = new HashMap<String, Object>(); Object oldThis = replaceThisInContext(args); for (int i = 0; i < Math.min(list1.size(), list2.size()); i++) { args.put("l", list1.get(i)); args.put("r", list2.get(i)); Object value = multBinaryOp.getValue(context, args); args.put("l", initial); args.put("r", value); initial = addBinaryOp.getValue(context, args); } replaceThisInContext(oldThis); } return initial; } // ---------------------------------------------------------- /** * Returns a new list that contains the elements in the first list followed * by the elements in the second. * * @param list1 the first list * @param list2 the second list * @return a list containing the elements in the first list followed by the * elements in the second */ public List<Object> joinLists(List<Object> list1, List<Object> list2) { ArrayList<Object> newList = new ArrayList<Object>(); newList.addAll(list1); newList.addAll(list2); return newList; } // ---------------------------------------------------------- /** * <p> * Returns a new list that contains the elements in each of the lists * passed to the function. These lists are themselves passed in as a list * of lists, permitting any number of lists to be joined. * </p><p> * As an example, <tt>#ENV.joinLists( {{1,2,3}, {4,5,6}, {7,8,9}} )</tt> * would return the list <tt>{1,2,3,4,5,6,7,8,9}</tt>. * </p> * * @param lists the list containing the lists that should be joined * @return a list containing the elements in each of the specified lists */ public List<Object> joinLists(List<List<Object>> lists) { ArrayList<Object> newList = new ArrayList<Object>(); for (List<Object> list : lists) { newList.addAll(list); } return newList; } // ---------------------------------------------------------- /** * Returns a new map formed by merging the keys and values from the two * specified maps. In the event that a key is found in both maps, the value * in map2 takes precedence over that in map1. * * @param map1 the first map to merge * @param map2 the second map to merge * @return a map containing the keys and values from map1 and map2 */ public Map<Object, Object> joinMaps(Map<Object, Object> map1, Map<Object, Object> map2) { Map<Object, Object> newMap = new LinkedHashMap<Object, Object>(); newMap.putAll(map1); newMap.putAll(map2); return newMap; } // ---------------------------------------------------------- /** * Returns a new map formed by merging the keys and values from the * specified maps. In the event that a key is found in multiple maps, the * value in the later map takes precedence over that in the earlier one. * * @param maps the list of maps to merge * @return a map containing the keys and values from the specified maps */ public Map<Object, Object> joinMaps(List<Map<Object, Object>> maps) { Map<Object, Object> newMap = new LinkedHashMap<Object, Object>(); for (Map<Object, Object> map : maps) { newMap.putAll(map); } return newMap; } // ---------------------------------------------------------- /** * Returns a list of partial sums, where the <i>i</i>th element in the * result is the sum of the 0th through <i>i</i>th elements in the * specified list. * * @param list the list of items from which to compute the partial sums * @return the list of partial sums * * @throws OgnlException if a nested OGNL expression has an error */ public List<Object> partialSum(List<Object> list) throws OgnlException { return partialSum(list, (Node) Ognl.parseExpression("#this.l + #this.r")); } // ---------------------------------------------------------- /** * <p> * Returns a list of partial sums, where the <i>i</i>th element in the * result is the sum of the 0th through <i>i</i>th elements in the * specified list. * </p><p> * This version of the method is a generalized operation where the * specified expression is used to accumulate the "sums", rather than * standard addition. * </p> * * @param list the list of items from which to compute the partial sums * @param binaryOp a <b>BinaryOp</b> OGNL lambda expression, where * <tt>#this.l</tt> is the currently accumulated "sum" so far, and * <tt>#this.r</tt> is the next item to be "added". The result of this * expression should be the "sum" of those two values. * @return the list of generalized partial sums * * @throws OgnlException if a nested OGNL expression has an error */ public List<Object> partialSum(List<Object> list, Node binaryOp) throws OgnlException { if (list == null) { return null; } ArrayList<Object> newList = new ArrayList<Object>(); if (!list.isEmpty()) { HashMap<String, Object> args = new HashMap<String, Object>(); Object oldThis = replaceThisInContext(args); Object value = list.get(0); newList.add(value); for (int i = 1; i < list.size(); i++) { args.put("l", value); args.put("r", list.get(i)); value = binaryOp.getValue(context, args); newList.add(value); } replaceThisInContext(oldThis); } return newList; } // ---------------------------------------------------------- /** * Returns a list containing <tt>sampleSize</tt> items randomly sampled * from the specified list. * * @param list the list of items to sample from * @param sampleSize the number of items to randomly sample from the list * @return a list containing <tt>sampleSize</tt> items randomly sampled * from the list */ public List<Object> randomSample(List<Object> list, int sampleSize) { ArrayList<Object> newList = new ArrayList<Object>(); int k = 0; for (Object item : list) { if (k < sampleSize) { newList.add(item); } else { int j = (int) (Math.random() * k); if (j < sampleSize) newList.set(j, list.get(k)); } k++; } return newList; } // ---------------------------------------------------------- /** * Returns a list that contains the elements of the specified list after * being randomly shuffled. * * @param list the list containing the items to shuffle * @return a list containing the items in random order */ public List<Object> randomShuffle(List<Object> list) { ArrayList<Object> newList = new ArrayList<Object>(list); for (int i = newList.size() - 1; i >= 1; i--) { int k = (int) (Math.random() * i); Object temp = newList.get(i); newList.set(i, newList.get(k)); newList.set(k, temp); } return newList; } // ---------------------------------------------------------- /** * Returns a list containing the integers from <tt>start</tt> to * <tt>end</tt>, inclusive. * * @param start the first integer in the range * @param end the last integer in the range * @return a list containing the integers from <tt>start</tt> to * <tt>end</tt>, inclusive */ public List<Integer> range(int start, int end) { ArrayList<Integer> list = new ArrayList<Integer>(); for (int i = start; i <= end; i++) { list.add(i); } return list; } // ---------------------------------------------------------- /** * Returns a list that contains the elements of the specified list in * reverse order. * * @param list the list containing the items to reverse * @return a list containing the items in reverse order */ public List<Object> reverse(List<Object> list) { ArrayList<Object> newList = new ArrayList<Object>(); for (int i = list.size() - 1; i >= 0; i--) newList.add(list.get(i)); return newList; } // ---------------------------------------------------------- /** * Given a list, this method returns a copy in which consecutive duplicate * elements have been removed, leaving only the first copy. * * @param list the list containing the items to process * @return a new list with consecutive duplicate items removed * * @throws OgnlException if a nested OGNL expression has an error */ public List<Object> unique(List<Object> list) throws OgnlException { return unique(list, (Node) Ognl.parseExpression("#this.l == #this.r")); } // ---------------------------------------------------------- /** * Given a list, this method returns a copy in which consecutive duplicate * elements have been removed, leaving only the first copy. The specified * predicate is used to determine if two items are equal for the purposes * of this method. * * @param list the list containing the items to process * @param predicate a <b>BinaryPredicate</b> OGNL lambda expression that * will be used to compare consecutive items in the list. * <tt>#this.l</tt> and <tt>#this.r</tt> are elements in the list. The * result of this expression must be a Boolean indicating whether the * items are considered to be equal. * @return a new list with consecutive duplicate items removed * * @throws OgnlException if a nested OGNL expression has an error */ public List<Object> unique(List<Object> list, Node predicate) throws OgnlException { ArrayList<Object> newList = new ArrayList<Object>(); Iterator<Object> first = list.iterator(); Object value = first.next(); newList.add(value); HashMap<String, Object> args = new HashMap<String, Object>(); Object oldThis = replaceThisInContext(args); while (first.hasNext()) { Object next = first.next(); args.put("l", value); args.put("r", next); boolean equiv = (Boolean) predicate.getValue(context, args); if (!equiv) { value = next; newList.add(value); } } replaceThisInContext(oldThis); return newList; } // ---------------------------------------------------------- /** * Replaces the <tt>#this</tt> reference in the current OGNL context with * the specified object, returning its old value. * * @param newThis the new <tt>#this</tt> object for the context * @return the previous value of <tt>#this</tt> */ private Object replaceThisInContext(Object newThis) { Object oldThis = context.getCurrentObject(); context.setCurrentObject(newThis); return oldThis; } // ---------------------------------------------------------- /** * Gets the default value for the specified numeric or string type. This * method is used to determine the initial value for the default overload * of the <tt>accumulate</tt> function. */ private static Object defaultValueForType(Class<?> klazz) { if (klazz == Byte.class || klazz == Byte.TYPE) { return Byte.valueOf((byte) 0); } else if (klazz == Short.class || klazz == Short.TYPE) { return Short.valueOf((short) 0); } else if (klazz == Integer.class || klazz == Integer.TYPE) { return Integer.valueOf(0); } else if (klazz == Long.class || klazz == Long.TYPE) { return Long.valueOf(0); } else if (klazz == Float.class || klazz == Float.TYPE) { return Float.valueOf(0); } else if (klazz == BigInteger.class) { return BigInteger.ZERO; } else if (klazz == BigDecimal.class) { return BigDecimal.ZERO; } else if (klazz == Double.class || klazz == Double.TYPE) { return Double.valueOf(0); } else if (klazz == String.class || klazz == Character.class || klazz == Character.TYPE) { return ""; } else { return null; } } //~ Static/instance variables ............................................. /** The OGNL context used during the execution of report expressions. */ private OgnlContext context; }