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

git commit: queuing wip

Updated Branches:
  refs/heads/sandbox/queueing [created] eab817441


queuing wip


Project: http://git-wip-us.apache.org/repos/asf/wicket/repo
Commit: http://git-wip-us.apache.org/repos/asf/wicket/commit/eab81744
Tree: http://git-wip-us.apache.org/repos/asf/wicket/tree/eab81744
Diff: http://git-wip-us.apache.org/repos/asf/wicket/diff/eab81744

Branch: refs/heads/sandbox/queueing
Commit: eab8174416fe11dd2e23ecea2cf94a1d08cce378
Parents: 061a478
Author: Igor Vaynberg <iv...@apache.org>
Authored: Fri Jan 6 21:32:23 2012 -0800
Committer: Igor Vaynberg <iv...@apache.org>
Committed: Fri Jan 6 21:32:23 2012 -0800

----------------------------------------------------------------------
 .../src/main/java/org/apache/wicket/Component.java |   21 +-
 .../java/org/apache/wicket/MarkupContainer.java    |   45 ++
 .../markup/queueing/ComponentQueueingTest.java     |  456 +++++++++++++++
 .../org/apache/wicket/markup/queueing/HasPath.java |  119 ++++
 .../apache/wicket/markup/queueing/IsParentOf.java  |   82 +++
 .../org/apache/wicket/markup/queueing/Path.java    |  103 ++++
 .../wicket/markup/queueing/WicketMatchers.java     |   35 ++
 7 files changed, 853 insertions(+), 8 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/wicket/blob/eab81744/wicket-core/src/main/java/org/apache/wicket/Component.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/Component.java b/wicket-core/src/main/java/org/apache/wicket/Component.java
