blob: cefbe7225f613d1b82f8d6c52edf378b54575f98 [file] [log] [blame]
package org.apache.velocity.tools.generic;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
import java.io.Serializable;
import java.lang.reflect.Array;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.regex.Pattern;
import org.apache.velocity.tools.Scope;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.velocity.tools.config.DefaultKey;
import org.apache.velocity.tools.config.ValidScope;
/**
* Provides general utility methods for controlling the display of references.
* Currently, this class contains methods for "pretty printing" an array or
* {@link Collection}, methods for truncating the string value of a reference
* at a configured or specified length, methods for displaying an alternate
* value when a specified value is null, a method for generating whitespace,
* a "printf" type of method for formatting messages, and
* methods for forcing values into "cells" of equal size (via truncation or
* padding with whitespace).
*
* <p><b>Example Use:</b>
* <pre>
* tools.xml...
* &lt;tools&gt;
* &lt;toolbox scope="application"&gt;
* &lt;tool class="org.apache.velocity.tools.generic.DisplayTool"/&gt;
* &lt;/toolbox&gt;
* &lt;/tools&gt;
*
* template...
* #set( $list = [1..5] )
* $display.list($list)
* $display.truncate("This is a long string.", 10)
* Not Null: $display.alt("not null", "--")
* Null: $display.alt($null, "--")
*
* output...
* 1, 2, 3, 4 and 5
* This is...
* Not Null: not null
* Null: --
*
* </pre>
*
* @since VelocityTools 2.0
* @author <a href="sean@somacity.com">Sean Legassick</a>
* @author <a href="dlr@collab.net">Daniel Rall</a>
* @author Nathan Bubna
* @version $Id: DisplayTool.java 463298 2006-10-12 16:10:32Z henning $
*/
@DefaultKey("display")
@ValidScope(Scope.APPLICATION)
public class DisplayTool extends LocaleConfig implements Serializable
{
private static final long serialVersionUID = 2841919854651475788L;
public static final String LIST_DELIM_KEY = "listDelim";
public static final String LIST_FINAL_DELIM_KEY = "listFinalDelim";
public static final String TRUNCATE_LENGTH_KEY = "truncateLength";
public static final String TRUNCATE_SUFFIX_KEY = "truncateSuffix";
public static final String TRUNCATE_AT_WORD_KEY = "truncateAtWord";
public static final String CELL_LENGTH_KEY = "cellLength";
public static final String CELL_SUFFIX_KEY = "cellSuffix";
public static final String DEFAULT_ALTERNATE_KEY = "defaultAlternate";
public static final String ALLOWED_TAGS_KEY = "allowedTags";
private String defaultDelim = ", ";
private String defaultFinalDelim = " and ";
private int defaultTruncateLength = 30;
private String defaultTruncateSuffix = "...";
private boolean defaultTruncateAtWord = false;
private int defaultCellLength = 30;
private String defaultCellSuffix = "...";
private String defaultAlternate = "null";
private String[] defaultAllowedTags = null;
/**
* Does the actual configuration. This is protected, so
* subclasses may share the same ValueParser and call configure
* at any time, while preventing templates from doing so when
* configure(Map) is locked.
*/
protected void configure(ValueParser values)
{
super.configure(values);
String listDelim = values.getString(LIST_DELIM_KEY);
if (listDelim != null)
{
setListDelimiter(listDelim);
}
String listFinalDelim = values.getString(LIST_FINAL_DELIM_KEY);
if (listFinalDelim != null)
{
setListFinalDelimiter(listFinalDelim);
}
Integer truncateLength = values.getInteger(TRUNCATE_LENGTH_KEY);
if (truncateLength != null)
{
setTruncateLength(truncateLength);
}
String truncateSuffix = values.getString(TRUNCATE_SUFFIX_KEY);
if (truncateSuffix != null)
{
setTruncateSuffix(truncateSuffix);
}
Boolean truncateAtWord = values.getBoolean(TRUNCATE_AT_WORD_KEY);
if (truncateAtWord != null)
{
setTruncateAtWord(truncateAtWord);
}
Integer cellLength = values.getInteger(CELL_LENGTH_KEY);
if (cellLength != null)
{
setCellLength(cellLength);
}
String cellSuffix = values.getString(CELL_SUFFIX_KEY);
if (cellSuffix != null)
{
setCellSuffix(cellSuffix);
}
String defaultAlternate = values.getString(DEFAULT_ALTERNATE_KEY);
if (defaultAlternate != null)
{
setDefaultAlternate(defaultAlternate);
}
String[] allowedTags = values.getStrings(ALLOWED_TAGS_KEY);
if (allowedTags != null)
{
setAllowedTags(allowedTags);
}
}
public String getListDelimiter()
{
return this.defaultDelim;
}
protected void setListDelimiter(String delim)
{
this.defaultDelim = delim;
}
public String getListFinalDelimiter()
{
return this.defaultFinalDelim;
}
protected void setListFinalDelimiter(String finalDelim)
{
this.defaultFinalDelim = finalDelim;
}
public int getTruncateLength()
{
return this.defaultTruncateLength;
}
protected void setTruncateLength(int maxlen)
{
this.defaultTruncateLength = maxlen;
}
public String getTruncateSuffix()
{
return this.defaultTruncateSuffix;
}
protected void setTruncateSuffix(String suffix)
{
this.defaultTruncateSuffix = suffix;
}
public boolean getTruncateAtWord()
{
return this.defaultTruncateAtWord;
}
protected void setTruncateAtWord(boolean atWord)
{
this.defaultTruncateAtWord = atWord;
}
public String getCellSuffix()
{
return this.defaultCellSuffix;
}
protected void setCellSuffix(String suffix)
{
this.defaultCellSuffix = suffix;
}
public int getCellLength()
{
return this.defaultCellLength;
}
protected void setCellLength(int maxlen)
{
this.defaultCellLength = maxlen;
}
public String getDefaultAlternate()
{
return this.defaultAlternate;
}
protected void setDefaultAlternate(String dflt)
{
this.defaultAlternate = dflt;
}
public String[] getAllowedTags()
{
return this.defaultAllowedTags;
}
protected void setAllowedTags(String[] tags)
{
this.defaultAllowedTags = tags;
}
/**
* Formats a collection or array into the form "A, B and C".
*
* @param list A collection or array.
* @return A String.
*/
public String list(Object list)
{
return list(list, this.defaultDelim, this.defaultFinalDelim);
}
/**
* Formats a collection or array into the form
* "A&lt;delim&gt;B&lt;delim&gt;C".
*
* @param list A collection or array.
* @param delim A String.
* @return A String.
*/
public String list(Object list, String delim)
{
return list(list, delim, delim);
}
/**
* Formats a collection or array into the form
* "A&lt;delim&gt;B&lt;finaldelim&gt;C".
*
* @param list A collection or array.
* @param delim A String.
* @param finaldelim A String.
* @return A String.
*/
public String list(Object list, String delim, String finaldelim)
{
return list(list, delim, finaldelim, null);
}
/**
* Formats a specified property of collection or array of objects into the
* form "A&lt;delim&gt;B&lt;finaldelim&gt;C".
*
* @param list A collection or array.
* @param delim A String.
* @param finaldelim A String.
* @param property An object property to format.
* @return A String.
*/
public String list(Object list, String delim, String finaldelim,
String property)
{
if (list == null)
{
return null;
}
if (list instanceof Collection)
{
return format((Collection) list, delim, finaldelim, property);
}
Collection items;
if (list.getClass().isArray())
{
int size = Array.getLength(list);
items = new ArrayList(size);
for (int i = 0; i < size; i++)
{
items.add(Array.get(list, i));
}
}
else
{
items = Collections.singletonList(list);
}
return format(items, delim, finaldelim, property);
}
/**
* Does the actual formatting of the collection.
* @param list collection of objects to format
* @param delim delimiter
* @param finaldelim final delimiter
* @param property objects property to use
* @return formatted string
*/
protected String format(Collection list, String delim, String finaldelim,
String property)
{
StringBuilder sb = new StringBuilder();
int size = list.size();
Iterator iterator = list.iterator();
for (int i = 0; i < size; i++)
{
if (property != null && property.length() > 0)
{
sb.append(getProperty(iterator.next(), property));
}
else
{
sb.append(iterator.next());
}
if (i < size - 2)
{
sb.append(delim);
}
else if (i < size - 1)
{
sb.append(finaldelim);
}
}
return sb.toString();
}
/**
* Uses {@link MessageFormat} to format the specified String with
* the specified arguments. If there are no arguments, then the String
* is returned directly. Please note that the format
* required here is quite different from that of
* {@link #printf(String,Object...)}.
*
* @param format format string
* @param args arguments
* @return formatted message
* @since VelocityTools 2.0
*/
public String message(String format, Object... args)
{
if (format == null)
{
return null;
}
if (args == null || args.length == 0)
{
return format;
}
else if (args.length == 1 && args[0] instanceof Collection)
{
Collection list = (Collection)args[0];
if (list.isEmpty())
{
return format;
}
else
{
args = list.toArray();
}
}
return MessageFormat.format(format, args);
}
/**
* Uses {@link String#format(Locale,String,Object...)} to format the specified String
* with the specified arguments. Please note that the format
* required here is quite different from that of
* {@link #message(String,Object...)}.
*
* @param format format string
* @param args printf arguments
* @return formatted string
* @see java.util.Formatter
* @since VelocityTools 2.0
*/
public String printf(String format, Object... args)
{
if (format == null)
{
return null;
}
if (args == null || args.length == 0)
{
return format;
}
if (args.length == 1 && args[0] instanceof Collection)
{
Collection list = (Collection)args[0];
if (list.isEmpty())
{
return format;
}
else
{
args = list.toArray();
}
}
return String.format(getLocale(), format, args);
}
/**
* Limits the string value of 'truncateMe' to the configured max length
* in characters (default is 30 characters).
* If the string gets curtailed, the configured suffix
* (default is "...") is used as the ending of the truncated string.
*
* @param truncateMe The value to be truncated.
* @return A String.
*/
public String truncate(Object truncateMe)
{
return truncate(truncateMe, this.defaultTruncateLength);
}
/**
* Limits the string value of 'truncateMe' to 'maxLength' characters.
* If the string gets curtailed, the configured suffix
* (default is "...") is used as the ending of the truncated string.
*
* @param maxLength An int with the maximum length.
* @param truncateMe The value to be truncated.
* @return A String.
*/
public String truncate(Object truncateMe, int maxLength)
{
return truncate(truncateMe, maxLength, this.defaultTruncateSuffix);
}
/**
* Limits the string value of 'truncateMe' to the configured max length
* in characters (default is 30 characters).
* If the string gets curtailed, the specified suffix
* is used as the ending of the truncated string.
*
* @param truncateMe The value to be truncated.
* @param suffix A String.
* @return A String.
*/
public String truncate(Object truncateMe, String suffix)
{
return truncate(truncateMe, this.defaultTruncateLength, suffix);
}
/**
* Limits the string value of 'truncateMe' to the specified max length in
* characters. If the string gets curtailed, the specified suffix is used as
* the ending of the truncated string.
*
* @param truncateMe The value to be truncated.
* @param maxLength An int with the maximum length.
* @param suffix A String.
* @return A String.
*/
public String truncate(Object truncateMe, int maxLength, String suffix)
{
return truncate(truncateMe, maxLength, suffix, defaultTruncateAtWord);
}
/**
* Limits the string value of 'truncateMe' to the latest complete word
* within the specified maxLength. If the string gets curtailed, the
* specified suffix is used as the ending of the truncated string.
*
* @param truncateMe The value to be truncated.
* @param maxLength An int with the maximum length.
* @param suffix A String.
* @param defaultTruncateAtWord Truncate at a word boundary if true.
* @return A String.
*/
public String truncate(Object truncateMe, int maxLength, String suffix,
boolean defaultTruncateAtWord)
{
if (truncateMe == null || maxLength <= 0)
{
return null;
}
String string = String.valueOf(truncateMe);
if (string.length() <= maxLength)
{
return string;
}
if (suffix == null || maxLength - suffix.length() <= 0)
{
// either no need or no room for suffix
return string.substring(0, maxLength);
}
if (defaultTruncateAtWord)
{
// find the latest space within maxLength
int lastSpace = string.substring(0, maxLength - suffix.length() + 1)
.lastIndexOf(" ");
if (lastSpace > suffix.length())
{
return string.substring(0, lastSpace) + suffix;
}
}
// truncate to exact character and append suffix
return string.substring(0, maxLength - suffix.length()) + suffix;
}
/**
* Returns a string of spaces of the specified length.
* @param length the number of spaces to return
* @return string of spaces
*/
public String space(int length)
{
if (length < 0)
{
return null;
}
StringBuilder space = new StringBuilder();
for (int i=0; i < length; i++)
{
space.append(' ');
}
return space.toString();
}
/**
* Truncates or pads the string value of the specified object as necessary
* to ensure that the returned string's length equals the default cell size.
* @param obj the value to be put in the 'cell'
* @return truncated/padded string
*/
public String cell(Object obj)
{
return cell(obj, this.defaultCellLength);
}
/**
* Truncates or pads the string value of the specified object as necessary
* to ensure that the returned string's length equals the specified cell size.
* @param obj the value to be put in the 'cell'
* @param cellsize the size of the cell into which the object must be placed
* @return truncated/padded string
*/
public String cell(Object obj, int cellsize)
{
return cell(obj, cellsize, this.defaultCellSuffix);
}
/**
* Truncates or pads the string value of the specified object as necessary
* to ensure that the returned string's length equals the default cell size.
* If truncation is necessary, the specified suffix will replace the end of
* the string value to indicate that.
* @param obj the value to be put in the 'cell'
* @param suffix the suffix to put at the end of any values that need truncating
* to indicate that they've been truncated
* @return truncated/padded string
*/
public String cell(Object obj, String suffix)
{
return cell(obj, this.defaultCellLength, suffix);
}
/**
* Truncates or pads the string value of the specified object as necessary
* to ensure that the returned string's length equals the specified cell size.
* @param obj the value to be put in the 'cell'
* @param cellsize the size of the cell into which the object must be placed
* @param suffix the suffix to put at the end of any values that need truncating
* to indicate that they've been truncated
* @return truncated/padded string
*/
public String cell(Object obj, int cellsize, String suffix)
{
if (obj == null || cellsize <= 0)
{
return null;
}
String value = String.valueOf(obj);
if (value.length() == cellsize)
{
return value;
}
else if (value.length() > cellsize)
{
return truncate(value, cellsize, suffix);
}
else
{
return value + space(cellsize - value.length());
}
}
/**
* Changes the first character of the string value of the specified object
* to upper case and returns the resulting string.
*
* @param capitalizeMe The value to be capitalized.
* @return capitalized string
*/
public String capitalize(Object capitalizeMe)
{
if (capitalizeMe == null)
{
return null;
}
String string = String.valueOf(capitalizeMe);
switch (string.length())
{
case 0:
return string;
case 1:
return string.toUpperCase();
default:
StringBuilder out = new StringBuilder(string.length());
out.append(string.substring(0,1).toUpperCase());
out.append(string.substring(1, string.length()));
return out.toString();
}
}
/**
* Changes the first character of the string value of the specified object
* to lower case and returns the resulting string.
*
* @param uncapitalizeMe The value to be uncapitalized.
* @return uncapitalized string
*/
public String uncapitalize(Object uncapitalizeMe)
{
if (uncapitalizeMe == null)
{
return null;
}
String string = String.valueOf(uncapitalizeMe);
switch (string.length())
{
case 0:
return string;
case 1:
return string.toLowerCase();
default:
StringBuilder out = new StringBuilder(string.length());
out.append(string.substring(0,1).toLowerCase());
out.append(string.substring(1, string.length()));
return out.toString();
}
}
/**
* Returns a configured default value if specified value is null.
* @param checkMe object to check
* @return a configured default value if the specified value is null.
*/
public Object alt(Object checkMe)
{
return alt(checkMe, this.defaultAlternate);
}
/**
* Returns the second argument if first argument specified is null.
* @param checkMe object to check
* @param alternate alternate value
* @return the second argument if the first is null.
*/
public Object alt(Object checkMe, Object alternate)
{
if (checkMe == null)
{
return alternate;
}
return checkMe;
}
/**
* Inserts HTML line break tag (&lt;br /&gt;) in front of all newline
* characters of the string value of the specified object and returns the
* resulting string.
* @param obj source object
* @return string with added br tags
*/
public String br(Object obj)
{
if (obj == null)
{
return null;
}
else
{
return String.valueOf(obj).replaceAll("\n", "<br />\n");
}
}
/**
* Removes HTML tags from the string value of the specified object and
* returns the resulting string.
* @param obj source object
* @return text stripped from HTML tags
*/
public String stripTags(Object obj)
{
return stripTags(obj, defaultAllowedTags);
}
/**
* Removes all not allowed HTML tags from the string value of the specified
* object and returns the resulting string.
* @param obj source object
* @param allowedTags An array of allowed tag names (i.e. "h1","br","img")
* @return text stripped from given HTML tags
*/
public String stripTags(Object obj, String... allowedTags)
{
if (obj == null)
{
return null;
}
//build list of tags to be used in regex pattern
StringBuilder allowedTagList = new StringBuilder();
if (allowedTags != null)
{
for (String tag : allowedTags)
{
if (tag !=null && tag.matches("[a-zA-Z0-9]+"))
{
if (allowedTagList.length() > 0)
{
allowedTagList.append("|");
}
allowedTagList.append(tag);
}
}
}
String tagRule = "<[^>]*?>";
if (allowedTagList.length() > 0)
{
tagRule = "<(?!/?(" + allowedTagList.toString() + ")[\\s>/])[^>]*?>";
}
return Pattern.compile(tagRule, Pattern.CASE_INSENSITIVE)
.matcher(String.valueOf(obj)).replaceAll("");
}
/**
* Builds plural form of a passed word if 'value' is plural, otherwise
* returns 'singular'. Plural form is built using some basic English
* language rules for nouns which does not guarantee correct syntax of a
* result in all cases.
* @param value count
* @param singular Singular form of a word.
* @return guessed plural
*/
public String plural(int value, String singular)
{
return plural(value, singular, null);
}
/**
* Returns 'plural' parameter if passed 'value' is plural, otherwise
* 'singular' is returned.
* @param value count
* @param singular Singular form of a word.
* @param plural Plural form of a word.
* @return guessed plural
*/
public String plural(int value, String singular, String plural)
{
if (value == 1 || value == -1)
{
return singular;
}
else if (plural != null)
{
return plural;
}
else if (singular == null || singular.length() == 0)
{
return singular;
}
else
{
//if the last letter is capital then we will append capital letters
boolean isCapital = !singular.substring(singular.length() - 1)
.toLowerCase().equals(singular
.substring(singular.length() - 1));
String word = singular.toLowerCase();
if (word.endsWith("x") || word.endsWith("sh")
|| word.endsWith("ch") || word.endsWith("s"))
{
return singular.concat(isCapital ? "ES" : "es");
}
else if (word.length() > 1
&& word.endsWith("y")
&& !word.substring(word.length() - 2, word.length() - 1)
.matches("[aeiou]"))
{
return singular.substring(0, singular.length() - 1)
.concat(isCapital ? "IES" : "ies");
}
else
{
return singular.concat(isCapital ? "S" : "s");
}
}
}
/**
* Safely retrieves the specified property from the specified object.
* Subclasses that wish to perform more advanced, efficient, or just
* different property retrieval methods should override this method to do
* so.
* @param object target object
* @param property target property
* @return property value
* @throws IllegalArgumentException if it wasn't possible to get the object's property value
*/
protected Object getProperty(Object object, String property)
{
try
{
return PropertyUtils.getProperty(object, property);
}
catch (Exception e)
{
throw new IllegalArgumentException("Could not retrieve '"
+ property + "' from " + object + ": " + e);
}
}
/**
* Returns the {@link Measurements} of the string value of the specified object.
* @param measureMe object to measure
* @return {@link Measurements} object
*/
public Measurements measure(Object measureMe)
{
if (measureMe == null)
{
return null;
}
return new Measurements(String.valueOf(measureMe));
}
/**
* Measures the dimensions of the string given to its constructor.
* Height is the number of lines in the string.
* Width is the number of characters in the longest line.
*/
public static class Measurements
{
private int height;
private int width;
public Measurements(String s)
{
String[] lines = s.split("\n");
height = lines.length;
for (String line : lines)
{
if (line.length() > width)
{
width = line.length();
}
}
}
public int getHeight()
{
return height;
}
public int getWidth()
{
return width;
}
public String toString()
{
StringBuilder out = new StringBuilder(28);
out.append("{ height: ");
out.append(height);
out.append(", width: ");
out.append(width);
out.append(" }");
return out.toString();
}
}
}