You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@wicket.apache.org by mg...@apache.org on 2012/03/07 11:42:23 UTC

[8/24] WICKET-4439 Move classes around so that there are no two packages with the same name in different modules

http://git-wip-us.apache.org/repos/asf/wicket/blob/ddc37320/wicket-core/src/main/java/org/apache/wicket/core/util/lang/PropertyResolver.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/lang/PropertyResolver.java b/wicket-core/src/main/java/org/apache/wicket/core/util/lang/PropertyResolver.java
new file mode 100644
index 0000000..01efa84
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/lang/PropertyResolver.java
@@ -0,0 +1,1542 @@
+/*
+ * 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.
+ */
+package org.apache.wicket.core.util.lang;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.wicket.Application;
+import org.apache.wicket.Session;
+import org.apache.wicket.WicketRuntimeException;
+import org.apache.wicket.util.convert.ConversionException;
+import org.apache.wicket.util.lang.Generics;
+import org.apache.wicket.util.string.Strings;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * NOTE: THIS CLASS IS NOT PART OF THE WICKET PUBLIC API, DO NOT USE IT UNLESS YOU KNOW WHAT YOU ARE
+ * DOING.
+ * <p>
+ * This class parses expressions to lookup or set a value on the object that is given. <br/>
+ * The supported expressions are:
+ * <p>
+ * "property": This can can then be a bean property with get and set method. Or if a map is given as
+ * an object it will be lookup with the property as a key when there is not get method for that
+ * property.
+ * <p/>
+ * <p>
+ * "property1.property2": Both properties are lookup as written above. If property1 evaluates to
+ * null then if there is a setMethod (or if it is a map) and the Class of the property has a default
+ * constructor then the object will be constructed and set on the object.
+ * <p/>
+ * <p>
+ * "property.index": If the property is a List or Array then the second property can be a index on
+ * that list like: 'mylist.0' this expression will also map on a getProperty(index) or
+ * setProperty(index,value) methods. If the object is a List then the list will grow automatically
+ * if the index is greater then the size
+ * <p/>
+ * <p>
+ * Index or map properties can also be written as: "property[index]" or "property[key]"
+ * <p/>
+ *
+ * @author jcompagner
+ */
+@SuppressWarnings("unused")
+public final class PropertyResolver
+{
+	/** Log. */
+	private static final Logger log = LoggerFactory.getLogger(PropertyResolver.class);
+
+	private final static int RETURN_NULL = 0;
+	private final static int CREATE_NEW_VALUE = 1;
+	private final static int RESOLVE_CLASS = 2;
+
+	private final static Map<Object, IClassCache> applicationToClassesToGetAndSetters = Generics.newConcurrentHashMap(2);
+
+	private static final String GET = "get";
+	private static final String IS = "is";
+	private static final String SET = "set";
+
+	/**
+	 * Looks up the value from the object with the given expression. If the expression, the object
+	 * itself or one property evaluates to null then a null will be returned.
+	 *
+	 * @param expression
+	 *            The expression string with the property to be lookup.
+	 * @param object
+	 *            The object which is evaluated.
+	 * @return The value that is evaluated. Null something in the expression evaluated to null.
+	 */
+	public final static Object getValue(final String expression, final Object object)
+	{
+		if (expression == null || expression.equals("") || object == null)
+		{
+			return object;
+		}
+
+		ObjectAndGetSetter getter = getObjectAndGetSetter(expression, object, RETURN_NULL);
+		if (getter == null)
+		{
+			return null;
+		}
+
+		return getter.getValue();
+	}
+
+	/**
+	 * Set the value on the object with the given expression. If the expression can't be evaluated
+	 * then a WicketRuntimeException will be thrown. If a null object is encountered then it will
+	 * try to generate it by calling the default constructor and set it on the object.
+	 *
+	 * The value will be tried to convert to the right type with the given converter.
+	 *
+	 * @param expression
+	 *            The expression string with the property to be set.
+	 * @param object
+	 *            The object which is evaluated to set the value on.
+	 * @param value
+	 *            The value to set.
+	 * @param converter
+	 *            The converter to convert the value if needed to the right type.
+	 */
+	public final static void setValue(final String expression, final Object object,
+		final Object value, final PropertyResolverConverter converter)
+	{
+		if (expression == null || expression.equals(""))
+		{
+			throw new WicketRuntimeException("Empty expression setting value: " + value +
+				" on object: " + object);
+		}
+		if (object == null)
+		{
+			throw new WicketRuntimeException(
+				"Attempted to set property value on a null object. Property expression: " +
+					expression + " Value: " + value);
+		}
+
+		ObjectAndGetSetter setter = getObjectAndGetSetter(expression, object, CREATE_NEW_VALUE);
+		if (setter == null)
+		{
+			throw new WicketRuntimeException("Null object returned for expression: " + expression +
+				" for setting value: " + value + " on: " + object);
+		}
+		setter.setValue(value, converter == null ? new PropertyResolverConverter(Application.get()
+			.getConverterLocator(), Session.get().getLocale()) : converter);
+	}
+
+	/**
+	 * @param expression
+	 * @param object
+	 * @return class of the target property object
+	 */
+	public final static Class<?> getPropertyClass(final String expression, final Object object)
+	{
+		ObjectAndGetSetter setter = getObjectAndGetSetter(expression, object, RESOLVE_CLASS);
+		if (setter == null)
+		{
+			throw new WicketRuntimeException("Null object returned for expression: " + expression +
+				" for getting the target classs of: " + object);
+		}
+		return setter.getTargetClass();
+	}
+
+	/**
+	 * @param <T>
+	 * @param expression
+	 * @param clz
+	 * @return class of the target Class property expression
+	 */
+	@SuppressWarnings("unchecked")
+	public static <T> Class<T> getPropertyClass(final String expression, final Class<?> clz)
+	{
+		ObjectAndGetSetter setter = getObjectAndGetSetter(expression, null, RESOLVE_CLASS, clz);
+		if (setter == null)
+		{
+			throw new WicketRuntimeException("No Class returned for expression: " + expression +
+				" for getting the target classs of: " + clz);
+		}
+		return (Class<T>)setter.getTargetClass();
+	}
+
+	/**
+	 * @param expression
+	 * @param object
+	 * @return Field for the property expression or null if such field doesn't exist (only getters
+	 *         and setters)
+	 */
+	public final static Field getPropertyField(final String expression, final Object object)
+	{
+		ObjectAndGetSetter setter = getObjectAndGetSetter(expression, object, RESOLVE_CLASS);
+		if (setter == null)
+		{
+			throw new WicketRuntimeException("Null object returned for expression: " + expression +
+				" for getting the target classs of: " + object);
+		}
+		return setter.getField();
+	}
+
+	/**
+	 * @param expression
+	 * @param object
+	 * @return Getter method for the property expression or null if such getter doesn't exist (only
+	 *         field)
+	 */
+	public final static Method getPropertyGetter(final String expression, final Object object)
+	{
+		ObjectAndGetSetter setter = getObjectAndGetSetter(expression, object, RESOLVE_CLASS);
+		if (setter == null)
+		{
+			throw new WicketRuntimeException("Null object returned for expression: " + expression +
+				" for getting the target classs of: " + object);
+		}
+		return setter.getGetter();
+	}
+
+	/**
+	 * @param expression
+	 * @param object
+	 * @return Setter method for the property expression or null if such setter doesn't exist (only
+	 *         field)
+	 */
+	public final static Method getPropertySetter(final String expression, final Object object)
+	{
+		ObjectAndGetSetter setter = getObjectAndGetSetter(expression, object, RESOLVE_CLASS);
+		if (setter == null)
+		{
+			throw new WicketRuntimeException("Null object returned for expression: " + expression +
+				" for getting the target classs of: " + object);
+		}
+		return setter.getSetter();
+	}
+
+	/**
+	 * Just delegating the call to the original getObjectAndGetSetter passing the object type as
+	 * parameter.
+	 *
+	 * @param expression
+	 * @param object
+	 * @param tryToCreateNull
+	 * @return {@link ObjectAndGetSetter}
+	 */
+	private static ObjectAndGetSetter getObjectAndGetSetter(final String expression,
+		final Object object, int tryToCreateNull)
+	{
+		return getObjectAndGetSetter(expression, object, tryToCreateNull, object.getClass());
+	}
+
+	/**
+	 * Receives the class parameter also, since this method can resolve the type for some
+	 * expression, only knowing the target class
+	 *
+	 * @param expression
+	 * @param object
+	 * @param tryToCreateNull
+	 * @param clz
+	 * @return {@link ObjectAndGetSetter}
+	 */
+	private static ObjectAndGetSetter getObjectAndGetSetter(final String expression,
+		final Object object, final int tryToCreateNull, Class<?> clz)
+	{
+		String expressionBracketsSeperated = Strings.replaceAll(expression, "[", ".[").toString();
+		int index = getNextDotIndex(expressionBracketsSeperated, 0);
+		while (index == 0 && expressionBracketsSeperated.startsWith("."))
+		{
+			// eat dots at the beginning of the expression since they will confuse
+			// later steps
+			expressionBracketsSeperated = expressionBracketsSeperated.substring(1);
+			index = getNextDotIndex(expressionBracketsSeperated, 0);
+		}
+		int lastIndex = 0;
+		Object value = object;
+		String exp = expressionBracketsSeperated;
+		while (index != -1)
+		{
+			exp = expressionBracketsSeperated.substring(lastIndex, index);
+			if (exp.length() == 0)
+			{
+				exp = expressionBracketsSeperated.substring(index + 1);
+				break;
+			}
+
+			IGetAndSet getAndSetter = null;
+			try
+			{
+				getAndSetter = getGetAndSetter(exp, clz);
+			}
+			catch (WicketRuntimeException ex)
+			{
+				// expression by it self can't be found. try to find a
+				// setPropertyByIndex(int,value) method
+				index = getNextDotIndex(expressionBracketsSeperated, index + 1);
+				if (index != -1)
+				{
+					String indexExpression = expressionBracketsSeperated.substring(lastIndex, index);
+					getAndSetter = getGetAndSetter(indexExpression, clz);
+				}
+				else
+				{
+					exp = expressionBracketsSeperated.substring(lastIndex);
+					break;
+				}
+			}
+			Object newValue = null;
+			if (value != null)
+			{
+				newValue = getAndSetter.getValue(value);
+			}
+			if (newValue == null)
+			{
+				if (tryToCreateNull == CREATE_NEW_VALUE)
+				{
+					newValue = getAndSetter.newValue(value);
+					if (newValue == null)
+					{
+						return null;
+					}
+				}
+				else if (tryToCreateNull == RESOLVE_CLASS)
+				{
+					clz = getAndSetter.getTargetClass();
+				}
+				else
+				{
+					return null;
+				}
+			}
+			value = newValue;
+			if (value != null)
+			{
+				// value can be null if we are in the RESOLVE_CLASS
+				clz = value.getClass();
+			}
+
+			lastIndex = index + 1;
+			index = getNextDotIndex(expressionBracketsSeperated, lastIndex);
+			if (index == -1)
+			{
+				exp = expressionBracketsSeperated.substring(lastIndex);
+				break;
+			}
+		}
+		IGetAndSet getAndSetter = getGetAndSetter(exp, clz);
+		return new ObjectAndGetSetter(getAndSetter, value);
+	}
+
+	/**
+	 *
+	 * @param expression
+	 * @param start
+	 * @return next dot index
+	 */
+	private static int getNextDotIndex(final String expression, final int start)
+	{
+		boolean insideBracket = false;
+		for (int i = start; i < expression.length(); i++)
+		{
+			char ch = expression.charAt(i);
+			if (ch == '.' && !insideBracket)
+			{
+				return i;
+			}
+			else if (ch == '[')
+			{
+				insideBracket = true;
+			}
+			else if (ch == ']')
+			{
+				insideBracket = false;
+			}
+		}
+		return -1;
+	}
+
+	private final static IGetAndSet getGetAndSetter(String exp, final Class<?> clz)
+	{
+		IClassCache classesToGetAndSetters = getClassesToGetAndSetters();
+		Map<String, IGetAndSet> getAndSetters = classesToGetAndSetters.get(clz);
+		if (getAndSetters == null)
+		{
+			getAndSetters = new ConcurrentHashMap<String, IGetAndSet>(8);
+			classesToGetAndSetters.put(clz, getAndSetters);
+		}
+
+		IGetAndSet getAndSetter = getAndSetters.get(exp);
+		if (getAndSetter == null)
+		{
+			Method method = null;
+			Field field = null;
+			if (exp.startsWith("["))
+			{
+				// if expression begins with [ skip method finding and use it as
+				// a key/index lookup on a map.
+				exp = exp.substring(1, exp.length() - 1);
+			}
+			else if (exp.endsWith("()"))
+			{
+				// if expression ends with (), don't test for setters just skip
+				// directly to method finding.
+				method = findMethod(clz, exp);
+			}
+			else
+			{
+				method = findGetter(clz, exp);
+			}
+			if (method == null)
+			{
+				if (List.class.isAssignableFrom(clz))
+				{
+					try
+					{
+						int index = Integer.parseInt(exp);
+						getAndSetter = new ListGetSet(index);
+					}
+					catch (NumberFormatException ex)
+					{
+						// can't parse the exp as an index, maybe the exp was a
+						// method.
+						method = findMethod(clz, exp);
+						if (method != null)
+						{
+							getAndSetter = new MethodGetAndSet(method, MethodGetAndSet.findSetter(
+								method, clz), null);
+						}
+						else
+						{
+							field = findField(clz, exp);
+							if (field != null)
+							{
+								getAndSetter = new FieldGetAndSetter(field);
+							}
+							else
+							{
+								throw new WicketRuntimeException(
+									"The expression '" +
+										exp +
+										"' is neither an index nor is it a method or field for the list " +
+										clz);
+							}
+						}
+					}
+				}
+				else if (Map.class.isAssignableFrom(clz))
+				{
+					getAndSetter = new MapGetSet(exp);
+				}
+				else if (clz.isArray())
+				{
+					try
+					{
+						int index = Integer.parseInt(exp);
+						getAndSetter = new ArrayGetSet(clz.getComponentType(), index);
+					}
+					catch (NumberFormatException ex)
+					{
+						if (exp.equals("length") || exp.equals("size"))
+						{
+							getAndSetter = new ArrayLengthGetSet();
+						}
+						else
+						{
+							throw new WicketRuntimeException("Can't parse the expression '" + exp +
+								"' as an index for an array lookup");
+						}
+					}
+				}
+				else
+				{
+					field = findField(clz, exp);
+					if (field == null)
+					{
+						method = findMethod(clz, exp);
+						if (method == null)
+						{
+							int index = exp.indexOf('.');
+							if (index != -1)
+							{
+								String propertyName = exp.substring(0, index);
+								String propertyIndex = exp.substring(index + 1);
+								try
+								{
+									int parsedIndex = Integer.parseInt(propertyIndex);
+									// if so then it could be a getPropertyIndex(int)
+									// and setPropertyIndex(int, object)
+									String name = Character.toUpperCase(propertyName.charAt(0)) +
+										propertyName.substring(1);
+									method = clz.getMethod(GET + name, new Class[] { int.class });
+									getAndSetter = new ArrayPropertyGetSet(method, parsedIndex);
+								}
+								catch (Exception e)
+								{
+									throw new WicketRuntimeException(
+										"No get method defined for class: " + clz +
+											" expression: " + propertyName);
+								}
+							}
+							else
+							{
+								// We do not look for a public FIELD because
+								// that is not good programming with beans patterns
+								throw new WicketRuntimeException(
+									"No get method defined for class: " + clz + " expression: " +
+										exp);
+							}
+						}
+						else
+						{
+							getAndSetter = new MethodGetAndSet(method, MethodGetAndSet.findSetter(
+								method, clz), null);
+						}
+					}
+					else
+					{
+						getAndSetter = new FieldGetAndSetter(field);
+					}
+				}
+			}
+			else
+			{
+				field = findField(clz, exp);
+				getAndSetter = new MethodGetAndSet(method, MethodGetAndSet.findSetter(method, clz),
+					field);
+			}
+			getAndSetters.put(exp, getAndSetter);
+		}
+		return getAndSetter;
+	}
+
+
+	/**
+	 * @param clz
+	 * @param expression
+	 * @return introspected field
+	 */
+	private static Field findField(final Class<?> clz, final String expression)
+	{
+		Field field = null;
+		try
+		{
+			field = clz.getField(expression);
+		}
+		catch (Exception e)
+		{
+			Class<?> tmp = clz;
+			while (tmp != null && tmp != Object.class)
+			{
+				Field[] fields = tmp.getDeclaredFields();
+				for (Field aField : fields)
+				{
+					if (aField.getName().equals(expression))
+					{
+						aField.setAccessible(true);
+						return aField;
+					}
+				}
+				tmp = tmp.getSuperclass();
+			}
+			log.debug("Cannot find field " + clz + "." + expression);
+		}
+		return field;
+	}
+
+	/**
+	 * @param clz
+	 * @param expression
+	 * @return The method for the expression null if not found
+	 */
+	private final static Method findGetter(final Class<?> clz, final String expression)
+	{
+		String name = Character.toUpperCase(expression.charAt(0)) + expression.substring(1);
+		Method method = null;
+		try
+		{
+			method = clz.getMethod(GET + name, (Class[])null);
+		}
+		catch (Exception ignored)
+		{
+		}
+		if (method == null)
+		{
+			try
+			{
+				method = clz.getMethod(IS + name, (Class[])null);
+			}
+			catch (Exception e)
+			{
+				log.debug("Cannot find getter " + clz + "." + expression);
+			}
+		}
+		return method;
+	}
+
+	private final static Method findMethod(final Class<?> clz, String expression)
+	{
+		if (expression.endsWith("()"))
+		{
+			expression = expression.substring(0, expression.length() - 2);
+		}
+		Method method = null;
+		try
+		{
+			method = clz.getMethod(expression, (Class[])null);
+		}
+		catch (Exception e)
+		{
+			log.debug("Cannot find method " + clz + "." + expression);
+		}
+		return method;
+	}
+
+	/**
+	 * Utility class: instantiation not allowed.
+	 */
+	private PropertyResolver()
+	{
+	}
+
+	/**
+	 * @author jcompagner
+	 *
+	 */
+	private final static class ObjectAndGetSetter
+	{
+		private final IGetAndSet getAndSetter;
+		private final Object value;
+
+		/**
+		 * @param getAndSetter
+		 * @param value
+		 */
+		public ObjectAndGetSetter(IGetAndSet getAndSetter, Object value)
+		{
+			this.getAndSetter = getAndSetter;
+			this.value = value;
+		}
+
+		/**
+		 * @param value
+		 * @param converter
+		 */
+		public void setValue(Object value, PropertyResolverConverter converter)
+		{
+			getAndSetter.setValue(this.value, value, converter);
+		}
+
+		/**
+		 * @return The value
+		 */
+		public Object getValue()
+		{
+			return getAndSetter.getValue(value);
+		}
+
+		/**
+		 * @return class of property value
+		 */
+		public Class<?> getTargetClass()
+		{
+			return getAndSetter.getTargetClass();
+		}
+
+		/**
+		 * @return Field or null if no field exists for expression
+		 */
+		public Field getField()
+		{
+			return getAndSetter.getField();
+		}
+
+		/**
+		 * @return Getter method or null if no getter exists for expression
+		 */
+		public Method getGetter()
+		{
+			return getAndSetter.getGetter();
+		}
+
+		/**
+		 * @return Setter method or null if no setter exists for expression
+		 */
+		public Method getSetter()
+		{
+			return getAndSetter.getSetter();
+		}
+	}
+
+	/**
+	 * @author jcompagner
+	 */
+	public static interface IGetAndSet
+	{
+		/**
+		 * @param object
+		 *            The object where the value must be taken from.
+		 *
+		 * @return The value of this property
+		 */
+		public Object getValue(final Object object);
+
+		/**
+		 * @return The target class of the object that as to be set.
+		 */
+		public Class<?> getTargetClass();
+
+		/**
+		 * @param object
+		 *            The object where the new value must be set on.
+		 *
+		 * @return The new value for the property that is set back on that object.
+		 */
+		public Object newValue(Object object);
+
+		/**
+		 * @param object
+		 * @param value
+		 * @param converter
+		 */
+		public void setValue(final Object object, final Object value,
+			PropertyResolverConverter converter);
+
+		/**
+		 * @return Field or null if there is no field
+		 */
+		public Field getField();
+
+		/**
+		 * @return Getter method or null if there is no getter
+		 */
+		public Method getGetter();
+
+		/**
+		 * @return Setter of null if there is no setter
+		 */
+		public Method getSetter();
+	}
+
+	private static abstract class AbstractGetAndSet implements IGetAndSet
+	{
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Field getField()
+		{
+			return null;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Method getGetter()
+		{
+			return null;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Method getSetter()
+		{
+			return null;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Class<?> getTargetClass()
+		{
+			return null;
+		}
+	}
+
+	private static final class MapGetSet extends AbstractGetAndSet
+	{
+		private final String key;
+
+		MapGetSet(String key)
+		{
+			this.key = key;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Object getValue(final Object object)
+		{
+			return ((Map<?, ?>)object).get(key);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		@SuppressWarnings("unchecked")
+		public void setValue(final Object object, final Object value,
+			final PropertyResolverConverter converter)
+		{
+			((Map<String, Object>)object).put(key, value);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Object newValue(final Object object)
+		{
+			// Map can't make a newValue or should it look what is more in the
+			// map and try to make one of the class if finds?
+			return null;
+		}
+	}
+
+	private static final class ListGetSet extends AbstractGetAndSet
+	{
+		final private int index;
+
+		ListGetSet(int index)
+		{
+			this.index = index;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Object getValue(final Object object)
+		{
+			if (((List<?>)object).size() <= index)
+			{
+				return null;
+			}
+			return ((List<?>)object).get(index);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		@SuppressWarnings("unchecked")
+		public void setValue(final Object object, final Object value,
+			final PropertyResolverConverter converter)
+		{
+			List<Object> lst = (List<Object>)object;
+
+			if (lst.size() > index)
+			{
+				lst.set(index, value);
+			}
+			else if (lst.size() == index)
+			{
+				lst.add(value);
+			}
+			else
+			{
+				while (lst.size() < index)
+				{
+					lst.add(null);
+				}
+				lst.add(value);
+			}
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Object newValue(Object object)
+		{
+			// List can't make a newValue or should it look what is more in the
+			// list and try to make one of the class if finds?
+			return null;
+		}
+	}
+
+	private static final class ArrayGetSet extends AbstractGetAndSet
+	{
+		private final int index;
+		private final Class<?> clzComponentType;
+
+		ArrayGetSet(Class<?> clzComponentType, int index)
+		{
+			this.clzComponentType = clzComponentType;
+			this.index = index;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Object getValue(Object object)
+		{
+			if (Array.getLength(object) > index)
+			{
+				return Array.get(object, index);
+			}
+			return null;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public void setValue(Object object, Object value, PropertyResolverConverter converter)
+		{
+			value = converter.convert(value, clzComponentType);
+			Array.set(object, index, value);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Object newValue(Object object)
+		{
+			Object value = null;
+			try
+			{
+				value = clzComponentType.newInstance();
+				Array.set(object, index, value);
+			}
+			catch (Exception e)
+			{
+				log.warn("Cannot set new value " + value + " at index " + index +
+					" for array holding elements of class " + clzComponentType, e);
+			}
+			return value;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Class<?> getTargetClass()
+		{
+			return clzComponentType;
+		}
+	}
+
+	private static final class ArrayLengthGetSet extends AbstractGetAndSet
+	{
+		ArrayLengthGetSet()
+		{
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Object getValue(final Object object)
+		{
+			return Array.getLength(object);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public void setValue(final Object object, final Object value,
+			final PropertyResolverConverter converter)
+		{
+			throw new WicketRuntimeException("You can't set the length on an array:" + object);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Object newValue(final Object object)
+		{
+			throw new WicketRuntimeException("Can't get a new value from a length of an array: " +
+				object);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Class<?> getTargetClass()
+		{
+			return int.class;
+		}
+	}
+
+	private static final class ArrayPropertyGetSet extends AbstractGetAndSet
+	{
+		final private Integer index;
+		final private Method getMethod;
+		private Method setMethod;
+
+		ArrayPropertyGetSet(final Method method, final int index)
+		{
+			this.index = index;
+			getMethod = method;
+			getMethod.setAccessible(true);
+		}
+
+		private final static Method findSetter(final Method getMethod, final Class<?> clz)
+		{
+			String name = getMethod.getName();
+			name = SET + name.substring(3);
+			try
+			{
+				return clz.getMethod(name, new Class[] { int.class, getMethod.getReturnType() });
+			}
+			catch (Exception e)
+			{
+				log.debug("Can't find setter method corresponding to " + getMethod);
+			}
+			return null;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Object getValue(Object object)
+		{
+			Object ret = null;
+			try
+			{
+				ret = getMethod.invoke(object, index);
+			}
+			catch (InvocationTargetException ex)
+			{
+				throw new WicketRuntimeException("Error calling index property method: " +
+					getMethod + " on object: " + object, ex.getCause());
+			}
+			catch (Exception ex)
+			{
+				throw new WicketRuntimeException("Error calling index property method: " +
+					getMethod + " on object: " + object, ex);
+			}
+			return ret;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public void setValue(final Object object, final Object value,
+			final PropertyResolverConverter converter)
+		{
+			if (setMethod == null)
+			{
+				setMethod = findSetter(getMethod, object.getClass());
+			}
+			if (setMethod != null)
+			{
+				setMethod.setAccessible(true);
+				Object converted = converter.convert(value, getMethod.getReturnType());
+				if (converted == null && value != null)
+				{
+					throw new ConversionException("Can't convert value: " + value + " to class: " +
+						getMethod.getReturnType() + " for setting it on " + object);
+				}
+				try
+				{
+					setMethod.invoke(object, index, converted);
+				}
+				catch (InvocationTargetException ex)
+				{
+					throw new WicketRuntimeException("Error index property calling method: " +
+						setMethod + " on object: " + object, ex.getCause());
+				}
+				catch (Exception ex)
+				{
+					throw new WicketRuntimeException("Error index property calling method: " +
+						setMethod + " on object: " + object, ex);
+				}
+			}
+			else
+			{
+				throw new WicketRuntimeException("No set method defined for value: " + value +
+					" on object: " + object);
+			}
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Class<?> getTargetClass()
+		{
+			return getMethod.getReturnType();
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Object newValue(Object object)
+		{
+			if (setMethod == null)
+			{
+				setMethod = findSetter(getMethod, object.getClass());
+			}
+
+			if (setMethod == null)
+			{
+				log.warn("Null setMethod");
+				return null;
+			}
+
+			Class<?> clz = getMethod.getReturnType();
+			Object value = null;
+			try
+			{
+				value = clz.newInstance();
+				setMethod.invoke(object, index, value);
+			}
+			catch (Exception e)
+			{
+				log.warn("Cannot set new value " + value + " at index " + index, e);
+			}
+			return value;
+		}
+	}
+
+	private static final class MethodGetAndSet extends AbstractGetAndSet
+	{
+		private final Method getMethod;
+		private final Method setMethod;
+		private final Field field;
+
+		MethodGetAndSet(Method getMethod, Method setMethod, Field field)
+		{
+			this.getMethod = getMethod;
+			this.getMethod.setAccessible(true);
+			this.field = field;
+			this.setMethod = setMethod;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public final Object getValue(final Object object)
+		{
+			Object ret = null;
+			try
+			{
+				ret = getMethod.invoke(object, (Object[])null);
+			}
+			catch (InvocationTargetException ex)
+			{
+				throw new WicketRuntimeException("Error calling method: " + getMethod +
+					" on object: " + object, ex.getCause());
+			}
+			catch (Exception ex)
+			{
+				throw new WicketRuntimeException("Error calling method: " + getMethod +
+					" on object: " + object, ex);
+			}
+			return ret;
+		}
+
+		/**
+		 * @param object
+		 * @param value
+		 * @param converter
+		 */
+		@Override
+		public final void setValue(final Object object, final Object value,
+			PropertyResolverConverter converter)
+		{
+			Class<?> type = null;
+			if (setMethod != null)
+			{
+				// getMethod is always there and if the value will be set through a setMethod then
+				// the getMethod return type will be its type. Else we have to look at the
+				// parameters if the setter but getting the return type is quicker
+				type = getMethod.getReturnType();
+			}
+			else if (field != null)
+			{
+				type = field.getType();
+			}
+
+			Object converted = null;
+			if (type != null)
+			{
+				converted = converter.convert(value, type);
+				if (converted == null)
+				{
+					if (value != null)
+					{
+						throw new ConversionException("Method [" + getMethod +
+							"]. Can't convert value: " + value + " to class: " +
+							getMethod.getReturnType() + " for setting it on " + object);
+					}
+					else if (getMethod.getReturnType().isPrimitive())
+					{
+						throw new ConversionException("Method [" + getMethod +
+							"]. Can't convert null value to a primitive class: " +
+							getMethod.getReturnType() + " for setting it on " + object);
+					}
+				}
+			}
+
+			if (setMethod != null)
+			{
+				try
+				{
+					setMethod.invoke(object, converted);
+				}
+				catch (InvocationTargetException ex)
+				{
+					throw new WicketRuntimeException("Error calling method: " + setMethod +
+						" on object: " + object, ex.getCause());
+				}
+				catch (Exception ex)
+				{
+					throw new WicketRuntimeException("Error calling method: " + setMethod +
+						" on object: " + object, ex);
+				}
+			}
+			else if (field != null)
+			{
+				try
+				{
+					field.set(object, converted);
+				}
+				catch (Exception ex)
+				{
+					throw new WicketRuntimeException("Error setting field: " + field +
+						" on object: " + object, ex);
+				}
+			}
+			else
+			{
+				throw new WicketRuntimeException("no set method defined for value: " + value +
+					" on object: " + object + " while respective getMethod being " +
+					getMethod.getName());
+			}
+		}
+
+		private final static Method findSetter(Method getMethod, Class<?> clz)
+		{
+			String name = getMethod.getName();
+			if (name.startsWith(GET))
+			{
+				name = SET + name.substring(3);
+			}
+			else
+			{
+				name = SET + name.substring(2);
+			}
+			try
+			{
+				Method method = clz.getMethod(name, new Class[] { getMethod.getReturnType() });
+				if (method != null)
+				{
+					method.setAccessible(true);
+				}
+				return method;
+			}
+			catch (NoSuchMethodException e)
+			{
+				Method[] methods = clz.getMethods();
+				for (Method method : methods)
+				{
+					if (method.getName().equals(name))
+					{
+						Class<?>[] parameterTypes = method.getParameterTypes();
+						if (parameterTypes.length == 1)
+						{
+							if (parameterTypes[0].isAssignableFrom(getMethod.getReturnType()))
+							{
+								return method;
+							}
+						}
+					}
+				}
+				log.debug("Cannot find setter corresponding to " + getMethod);
+			}
+			catch (Exception e)
+			{
+				log.debug("Cannot find setter corresponding to " + getMethod);
+			}
+			return null;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Object newValue(Object object)
+		{
+			if (setMethod == null)
+			{
+				log.warn("Null setMethod");
+				return null;
+			}
+
+			Class<?> clz = getMethod.getReturnType();
+			Object value = null;
+			try
+			{
+				value = clz.newInstance();
+				setMethod.invoke(object, value);
+			}
+			catch (Exception e)
+			{
+				log.warn("Cannot set new value " + value, e);
+			}
+			return value;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Class<?> getTargetClass()
+		{
+			return getMethod.getReturnType();
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Method getGetter()
+		{
+			return getMethod;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Method getSetter()
+		{
+			return setMethod;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Field getField()
+		{
+			return field;
+		}
+	}
+
+	/**
+	 * @author jcompagner
+	 */
+	private static class FieldGetAndSetter extends AbstractGetAndSet
+	{
+		private final Field field;
+
+		/**
+		 * Construct.
+		 *
+		 * @param field
+		 */
+		public FieldGetAndSetter(final Field field)
+		{
+			super();
+			this.field = field;
+			this.field.setAccessible(true);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Object getValue(final Object object)
+		{
+			try
+			{
+				return field.get(object);
+			}
+			catch (Exception ex)
+			{
+				throw new WicketRuntimeException("Error getting field value of field " + field +
+					" from object " + object, ex);
+			}
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Object newValue(final Object object)
+		{
+			Class<?> clz = field.getType();
+			Object value = null;
+			try
+			{
+				value = clz.newInstance();
+				field.set(object, value);
+			}
+			catch (Exception e)
+			{
+				log.warn("Cannot set field " + field + " to " + value, e);
+			}
+			return value;
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public void setValue(final Object object, Object value,
+			final PropertyResolverConverter converter)
+		{
+			value = converter.convert(value, field.getType());
+			try
+			{
+				field.set(object, value);
+			}
+			catch (Exception ex)
+			{
+				throw new WicketRuntimeException("Error setting field value of field " + field +
+					" on object " + object + ", value " + value, ex);
+			}
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Class<?> getTargetClass()
+		{
+			return field.getType();
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public Field getField()
+		{
+			return field;
+		}
+	}
+
+	private static IClassCache getClassesToGetAndSetters()
+	{
+		Object key = null;
+		if (Application.exists())
+		{
+			key = Application.get();
+		}
+		else
+		{
+			key = PropertyResolver.class;
+		}
+		IClassCache result = applicationToClassesToGetAndSetters.get(key);
+		if (result == null)
+		{
+			applicationToClassesToGetAndSetters.put(key, result = new DefaultClassCache());
+		}
+		return result;
+	}
+
+	/**
+	 * Clean up cache for this app.
+	 *
+	 * @param application
+	 */
+	public static void destroy(Application application)
+	{
+		applicationToClassesToGetAndSetters.remove(application);
+	}
+
+	/**
+	 * Sets the {@link IClassCache} for the given application.
+	 *
+	 * If the Application is null then it will be the default if no application is found. So if you
+	 * want to be sure that your {@link IClassCache} is handled in all situations then call this
+	 * method twice with your implementations. One time for the application and the second time with
+	 * null.
+	 *
+	 * @param application
+	 *            to use or null if the default must be set.
+	 * @param classCache
+	 */
+	public static void setClassCache(final Application application, final IClassCache classCache)
+	{
+		if (application != null)
+		{
+			applicationToClassesToGetAndSetters.put(application, classCache);
+		}
+		else
+		{
+			applicationToClassesToGetAndSetters.put(PropertyResolver.class, classCache);
+		}
+	}
+
+	/**
+	 * An implementation of the class can be set on the
+	 * {@link PropertyResolver#setClassCache(org.apache.wicket.Application, org.apache.wicket.core.util.lang.PropertyResolver.IClassCache)}
+	 * method for a specific application. This class cache can then be a special map with
+	 * an eviction policy or do nothing if nothing should be cached for the given class.
+	 *
+	 * For example if you have proxy classes that are constantly created you could opt for not
+	 * caching those at all or have a special Map implementation that will evict that class at a
+	 * certain point.
+	 *
+	 * @author jcompagner
+	 */
+	public static interface IClassCache
+	{
+		/**
+		 * Put the class into the cache, or if that class shouldn't be cached do nothing.
+		 *
+		 * @param clz
+		 * @param values
+		 */
+		void put(Class<?> clz, Map<String, IGetAndSet> values);
+
+		/**
+		 * Returns the class map from the cache.
+		 *
+		 * @param clz
+		 * @return the map of the given class
+		 */
+		Map<String, IGetAndSet> get(Class<?> clz);
+	}
+
+	private static class DefaultClassCache implements IClassCache
+	{
+		private final ConcurrentHashMap<Class<?>, Map<String, IGetAndSet>> map = Generics.newConcurrentHashMap(16);
+
+		@Override
+		public Map<String, IGetAndSet> get(Class<?> clz)
+		{
+			return map.get(clz);
+		}
+
+		@Override
+		public void put(Class<?> clz, Map<String, IGetAndSet> values)
+		{
+			map.put(clz, values);
+		}
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/ddc37320/wicket-core/src/main/java/org/apache/wicket/core/util/lang/PropertyResolverConverter.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/lang/PropertyResolverConverter.java b/wicket-core/src/main/java/org/apache/wicket/core/util/lang/PropertyResolverConverter.java
new file mode 100644
index 0000000..d069041
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/lang/PropertyResolverConverter.java
@@ -0,0 +1,109 @@
+/*
+ * 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.
+ */
+package org.apache.wicket.core.util.lang;
+
+import java.util.Locale;
+
+import org.apache.wicket.util.io.IClusterable;
+import org.apache.wicket.IConverterLocator;
+import org.apache.wicket.util.convert.IConverter;
+import org.apache.wicket.util.lang.Objects;
+
+
+/**
+ * @author jcompagner
+ */
+public class PropertyResolverConverter implements IClusterable
+{
+	private static final long serialVersionUID = 1L;
+
+
+	private final IConverterLocator converterSupplier;
+	private final Locale locale;
+
+	/**
+	 * Construct.
+	 *
+	 * @param converterSupplier
+	 * @param locale
+	 */
+	public PropertyResolverConverter(IConverterLocator converterSupplier, Locale locale)
+	{
+		this.converterSupplier = converterSupplier;
+		this.locale = locale;
+	}
+
+	/**
+	 * @param <T>
+	 *            target type
+	 * @param object
+	 * @param clz
+	 * @return converted value of the type given, or null if the value cannot be converted to the
+	 *         given type.
+	 */
+	public <T> T convert(Object object, Class<T> clz)
+	{
+		if (object == null)
+		{
+			return null;
+		}
+		if (clz.isAssignableFrom(object.getClass()))
+		{
+			@SuppressWarnings("unchecked")
+			T result = (T)object;
+			return result;
+		}
+		IConverter<T> converter = converterSupplier.getConverter(clz);
+		if (object instanceof String)
+		{
+			return converter.convertToObject((String)object, locale);
+		}
+		else if (clz == String.class)
+		{
+			@SuppressWarnings("unchecked")
+			T result = (T)convertToString(object, locale);
+			return result;
+		}
+		else
+		{
+			T result;
+			try
+			{
+				result = Objects.convertValue(object, clz);
+			}
+			catch (RuntimeException ex)
+			{
+				result = null;
+			}
+			if (result == null)
+			{
+				String tmp = convertToString(object, locale);
+				result = converter.convertToObject(tmp, locale);
+			}
+			return result;
+		}
+	}
+
+	protected <C> String convertToString(C object, Locale locale)
+	{
+		@SuppressWarnings("unchecked")
+		Class<C> type = (Class<C>)object.getClass();
+
+		IConverter<C> converterForObj = converterSupplier.getConverter(type);
+		return converterForObj.convertToString(object, locale);
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/ddc37320/wicket-core/src/main/java/org/apache/wicket/core/util/lang/WicketObjects.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/lang/WicketObjects.java b/wicket-core/src/main/java/org/apache/wicket/core/util/lang/WicketObjects.java
new file mode 100644
index 0000000..b2c41e2
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/lang/WicketObjects.java
@@ -0,0 +1,420 @@
+/*
+ * 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.
+ */
+package org.apache.wicket.core.util.lang;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.ObjectStreamClass;
+import java.io.OutputStream;
+import java.io.Serializable;
+import java.util.HashMap;
+
+import org.apache.wicket.Application;
+import org.apache.wicket.Component;
+import org.apache.wicket.WicketRuntimeException;
+import org.apache.wicket.application.IClassResolver;
+import org.apache.wicket.settings.IApplicationSettings;
+import org.apache.wicket.util.io.ByteCountingOutputStream;
+import org.apache.wicket.util.lang.Generics;
+import org.apache.wicket.util.string.Strings;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Object (de)serialization utilities.
+ */
+public class WicketObjects
+{
+	/** log. */
+	private static final Logger log = LoggerFactory.getLogger(WicketObjects.class);
+
+	private WicketObjects()
+	{
+	}
+
+	/**
+	 * @param <T>
+	 *            class type
+	 * @param className
+	 *            Class to resolve
+	 * @return Resolved class
+	 * @throws ClassNotFoundException
+	 */
+	@SuppressWarnings("unchecked")
+	public static <T> Class<T> resolveClass(final String className)
+	{
+		Class<T> resolved = null;
+		try
+		{
+			if (Application.exists())
+			{
+				resolved = (Class<T>)Application.get()
+					.getApplicationSettings()
+					.getClassResolver()
+					.resolveClass(className);
+			}
+
+			if (resolved == null)
+			{
+				resolved = (Class<T>)Class.forName(className, false, Thread.currentThread()
+					.getContextClassLoader());
+			}
+		}
+		catch (ClassNotFoundException cnfx)
+		{
+			log.warn("Could not resolve class [" + className + "]", cnfx);
+		}
+		return resolved;
+	}
+
+	/**
+	 * Interface that enables users to plugin the way object sizes are calculated with Wicket.
+	 */
+	public static interface IObjectSizeOfStrategy
+	{
+		/**
+		 * Computes the size of an object. This typically is an estimation, not an absolute accurate
+		 * size.
+		 *
+		 * @param object
+		 *            The serializable object to compute size of
+		 * @return The size of the object in bytes.
+		 */
+		long sizeOf(Serializable object);
+	}
+
+	/**
+	 * {@link IObjectSizeOfStrategy} that works by serializing the object to an instance of
+	 * {@link ByteCountingOutputStream}, which records the number of bytes written to it. Hence,
+	 * this gives the size of the object as it would be serialized,including all the overhead of
+	 * writing class headers etc. Not very accurate (the real memory consumption should be lower)
+	 * but the best we can do in a cheap way pre JDK 5.
+	 */
+	public static final class SerializingObjectSizeOfStrategy implements IObjectSizeOfStrategy
+	{
+		/**
+		 * @see org.apache.wicket.core.util.lang.WicketObjects.IObjectSizeOfStrategy#sizeOf(java.io.Serializable)
+		 */
+		@Override
+		public long sizeOf(Serializable object)
+		{
+			if (object == null)
+			{
+				return 0;
+			}
+			try
+			{
+				final ByteCountingOutputStream out = new ByteCountingOutputStream();
+				new ObjectOutputStream(out).writeObject(object);
+				out.close();
+				return out.size();
+			}
+			catch (IOException e)
+			{
+				if (log.isWarnEnabled())
+				{
+					log.warn("Unable to determine object size: " + object.toString(), e);
+				}
+				return -1;
+			}
+		}
+
+	}
+
+	private static final class ReplaceObjectInputStream extends ObjectInputStream
+	{
+		private final ClassLoader classloader;
+		private final HashMap<String, Component> replacedComponents;
+
+		private ReplaceObjectInputStream(InputStream in,
+			HashMap<String, Component> replacedComponents, ClassLoader classloader)
+			throws IOException
+		{
+			super(in);
+			this.replacedComponents = replacedComponents;
+			this.classloader = classloader;
+			enableResolveObject(true);
+		}
+
+		// This override is required to resolve classes inside in different
+		// bundle, i.e.
+		// The classes can be resolved by OSGI classresolver implementation
+		@Override
+		protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException,
+			ClassNotFoundException
+		{
+			String className = desc.getName();
+
+			try
+			{
+				return Class.forName(className, true, classloader);
+			}
+			catch (ClassNotFoundException ex1)
+			{
+				// ignore this exception.
+				log.debug("Class not found by using objects own classloader, trying the IClassResolver");
+			}
+
+			Application application = Application.get();
+			IApplicationSettings applicationSettings = application.getApplicationSettings();
+			IClassResolver classResolver = applicationSettings.getClassResolver();
+
+			Class<?> candidate = null;
+			try
+			{
+				candidate = classResolver.resolveClass(className);
+				if (candidate == null)
+				{
+					candidate = super.resolveClass(desc);
+				}
+			}
+			catch (WicketRuntimeException ex)
+			{
+				if (ex.getCause() instanceof ClassNotFoundException)
+				{
+					throw (ClassNotFoundException)ex.getCause();
+				}
+			}
+			return candidate;
+		}
+
+		@Override
+		protected Object resolveObject(Object obj) throws IOException
+		{
+			Object replaced = replacedComponents.get(obj);
+			if (replaced != null)
+			{
+				return replaced;
+			}
+			return super.resolveObject(obj);
+		}
+	}
+
+	private static final class ReplaceObjectOutputStream extends ObjectOutputStream
+	{
+		private final HashMap<String, Component> replacedComponents;
+
+		private ReplaceObjectOutputStream(OutputStream out,
+			HashMap<String, Component> replacedComponents) throws IOException
+		{
+			super(out);
+			this.replacedComponents = replacedComponents;
+			enableReplaceObject(true);
+		}
+
+		@Override
+		protected Object replaceObject(Object obj) throws IOException
+		{
+			if (obj instanceof Component)
+			{
+				final Component component = (Component)obj;
+				String name = component.getPath();
+				replacedComponents.put(name, component);
+				return name;
+			}
+			return super.replaceObject(obj);
+		}
+	}
+
+	/**
+	 * Makes a deep clone of an object by serializing and deserializing it. The object must be fully
+	 * serializable to be cloned. This method will not clone wicket Components, it will just reuse
+	 * those instances so that the complete component tree is not copied over only the model data.
+	 *
+	 * @param object
+	 *            The object to clone
+	 * @return A deep copy of the object
+	 */
+	public static Object cloneModel(final Object object)
+	{
+		if (object == null)
+		{
+			return null;
+		}
+		else
+		{
+			try
+			{
+				final ByteArrayOutputStream out = new ByteArrayOutputStream(256);
+				final HashMap<String, Component> replacedObjects = Generics.newHashMap();
+				ObjectOutputStream oos = new ReplaceObjectOutputStream(out, replacedObjects);
+				oos.writeObject(object);
+				ObjectInputStream ois = new ReplaceObjectInputStream(new ByteArrayInputStream(
+					out.toByteArray()), replacedObjects, object.getClass().getClassLoader());
+				return ois.readObject();
+			}
+			catch (ClassNotFoundException e)
+			{
+				throw new WicketRuntimeException("Internal error cloning object", e);
+			}
+			catch (IOException e)
+			{
+				throw new WicketRuntimeException("Internal error cloning object", e);
+			}
+		}
+	}
+
+	/**
+	 * Strategy for calculating sizes of objects. Note: I didn't make this an application setting as
+	 * we have enough of those already, and the typical way this probably would be used is that
+	 * install a different one according to the JDK version used, so varying them between
+	 * applications doesn't make a lot of sense.
+	 */
+	private static IObjectSizeOfStrategy objectSizeOfStrategy = new SerializingObjectSizeOfStrategy();
+
+	/**
+	 * Makes a deep clone of an object by serializing and deserializing it. The object must be fully
+	 * serializable to be cloned. No extra debug info is gathered.
+	 *
+	 * @param object
+	 *            The object to clone
+	 * @return A deep copy of the object
+	 * @see #cloneModel(Object)
+	 */
+	public static Object cloneObject(final Object object)
+	{
+		if (object == null)
+		{
+			return null;
+		}
+		else
+		{
+			try
+			{
+				final ByteArrayOutputStream out = new ByteArrayOutputStream(256);
+				ObjectOutputStream oos = new ObjectOutputStream(out);
+				oos.writeObject(object);
+				ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(
+					out.toByteArray()))
+				{
+					// This override is required to resolve classes inside in different bundle, i.e.
+					// The classes can be resolved by OSGI classresolver implementation
+					@Override
+					protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException,
+						ClassNotFoundException
+					{
+						String className = desc.getName();
+
+						try
+						{
+							return Class.forName(className, true, object.getClass()
+								.getClassLoader());
+						}
+						catch (ClassNotFoundException ex1)
+						{
+							// ignore this exception.
+							log.debug("Class not found by using objects own classloader, trying the IClassResolver");
+						}
+
+
+						Application application = Application.get();
+						IApplicationSettings applicationSettings = application.getApplicationSettings();
+						IClassResolver classResolver = applicationSettings.getClassResolver();
+
+						Class<?> candidate = null;
+						try
+						{
+							candidate = classResolver.resolveClass(className);
+							if (candidate == null)
+							{
+								candidate = super.resolveClass(desc);
+							}
+						}
+						catch (WicketRuntimeException ex)
+						{
+							if (ex.getCause() instanceof ClassNotFoundException)
+							{
+								throw (ClassNotFoundException)ex.getCause();
+							}
+						}
+						return candidate;
+					}
+				};
+				return ois.readObject();
+			}
+			catch (ClassNotFoundException e)
+			{
+				throw new WicketRuntimeException("Internal error cloning object", e);
+			}
+			catch (IOException e)
+			{
+				throw new WicketRuntimeException("Internal error cloning object", e);
+			}
+		}
+	}
+
+	/**
+	 * Creates a new instance using the current application's class resolver. Returns null if
+	 * className is null.
+	 *
+	 * @param className
+	 *            The full class name
+	 * @return The new object instance
+	 */
+	public static Object newInstance(final String className)
+	{
+		if (!Strings.isEmpty(className))
+		{
+			try
+			{
+				Class<?> c = WicketObjects.resolveClass(className);
+				return c.newInstance();
+			}
+			catch (Exception e)
+			{
+				throw new WicketRuntimeException("Unable to create " + className, e);
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * Sets the strategy for determining the sizes of objects.
+	 *
+	 * @param objectSizeOfStrategy
+	 *            the strategy. Pass null to reset to the default.
+	 */
+	public static void setObjectSizeOfStrategy(IObjectSizeOfStrategy objectSizeOfStrategy)
+	{
+		if (objectSizeOfStrategy == null)
+		{
+			WicketObjects.objectSizeOfStrategy = new SerializingObjectSizeOfStrategy();
+		}
+		else
+		{
+			WicketObjects.objectSizeOfStrategy = objectSizeOfStrategy;
+		}
+		log.info("using " + objectSizeOfStrategy + " for calculating object sizes");
+	}
+
+	/**
+	 * Computes the size of an object. Note that this is an estimation, never an absolute accurate
+	 * size.
+	 *
+	 * @param object
+	 *            Object to compute size of
+	 * @return The size of the object in bytes
+	 */
+	public static long sizeof(final Serializable object)
+	{
+		return objectSizeOfStrategy.sizeOf(object);
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/ddc37320/wicket-core/src/main/java/org/apache/wicket/core/util/lang/package.html
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/lang/package.html b/wicket-core/src/main/java/org/apache/wicket/core/util/lang/package.html
new file mode 100644
index 0000000..4e26359
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/lang/package.html
@@ -0,0 +1,27 @@
+<!--
+   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.
+-->
+<!DOCTYPE HTML PUBLIC "-//W3C/DTD HTML 3.2 Final//NL">
+<html>
+<head>
+<title>wicket.util.lang package</title>
+</head>
+<body>
+<p>
+Java utilities.
+</p>
+</body>
+</html>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/wicket/blob/ddc37320/wicket-core/src/main/java/org/apache/wicket/core/util/resource/AbstractResourceStreamWriter.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/resource/AbstractResourceStreamWriter.java b/wicket-core/src/main/java/org/apache/wicket/core/util/resource/AbstractResourceStreamWriter.java
new file mode 100644
index 0000000..7971b74
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/resource/AbstractResourceStreamWriter.java
@@ -0,0 +1,124 @@
+/*
+ * 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.
+ */
+package org.apache.wicket.core.util.resource;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Locale;
+
+import org.apache.wicket.util.lang.Bytes;
+import org.apache.wicket.util.resource.ResourceStreamNotFoundException;
+import org.apache.wicket.util.time.Time;
+
+/**
+ * Base implementation of an IResourceStreamWriter so that you only have to override the
+ * {@link IResourceStreamWriter#write(java.io.OutputStream)} Don't forget to overwrite the
+ * {@link IResourceStream#length()} method if you do know the total length that will be generated.
+ *
+ * @see IResourceStreamWriter
+ */
+public abstract class AbstractResourceStreamWriter implements IResourceStreamWriter
+{
+	private static final long serialVersionUID = 1L;
+
+	private Locale locale;
+
+	private String variation;
+
+	private String style;
+
+	/**
+	 * Default implementation to return -1. Do override this if you know the length up front.
+	 */
+	@Override
+	public Bytes length()
+	{
+		return null;
+	}
+
+	@Override
+	public Locale getLocale()
+	{
+		return locale;
+	}
+
+	@Override
+	public void setLocale(final Locale locale)
+	{
+		this.locale = locale;
+	}
+
+	/**
+	 * Just returns now.
+	 */
+	@Override
+	public Time lastModifiedTime()
+	{
+		return Time.now();
+	}
+
+	/**
+	 * this method should not be used as it is not required for resource writers
+	 */
+	@Override
+	public final InputStream getInputStream() throws ResourceStreamNotFoundException
+	{
+		throw new IllegalStateException("getInputStream is not used with IResourceStreamWriter");
+	}
+
+	/**
+	 * this method should not be used as it is not required for resource writers
+	 * <p>
+	 * resource write generate content directly, not using an input stream, so there's nothing to
+	 * close later.
+	 */
+	@Override
+	public final void close() throws IOException
+	{
+	}
+
+	@Override
+	public String getContentType()
+	{
+		return null;
+	}
+
+	@Override
+	public String getStyle()
+	{
+		return style;
+	}
+
+	@Override
+	public void setStyle(String style)
+	{
+		this.style = style;
+	}
+
+	@Override
+	public String getVariation()
+	{
+		return variation;
+	}
+
+	@Override
+	public void setVariation(String variation)
+	{
+		this.variation = variation;
+	}
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/wicket/blob/ddc37320/wicket-core/src/main/java/org/apache/wicket/core/util/resource/IResourceStreamWriter.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/resource/IResourceStreamWriter.java b/wicket-core/src/main/java/org/apache/wicket/core/util/resource/IResourceStreamWriter.java
new file mode 100644
index 0000000..394f2a4
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/resource/IResourceStreamWriter.java
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+package org.apache.wicket.core.util.resource;
+
+import org.apache.wicket.request.Response;
+import org.apache.wicket.util.resource.IResourceStream;
+
+/**
+ * Special IResourceStream implementation that a Resource can return when it directly wants to write
+ * to an output stream instead of return the {@link IResourceStream#getInputStream()}. That one can
+ * return null for a IResourceStreamWriter.
+ *
+ * This behavior comes in use when the resource is generated on the fly and should be streamed
+ * directly to the client so that it won't be buffered completely if the generated resource is
+ * really big.
+ *
+ * @author jcompagner
+ */
+public interface IResourceStreamWriter extends IResourceStream
+{
+	/**
+	 * Implement this method to write the resource data directly the the given OutputStream.
+	 *
+	 * @param output
+	 *            The response where the resource can write its content into.
+	 */
+	void write(Response output);
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/ddc37320/wicket-core/src/main/java/org/apache/wicket/core/util/resource/PackageResourceStream.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/resource/PackageResourceStream.java b/wicket-core/src/main/java/org/apache/wicket/core/util/resource/PackageResourceStream.java
new file mode 100644
index 0000000..770d29b
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/resource/PackageResourceStream.java
@@ -0,0 +1,101 @@
+/*
+ * 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.
+ */
+package org.apache.wicket.core.util.resource;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.wicket.Application;
+import org.apache.wicket.WicketRuntimeException;
+import org.apache.wicket.core.util.resource.locator.IResourceStreamLocator;
+import org.apache.wicket.util.lang.Bytes;
+import org.apache.wicket.util.lang.Packages;
+import org.apache.wicket.util.resource.AbstractResourceStream;
+import org.apache.wicket.util.resource.IResourceStream;
+import org.apache.wicket.util.resource.ResourceStreamNotFoundException;
+import org.apache.wicket.util.time.Time;
+
+
+/**
+ * An {@link IResourceStream} that reads data from a resource in the classpath. It simply delegates
+ * all operations to the {@link IResourceStream} returned by the application's
+ * {@link IResourceStreamLocator}.
+ *
+ * @author <a href="mailto:jbq@apache.org">Jean-Baptiste Quenot</a>
+ */
+public class PackageResourceStream extends AbstractResourceStream
+{
+	/** */
+	private static final long serialVersionUID = 1L;
+
+	private IResourceStream resourceStream;
+
+	/**
+	 * Obtains an {@link IResourceStream} from the application's
+	 * {@link IResourceStreamLocator#locate(Class, String)}
+	 *
+	 * @param scope
+	 *            This argument will be used to get the class loader for loading the package
+	 *            resource, and to determine what package it is in.
+	 * @param path
+	 *            The path to the resource
+	 */
+	public PackageResourceStream(Class<?> scope, String path)
+	{
+		String absolutePath = Packages.absolutePath(scope, path);
+		resourceStream = Application.get()
+			.getResourceSettings()
+			.getResourceStreamLocator()
+			.locate(scope, absolutePath, null, null, null, null, false);
+
+		if (resourceStream == null)
+		{
+			throw new WicketRuntimeException("Cannot find resource with " + scope.getName() +
+				" and path " + path);
+		}
+	}
+
+	@Override
+	public void close() throws IOException
+	{
+		resourceStream.close();
+	}
+
+	@Override
+	public String getContentType()
+	{
+		return resourceStream.getContentType();
+	}
+
+	@Override
+	public InputStream getInputStream() throws ResourceStreamNotFoundException
+	{
+		return resourceStream.getInputStream();
+	}
+
+	@Override
+	public Bytes length()
+	{
+		return resourceStream.length();
+	}
+
+	@Override
+	public Time lastModifiedTime()
+	{
+		return resourceStream.lastModifiedTime();
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/ddc37320/wicket-core/src/main/java/org/apache/wicket/core/util/resource/UrlResourceStream.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/resource/UrlResourceStream.java b/wicket-core/src/main/java/org/apache/wicket/core/util/resource/UrlResourceStream.java
new file mode 100644
index 0000000..98aed1a
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/resource/UrlResourceStream.java
@@ -0,0 +1,277 @@
+/*
+ * 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.
+ */
+package org.apache.wicket.core.util.resource;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.wicket.Application;
+import org.apache.wicket.util.io.Connections;
+import org.apache.wicket.util.io.IOUtils;
+import org.apache.wicket.util.lang.Args;
+import org.apache.wicket.util.lang.Bytes;
+import org.apache.wicket.util.lang.Objects;
+import org.apache.wicket.util.resource.AbstractResourceStream;
+import org.apache.wicket.util.resource.IFixedLocationResourceStream;
+import org.apache.wicket.util.resource.ResourceStreamNotFoundException;
+import org.apache.wicket.util.time.Time;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * UrlResourceStream implements IResource for URLs.
+ *
+ * @see org.apache.wicket.util.resource.IResourceStream
+ * @see org.apache.wicket.util.watch.IModifiable
+ * @author Jonathan Locke
+ * @author Igor Vaynberg
+ */
+public class UrlResourceStream extends AbstractResourceStream
+	implements
+		IFixedLocationResourceStream
+{
+	private static final long serialVersionUID = 1L;
+
+	/** Logging. */
+	private static final Logger log = LoggerFactory.getLogger(UrlResourceStream.class);
+
+	/**
+	 * The meta data for this stream. Lazy loaded on demand.
+	 */
+	private transient StreamData streamData;
+
+	/** The URL to this resource. */
+	private final URL url;
+
+	/** Last known time the stream was last modified. */
+	private Time lastModified;
+
+	/**
+	 * Meta data class for the stream attributes
+	 */
+	private static class StreamData
+	{
+		private URLConnection connection;
+
+		/**
+		 * The streams read from this connection.
+		 * Some URLConnection implementations return the same instance of InputStream
+		 * every time URLConnection#getInputStream() is called. Other return a new instance
+		 * of InputStream.
+		 * Here we keep a list of all returned ones and close them in UrlResourceStream#close().
+		 * Even it is the same instance several times we will try to close it quietly several times.
+		 */
+		private List<InputStream> inputStreams;
+
+		/** Length of stream. */
+		private long contentLength;
+
+		/** Content type for stream. */
+		private String contentType;
+
+	}
+
+	/**
+	 * Construct.
+	 *
+	 * @param url
+	 *            URL of resource
+	 */
+	public UrlResourceStream(final URL url)
+	{
+		// save the url
+		this.url = Args.notNull(url, "url");
+	}
+
+	/**
+	 * Lazy loads the stream settings on demand
+	 *
+	 * @param initialize
+	 *            a flag indicating whether to load the settings
+	 * @return the meta data with the stream settings
+	 */
+	private StreamData getData(boolean initialize)
+	{
+		if (streamData == null && initialize)
+		{
+			streamData = new StreamData();
+
+			try
+			{
+				streamData.connection = url.openConnection();
+				streamData.contentLength = streamData.connection.getContentLength();
+				streamData.contentType = streamData.connection.getContentType();
+
+				if (streamData.contentType == null || streamData.contentType.contains("unknown"))
+				{
+					if (Application.exists())
+					{
+						streamData.contentType = Application.get().getMimeType(url.getFile());
+					}
+					else
+					{
+						streamData.contentType = URLConnection.getFileNameMap().getContentTypeFor(
+							url.getFile());
+					}
+				}
+			}
+			catch (IOException ex)
+			{
+				throw new IllegalArgumentException("Invalid URL parameter " + url, ex);
+			}
+		}
+
+		return streamData;
+	}
+
+	/**
+	 * Closes this resource.
+	 *
+	 * @throws IOException
+	 */
+	@Override
+	public void close() throws IOException
+	{
+		StreamData data = getData(false);
+
+		if (data != null)
+		{
+			Connections.closeQuietly(data.connection);
+			if (data.inputStreams != null)
+			{
+				for (InputStream is : data.inputStreams) {
+					IOUtils.closeQuietly(is);
+				}
+			}
+			streamData = null;
+		}
+	}
+
+	/**
+	 * @return A readable input stream for this resource.
+	 * @throws ResourceStreamNotFoundException
+	 */
+	@Override
+	public InputStream getInputStream() throws ResourceStreamNotFoundException
+	{
+		try
+		{
+			StreamData data = getData(true);
+			InputStream is = data.connection.getInputStream();
+			if (data.inputStreams == null) {
+				data.inputStreams = new ArrayList<InputStream>();
+			}
+			data.inputStreams.add(is);
+			return is;
+		}
+		catch (IOException e)
+		{
+			throw new ResourceStreamNotFoundException("Resource " + url + " could not be opened", e);
+		}
+	}
+
+	/**
+	 * @return The URL to this resource (if any)
+	 */
+	public URL getURL()
+	{
+		return url;
+	}
+
+	/**
+	 * @see org.apache.wicket.util.watch.IModifiable#lastModifiedTime()
+	 * @return The last time this resource was modified
+	 */
+	@Override
+	public Time lastModifiedTime()
+	{
+		try
+		{
+			// get url modification timestamp
+			final Time time = Connections.getLastModified(url);
+
+			// if timestamp changed: update content length and last modified date
+			if (Objects.equal(time, lastModified) == false)
+			{
+				lastModified = time;
+				updateContentLength();
+			}
+			return lastModified;
+		}
+		catch (IOException e)
+		{
+			log.warn("getLastModified for " + url + " failed: " + e.getMessage());
+
+			// allow modification watcher to detect the problem
+			return null;
+		}
+	}
+
+	private void updateContentLength() throws IOException
+	{
+		StreamData data = getData(false);
+
+		if (data != null)
+		{
+			URLConnection connection = url.openConnection();
+			try {
+				data.contentLength = connection.getContentLength();
+			} finally {
+				Connections.close(connection);
+			}
+		}
+	}
+
+	@Override
+	public String toString()
+	{
+		return url.toString();
+	}
+
+	/**
+	 * @return The content type of this resource, such as "image/jpeg" or "text/html"
+	 */
+	@Override
+	public String getContentType()
+	{
+		return getData(true).contentType;
+	}
+
+	@Override
+	public Bytes length()
+	{
+		long length = getData(true).contentLength;
+
+		if (length == -1)
+		{
+			return null;
+		}
+
+		return Bytes.bytes(length);
+	}
+
+	@Override
+	public String locationAsString()
+	{
+		return url.toExternalForm();
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/ddc37320/wicket-core/src/main/java/org/apache/wicket/core/util/resource/WebExternalResourceStream.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/resource/WebExternalResourceStream.java b/wicket-core/src/main/java/org/apache/wicket/core/util/resource/WebExternalResourceStream.java
new file mode 100644
index 0000000..3991b28
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/resource/WebExternalResourceStream.java
@@ -0,0 +1,122 @@
+/*
+ * 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.
+ */
+package org.apache.wicket.core.util.resource;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+import javax.servlet.ServletContext;
+
+import org.apache.wicket.Application;
+import org.apache.wicket.protocol.http.WebApplication;
+import org.apache.wicket.util.io.Connections;
+import org.apache.wicket.util.io.IOUtils;
+import org.apache.wicket.util.lang.Bytes;
+import org.apache.wicket.util.resource.AbstractResourceStream;
+import org.apache.wicket.util.resource.ResourceStreamNotFoundException;
+import org.apache.wicket.util.time.Time;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An {@link org.apache.wicket.util.resource.IResourceStream} that reads data from a file in the web application
+ *
+ * @author <a href="mailto:jbq@apache.org">Jean-Baptiste Quenot</a>
+ */
+public class WebExternalResourceStream extends AbstractResourceStream
+{
+	private static final Logger log = LoggerFactory.getLogger(WebExternalResourceStream.class);
+	private static final long serialVersionUID = 1L;
+
+	transient InputStream in;
+
+	/** the relative url of the external resource. */
+	private final String url;
+
+	/**
+	 * Construct.
+	 *
+	 * @param url
+	 *            the relative url of the external resource
+	 */
+	public WebExternalResourceStream(String url)
+	{
+		if (url == null)
+		{
+			throw new IllegalArgumentException("Argument url must be not null");
+		}
+
+		this.url = url;
+	}
+
+	@Override
+	public Bytes length()
+	{
+		return null;
+	}
+
+	@Override
+	public void close() throws IOException
+	{
+		// getInputStream() is not always called (WICKET-790)
+		IOUtils.close(in);
+	}
+
+	@Override
+	public Time lastModifiedTime()
+	{
+		try
+		{
+			final ServletContext context = ((WebApplication)Application.get()).getServletContext();
+			final URL resourceURL = context.getResource(url);
+			if (resourceURL == null)
+			{
+				throw new FileNotFoundException("Unable to find resource '" + url +
+					"' in the serlvet context");
+			}
+
+			return Connections.getLastModified(resourceURL);
+		}
+		catch (IOException e)
+		{
+			log.warn("failed to retrieve last modified timestamp", e);
+			return null;
+		}
+	}
+
+	@Override
+	public String getContentType()
+	{
+		return WebApplication.get().getServletContext().getMimeType(url);
+	}
+
+	@Override
+	public InputStream getInputStream() throws ResourceStreamNotFoundException
+	{
+		final ServletContext context = ((WebApplication)Application.get()).getServletContext();
+
+		in = context.getResourceAsStream(url);
+		if (in == null)
+		{
+			throw new ResourceStreamNotFoundException("The requested resource was not found: " +
+				url);
+		}
+		return in;
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/ddc37320/wicket-core/src/main/java/org/apache/wicket/core/util/resource/locator/EmptyResourceNameIterator.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/resource/locator/EmptyResourceNameIterator.java b/wicket-core/src/main/java/org/apache/wicket/core/util/resource/locator/EmptyResourceNameIterator.java
new file mode 100644
index 0000000..9dcd5aa
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/resource/locator/EmptyResourceNameIterator.java
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+package org.apache.wicket.core.util.resource.locator;
+
+/**
+ * A ResourceNameIterator that doesn't find any resources.
+ *
+ * @since 1.5.5
+ */
+public final class EmptyResourceNameIterator extends ResourceNameIterator
+{
+	/**
+	 * Constructor.
+	 */
+	public EmptyResourceNameIterator()
+	{
+		super(null, null, null, null, null, true);
+	}
+
+	@Override
+	public boolean hasNext()
+	{
+		return false;
+	}
+
+	@Override
+	public void remove()
+	{
+		// noop
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/ddc37320/wicket-core/src/main/java/org/apache/wicket/core/util/resource/locator/ExtensionResourceNameIterator.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/resource/locator/ExtensionResourceNameIterator.java b/wicket-core/src/main/java/org/apache/wicket/core/util/resource/locator/ExtensionResourceNameIterator.java
new file mode 100644
index 0000000..b1ffe27
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/resource/locator/ExtensionResourceNameIterator.java
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+package org.apache.wicket.core.util.resource.locator;
+
+import java.util.Arrays;
+import java.util.Iterator;
+
+/**
+ * Iterate over a set of extensions. If null is provided, hasNext() will
+ * successfully return once with next() returning {@code null}.
+ *
+ * @author Juergen Donnerstag
+ */
+public class ExtensionResourceNameIterator implements Iterator<String>
+{
+	private static final Iterable<String> NULL_ITERABLE = Arrays.asList((String)null);
+
+	private final Iterator<String> iterator;
+
+	private String current;
+
+	/**
+	 * Construct.
+	 *
+	 * @param extensions
+	 *            {@code null} or iterable with extensions
+	 */
+	public ExtensionResourceNameIterator(final Iterable<String> extensions)
+	{
+		// Fail safe: hasNext() needs to return at least once with true
+		if (extensions == null || !extensions.iterator().hasNext())
+		{
+			this.iterator = NULL_ITERABLE.iterator();
+		}
+		else
+		{
+			this.iterator = extensions.iterator();
+		}
+	}
+
+	@Override
+	public boolean hasNext()
+	{
+		return iterator.hasNext();
+	}
+
+	/**
+	 * @return The next filename extension.
+	 */
+	@Override
+	public String next()
+	{
+		current = iterator.next();
+		return getExtension();
+	}
+
+	/**
+	 * @return Assuming you've called next() already, it'll return the very same value.
+	 */
+	public final String getExtension()
+	{
+		String ext = current;
+
+		if (ext != null)
+		{
+			if (ext.startsWith("."))
+			{
+				ext = ext.substring(1);
+			}
+		}
+		return ext;
+	}
+
+	@Override
+	public void remove()
+	{
+		iterator.remove();
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/ddc37320/wicket-core/src/main/java/org/apache/wicket/core/util/resource/locator/IResourceStreamLocator.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/resource/locator/IResourceStreamLocator.java b/wicket-core/src/main/java/org/apache/wicket/core/util/resource/locator/IResourceStreamLocator.java
new file mode 100644
index 0000000..cf52ce4
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/resource/locator/IResourceStreamLocator.java
@@ -0,0 +1,92 @@
+/*
+ * 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.
+ */
+package org.apache.wicket.core.util.resource.locator;
+
+import java.util.Locale;
+
+import org.apache.wicket.util.resource.IResourceStream;
+
+
+/**
+ * Interface for code that locates resources.
+ *
+ * @author Jonathan Locke
+ */
+public interface IResourceStreamLocator
+{
+	/**
+	 * Locate a resource, given a path and class. Typically this method is either called by external
+	 * clients if they are not interested in a lookup that takes the style and locale into account,
+	 * or it is called by the implementation of
+	 * {@link #locate(Class, String, String, String, java.util.Locale, String, boolean)} where the latter just takes care of
+	 * trying out the different combinations for the provided style and locale and uses this method
+	 * to actually load the resource stream.
+	 *
+	 * @param clazz
+	 *            The class loader for delegating the loading of the resource
+	 * @param path
+	 *            The path of the resource
+	 *
+	 * @return The resource or null
+	 */
+	IResourceStream locate(Class<?> clazz, String path);
+
+	/**
+	 * Locate a resource by combining the given path, style, variation, locale and extension
+	 * parameters. The exact search order depends on the implementation.
+	 *
+	 * @param clazz
+	 *            The class loader for delegating the loading of the resource
+	 * @param path
+	 *            The path of the resource
+	 * @param style
+	 *            Any resource style, such as a skin style (see {@link org.apache.wicket.Session})
+	 * @param variation
+	 *            The component's variation (of the style)
+	 * @param locale
+	 *            The locale of the resource to load
+	 * @param extension
+	 *            A comma separate list of extensions
+	 * @param strict
+	 *            whether the specified attributes must match exactly
+	 * @return The resource or null
+	 */
+	IResourceStream locate(Class<?> clazz, String path, String style, String variation,
+		Locale locale, String extension, boolean strict);
+
+	/**
+	 * Markup resources and Properties files both need to iterate over different combinations of
+	 * locale, style, etc.. And though no single locate(..) method exists which is used by both,
+	 * they both use ResourceNameIterators.
+	 *
+	 * @param path
+	 *            The path of the resource
+	 * @param style
+	 *            Any resource style, such as a skin style (see {@link org.apache.wicket.Session})
+	 * @param variation
+	 *            The component's variation (of the style)
+	 * @param locale
+	 *            The locale of the resource to load
+	 * @param extension
+	 *            A comma separate list of extensions
+	 * @param strict
+	 *            whether the specified attributes must match exactly
+	 * @return resource name iterator
+	 */
+	ResourceNameIterator newResourceNameIterator(String path, Locale locale, String style,
+		String variation, String extension, boolean strict);
+}