Updated Branches: refs/heads/sandbox/hierarchy-completion [created] 214c95ca4
wip impl of hierarchy-completion Project: http://git-wip-us.apache.org/repos/asf/wicket/repo Commit: http://git-wip-us.apache.org/repos/asf/wicket/commit/214c95ca Tree: http://git-wip-us.apache.org/repos/asf/wicket/tree/214c95ca Diff: http://git-wip-us.apache.org/repos/asf/wicket/diff/214c95ca Branch: refs/heads/sandbox/hierarchy-completion Commit: 214c95ca4a33e08b003fc46fc62ea8da181e739e Parents: 407503d Author: Igor Vaynberg <[email protected]> Authored: Sun Dec 25 09:21:31 2011 -0800 Committer: Igor Vaynberg <[email protected]> Committed: Sun Dec 25 09:21:31 2011 -0800 ---------------------------------------------------------------------- .../src/main/java/org/apache/wicket/Component.java | 18 +- .../java/org/apache/wicket/MarkupContainer.java | 375 ++++++++++- .../apache/wicket/hierarchy/complete/HasPath.java | 119 ++++ .../complete/HierarchyCompletionTest.java | 515 +++++++++++++++ .../wicket/hierarchy/complete/IsParentOf.java | 82 +++ .../org/apache/wicket/hierarchy/complete/Path.java | 103 +++ .../wicket/hierarchy/complete/TesterRule.java | 96 +++ .../wicket/hierarchy/complete/WicketMatchers.java | 35 + 8 files changed, 1299 insertions(+), 44 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/wicket/blob/214c95ca/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 4c25361..c335ddc 100644 --- a/wicket-core/src/main/java/org/apache/wicket/Component.java +++ b/wicket-core/src/main/java/org/apache/wicket/Component.java @@ -892,20 +892,16 @@ public abstract class Component return getFlag(FLAG_INITIALIZED); } - /** - * THIS METHOD IS NOT PART OF THE PUBLIC API, DO NOT CALL IT - * - * Used to call {@link #onInitialize()} - */ - public void internalInitialize() + void completeHierarchy() { - fireInitialize(); } /** + * THIS METHOD IS NOT PART OF THE PUBLIC API, DO NOT CALL IT + * * Used to call {@link #onInitialize()} */ - final void fireInitialize() + public void internalInitialize() { if (!getFlag(FLAG_INITIALIZED)) { @@ -961,7 +957,7 @@ public abstract class Component /** * */ - private final void internalBeforeRender() + final void internalBeforeRender() { configure(); @@ -973,6 +969,9 @@ public abstract class Component getApplication().getComponentPreOnBeforeRenderListeners().onBeforeRender(this); onBeforeRender(); + + completeHierarchy(); + getApplication().getComponentPostOnBeforeRenderListeners().onBeforeRender(this); if (!getRequestFlag(RFLAG_BEFORE_RENDER_SUPER_CALL_VERIFIED)) @@ -1070,6 +1069,7 @@ public abstract class Component */ public final void configure() { + internalInitialize(); if (!getRequestFlag(RFLAG_CONFIGURED)) { clearEnabledInHierarchyCache(); http://git-wip-us.apache.org/repos/asf/wicket/blob/214c95ca/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..7b4a1ca 100644 --- a/wicket-core/src/main/java/org/apache/wicket/MarkupContainer.java +++ b/wicket-core/src/main/java/org/apache/wicket/MarkupContainer.java @@ -16,12 +16,14 @@ */ package org.apache.wicket; +import java.io.Serializable; import java.util.AbstractList; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; +import java.util.Stack; import org.apache.wicket.markup.ComponentTag; import org.apache.wicket.markup.IMarkupFragment; @@ -35,6 +37,7 @@ import org.apache.wicket.markup.MarkupType; import org.apache.wicket.markup.WicketTag; import org.apache.wicket.markup.html.border.Border; import org.apache.wicket.markup.html.internal.InlineEnclosure; +import org.apache.wicket.markup.repeater.AbstractRepeater; import org.apache.wicket.markup.resolver.ComponentResolvers; import org.apache.wicket.model.IComponentInheritedModel; import org.apache.wicket.model.IModel; @@ -96,6 +99,10 @@ public abstract class MarkupContainer extends Component implements Iterable<Comp { private static final long serialVersionUID = 1L; + private static MetaDataKey<ArrayList<Component>> QUEUE = new MetaDataKey<ArrayList<Component>>() + { + }; + /** Log for reporting. */ private static final Logger log = LoggerFactory.getLogger(MarkupContainer.class); @@ -118,6 +125,305 @@ public abstract class MarkupContainer extends Component implements Iterable<Comp super(id, model); } + 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; + } + + // TODO hierarchy-completion + public static class ResolvedMeta implements Serializable + { + private String tagComponentId; + private int index; + + public ResolvedMeta(MarkupStream stream) + { + tagComponentId = ((ComponentTag)stream.get()).getId(); + index = stream.getCurrentIndex(); + } + + public boolean isFor(MarkupStream stream) + { + if (stream.getCurrentIndex() != index) + { + return false; + } + if (!tagComponentId.equals(((ComponentTag)stream.get()).getId())) + { + return false; + } + return true; + } + } + + public static final MetaDataKey<ResolvedMeta> RESOLVED_KEY = new MetaDataKey<ResolvedMeta>() + { + + }; + + /** + * Completes component's hierarchy + * + * @see org.apache.wicket.Component#completeHierarchy() + */ + @Override + void completeHierarchy() + { + /* + * WARNING: THIS CODE IS EXTREMELY ROUGH AND DOES NOT FUNCTION IN THE SAME WAY THAT FINAL + * CODE WILL. IT IS HERE ONLY SO VARIOUS TESTS CAN BE WRITTEN IN HierarchyCompletionTest TO + * EXPLORE THIS IDEA + */ + + class ComponentAndTag + { + ComponentTag tag; + MarkupContainer component; + + public ComponentAndTag(ComponentTag tag, MarkupContainer component) + { + this.tag = tag; + this.component = component; + } + } + + MarkupStream markup = null; + + if (hasAssociatedMarkup()) + { + markup = new MarkupStream(getMarkupSourcingStrategy().getMarkup(this, null)); + // FIXME a hack to skip the first tag that does not resolve to any component such as + // wicket:panel or wicket:border + MarkupElement e = markup.get(); + if (e instanceof WicketTag) + { + if (((WicketTag)e).isMajorWicketComponentTag()) + { + markup.next(); + } + } + } + else if (getParent() instanceof AbstractRepeater) + { + markup = new MarkupStream(getParent().getMarkup()); + + // skip the repeater tag, we only want to traverse the body + markup.next(); + } + else + { + // we only complete the hierarchy of components with associated markup and direct + // children of repeaters. the rest of components should be inside the previous two + // types. + + return; + } + + // stack of components between the current tag in the markup and the markup's owner + Stack<ComponentAndTag> stack = new Stack<ComponentAndTag>(); + + // current component is the root of the stack. it has no component tag. + stack.push(new ComponentAndTag(null, this)); + + ComponentTag tag = null; + while (markup.hasMore()) + { + if (tag != null) + { + // advance the markup stream if this is not the first time through + markup.next(); + } + if (!markup.skipUntil(ComponentTag.class)) + { + // TODO error if stack is not empty + return; + } + + // the current markup tag + tag = (ComponentTag)markup.get(); + + + if (tag.isClose()) + { + if (stack.isEmpty()) + { + // we are now out of the markup owner's body markup, most likely on a + // </wicket:panel> or something similar, we are done + return; + } + stack.pop(); + continue; + } + + final MarkupContainer parent = stack.peek().component; + + // attempt to find a child component that corresponds to the markup tag + + // TODO this should be get or resolve so transparent things work + Component child = parent.get(tag.getId()); + if (child == null) + { + // try to deque a child component if one has not been found + + + for (int j = stack.size() - 1; j >= 0; j--) + { + // we try to find a queued component from the deepest nested parent all the way + // to the owner of the markup + ComponentAndTag cat = stack.get(j); + ArrayList<Component> queue = cat.component.getMetaData(QUEUE); + if (queue == null) + { + continue; + } + + for (Component queued : queue) + { + if (queued.getId().equals(tag.getId())) + { + child = queued; + break; + } + } + if (child != null) + { + queue.remove(child); + break; + } + } + } + + for (Component potential : parent) + { + ResolvedMeta meta = potential.getMetaData(RESOLVED_KEY); + if (meta != null) + { + int a = 2; + int b = a + 2; + } + if (meta != null && meta.isFor(markup)) + { + child = potential; + break; + } + } + + if (child == null) + { + // if we didnt find a queued child we could use try the resolvers + + child = ComponentResolvers.resolve(parent, markup, tag, null); + if (child != null) + { + // tag.setId(child.getId()); + // tag.setModified(true); + child.setMetaData(RESOLVED_KEY, new ResolvedMeta(markup)); + } + } + + if (child != null && child.getParent() == null) + { + lateAdd(parent, child); + // TODO do we need to continue unqueuing or can we skip this component + // and all its children if it has been deemed invisible? - dont think we can because + // that will leave components in the queue and ondetach() will bomb + } + + if (child != null && child.isAuto()) + { + // TODO this is yet another hack, need to figure out how auto components fit into + // this and why they dont get correctly resolved second time around + child.setAuto(false); + } + + if (child == null) + { + // cannot resolve child component, error + + // TODO make the mesage less queue-dependent. should be the same message that we + // throw during render when we cant resolve a child + + String error = "Could not dequeue or resolve child: `" + tag.getId() + "`. "; + error += "Parent search stack: ["; + for (int j = stack.size() - 1; j >= 0; j--) + { + if (j < stack.size() - 1) + { + error += ", "; + } + ComponentAndTag cat = stack.get(j); + error += cat.component.getClass().getSimpleName() + "('" + + cat.component.getId() + "')"; + } + error += "]"; + throw new WicketRuntimeException(error); + } + + if (tag.isOpenClose()) + { + // if this is an open/close tag we are done + continue; + } + + if (child instanceof AbstractRepeater) + { + // TODO hack for repeaters, this will be delegated to repeaters themselves later + + // skip inner markup, it will be processed by repeater items + markup.skipToMatchingCloseTag(tag); + } + else if (child instanceof MarkupContainer) + { + stack.push(new ComponentAndTag(tag, (MarkupContainer)child)); + } + else + { + // the child is not a container so we can skip its inner markup + markup.skipToMatchingCloseTag(tag); + } + } + } + + private void lateAdd(MarkupContainer parent, Component queued) + { + parent.add(queued); + + // at this point queued.onInitialize() wouldve been called by add() + + if (parent.isVisibleInHierarchy()) + { + // call configure() and onbeforerender() which are done from inside internalBeforeRender + queued.internalBeforeRender(); + } + } + /** * Adds a child component to this container. * @@ -973,19 +1279,6 @@ public abstract class MarkupContainer extends Component implements Iterable<Comp ComponentStrings.toString(child, new MarkupException("added"))); } - final Page page = findPage(); - if (page != null) - { - // tell the page a component has been added first, to allow it to initialize - page.componentAdded(child); - - // initialize the component - if (page.isInitialized()) - { - child.internalInitialize(); - } - } - // if the PREPARED_FOR_RENDER flag is set, we have already called // beforeRender on this component's children. So we need to initialize the newly added one if (isPreparedForRender()) @@ -995,28 +1288,6 @@ public abstract class MarkupContainer extends Component implements Iterable<Comp } /** - * THIS METHOD IS NOT PART OF THE PUBLIC API, DO NOT CALL IT - * - * Overrides {@link Component#internalInitialize()} to call {@link Component#fireInitialize()} - * for itself and for all its children. - * - * @see org.apache.wicket.Component#fireInitialize() - */ - @Override - public final void internalInitialize() - { - super.fireInitialize(); - visitChildren(new IVisitor<Component, Void>() - { - @Override - public void component(final Component component, final IVisit<Void> visit) - { - component.fireInitialize(); - } - }); - } - - /** * @param child * Child to add */ @@ -1426,6 +1697,18 @@ public abstract class MarkupContainer extends Component implements Iterable<Comp Component component = get(id); if (component == null) { + for (Component c : this) + { + ResolvedMeta meta = c.getMetaData(RESOLVED_KEY); + if (meta != null && meta.isFor(markupStream)) + { + component = c; + break; + } + } + } + if (component == null) + { component = ComponentResolvers.resolve(this, markupStream, tag, null); if ((component != null) && (component.getParent() == null)) { @@ -1647,6 +1930,28 @@ public abstract class MarkupContainer extends Component implements Iterable<Comp } @Override + protected void onDetach() + { + super.onDetach(); + detachQueue(); + } + + private void detachQueue() + { + List<Component> queue = getMetaData(QUEUE); + if (queue != null) + { + + if (!queue.isEmpty()) + { + // FIXME QUEUEING error message + throw new WicketRuntimeException("SOME COMPONENTS WERE NOT DEQUEUED"); + } + setMetaData(QUEUE, null); + } + } + + @Override void detachChildren() { super.detachChildren(); http://git-wip-us.apache.org/repos/asf/wicket/blob/214c95ca/wicket-core/src/test/java/org/apache/wicket/hierarchy/complete/HasPath.java ---------------------------------------------------------------------- diff --git a/wicket-core/src/test/java/org/apache/wicket/hierarchy/complete/HasPath.java b/wicket-core/src/test/java/org/apache/wicket/hierarchy/complete/HasPath.java new file mode 100755 index 0000000..a8ab2d7 --- /dev/null +++ b/wicket-core/src/test/java/org/apache/wicket/hierarchy/complete/HasPath.java @@ -0,0 +1,119 @@ +package org.apache.wicket.hierarchy.complete; + +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/214c95ca/wicket-core/src/test/java/org/apache/wicket/hierarchy/complete/HierarchyCompletionTest.java ---------------------------------------------------------------------- diff --git a/wicket-core/src/test/java/org/apache/wicket/hierarchy/complete/HierarchyCompletionTest.java b/wicket-core/src/test/java/org/apache/wicket/hierarchy/complete/HierarchyCompletionTest.java new file mode 100755 index 0000000..b966af0 --- /dev/null +++ b/wicket-core/src/test/java/org/apache/wicket/hierarchy/complete/HierarchyCompletionTest.java @@ -0,0 +1,515 @@ +/* + * 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.hierarchy.complete; + +import static org.apache.wicket.hierarchy.complete.WicketMatchers.hasPath; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.fail; + +import java.util.ArrayList; + +import org.apache.wicket.Component; +import org.apache.wicket.MarkupContainer; +import org.apache.wicket.WicketRuntimeException; +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.Rule; +import org.junit.Test; + +public class HierarchyCompletionTest +{ + @Rule + public TesterRule tester = new TesterRule(); + + + /** {@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); + 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.getTester().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()); + fail("Should not be able to queue two components with the same id under the same parent"); + } + catch (WicketRuntimeException e) + { + // expected + } + } + + @Test + public void resolveHeader1() + { + TestPage p = new TestPage(); + p.setPageMarkup("<html><head><wicket:head></wicket:head></head><body></body></html>"); + + tester.startPage(p); + } + + /** resolve header, then rerender the page instance after a callback is executed */ + @Test + public void resolveHeaderWithCallback() + { + TestPage p = new TestPage(); + p.setPageMarkup("<html><head><wicket:head><wicket:link><a href='Foo.html'>foo</a></wicket:link></wicket:head></head>" + + "<body><p wicket:id='a'><a wicket:id='l'><p wicket:id='b'></p></a></p></body></html>"); + 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.getTester().clickLink(l); + + assertThat(l.isClicked(), is(true)); + } + + @Test + public void resolveHeaderWithRepeatedRenderOfSameInstance() + { + TestPage p = new TestPage(); + p.setPageMarkup("<html><head><wicket:head><wicket:link><a href='Foo.html'>foo</a></wicket:link></wicket:head></head><body></body></html>"); + + tester.startPage(p); + tester.startPage(p); + } + + @Test + public void resolveHeaderWithRepeatedRenderOfNewInstances() + { + class MyPage extends TestPage + { + public MyPage() + { + setPageMarkup("<html><head><wicket:head><wicket:link><a href='Foo.html'>foo</a></wicket:link></wicket:head></head><body></body></html>"); + } + } + tester.startPage(new MyPage()); + tester.startPage(new MyPage()); + } + + + 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/214c95ca/wicket-core/src/test/java/org/apache/wicket/hierarchy/complete/IsParentOf.java ---------------------------------------------------------------------- diff --git a/wicket-core/src/test/java/org/apache/wicket/hierarchy/complete/IsParentOf.java b/wicket-core/src/test/java/org/apache/wicket/hierarchy/complete/IsParentOf.java new file mode 100755 index 0000000..ed978e6 --- /dev/null +++ b/wicket-core/src/test/java/org/apache/wicket/hierarchy/complete/IsParentOf.java @@ -0,0 +1,82 @@ +package org.apache.wicket.hierarchy.complete; + +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/214c95ca/wicket-core/src/test/java/org/apache/wicket/hierarchy/complete/Path.java ---------------------------------------------------------------------- diff --git a/wicket-core/src/test/java/org/apache/wicket/hierarchy/complete/Path.java b/wicket-core/src/test/java/org/apache/wicket/hierarchy/complete/Path.java new file mode 100755 index 0000000..8b25bd2 --- /dev/null +++ b/wicket-core/src/test/java/org/apache/wicket/hierarchy/complete/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.hierarchy.complete; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.apache.wicket.Component; +import org.apache.wicket.hierarchy.complete.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/214c95ca/wicket-core/src/test/java/org/apache/wicket/hierarchy/complete/TesterRule.java ---------------------------------------------------------------------- diff --git a/wicket-core/src/test/java/org/apache/wicket/hierarchy/complete/TesterRule.java b/wicket-core/src/test/java/org/apache/wicket/hierarchy/complete/TesterRule.java new file mode 100755 index 0000000..acc24cb --- /dev/null +++ b/wicket-core/src/test/java/org/apache/wicket/hierarchy/complete/TesterRule.java @@ -0,0 +1,96 @@ +/* + * 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.hierarchy.complete; + +import org.apache.wicket.Page; +import org.apache.wicket.mock.MockApplication; +import org.apache.wicket.protocol.http.WebApplication; +import org.apache.wicket.util.tester.WicketTester; +import org.junit.rules.MethodRule; +import org.junit.runners.model.FrameworkMethod; +import org.junit.runners.model.Statement; + +class TesterRule implements MethodRule +{ + private WebApplication application; + private WicketTester tester; + + public TesterRule() + { + } + + public WicketTester getTester() + { + return tester; + } + + public void startPage(Page page) + { + tester.startPage(page); + } + + public <T extends Page> void startPage(Class<T> page) + { + tester.startPage(page); + } + + + @Override + public Statement apply(final Statement base, FrameworkMethod method, Object target) + { + return new Statement() + { + @Override + public void evaluate() throws Throwable + { + WebApplication application = getApplication(); + if (application == null) + { + application = new MockApplication(); + } + tester = new WicketTester(application); + tester.setExposeExceptions(true); + try + { + base.evaluate(); + } + finally + { + tester.destroy(); + tester = null; + } + } + }; + } + + public WebApplication getApplication() + { + return application; + } + + public TesterRule setApplication(WebApplication application) + { + this.application = application; + return this; + } + + public Page getLastRenderedPage() + { + return tester.getLastRenderedPage(); + } + +} http://git-wip-us.apache.org/repos/asf/wicket/blob/214c95ca/wicket-core/src/test/java/org/apache/wicket/hierarchy/complete/WicketMatchers.java ---------------------------------------------------------------------- diff --git a/wicket-core/src/test/java/org/apache/wicket/hierarchy/complete/WicketMatchers.java b/wicket-core/src/test/java/org/apache/wicket/hierarchy/complete/WicketMatchers.java new file mode 100755 index 0000000..37d501e --- /dev/null +++ b/wicket-core/src/test/java/org/apache/wicket/hierarchy/complete/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.hierarchy.complete; + +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
