Commit 758f198f authored by Andreas Jonsson's avatar Andreas Jonsson
Browse files

XCOMMONS-278: Changed to builder pattern for declaring a property in the execution context.

parent 17d5092d
......@@ -25,6 +25,8 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xwiki.context.internal.ExecutionContextProperty;
/**
* Contains all state data related to the current user action. Note that the execution context is independent of the
* environment and all environment-dependent data are stored in the Container component instead.
......@@ -58,6 +60,16 @@ public Object getProperty(String key)
return property.getValue();
}
/**
* @param key the key of the property.
* @return a builder object for performing the declaration. The property will not be declared until the declare
* method is called on the builder object.
*/
public DeclarationBuilder newProperty(String key)
{
return new DeclarationBuilder(key);
}
/**
* @param key the key under which is stored the property to retrieve
* @return {@code true} if there is a property declared for the given key.
......@@ -110,8 +122,8 @@ public void setProperty(String key, Object value)
if (property == null) {
LOGGER.debug("Implicit declaration of property {}.", key);
property = new ExecutionContextProperty(key);
properties.put(key, property);
newProperty(key).declare();
property = properties.get(key);
} else if (property.isFinal()) {
throw new PropertyIsFinalException(key);
}
......@@ -138,7 +150,7 @@ public void setProperties(Map<String, Object> properties)
*
* @since 4.3M1
*/
public void declareProperty(ExecutionContextProperty property)
private void declareProperty(ExecutionContextProperty property)
{
if (properties.containsKey(property.getKey())) {
throw new PropertyAlreadyExistsException(property.getKey());
......@@ -176,6 +188,18 @@ public void inheritFrom(ExecutionContext executionContext)
}
}
/**
* This method is intentionally non-public and should only be used for unit testing the property objects.
*
* @param key The property key.
* @return The declared property.
* @since 4.3M2
*/
ExecutionContextProperty fetchProperty(String key)
{
return properties.get(key);
}
/**
* @param property Property to check.
* @throws IllegalStateException if the property may not be ignored.
......@@ -192,4 +216,121 @@ private void checkIfInheritedPropertyMayBeIgnored(ExecutionContextProperty prope
}
}
}
/**
* Builder class for declaring a new proprety.
*
* @since 4.3M2
*/
public final class DeclarationBuilder
{
/** @see ExecutionContextProperty#key */
private final String key;
/** @see ExecutionContextProperty#value */
private Object value;
/** @see ExecutionContextProperty#cloneValue */
private boolean cloneValue;
/** @see ExecutionContextProperty#isFinal */
private boolean isFinal;
/** @see ExecutionContextProperty#inherited */
private boolean inherited;
/** @see ExecutionContextProperty#nonNull */
private boolean nonNull;
/** @see ExecutionContextProperty#type */
private Class<?> type;
/**
* Start building a property for the given key.
*
* @param key The property key.
*/
private DeclarationBuilder(String key)
{
this.key = key;
}
/**
* Finish the building by declaring the property in this execution context.
*/
public void declare()
{
ExecutionContext.this.declareProperty(
new ExecutionContextProperty(key, value, cloneValue, isFinal, inherited, nonNull, type));
}
/**
* @param value The initial value.
* @return this declaration builder.
*/
public DeclarationBuilder initial(Object value)
{
this.value = value;
return this;
}
/**
* Make the initial value the final value.
*
* @return this declaration builder.
*/
public DeclarationBuilder makeFinal()
{
isFinal = true;
return this;
}
/**
* Indicate that the value should be cloned when the property is cloned.
*
* @return this declaration builder.
*/
public DeclarationBuilder cloneValue()
{
cloneValue = true;
return this;
}
/**
* Set the type of the value.
*
* @param type The type to declare for the property.
*
* @return this declaration builder.
*/
public DeclarationBuilder type(Class<?> type)
{
this.type = type;
return this;
}
/**
* Indicate that the property should be inherited.
*
* @return this declaration builder.
*/
public DeclarationBuilder inherited()
{
inherited = true;
return this;
}
/**
* Indicate that the property value may not be {@literal null}.
*
* @return this declaration builder.
*/
public DeclarationBuilder nonNull()
{
nonNull = true;
return this;
}
}
}
......@@ -17,7 +17,7 @@
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.context;
package org.xwiki.context.internal;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
......@@ -33,60 +33,84 @@
public class ExecutionContextProperty implements Cloneable
{
/** The key that is the name of this property in the execution context. */
private String key;
private final String key;
/** This is the actual property value. */
private Object value;
/**
* Clone the value when this property is cloned.
*
* ReflectPermission suppressAccessChecks may be required.
*/
private boolean cloneValue;
private final boolean cloneValue;
/** Controls whether the value of this property is final. */
private boolean isFinal;
private final boolean isFinal;
/**
* Controls whether this property should be inherited across execution contexts. It will be inherited as long as
* an execution context containing the property is the current execution context. It will not be propagated to
* parent execution contexts. Hence, it may be removed by a call to popExecutionContext.
*/
private boolean inherited;
private final boolean inherited;
/** Indicate that the value may not be {@code null}. */
private boolean nonNull;
private final boolean nonNull;
/** The type of the value. */
private Class<?> type;
private final Class<?> type;
/** @see isClonedFrom(ExecutionContextProperty property). */
private WeakReference<ExecutionContextProperty> clonedFrom;
/**
* @param key The execution context key.
* @param initialValue The initial value.
* @param cloneValue Indicate that the value should be cloned when the property is cloned.
* @param isFinal Indicate that the value may not be updated from the initial value.
* @param inherited Indicate that the property should be inherited when activating a new execution context.
* @param nonNull Indicate that the property value may not be {@literal null}.
* @param type Set a class which the value must be assignable to.
*/
public ExecutionContextProperty(String key)
public ExecutionContextProperty(String key, Object initialValue, boolean cloneValue, boolean isFinal,
boolean inherited, boolean nonNull, Class<?> type)
{
this.key = key;
this.value = initialValue;
this.cloneValue = cloneValue;
this.isFinal = isFinal;
this.inherited = inherited;
this.nonNull = nonNull;
this.type = type;
checkValue(initialValue);
}
/**
* @param value The object value.
* Check that the value is compatible with the configure constraints.
*
* @param value The value.
* @throws IllegalArgumentException if the value is null and this property has the nonNull attribute set, or if the
* type is set for this value, but the value is not assignable to the set type.
*/
public void setValue(Object value)
private void checkValue(Object value)
{
if (nonNull && value == null) {
throw new IllegalArgumentException(String.format("The property [%s] may not be null!", key));
}
if (type != null && !type.isAssignableFrom(value.getClass())) {
if (type != null && value != null && !type.isAssignableFrom(value.getClass())) {
throw new IllegalArgumentException(
String.format("The value of property [%s] must be of type [%s], but was [%s]",
key, type, value.getClass()));
}
}
/**
* @param value The object value.
* @throws IllegalArgumentException if the value is null and this property has the nonNull attribute set, or if the
* type is set for this value, but the value is not assignable to the set type.
*/
public void setValue(Object value)
{
checkValue(value);
this.value = value;
}
......@@ -102,61 +126,28 @@ public String getKey()
return this.key;
}
/** @param cloneValue Set wether the value should be cloned when this property is cloned. */
public void setCloneValue(boolean cloneValue)
{
this.cloneValue = cloneValue;
}
/** @param isFinal Set wether the property is final or not. */
public void setFinal(boolean isFinal)
{
this.isFinal = isFinal;
}
/** @return wether this property is final or not. */
public boolean isFinal()
{
return this.isFinal;
}
/** @param inherited Set wether this property should be inherited across execution contexts or not. */
public void setInherited(boolean inherited)
{
this.inherited = inherited;
}
/** @return wether this property should be inherited across execution contexts or not. */
public boolean isInherited()
{
return this.inherited;
}
/** @param nonNull Indicate wether it is an error if th value of this property is set to {@code null} or not. */
public void setNonNull(boolean nonNull)
{
this.nonNull = nonNull;
}
/** @param type The type of this property's value. If set, the value will be typechecked when set. */
public void setType(Class<?> type)
{
this.type = type;
}
@Override
public ExecutionContextProperty clone()
{
ExecutionContextProperty clone;
try {
clone = (ExecutionContextProperty) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
Object clonedValue;
if (cloneValue) {
try {
clone.value = value.getClass().getMethod("clone").invoke(value);
clonedValue = value.getClass().getMethod("clone").invoke(value);
} catch (NoSuchMethodException e) {
throw new IllegalStateException(String.format(
"cloneValue attribute was set on property [%s], "
......@@ -168,14 +159,11 @@ public ExecutionContextProperty clone()
throw new RuntimeException(e);
}
} else {
clone.value = value;
clonedValue = value;
}
clone.key = key;
clone.cloneValue = cloneValue;
clone.isFinal = isFinal;
clone.inherited = inherited;
clone.nonNull = nonNull;
clone.type = type;
clone = new ExecutionContextProperty(key, clonedValue, cloneValue, isFinal, inherited, nonNull, type);
if (isFinal && inherited) {
// We make this a weak reference, because we are only interested in it as long
// as it is references by the current execution co
......@@ -195,7 +183,7 @@ public ExecutionContextProperty clone()
* @param property The original property.
* @return If the return value is {@code true}, then this property was cloned from the given property.
*/
boolean isClonedFrom(ExecutionContextProperty property)
public boolean isClonedFrom(ExecutionContextProperty property)
{
return clonedFrom != null && clonedFrom.get() == property;
}
......
......@@ -32,105 +32,99 @@
@Test
public void defaultValues() throws Exception
{
ExecutionContextProperty property = new ExecutionContextProperty("test");
final String key = "test";
Assert.assertFalse(property.isFinal());
Assert.assertTrue("test".equals(property.getKey()));
Assert.assertTrue(null == property.getValue());
Assert.assertFalse(property.isInherited());
ExecutionContext context = new ExecutionContext();
context.newProperty(key).declare();
Assert.assertFalse(context.fetchProperty(key).isFinal());
Assert.assertTrue(key.equals(context.fetchProperty(key).getKey()));
Assert.assertTrue(null == context.fetchProperty(key).getValue());
Assert.assertFalse(context.fetchProperty(key).isInherited());
Object o = new Object();
property.setValue(o);
Assert.assertTrue(property.getValue() == o);
property.setValue(null);
Assert.assertTrue(property.getValue() == null);
context.setProperty(key, o);
Assert.assertTrue(context.fetchProperty(key).getValue() == o);
context.setProperty(key, null);
Assert.assertTrue(context.fetchProperty(key).getValue() == null);
}
@Test
public void cloning() throws Exception
{
ExecutionContextProperty property = new ExecutionContextProperty("test");
TestCloneable value = new TestCloneable();
final String k1 = "test1";
final String k2 = "test2";
final String k3 = "test3";
property.setValue(value);
ExecutionContext context = new ExecutionContext();
ExecutionContextProperty clone = property.clone();
TestCloneable value = new TestCloneable();
Assert.assertTrue(value == clone.getValue());
context.newProperty(k1).initial(value).declare();
property.setCloneValue(true);
Assert.assertTrue(value == context.fetchProperty(k1).clone().getValue());
clone = property.clone();
context.newProperty(k2).initial(value).cloneValue().declare();
Assert.assertTrue(value != clone.getValue() && ((TestCloneable) clone.getValue()).value.equals("clone"));
TestCloneable clonedValue = (TestCloneable) context.fetchProperty(k2).clone().getValue();
property.setFinal(true);
property.setInherited(true);
Assert.assertTrue(value != clonedValue && clonedValue.value.equals("clone"));
clone = property.clone();
context.newProperty(k3).initial(value).cloneValue().makeFinal().inherited().declare();
Assert.assertTrue(clone.isClonedFrom(property));
Assert.assertTrue(context.fetchProperty(k3).clone().isClonedFrom(context.fetchProperty(k3)));
}
@Test(expected=IllegalStateException.class)
public void cloningNonPublicCloneMethod()
{
ExecutionContextProperty property = new ExecutionContextProperty("test");
TestNonpublicClone value = new TestNonpublicClone();
property.setCloneValue(true);
property.setValue(value);
ExecutionContext context = new ExecutionContext();
property.clone();
}
@Test
public void assertClonedFrom()
{
ExecutionContextProperty property = new ExecutionContextProperty("test");
final String key = "test";
property.setFinal(true);
property.setInherited(true);
TestNonpublicClone value = new TestNonpublicClone();
ExecutionContextProperty clone = property.clone();
context.newProperty(key).cloneValue().initial(value).declare();
Assert.assertFalse(clone.isClonedFrom(clone));
context.fetchProperty(key).clone();
}
@Test(expected=IllegalArgumentException.class)
public void nonNullCheck()
{
ExecutionContextProperty property = new ExecutionContextProperty("test");
ExecutionContext context = new ExecutionContext();
property.setNonNull(true);
final String key = "test";
property.setValue(null);
context.newProperty(key).nonNull().initial(null).declare();
}
@Test
public void typeChecking()
{
ExecutionContextProperty property = new ExecutionContextProperty("test");
ExecutionContext context = new ExecutionContext();
final String key = "test";
property.setType(SomeClass.class);
context.newProperty(key).type(SomeClass.class).declare();
property.setValue(new SomeClass());
property.setValue(new SomeSubClass());
context.setProperty(key, new SomeClass());
context.setProperty(key, new SomeSubClass());
}
@Test(expected=IllegalArgumentException.class)
public void typeCheckingMismatch()
{
ExecutionContextProperty property = new ExecutionContextProperty("test");
ExecutionContext context = new ExecutionContext();
final String key = "test";
property.setType(SomeSubClass.class);
context.newProperty(key).type(SomeSubClass.class).declare();
property.setValue(new SomeClass());
context.setProperty(key, new SomeClass());
}
private static class TestCloneable implements Cloneable
public static class TestCloneable implements Cloneable
{
public String value = "original";
......@@ -144,7 +138,7 @@ public Object clone() throws CloneNotSupportedException
}
}
private static class TestNonpublicClone implements Cloneable
public static class TestNonpublicClone implements Cloneable
{
@Override
protected Object clone() throws CloneNotSupportedException
......@@ -153,11 +147,11 @@ protected Object clone() throws CloneNotSupportedException
}
}
private static class SomeClass
public static class SomeClass
{
}
private static class SomeSubClass extends SomeClass
public static class SomeSubClass extends SomeClass
{
}
......
......@@ -36,37 +36,16 @@ public void inheritance()
ExecutionContext context = new ExecutionContext();
ExecutionContext parent = new ExecutionContext();
ExecutionContextProperty inherited = new ExecutionContextProperty("inherited");
inherited.setInherited(true);
inherited.setValue("test");