blob: c299312518d4bf767a9474d5be5b9a6712e09cbe [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.ioc.internal.util;
import org.apache.tapestry5.ioc.AnnotationProvider;
import org.apache.tapestry5.ioc.Locatable;
import org.apache.tapestry5.ioc.Location;
import org.apache.tapestry5.ioc.ObjectLocator;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.annotations.InjectService;
import static org.apache.tapestry5.ioc.internal.util.CollectionFactory.newList;
import static org.apache.tapestry5.ioc.internal.util.Defense.notBlank;
import org.apache.tapestry5.ioc.services.ClassFactory;
import java.io.Closeable;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
/**
* Utilities used within various internal implemenations of Tapestry IOC and the rest of the tapestry-core framework.
*/
public class InternalUtils
{
/**
* Leading punctiation on member names that is stripped off to form a property name or new member name.
*/
private static final String NAME_PREFIX = "_$";
/**
* Converts a method to a user presentable string using a {@link ClassFactory} to obtain a {@link Location} (where
* possible). {@link #asString(Method)} is used under the covers, to present a detailed, but not excessive,
* description of the class, method and parameters.
*
* @param method method to convert to a string
* @param classFactory used to obtain the {@link Location}
* @return the method formatted for presentation to the user
*/
public static String asString(Method method, ClassFactory classFactory)
{
Location location = classFactory.getMethodLocation(method);
return location != null ? location.toString() : asString(method);
}
/**
* Converts a method to a user presentable string consisting of the containing class name, the method name, and the
* short form of the parameter list (the class name of each parameter type, shorn of the package name portion).
*
* @param method
* @return short string representation
*/
public static String asString(Method method)
{
StringBuilder buffer = new StringBuilder();
buffer.append(method.getDeclaringClass().getName());
buffer.append(".");
buffer.append(method.getName());
buffer.append("(");
for (int i = 0; i < method.getParameterTypes().length; i++)
{
if (i > 0) buffer.append(", ");
String name = method.getParameterTypes()[i].getSimpleName();
buffer.append(name);
}
return buffer.append(")").toString();
}
/**
* Returns the size of an object array, or null if the array is empty.
*/
public static int size(Object[] array)
{
return array == null ? 0 : array.length;
}
/**
* Strips leading punctuation ("_" and "$") from the provided name.
*/
public static String stripMemberPrefix(String memberName)
{
StringBuilder builder = new StringBuilder(memberName);
// There may be other prefixes we want to strip off, at some point!
// Strip off leading characters defined by NAME_PREFIX
// This code is really ugly and needs to be fixed.
while (true)
{
char ch = builder.charAt(0);
if (InternalUtils.NAME_PREFIX.indexOf(ch) < 0) break;
builder.deleteCharAt(0);
}
return builder.toString();
}
/**
* Strips leading characters defined by {@link InternalUtils#NAME_PREFIX}, then adds the prefix back in.
*/
public static String createMemberName(String memberName)
{
return NAME_PREFIX + stripMemberPrefix(memberName);
}
/**
* Converts an enumeration (of Strings) into a sorted list of Strings.
*/
public static List<String> toList(Enumeration e)
{
List<String> result = newList();
while (e.hasMoreElements())
{
String name = (String) e.nextElement();
result.add(name);
}
Collections.sort(result);
return result;
}
/**
* Finds a specific annotation type within an array of annotations.
*
* @param <T>
* @param annotations to search
* @param annotationClass to match
* @return the annotation instance, if found, or null otherwise
*/
public static <T extends Annotation> T findAnnotation(Annotation[] annotations, Class<T> annotationClass)
{
for (Annotation a : annotations)
{
if (annotationClass.isInstance(a)) return annotationClass.cast(a);
}
return null;
}
@SuppressWarnings("unchecked")
private static Object calculateParameterValue(Class parameterType, final Annotation[] parameterAnnotations,
ObjectLocator locator, Map<Class, Object> parameterDefaults)
{
AnnotationProvider provider = new AnnotationProvider()
{
public <T extends Annotation> T getAnnotation(Class<T> annotationClass)
{
return findAnnotation(parameterAnnotations, annotationClass);
}
};
// At some point, it would be nice to eliminate InjectService, and rely
// entirely on service interface type and point-of-injection markers.
InjectService is = provider.getAnnotation(InjectService.class);
if (is != null)
{
String serviceId = is.value();
return locator.getService(serviceId, parameterType);
}
// In the absence of @InjectService, try some autowiring. First, does the
// parameter type match on of the resources (the parameter defaults)?
if (provider.getAnnotation(Inject.class) == null)
{
Object result = parameterDefaults.get(parameterType);
if (result != null) return result;
}
// Otherwise, make use of the MasterObjectProvider service to resolve this type (plus
// any other information gleaned from additional annotation) into the correct object.
return locator.getObject(parameterType, provider);
}
public static Object[] calculateParametersForMethod(Method method, ObjectLocator locator,
Map<Class, Object> parameterDefaults)
{
Class[] parameterTypes = method.getParameterTypes();
Annotation[][] annotations = method.getParameterAnnotations();
return calculateParameters(locator, parameterDefaults, parameterTypes, annotations);
}
public static Object[] calculateParametersForConstructor(Constructor constructor, ObjectLocator locator,
Map<Class, Object> parameterDefaults)
{
Class[] parameterTypes = constructor.getParameterTypes();
Annotation[][] annotations = constructor.getParameterAnnotations();
return calculateParameters(locator, parameterDefaults, parameterTypes, annotations);
}
public static Object[] calculateParameters(ObjectLocator locator, Map<Class, Object> parameterDefaults,
Class[] parameterTypes, Annotation[][] parameterAnnotations)
{
int parameterCount = parameterTypes.length;
Object[] parameters = new Object[parameterCount];
for (int i = 0; i < parameterCount; i++)
{
parameters[i] = calculateParameterValue(parameterTypes[i], parameterAnnotations[i], locator,
parameterDefaults);
}
return parameters;
}
/**
* Joins together some number of elements to form a comma separated list.
*/
public static String join(List elements)
{
return join(elements, ", ");
}
/**
* Joins together some number of elements. If a value in the list is the empty string, it is replaced with the
* string "(blank)".
*
* @param elements objects to be joined together
* @param separator used between elements when joining
*/
public static String join(List elements, String separator)
{
switch (elements.size())
{
case 0:
return "";
case 1:
return elements.get(0).toString();
default:
StringBuilder buffer = new StringBuilder();
boolean first = true;
for (Object o : elements)
{
if (!first) buffer.append(separator);
String string = String.valueOf(o);
if (string.equals("")) string = "(blank)";
buffer.append(string);
first = false;
}
return buffer.toString();
}
}
/**
* Creates a sorted copy of the provided elements, then turns that into a comma separated list.
*
* @return the elements converted to strings, sorted, joined with comma ... or "(none)" if the elements are null or
* empty
*/
public static String joinSorted(Collection elements)
{
if (elements == null || elements.isEmpty()) return "(none)";
List<String> list = newList();
for (Object o : elements)
list.add(String.valueOf(o));
Collections.sort(list);
return join(list);
}
/**
* Returns true if the input is null, or is a zero length string (excluding leading/trailing whitespace).
*/
public static boolean isBlank(String input)
{
return input == null || input.length() == 0 || input.trim().length() == 0;
}
public static boolean isNonBlank(String input)
{
return !isBlank(input);
}
/**
* Capitalizes a string, converting the first character to uppercase.
*/
public static String capitalize(String input)
{
if (input.length() == 0) return input;
return input.substring(0, 1).toUpperCase() + input.substring(1);
}
/**
* Sniffs the object to see if it is a {@link Location} or {@link Locatable}. Returns null if null or not
* convertable to a location.
*/
public static Location locationOf(Object location)
{
if (location == null) return null;
if (location instanceof Location) return (Location) location;
if (location instanceof Locatable) return ((Locatable) location).getLocation();
return null;
}
/**
* Extracts the string keys from a map and returns them in sorted order. The keys are converted to strings.
*
* @param map the map to extract keys from (may be null)
* @return the sorted keys, or the empty set if map is null
*/
public static List<String> sortedKeys(Map map)
{
if (map == null) return Collections.emptyList();
List<String> keys = newList();
for (Object o : map.keySet())
keys.add(String.valueOf(o));
Collections.sort(keys);
return keys;
}
/**
* Gets a value from a map (which may be null).
*
* @param <K>
* @param <V>
* @param map the map to extract from (may be null)
* @param key
* @return the value from the map, or null if the map is null
*/
public static <K, V> V get(Map<K, V> map, K key)
{
if (map == null) return null;
return map.get(key);
}
/**
* Returns true if the method provided is a static method.
*/
public static boolean isStatic(Method method)
{
return Modifier.isStatic(method.getModifiers());
}
public static <T> Iterator<T> reverseIterator(final List<T> list)
{
final ListIterator<T> normal = list.listIterator(list.size());
return new Iterator<T>()
{
public boolean hasNext()
{
return normal.hasPrevious();
}
public T next()
{
// TODO Auto-generated method stub
return normal.previous();
}
public void remove()
{
throw new UnsupportedOperationException();
}
};
}
/**
* Return true if the input string contains the marker for symbols that must be expanded.
*/
public static boolean containsSymbols(String input)
{
return input.contains("${");
}
/**
* Searches the string for the final period ('.') character and returns everything after that. The input string is
* generally a fully qualified class name, though tapestry-core also uses this method for the occasional property
* expression (which is also dot separated). Returns the input string unchanged if it does not contain a period
* character.
*/
public static String lastTerm(String input)
{
notBlank(input, "input");
int dotx = input.lastIndexOf('.');
if (dotx < 0) return input;
return input.substring(dotx + 1);
}
/**
* Searches a class for the "best" constructor, the public constructor with the most parameters. Returns null if
* there are no public constructors. If there is more than one constructor with the maximum number of parameters, it
* is not determined which will be returned (don't build a class like that!). In addition, if a constructor is
* annotated with {@link org.apache.tapestry5.ioc.annotations.Inject}, it will be used (no check for multiple such
* constructors is made, only at most a single constructor should have the annotation).
*
* @param clazz to search for a constructor for
* @return the constructor to be used to instantiate the class, or null if no appropriate constructor was found
*/
public static Constructor findAutobuildConstructor(Class clazz)
{
Constructor[] constructors = clazz.getConstructors();
switch (constructors.length)
{
case 1:
return constructors[0];
case 0:
return null;
default:
break;
}
for (Constructor c : constructors)
{
if (c.getAnnotation(Inject.class) != null) return c;
}
// Choose a constructor with the most parameters.
Comparator<Constructor> comparator = new Comparator<Constructor>()
{
public int compare(Constructor o1, Constructor o2)
{
return o2.getParameterTypes().length - o1.getParameterTypes().length;
}
};
Arrays.sort(constructors, comparator);
return constructors[0];
}
/**
* Adds a value to a specially organized map where the values are lists of objects. This somewhat simulates a map
* that allows mutiple values for the same key.
*
* @param map to store value into
* @param key for which a value is added
* @param value to add
* @param <K> the type of key
* @param <V> the type of the list
*/
public static <K, V> void addToMapList(Map<K, List<V>> map, K key, V value)
{
List<V> list = map.get(key);
if (list == null)
{
list = newList();
map.put(key, list);
}
list.add(value);
}
/**
* Validates that the marker annotation class had a retention policy of runtime.
*
* @param markerClass the marker annotation class
*/
public static void validateMarkerAnnotation(Class markerClass)
{
Retention policy = (Retention) markerClass.getAnnotation(Retention.class);
if (policy != null && policy.value() == RetentionPolicy.RUNTIME) return;
throw new IllegalArgumentException(UtilMessages.badMarkerAnnotation(markerClass));
}
public static void validateMarkerAnnotations(Class[] markerClasses)
{
for (Class markerClass : markerClasses) validateMarkerAnnotation(markerClass);
}
public static void close(Closeable stream)
{
if (stream != null) try
{
stream.close();
}
catch (IOException ex)
{
// Ignore.
}
}
/**
* Extracts the message from an exception. If the exception's message is null, returns the exceptions class name.
*
* @param exception to extract message from
* @return message or class name
*/
public static String toMessage(Throwable exception)
{
String message = exception.getMessage();
if (message != null) return message;
return exception.getClass().getName();
}
}