index 8725706..aefc425 100644
--- a/wicket-core/src/main/java/org/apache/wicket/Component.java
+++ b/wicket-core/src/main/java/org/apache/wicket/Component.java
@@ -104,8 +104,7 @@ import org.slf4j.LoggerFactory;
  * concatenation with colon separators of each id along the way. For example, the path "a:b:c" would
  * refer to the component named "c" inside the MarkupContainer named "b" inside the container named
  * "a". The path to a component can be retrieved by calling {@link #getPath()}. To get a Component
- * path relative to the page that contains it, you can call {@link #getPageRelativePath()}.
- * </li>
+ * path relative to the page that contains it, you can call {@link #getPageRelativePath()}.</li>
  * <li><b>LifeCycle </b>- Components participate in the following lifecycle phases:
  * <ul>
  * <li><b>Construction </b>- A Component is constructed with the Java language new operator.
@@ -113,8 +112,7 @@ import org.slf4j.LoggerFactory;
  * {@link IComponentInstantiationListener}s are notified of component instantiation.
  * <p>
  * {@link #onInitialize()} is called on the component as soon as the component is part of a page's
- * component tree. At this state the component is able to access its markup.
- * </li>
+ * component tree. At this state the component is able to access its markup.</li>
  * <li><b>Request Handling </b>- An incoming request is processed by a protocol request handler such
  * as {@link WicketFilter}. An associated Application object creates {@link Session},
  * {@link Request} and {@link Response} objects for use by a given Component in updating its model
@@ -973,6 +971,7 @@ public abstract class Component
 			getApplication().getComponentPreOnBeforeRenderListeners().onBeforeRender(this);
 
 			onBeforeRender();
+
 			getApplication().getComponentPostOnBeforeRenderListeners().onBeforeRender(this);
 
 			if (!getRequestFlag(RFLAG_BEFORE_RENDER_SUPER_CALL_VERIFIED))
@@ -1074,6 +1073,7 @@ public abstract class Component
 		{
 			clearEnabledInHierarchyCache();
 			clearVisibleInHierarchyCache();
+			dequeue();
 			onConfigure();
 			for (Behavior behavior : getBehaviors())
 			{
@@ -1090,6 +1090,10 @@ public abstract class Component
 		}
 	}
 
+	void dequeue()
+	{
+	}
+
 	/**
 	 * Redirects to any intercept page previously specified by a call to redirectToInterceptPage.
 	 * The redirect is done by throwing an exception. If there is no intercept page no exception
@@ -2678,10 +2682,11 @@ public abstract class Component
 					// to be backward compatible. WICKET-3761
 					getMarkupSourcingStrategy().renderHead(this, container);
 					CharSequence headerContribution = markupHeaderResponse.getBuffer();
-					if (Strings.isEmpty(headerContribution) == false) {
+					if (Strings.isEmpty(headerContribution) == false)
+					{
 						response.render(StringHeaderItem.forString(headerContribution));
 					}
-				} 
+				}
 				finally
 				{
 					RequestCycle.get().setResponse(oldResponse);
@@ -3309,7 +3314,7 @@ public abstract class Component
 	{
 		return getRequestCycle().urlFor(pageClass, parameters);
 	}
-	
+
 	/**
 	 * Gets a URL for the listener interface on a behavior (e.g. IBehaviorListener on
 	 * AjaxPagingNavigationBehavior).
@@ -3354,7 +3359,7 @@ public abstract class Component
 	{
 		return getRequestCycle().urlFor(requestHandler);
 	}
-	
+
 	/**
 	 * Gets a URL for the listener interface (e.g. ILinkListener).
 	 * 

http://git-wip-us.apache.org/repos/asf/wicket/blob/eab81744/wicket-core/src/main/java/org/apache/wicket/MarkupContainer.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/main/java/org/apache/wicket/MarkupContainer.java b/wicket-core/src/main/java/org/apache/wicket/MarkupContainer.java
index 621ea05..d35051c 100644
--- a/wicket-core/src/main/java/org/apache/wicket/MarkupContainer.java
+++ b/wicket-core/src/main/java/org/apache/wicket/MarkupContainer.java
@@ -99,6 +99,10 @@ public abstract class MarkupContainer extends Component implements Iterable<Comp
 	/** Log for reporting. */
 	private static final Logger log = LoggerFactory.getLogger(MarkupContainer.class);
 
+	private static MetaDataKey<ArrayList<Component>> QUEUE = new MetaDataKey<ArrayList<Component>>()
+	{
+	};
+
 	/** List of children or single child */
 	private Object children;
 
@@ -303,6 +307,47 @@ public abstract class MarkupContainer extends Component implements Iterable<Comp
 		return true;
 	}
 
+	public MarkupContainer queue(final Component... childs)
+	{
+		ArrayList<Component> queue = getMetaData(QUEUE);
+		if (queue == null)
+		{
+			queue = new ArrayList<Component>();
+			setMetaData(QUEUE, queue);
+		}
+
+		if (getApplication().usesDevelopmentConfig())
+		{
+			for (Component child : childs)
+			{
+				for (Component queued : queue)
+				{
+					if (queued.getId().equals(child.getId()))
+					{
+						throw new WicketRuntimeException(
+							"Component with id: '" +
+								queued.getId() +
+								"' is already queued in container: " +
+								this +
+								". Two components with the same id cannot be queued under the same container. Component alread queued: " +
+								queued + ". Component attempted to be queued: " + child);
+					}
+				}
+				queue.add(child);
+			}
+		}
+		return this;
+	}
+
+	/**
+	 * Implements the process to dequeue all components registered
+	 */
+	@Override
+	void dequeue()
+	{
+		// FIXME
+	}
+
 	/**
 	 * @param component
 	 *            The component to check

http://git-wip-us.apache.org/repos/asf/wicket/blob/eab81744/wicket-core/src/test/java/org/apache/wicket/markup/queueing/ComponentQueueingTest.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/test/java/org/apache/wicket/markup/queueing/ComponentQueueingTest.java b/wicket-core/src/test/java/org/apache/wicket/markup/queueing/ComponentQueueingTest.java
new file mode 100755
index 0000000..939ba32
--- /dev/null
+++ b/wicket-core/src/test/java/org/apache/wicket/markup/queueing/ComponentQueueingTest.java
@@ -0,0 +1,456 @@
+/*
+ * 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.markup.queueing;
+
+import static org.apache.wicket.markup.queueing.WicketMatchers.hasPath;
+import static org.hamcrest.Matchers.is;
+
+import java.util.ArrayList;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.MarkupContainer;
+import org.apache.wicket.WicketRuntimeException;
+import org.apache.wicket.WicketTestCase;
+import org.apache.wicket.markup.IMarkupResourceStreamProvider;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.WebPage;
+import org.apache.wicket.markup.html.link.Link;
+import org.apache.wicket.markup.html.list.ListItem;
+import org.apache.wicket.markup.html.list.ListView;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.util.resource.IResourceStream;
+import org.apache.wicket.util.resource.StringResourceStream;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ComponentQueueingTest extends WicketTestCase
+{
+	/** {@code [a,b,c] -> [a[b[c]]] } */
+	@Test
+	public void dequeue1()
+	{
+		TestPage p = new TestPage();
+		p.setPageMarkup("<p wicket:id='a'><p wicket:id='b'><p wicket:id='c'></p></p></p>");
+		MarkupContainer a = new A(), b = new B(), c = new C();
+
+		p.queue(b, c, a);
+
+		tester.startPage(p);
+
+		assertThat(p, hasPath(new Path(a, b, c)));
+	}
+
+	/** {@code [a[b,c]] -> [a[b[c]]] } */
+	@Test
+	public void dequeue2()
+	{
+		TestPage p = new TestPage();
+		p.setPageMarkup("<p wicket:id='a'><p wicket:id='b'><p wicket:id='c'></p></p></p>");
+		MarkupContainer a = new A(), b = new B(), c = new C();
+
+		p.queue(a);
+		a.queue(b, c);
+
+		tester.startPage(p);
+
+		assertThat(p, hasPath(new Path(a, b, c)));
+	}
+
+	/** {@code [a[b[c]] -> [a[b[c]]] } */
+	@Test
+	public void dequeue3()
+	{
+		TestPage p = new TestPage();
+		p.setPageMarkup("<p wicket:id='a'><p wicket:id='b'><p wicket:id='c'></p></p></p>");
+		MarkupContainer a = new A(), b = new B(), c = new C();
+
+		p.queue(a);
+		a.queue(b);
+		b.queue(c);
+
+		tester.startPage(p);
+
+		assertThat(p, hasPath(new Path(a, b, c)));
+	}
+
+	/** {@code [a[b],c] -> [a[b[c]]] } */
+	@Test
+	public void dequeue4()
+	{
+		TestPage p = new TestPage();
+		p.setPageMarkup("<p wicket:id='a'><p wicket:id='b'><p wicket:id='c'></p></p></p>");
+		MarkupContainer a = new A(), b = new B(), c = new C();
+
+		p.queue(a, c);
+		a.queue(b);
+
+		tester.startPage(p);
+
+		assertThat(p, hasPath(new Path(a, b, c)));
+	}
+
+	/** {@code [a(b)],c] -> [a[b[c]]] } */
+	@Test
+	public void dequeue5()
+	{
+		TestPage p = new TestPage();
+		p.setPageMarkup("<p wicket:id='a'><p wicket:id='b'><p wicket:id='c'></p></p></p>");
+		MarkupContainer a = new A(), b = new B(), c = new C();
+		p.queue(a, c);
+		a.add(b);
+
+		tester.startPage(p);
+
+		assertThat(p, hasPath(new Path(a, b, c)));
+	}
+
+	/** {@code [a,b,c] -> [a[b,c]] } */
+	@Test
+	public void dequeue6()
+	{
+		TestPage p = new TestPage();
+		p.setPageMarkup("<p wicket:id='a'><p wicket:id='b'></p><p wicket:id='c'></p></p>");
+		MarkupContainer a = new A(), b = new B(), c = new C();
+
+		p.queue(a, b, c);
+
+		tester.startPage(p);
+
+		assertThat(p, hasPath(new Path(a, b)));
+		assertThat(p, hasPath(new Path(a, c)));
+	}
+
+	/** {@code [a,c[b]] ->| [a[b[c]]] } */
+	@Test
+	public void dequeueError1()
+	{
+		TestPage p = new TestPage();
+		p.setPageMarkup("<p wicket:id='a'><p wicket:id='b'><p wicket:id='c'></p></p></p>");
+		MarkupContainer a = new A(), b = new B(), c = new C();
+
+		p.queue(b, c);
+		c.queue(a);
+
+		try
+		{
+			tester.startPage(p);
+			Assert.fail();
+		}
+		catch (WicketRuntimeException e)
+		{
+			// expected
+		}
+	}
+
+	/** {@code [a,q[r,s]] - > [a[q[r[s]]]] } */
+	@Test
+	public void dequeueWithPanel1()
+	{
+		MarkupContainer a = new A(), r = new R(), s = new S();
+
+		TestPanel q = new TestPanel("q");
+		q.setPanelMarkup("<wicket:panel><p wicket:id='r'><p wicket:id='s'></p></p></wicket:panel>");
+		q.queue(r, s);
+
+		TestPage p = new TestPage();
+		p.setPageMarkup("<p wicket:id='a'><p wicket:id='q'></p></p>");
+
+		p.queue(a, q);
+
+		tester.startPage(p);
+
+		assertThat(p, hasPath(new Path(a, q, r, s)));
+	}
+
+	/** panel has leading markup */
+	@Test
+	public void dequeueWithPanel2()
+	{
+		MarkupContainer r = new R();
+
+		TestPanel q = new TestPanel("q");
+		q.setPanelMarkup("<html><body><wicket:panel><p wicket:id='r'></p></wicket:panel></body></html>");
+		q.queue(r);
+
+		TestPage p = new TestPage();
+		p.setPageMarkup("<p wicket:id='q'></p>");
+		p.queue(q);
+
+		tester.startPage(p);
+
+		assertThat(p, hasPath(new Path(q, r)));
+	}
+
+	/** panel with a static header section */
+	@Test
+	public void dequeueWithPanel3()
+	{
+		MarkupContainer r = new R();
+
+		TestPanel q = new TestPanel("q");
+		q.setPanelMarkup("<html><head><wicket:head><meta/></wicket:head></head>"
+			+ "<body><wicket:panel><p wicket:id='r'></p></wicket:panel></body></html>");
+		q.queue(r);
+
+		TestPage p = new TestPage();
+		p.setPageMarkup("<html><head></head><body><p wicket:id='q'></p></body></html>");
+		p.queue(q);
+
+		tester.startPage(p);
+
+		assertThat(p, hasPath(new Path(q, r)));
+	}
+
+	/** repeater */
+	@Test
+	public void dequeueWithRepeater1()
+	{
+		TestPage p = new TestPage();
+		p.setPageMarkup("<p wicket:id='a'><p wicket:id='lv'><p wicket:id='b'><p wicket:id='c'></p></p></p></p>");
+
+		MarkupContainer a = new A();
+		LV l = new LV(3)
+		{
+			@Override
+			protected void populateItem(ListItem<Integer> item)
+			{
+				item.queue(new B(), new C());
+			}
+		};
+
+		p.queue(a, l);
+
+		tester.startPage(p);
+
+		assertThat(l.size(), is(3));
+		for (Component item : l)
+		{
+			assertThat(p, hasPath(new Path(a, l, item, new B(), new C())));
+		}
+	}
+
+	/** repeater with a panel inside */
+	@Test
+	public void dequeueWithRepeater2()
+	{
+		TestPage p = new TestPage();
+		p.setPageMarkup("<p wicket:id='a'><p wicket:id='lv'><p wicket:id='b'><p wicket:id='q'></p></p></p></p>");
+
+		MarkupContainer a = new A();
+		LV l = new LV(3)
+		{
+			@Override
+			protected void populateItem(ListItem<Integer> item)
+			{
+				TestPanel q = new TestPanel("q");
+				q.setPanelMarkup("<wicket:panel><p wicket:id='r'><p wicket:id='s'></p></p></wicket:panel>");
+				q.queue(new R(), new S());
+
+				item.queue(q, new B());
+			}
+		};
+
+		p.queue(a, l);
+
+		tester.startPage(p);
+
+		assertThat(l.size(), is(3));
+		for (Component item : l)
+		{
+			assertThat(p, hasPath(new Path(a, l, item, new B()).add("q").add(new R(), new S())));
+		}
+	}
+
+	/** dequeue, then rerender the page instance after a callback is executed */
+	@Test
+	public void dequeueWithCallback()
+	{
+		TestPage p = new TestPage();
+		p.setPageMarkup("<p wicket:id='a'><a wicket:id='l'><p wicket:id='b'></p></a></p>");
+		MarkupContainer a = new A(), b = new B();
+		L l = new L();
+		p.queue(a, b, l);
+
+		tester.startPage(p);
+
+		assertThat(p, hasPath(new Path(a, l, b)));
+		assertThat(l.isClicked(), is(false));
+
+		tester.clickLink(l);
+
+		assertThat(l.isClicked(), is(true));
+	}
+
+
+	/** queuing two components with the same id */
+	@Test
+	public void queueIdCollission()
+	{
+		try
+		{
+			new A().queue(new B(), new B());
+			Assert.fail("Should not be able to queue two components with the same id under the same parent");
+		}
+		catch (WicketRuntimeException e)
+		{
+			// expected
+		}
+	}
+
+
+	private static class A extends WebMarkupContainer
+	{
+		public A()
+		{
+			super("a");
+		}
+	}
+
+	private static class B extends WebMarkupContainer
+	{
+		public B()
+		{
+			super("b");
+		}
+	}
+
+	private static class C extends WebMarkupContainer
+	{
+		public C()
+		{
+			super("c");
+		}
+	}
+
+	private static class R extends WebMarkupContainer
+	{
+		public R()
+		{
+			super("r");
+		}
+	}
+
+	private static class S extends WebMarkupContainer
+	{
+		public S()
+		{
+			super("s");
+		}
+	}
+
+	private static abstract class LV extends ListView<Integer>
+	{
+		public LV(int size)
+		{
+			super("lv");
+			ArrayList<Integer> values = new ArrayList<Integer>();
+			for (int i = 0; i < size; i++)
+				values.add(i);
+			setModel(new Model<ArrayList<Integer>>(values));
+		}
+	}
+
+	private static class L extends Link<Void>
+	{
+		private boolean clicked = false;
+
+		public L()
+		{
+			super("l");
+		}
+
+		@Override
+		public void onClick()
+		{
+			clicked = true;
+		}
+
+		public boolean isClicked()
+		{
+			return clicked;
+		}
+	}
+
+
+	private static class TestPage extends WebPage implements IMarkupResourceStreamProvider
+	{
+		private String markup;
+
+		public TestPage()
+		{
+		}
+
+		public TestPage(String markup)
+		{
+			this.markup = markup;
+		}
+
+		protected String getPageMarkup()
+		{
+			return markup;
+		}
+
+		public void setPageMarkup(String markup)
+		{
+			this.markup = markup;
+		}
+
+		@Override
+		public IResourceStream getMarkupResourceStream(MarkupContainer container,
+			Class<?> containerClass)
+		{
+			return new StringResourceStream(getPageMarkup());
+		}
+
+	}
+
+	private static class TestPanel extends Panel implements IMarkupResourceStreamProvider
+	{
+
+		private String markup;
+
+		public TestPanel(String id)
+		{
+			super(id);
+		}
+
+		public TestPanel(String id, String markup)
+		{
+			super(id);
+			this.markup = markup;
+		}
+
+		protected void setPanelMarkup(String markup)
+		{
+			this.markup = markup;
+		}
+
+		protected String getPanelMarkup()
+		{
+			return markup;
+		}
+
+		@Override
+		public IResourceStream getMarkupResourceStream(MarkupContainer container,
+			Class<?> containerClass)
+		{
+			return new StringResourceStream(getPanelMarkup());
+		}
+
+	}
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/eab81744/wicket-core/src/test/java/org/apache/wicket/markup/queueing/HasPath.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/test/java/org/apache/wicket/markup/queueing/HasPath.java b/wicket-core/src/test/java/org/apache/wicket/markup/queueing/HasPath.java
new file mode 100755
index 0000000..dac0d12
--- /dev/null
+++ b/wicket-core/src/test/java/org/apache/wicket/markup/queueing/HasPath.java
@@ -0,0 +1,119 @@
+package org.apache.wicket.markup.queueing;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.MarkupContainer;
+import org.hamcrest.Description;
+import org.hamcrest.Factory;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+
+class HasPath extends TypeSafeMatcher<Component>
+{
+	private final Path path;
+
+	public HasPath(Path path)
+	{
+		this.path = path;
+	}
+
+	public void describeTo(Description description)
+	{
+		description.appendText("path ").appendText(toString(path, 0, path.size()));
+	}
+
+	@Override
+	protected boolean matchesSafely(Component item)
+	{
+		Component cursor = item;
+		for (int i = 0; i < path.size(); i++)
+		{
+			if (!(cursor instanceof MarkupContainer))
+			{
+				return false;
+			}
+
+			cursor = ((MarkupContainer)cursor).get(path.get(i).getId());
+			if (cursor == null)
+			{
+				return false;
+			}
+			if (!path.get(i).getType().isAssignableFrom(cursor.getClass()))
+			{
+				return false;
+			}
+		}
+		return true;
+	}
+
+	@Override
+	public void describeMismatchSafely(Component item, Description desc)
+	{
+		Component cursor = item;
+
+		int matched = 0;
+
+		String error = null;
+
+		for (int i = 0; i < path.size(); i++)
+		{
+			matched = i;
+			if (!(cursor instanceof MarkupContainer))
+			{
+				error = "next component has to be at least a MarkupContainer to contain children, but was: " +
+					toString(cursor);
+				break;
+			}
+
+			cursor = ((MarkupContainer)cursor).get(path.get(i).getId());
+			if (cursor == null)
+			{
+				error = "next child with id: '" + path.get(i).getId() + "' not found";
+				break;
+			}
+			if (!path.get(i).getType().isAssignableFrom(cursor.getClass()))
+			{
+				error = "expected next child of type: " + path.get(i).getType().getSimpleName() +
+					", but found: " + toString(cursor);
+				break;
+			}
+		}
+
+		desc.appendText("\n       root: ").appendText(toString(item));
+		desc.appendText("\n       matched segments: ").appendText(toString(path, 0, matched));
+		desc.appendText("\n       error: ").appendText(error);
+	}
+
+	private static String toString(Component c)
+	{
+		return toString(c.getClass(), c.getId());
+	}
+
+	private static String toString(Class<?> type, String id)
+	{
+		return type.getSimpleName() + "('" + id + "')";
+	}
+
+	private static String toString(Path path, int start, int end)
+	{
+		String str = "[";
+		for (int i = start; i < end; i++)
+		{
+			if (i > 0)
+			{
+				str += ", ";
+			}
+			str += toString(path.get(i).getType(), path.get(i).getId());
+		}
+		str += "]";
+		return str;
+	}
+
+
+	@Factory
+	public static <T> Matcher<Component> hasPath(Path path)
+	{
+		return new HasPath(path);
+	}
+
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/wicket/blob/eab81744/wicket-core/src/test/java/org/apache/wicket/markup/queueing/IsParentOf.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/test/java/org/apache/wicket/markup/queueing/IsParentOf.java b/wicket-core/src/test/java/org/apache/wicket/markup/queueing/IsParentOf.java
new file mode 100755
index 0000000..33da758
--- /dev/null
+++ b/wicket-core/src/test/java/org/apache/wicket/markup/queueing/IsParentOf.java
@@ -0,0 +1,82 @@
+package org.apache.wicket.markup.queueing;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.MarkupContainer;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.hamcrest.Description;
+import org.hamcrest.Factory;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+
+class IsParentOf extends TypeSafeMatcher<Component>
+{
+	private final Component child;
+
+	public IsParentOf(Component child)
+	{
+		this.child = child;
+	}
+
+	public void describeTo(Description description)
+	{
+		description.appendText(toString(child.getParent()));
+	}
+
+	@Override
+	protected boolean matchesSafely(Component item)
+	{
+		if (!(item instanceof MarkupContainer))
+		{
+			return false;
+		}
+
+		if (!(item instanceof MarkupContainer))
+			return false;
+		MarkupContainer container = (MarkupContainer)item;
+		if (container.get(child.getId()) != child)
+			return false;
+		if (child.getParent() != container)
+			return false;
+		return true;
+	}
+
+	@Override
+	public void describeMismatchSafely(Component item, Description description)
+	{
+		if (child.getParent() != item)
+		{
+			description.appendText("found " + toString(item));
+			return;
+		}
+
+		if (!(item instanceof MarkupContainer))
+		{
+			description.appendText("found ")
+				.appendText(toString(item))
+				.appendText(" which is not a container");
+			return;
+		}
+
+		if (((WebMarkupContainer)item).get(child.getId()) == null)
+		{
+			description.appendText(toString(item))
+				.appendText(" does not contain ")
+				.appendText(toString(child));
+			return;
+		}
+		super.describeMismatchSafely(item, description);
+	}
+
+	private static String toString(Component c)
+	{
+		return c.getClass().getSimpleName() + "('" + c.getId() + "')";
+	}
+
+	@Factory
+	public static <T> Matcher<Component> isParentOf(Component child)
+	{
+		return new IsParentOf(child);
+	}
+
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/wicket/blob/eab81744/wicket-core/src/test/java/org/apache/wicket/markup/queueing/Path.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/test/java/org/apache/wicket/markup/queueing/Path.java b/wicket-core/src/test/java/org/apache/wicket/markup/queueing/Path.java
new file mode 100755
index 0000000..96f6ab8
--- /dev/null
+++ b/wicket-core/src/test/java/org/apache/wicket/markup/queueing/Path.java
@@ -0,0 +1,103 @@
+/*
+ * 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.markup.queueing;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.markup.queueing.Path.Segment;
+
+class Path implements Iterable<Segment>
+{
+
+	private List<Segment> segments;
+
+	public Path()
+	{
+		segments = new ArrayList<Segment>();
+	}
+
+	public Path(Component... components)
+	{
+		this();
+		add(components);
+	}
+
+	public Path add(Class<?> type, String id)
+	{
+		segments.add(new Segment(type, id));
+		return this;
+	}
+
+	public Path add(String id)
+	{
+		add(Component.class, id);
+		return this;
+	}
+
+	public Path add(Component... components)
+	{
+		for (Component c : components)
+		{
+			add(c.getClass(), c.getId());
+		}
+		return this;
+	}
+
+
+	@Override
+	public Iterator<Segment> iterator()
+	{
+		return segments.iterator();
+	}
+
+	public int size()
+	{
+		return segments.size();
+	}
+
+	public Segment get(int index)
+	{
+		return segments.get(index);
+	}
+
+	public static class Segment
+	{
+		Class<?> type;
+		String id;
+
+		public Segment(Class<?> type, String id)
+		{
+			this.type = type;
+			this.id = id;
+		}
+
+		public Class<?> getType()
+		{
+			return type;
+		}
+
+		public String getId()
+		{
+			return id;
+		}
+
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/eab81744/wicket-core/src/test/java/org/apache/wicket/markup/queueing/WicketMatchers.java
----------------------------------------------------------------------
diff --git a/wicket-core/src/test/java/org/apache/wicket/markup/queueing/WicketMatchers.java b/wicket-core/src/test/java/org/apache/wicket/markup/queueing/WicketMatchers.java
new file mode 100755
index 0000000..d379a1a
--- /dev/null
+++ b/wicket-core/src/test/java/org/apache/wicket/markup/queueing/WicketMatchers.java
@@ -0,0 +1,35 @@
+/*
+ * 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.markup.queueing;
+
+import org.apache.wicket.Component;
+import org.hamcrest.Matcher;
+import org.hamcrest.Matchers;
+
+class WicketMatchers extends Matchers
+{
+	public static <T> Matcher<Component> isParentOf(Component child)
+	{
+		return new IsParentOf(child);
+	}
+
+
+	public static <T> Matcher<Component> hasPath(Path path)
+	{
+		return new HasPath(path);
+	}
+}
\ No newline at end of file