blob: 251c705bd4ef1932305bbb8a3c0a91f11f86a6d3 [file] [log] [blame]
// Copyright 2006, 2007, 2008 The Apache Software Foundation
//
// 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 org.apache.tapestry5.internal;
import org.apache.commons.codec.EncoderException;
import org.apache.commons.codec.net.URLCodec;
import org.apache.tapestry5.OptionModel;
import org.apache.tapestry5.SelectModel;
import org.apache.tapestry5.beaneditor.OrderAfter;
import org.apache.tapestry5.beaneditor.OrderBefore;
import org.apache.tapestry5.ioc.Location;
import org.apache.tapestry5.ioc.Messages;
import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
import static org.apache.tapestry5.ioc.internal.util.CollectionFactory.newList;
import org.apache.tapestry5.ioc.internal.util.Defense;
import static org.apache.tapestry5.ioc.internal.util.Defense.notNull;
import org.apache.tapestry5.ioc.internal.util.InternalUtils;
import org.apache.tapestry5.ioc.internal.util.Orderer;
import org.apache.tapestry5.ioc.services.ClassFactory;
import org.apache.tapestry5.ioc.services.ClassPropertyAdapter;
import org.apache.tapestry5.ioc.services.PropertyAdapter;
import org.slf4j.Logger;
import java.lang.reflect.Method;
import java.util.BitSet;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/**
* Shared utility methods used by various implementation classes.
*/
public class TapestryInternalUtils
{
private static final Pattern NON_WORD_PATTERN = Pattern.compile("[^\\w]");
private static final URLCodec CODEC = new URLCodec()
{
private BitSet contextSafe = (BitSet) WWW_FORM_URL.clone();
{
// Servlet container does not decode '+' in path to ' ',
// so we encode ' ' to %20, not to '+'.
contextSafe.clear(' ');
}
@Override
public byte[] encode(byte[] bytes)
{
return encodeUrl(contextSafe, bytes);
}
};
private TapestryInternalUtils()
{
// Prevent instantiation.
}
/**
* Capitalizes the string, and inserts a space before each upper case character (or sequence of upper case
* characters). Thus "userId" becomes "User Id", etc. Also, converts underscore into space (and capitalizes the
* following word), thus "user_id" also becomes "User Id".
*/
public static String toUserPresentable(String id)
{
StringBuilder builder = new StringBuilder(id.length() * 2);
char[] chars = id.toCharArray();
boolean postSpace = true;
boolean upcaseNext = true;
for (char ch : chars)
{
if (upcaseNext)
{
builder.append(Character.toUpperCase(ch));
upcaseNext = false;
continue;
}
if (ch == '_')
{
builder.append(' ');
upcaseNext = true;
continue;
}
boolean upperCase = Character.isUpperCase(ch);
if (upperCase && !postSpace) builder.append(' ');
builder.append(ch);
postSpace = upperCase;
}
return builder.toString();
}
public static Map<String, String> mapFromKeysAndValues(String... keysAndValues)
{
Map<String, String> result = CollectionFactory.newMap();
int i = 0;
while (i < keysAndValues.length)
{
String key = keysAndValues[i++];
String value = keysAndValues[i++];
result.put(key, value);
}
return result;
}
/**
* Converts a string to an {@link OptionModel}. The string is of the form "value=label". If the equals sign is
* omitted, then the same value is used for both value and label.
*
* @param input
* @return
*/
public static OptionModel toOptionModel(String input)
{
Defense.notNull(input, "input");
int equalsx = input.indexOf('=');
if (equalsx < 0) return new OptionModelImpl(input);
String value = input.substring(0, equalsx);
String label = input.substring(equalsx + 1);
return new OptionModelImpl(label, value);
}
/**
* Parses a string input into a series of value=label pairs compatible with {@link #toOptionModel(String)}. Splits
* on commas. Ignores whitespace around commas.
*
* @param input comma seperated list of terms
* @return list of option models
*/
public static List<OptionModel> toOptionModels(String input)
{
Defense.notNull(input, "input");
List<OptionModel> result = newList();
for (String term : input.split(","))
result.add(toOptionModel(term.trim()));
return result;
}
/**
* Wraps the result of {@link #toOptionModels(String)} as a {@link SelectModel} (with no option groups).
*
* @param input
* @return
*/
public static SelectModel toSelectModel(String input)
{
List<OptionModel> options = toOptionModels(input);
return new SelectModelImpl(null, options);
}
/**
* Converts a map entry to an {@link OptionModel}.
*
* @param input
* @return
*/
public static OptionModel toOptionModel(Map.Entry input)
{
notNull(input, "input");
String label = input.getValue() != null ? String.valueOf(input.getValue()) : "";
return new OptionModelImpl(label, input.getKey());
}
/**
* Processes a map input into a series of map entries compatible with {@link #toOptionModel(Map.Entry)}.
*
* @param input map of elements
* @return list of option models
*/
public static <K, V> List<OptionModel> toOptionModels(Map<K, V> input)
{
Defense.notNull(input, "input");
List<OptionModel> result = newList();
for (Map.Entry entry : input.entrySet())
result.add(toOptionModel(entry));
return result;
}
/**
* Wraps the result of {@link #toOptionModels(Map)} as a {@link SelectModel} (with no option groups).
*
* @param input
* @return
*/
public static <K, V> SelectModel toSelectModel(Map<K, V> input)
{
List<OptionModel> options = toOptionModels(input);
return new SelectModelImpl(null, options);
}
/**
* Converts an object to an {@link OptionModel}.
*
* @param input
* @return
*/
public static OptionModel toOptionModel(Object input)
{
String label = (input != null ? String.valueOf(input) : "");
return new OptionModelImpl(label, input);
}
/**
* Processes a list input into a series of objects compatible with {@link #toOptionModel(Object)}.
*
* @param input list of elements
* @return list of option models
*/
public static <E> List<OptionModel> toOptionModels(List<E> input)
{
Defense.notNull(input, "input");
List<OptionModel> result = newList();
for (E element : input)
result.add(toOptionModel(element));
return result;
}
/**
* Wraps the result of {@link #toOptionModels(List)} as a {@link SelectModel} (with no option groups).
*
* @param input
* @return
*/
public static <E> SelectModel toSelectModel(List<E> input)
{
List<OptionModel> options = toOptionModels(input);
return new SelectModelImpl(null, options);
}
/**
* Parses a key/value pair where the key and the value are seperated by an equals sign. The key and value are
* trimmed of leading and trailing whitespace, and returned as a {@link KeyValue}.
*
* @param input
* @return
*/
public static KeyValue parseKeyValue(String input)
{
int pos = input.indexOf('=');
if (pos < 1) throw new IllegalArgumentException(InternalMessages.badKeyValue(input));
String key = input.substring(0, pos);
String value = input.substring(pos + 1);
return new KeyValue(key.trim(), value.trim());
}
/**
* Used to convert a property expression into a key that can be used to locate various resources (Blocks, messages,
* etc.). Strips out any punctuation characters, leaving just words characters (letters, number and the
* underscore).
*
* @param expression a property expression
* @return the expression with punctuation removed
*/
public static String extractIdFromPropertyExpression(String expression)
{
return replace(expression, NON_WORD_PATTERN, "");
}
/**
* Looks for a label within the messages based on the id. If found, it is used, otherwise the name is converted to a
* user presentable form.
*/
public static String defaultLabel(String id, Messages messages, String propertyExpression)
{
String key = id + "-label";
if (messages.contains(key)) return messages.get(key);
return toUserPresentable(extractIdFromPropertyExpression(lastTerm(propertyExpression)));
}
/**
* Strips a dotted sequence (such as a property expression, or a qualified class name) down to the last term of that
* expression, by locating the last period ('.') in the string.
*/
public static String lastTerm(String input)
{
int dotx = input.lastIndexOf('.');
return input.substring(dotx + 1);
}
/**
* Converts an list of strings into a space-separated string combining them all, suitable for use as an HTML class
* attribute value.
*
* @param classes classes to combine
* @return the joined classes, or null if classes is empty
*/
public static String toClassAttributeValue(List<String> classes)
{
if (classes.isEmpty()) return null;
return InternalUtils.join(classes, " ");
}
private static class PropertyOrder implements Comparable<PropertyOrder>
{
final String propertyName;
final int classDepth;
final int sortKey;
public PropertyOrder(final String propertyName, int classDepth, int sortKey)
{
this.propertyName = propertyName;
this.classDepth = classDepth;
this.sortKey = sortKey;
}
public int compareTo(PropertyOrder o)
{
int result = classDepth - o.classDepth;
if (result == 0) result = sortKey - o.sortKey;
if (result == 0) result = propertyName.compareTo(o.propertyName);
return result;
}
}
/**
* Sorts the property names into presentation order. Filters out any properties that have an explicit {@link
* OrderBefore}, leaving the remainder. Estimates each propertie's position based on the relative position of the
* property's getter. The code assumes that all methods are readable (have a getter method).
*
* @param classAdapter defines the bean that contains the properties
* @param classFactory used to access method line number information
* @param propertyNames the initial set of property names
* @return propertyNames filtered and sorted
*/
public static List<String> orderProperties(Logger logger, ClassPropertyAdapter classAdapter,
ClassFactory classFactory, List<String> propertyNames)
{
// Property name to a list of constraints.
Map<String, List<String>> constraints = CollectionFactory.newMap();
List<PropertyOrder> properties = newList();
for (String name : propertyNames)
{
PropertyAdapter pa = classAdapter.getPropertyAdapter(name);
List<String> propertyConstraints = CollectionFactory.newList();
OrderBefore beforeAnnotation = pa.getAnnotation(OrderBefore.class);
if (beforeAnnotation != null) propertyConstraints.add("before:" + beforeAnnotation.value());
OrderAfter afterAnnotation = pa.getAnnotation(OrderAfter.class);
if (afterAnnotation != null) propertyConstraints.add("after:" + afterAnnotation.value());
if (!propertyConstraints.isEmpty()) constraints.put(name, propertyConstraints);
Method readMethod = pa.getReadMethod();
Location location = classFactory.getMethodLocation(readMethod);
properties.add(new PropertyOrder(name, computeDepth(readMethod), location.getLine()));
}
Collections.sort(properties);
Orderer<String> orderer = new Orderer<String>(logger);
String prev = null;
for (PropertyOrder po : properties)
{
String name = po.propertyName;
List<String> propertyConstraints = constraints.get(name);
if (propertyConstraints != null)
{
String[] asArray = propertyConstraints.toArray(new String[propertyConstraints
.size()]);
orderer.add(name, name, asArray);
prev = name;
continue;
}
if (prev == null) orderer.add(name, name);
else orderer.add(name, name, "after:" + prev);
prev = name;
}
return orderer.getOrdered();
}
private static int computeDepth(Method method)
{
int depth = 0;
Class c = method.getDeclaringClass();
// When the method originates in an interface, the parent may be null, not Object.
while (c != null && c != Object.class)
{
depth++;
c = c.getSuperclass();
}
return depth;
}
/**
* Converts an enum to a label string, allowing for overrides from a message catalog.
* <p/>
* <ul> <li>As key <em>prefix</em>.<em>name</em> if present. Ex: "ElementType.LOCAL_VARIABLE" <li>As key
* <em>name</em> if present, i.e., "LOCAL_VARIABLE". <li>As a user-presentable version of the name, i.e., "Local
* Variable". </ul>
*
* @param messages the messages to search for the label
* @param prefix
* @param value to get a label for
* @return the label
*/
public static String getLabelForEnum(Messages messages, String prefix, Enum value)
{
String name = value.name();
String key = prefix + "." + name;
if (messages.contains(key)) return messages.get(key);
if (messages.contains(name)) return messages.get(name);
return toUserPresentable(name.toLowerCase());
}
public static String getLabelForEnum(Messages messages, Enum value)
{
String prefix = lastTerm(value.getClass().getName());
return getLabelForEnum(messages, prefix, value);
}
/**
* Encodes a string for inclusion in a URL. Slashes and percents are converted to "%25" and "%2F" respectively,
* then the entire string is URL encoded.
*
* @param input string to include, may not be blank
* @return encoded input
*/
public static String encodeContext(String input)
{
Defense.notBlank(input, "input");
try
{
return CODEC.encode(escapePercentAndSlash(input));
}
catch (EncoderException ex)
{
throw new RuntimeException(ex);
}
}
private static final String PERCENT = "%";
private static final Pattern PERCENT_PATTERN = Pattern.compile(PERCENT);
private static final String ENCODED_PERCENT = "%25";
private static final Pattern ENCODED_PERCENT_PATTERN = Pattern.compile(ENCODED_PERCENT);
private static final String SLASH = "/";
private static final Pattern SLASH_PATTERN = Pattern.compile(SLASH);
private static final String ENCODED_SLASH = "%2F";
private static final Pattern ENCODED_SLASH_PATTERN = Pattern.compile(ENCODED_SLASH, Pattern.CASE_INSENSITIVE);
/**
* Encodes percent and slash characters in the string for later decoding via {@link
* #unescapePercentAndSlash(String)}.
*
* @param input string to encode
* @return modified string
*/
public static String escapePercentAndSlash(String input)
{
return replace(replace(input, PERCENT_PATTERN, ENCODED_PERCENT), SLASH_PATTERN, ENCODED_SLASH);
}
/**
* Used to decode certain escaped characters that are replaced when using {@link #encodeContext(String)}}.
*
* @param input a previously encoded string
* @return the string with slash and percent characters restored
*/
public static String unescapePercentAndSlash(String input)
{
return replace(replace(input, ENCODED_SLASH_PATTERN, SLASH), ENCODED_PERCENT_PATTERN, PERCENT);
}
private static String replace(String input, Pattern pattern, String replacement)
{
return pattern.matcher(input).replaceAll(replacement);
}
/**
* Determines if the two values are equal. They are equal if they are the exact same value (including if they are
* both null). Otherwise standard equals() comparison is used.
*
* @param <T>
* @param left value to compare, possibly null
* @param right value to compare, possibly null
* @return true if same value, both null, or equal
*/
public static <T> boolean isEqual(T left, T right)
{
if (left == right) return true;
if (left == null) return right == null;
return left.equals(right);
}
/**
* Splits a path at each slash.
*/
public static String[] splitPath(String path)
{
return SLASH_PATTERN.split(path);
}
}