This is an automated email from the ASF dual-hosted git repository. dashorst pushed a commit to branch spike/builder-api in repository https://gitbox.apache.org/repos/asf/wicket.git
commit e572bc085a27c7b9ce2070b5ca04ba7c3156138c Author: Martijn Dashorst <[email protected]> AuthorDate: Fri Oct 23 16:16:46 2026 +0200 Add a builder API spike to the examples - Add a throwaway, marked SPIKE in the sources, builder vocabulary for components (ComponentBuilder, Builders, CoreUi, ExtensionsUi), with event handlers given as lambdas. - Rewrite the guestbook, todo list and editable label examples on that vocabulary as BuilderApplication pages, mounted at /guestbook, /todo and /editable-label. - Add BuilderSpikeTest. - Port build-versions.py to Python 3 and derive the module list from the module poms instead of hardcoding it. --- build-versions.py | 39 +- .../apache/wicket/examples/builder/BasePage.java | 33 ++ .../examples/builder/BuilderApplication.java | 42 +++ .../wicket/examples/builder/EditableLabelPage.html | 31 ++ .../wicket/examples/builder/EditableLabelPage.java | 57 +++ .../examples/builder/EditableLabelPage.properties | 16 + .../apache/wicket/examples/builder/GuestBook.html | 29 ++ .../apache/wicket/examples/builder/GuestBook.java | 71 ++++ .../wicket/examples/builder/GuestBook.properties | 16 + .../org/apache/wicket/examples/builder/Index.html | 6 + .../org/apache/wicket/examples/builder/Index.java | 32 ++ .../wicket/examples/builder/Index.properties | 15 + .../apache/wicket/examples/builder/TodoList.html | 28 ++ .../apache/wicket/examples/builder/TodoList.java | 93 +++++ .../wicket/examples/builder/TodoList.properties | 16 + .../org/apache/wicket/examples/builder/Ui.java | 27 ++ .../wicket/examples/builder/spike/Builders.java | 400 +++++++++++++++++++++ .../examples/builder/spike/ComponentBuilder.java | 112 ++++++ .../wicket/examples/builder/spike/CoreUi.java | 130 +++++++ .../wicket/examples/builder/spike/Scope.java | 85 +++++ .../spike/extensions/EditableLabelBuilder.java | 208 +++++++++++ .../builder/spike/extensions/ExtensionsUi.java | 44 +++ .../apache/wicket/examples/homepage/HomePage.html | 1 + wicket-examples/src/main/webapp/WEB-INF/web.xml | 16 + .../wicket/examples/builder/BuilderSpikeTest.java | 140 ++++++++ 25 files changed, 1667 insertions(+), 20 deletions(-) diff --git a/build-versions.py b/build-versions.py index 3f6ae085d2..d5a5e56df3 100755 --- a/build-versions.py +++ b/build-versions.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python3 # # prints a release.properties file for instructing the Maven Release Plugin # to generate the proper release artefacts without having to manually version @@ -13,16 +13,17 @@ # # Example: # -# build-milestone.py 7.0.0-M1 7.0.0-SNAPSHOT +# build-versions.py 7.0.0-M1 7.0.0-SNAPSHOT # +import os import sys -from xml.dom.minidom import parse +import xml.etree.ElementTree as ET -groupId = "org.apache.wicket" +NS = {"pom": "http://maven.apache.org/POM/4.0.0"} if len(sys.argv) != 3: - print "Usage: %s <release-version> <dev-version>" % sys.argv[0] + print("Usage: %s <release-version> <dev-version>" % sys.argv[0]) sys.exit(1) relVersion = sys.argv[1] @@ -31,27 +32,25 @@ devVersion = sys.argv[2] relVersions = [] devVersions = [] -def addVersions(groupId, module): - relVersions.append("project.rel." + groupId + "\\:" + module + "=" + relVersion) - devVersions.append("project.dev." + groupId + "\\:" + module + "=" + devVersion) +def addVersions(groupId, artifactId): + relVersions.append("project.rel." + groupId + "\\:" + artifactId + "=" + relVersion) + devVersions.append("project.dev." + groupId + "\\:" + artifactId + "=" + devVersion) -def getModulesFromParent(parentPomFile): - pom = parse(parentPomFile) +def addModule(directory): + pom = ET.parse(os.path.join(directory, "pom.xml")).getroot() - for moduleTag in pom.getElementsByTagName('module'): - module = moduleTag.childNodes[0].nodeValue.replace("testing/", "").replace("archetypes/quickstart", "wicket-archetype-quickstart") - addVersions(groupId, module) + groupId = pom.findtext("pom:groupId", namespaces=NS) or pom.findtext("pom:parent/pom:groupId", namespaces=NS) + addVersions(groupId, pom.findtext("pom:artifactId", namespaces=NS)) -addVersions(groupId, "wicket-parent") -addVersions("org.apache.wicket.experimental.wicket8", "wicket-experimental") + for module in pom.iterfind(".//pom:modules/pom:module", NS): + addModule(os.path.join(directory, module.text)) -getModulesFromParent("pom.xml") -getModulesFromParent("wicket-native-websocket/pom.xml") +addModule(".") for version in relVersions: - print version + print(version) -print +print() for version in devVersions: - print version + print(version) diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/builder/BasePage.java b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/BasePage.java new file mode 100644 index 0000000000..16586c3515 --- /dev/null +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/BasePage.java @@ -0,0 +1,33 @@ +/* + * 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.examples.builder; + +import org.apache.wicket.examples.WicketExamplePage; + +/** + * Base page of the builder spike. + */ +public abstract class BasePage extends WicketExamplePage implements Ui +{ + private static final long serialVersionUID = 1L; + + @Override + protected Class<? extends WicketExamplePage> getIndexPage() + { + return Index.class; + } +} diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/builder/BuilderApplication.java b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/BuilderApplication.java new file mode 100644 index 0000000000..fd88332dba --- /dev/null +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/BuilderApplication.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.examples.builder; + +import org.apache.wicket.Page; +import org.apache.wicket.examples.WicketExampleApplication; + +/** + * SPIKE - throwaway. Application for the builder API spike. + */ +public class BuilderApplication extends WicketExampleApplication +{ + @Override + protected void init() + { + super.init(); + + mountPage("guestbook", GuestBook.class); + mountPage("todo", TodoList.class); + mountPage("editable-label", EditableLabelPage.class); + } + + @Override + public Class<? extends Page> getHomePage() + { + return Index.class; + } +} diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/builder/EditableLabelPage.html b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/EditableLabelPage.html new file mode 100644 index 0000000000..2c17aa79e2 --- /dev/null +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/EditableLabelPage.html @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<wicket:extend xmlns:wicket="http://wicket.apache.org"> +<form wicket:id="form"> +<div wicket:id="feedback"></div> + +<p> +Click on the area with a green outline to begin an inplace edit. Press enter or outside the area to save, pless esc to cancel. +</p> + +<p> +The quick brown <span wicket:id="text1" class="inline-edit">[[editable label 1]]</span> jumped over the lazy <span wicket:id="text2" class="inline-edit">[[editable label 2]]</span>. +</p> + +<p> +click to edit multiple lines: <div wicket:id="text3" class="inline-edit">[[multiple lines]]</div> +</p> + +<p> +click to select another site: <span wicket:id="site" class="inline-edit">[[site selection]]</span> +</p> + +<p> +click to enter another number: <span wicket:id="number" class="inline-edit">[[number selection]]</span> +</p> + +<br/><br/> +This page has been refreshed: <span wicket:id="refresh-counter">[[refresh count]]</span> times. +<a href="#" wicket:id="refresh-link">refresh</a> +</form> + +</wicket:extend> \ No newline at end of file diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/builder/EditableLabelPage.java b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/EditableLabelPage.java new file mode 100644 index 0000000000..dd4f4bf358 --- /dev/null +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/EditableLabelPage.java @@ -0,0 +1,57 @@ +/* + * 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.examples.builder; + +import java.util.List; + +import org.apache.wicket.markup.html.panel.FeedbackPanel; +import org.apache.wicket.model.IModel; +import org.apache.wicket.model.Model; + +/** + * The in-place editors of {@link org.apache.wicket.examples.ajax.builtin.EditableLabelPage}, + * built. + */ +public class EditableLabelPage extends BasePage +{ + private static final long serialVersionUID = 1L; + + private static final List<String> SITES = List.of("The Server Side", "Java Lobby", + "Java.Net"); + + public EditableLabelPage() + { + IModel<Integer> refreshes = Model.of(0); + FeedbackPanel feedback = feedback("feedback").outputMarkupId().build(); + + add(form("form").body(form -> form + .add(feedback) + .add(editableLabel("text1", Model.of("fox")), + editableLabel("text2", Model.of("dog")), + editableMultiLineLabel("text3", Model.of("multiple\nlines of\ntextual content")), + editableChoiceLabel("site", Model.of(SITES.get(0)), SITES), + editableLabel("number", Model.of(42)) + .converter(converter(Integer::parseInt, number -> String.format("%010d", number))) + .apply(number -> number.setType(Integer.class) + .setRequired(true) + .setLabel(Model.of("Number"))) + .onSubmit(target -> target.add(feedback)) + .onError(target -> target.add(feedback)), + label("refresh-counter", refreshes), + link("refresh-link").onClick(() -> refreshes.setObject(refreshes.getObject() + 1))))); + } +} diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/builder/EditableLabelPage.properties b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/EditableLabelPage.properties new file mode 100644 index 0000000000..ba20ce7b9e --- /dev/null +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/EditableLabelPage.properties @@ -0,0 +1,16 @@ +# 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. +title=Editable labels, built +description=the in-place editors from wicket-extensions, reached through the extensions vocabulary next to the core one. diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/builder/GuestBook.html b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/GuestBook.html new file mode 100644 index 0000000000..79813fdcca --- /dev/null +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/GuestBook.html @@ -0,0 +1,29 @@ +<html xmlns:wicket="http://wicket.apache.org"> +<body> + <wicket:extend> + + <form wicket:id = "commentForm" id = "commentForm"> + Add your comment here: + <p> + <textarea wicket:id = "text">This is a comment</textarea> + </p> + <input type="text" wicket:id="comment" class="nospam" onfocus="getElementById('formsubmit').focus();"/> + <br /> + <input type = "submit" value = "Submit" id="formsubmit"/> + </form> + <p/> + <span wicket:id = "comments" id="comments"> + <p> + <span wicket:id="date">1/1/2004</span><br /> + <span wicket:id="text">Comment text goes here.</span> + </p> + </span> + <wicket:remove> + <p> + 1/2/2004<br/> + More comment text here. + </p> + </wicket:remove> +</wicket:extend> +</body> +</html> diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/builder/GuestBook.java b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/GuestBook.java new file mode 100644 index 0000000000..95a708aff6 --- /dev/null +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/GuestBook.java @@ -0,0 +1,71 @@ +/* + * 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.examples.builder; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import org.apache.wicket.examples.guestbook.Comment; +import org.apache.wicket.model.IModel; +import org.apache.wicket.model.Model; +import org.apache.wicket.util.string.Strings; +import org.apache.wicket.validation.IValidatable; +import org.apache.wicket.validation.ValidationError; + +/** + * The guestbook of {@link org.apache.wicket.examples.guestbook.GuestBook}, built. + */ +public class GuestBook extends BasePage +{ + private static final long serialVersionUID = 1L; + + private static final List<Comment> comments = new ArrayList<>(); + + public GuestBook() + { + IModel<Comment> draft = Model.of(new Comment()); + + add(form("commentForm", draft) + .body(form -> form.add( + textArea("text", form.bind(Comment::getText, Comment::setText)), + textField("comment", Model.<String> of()) + .apply(honeypot -> honeypot.add(GuestBook::rejectSpam)))) + .onSubmit(GuestBook::save), + list("comments", () -> comments) + .each(row -> row.add( + label("date", row.bind(Comment::getDate)), + multiLineLabel("text", row.bind(Comment::getText)))) + .apply(list -> list.setVersioned(false))); + } + + private static void save(Comment draft) + { + Comment comment = new Comment(draft); + comment.setDate(new Date()); + comments.add(0, comment); + draft.setText(""); + } + + private static void rejectSpam(IValidatable<String> honeypot) + { + if (!Strings.isEmpty(honeypot.getValue())) + { + honeypot.error(new ValidationError("Caught a spammer!!!")); + } + } +} diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/builder/GuestBook.properties b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/GuestBook.properties new file mode 100644 index 0000000000..b9c6cb65c3 --- /dev/null +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/GuestBook.properties @@ -0,0 +1,16 @@ +# 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. +title=Guestbook, built +description=the guestbook example rewritten with the builder vocabulary, binding form fields and list rows through method references. diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/builder/Index.html b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/Index.html new file mode 100644 index 0000000000..b300a7aff9 --- /dev/null +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/Index.html @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<wicket:extend xmlns:wicket="http://wicket.apache.org"> + +<div wicket:id="examples">[examples]</div> + +</wicket:extend> diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/builder/Index.java b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/Index.java new file mode 100644 index 0000000000..f8e179ba3a --- /dev/null +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/Index.java @@ -0,0 +1,32 @@ +/* + * 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.examples.builder; + +import org.apache.wicket.examples.ExampleIndexPanel; + +/** + * Lists the examples of the builder spike. + */ +public class Index extends BasePage +{ + private static final long serialVersionUID = 1L; + + public Index() + { + add(new ExampleIndexPanel("examples", Index.class)); + } +} diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/builder/Index.properties b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/Index.properties new file mode 100644 index 0000000000..917104b88d --- /dev/null +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/Index.properties @@ -0,0 +1,15 @@ +# 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. +title=Builder API Spike diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/builder/TodoList.html b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/TodoList.html new file mode 100644 index 0000000000..b2b6f737fe --- /dev/null +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/TodoList.html @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<html xmlns:wicket="http://wicket.apache.org"> +<head> +</head> +<body> + <wicket:extend> + <a href="#" wicket:id="ajaxback">Ajax Back</a> + <div wicket:id="showItems"> + <div class="item" wicket:id="item"> + <div class="itemCheck"><input type="checkbox" wicket:id="check" /></div> + <div class="itemText"><span wicket:id="text">item text</span></div> + </div> + </div> + + <div class="item"> + <div class="itemCheck"> </div> + <div class="itemText" wicket:id="addItems"> + <a href="#" wicket:id="link">New item</a>    <a href="#" wicket:id="remove">Remove completed</a> + <form wicket:id="form"> + <input type="text" wicket:id="text" /> + <input type="button" wicket:id="add" value="Add item" /> or + <input type="button" wicket:id="cancel" value="Cancel" /> + </form> + </div> + </div> + </wicket:extend> +</body> +</html> diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/builder/TodoList.java b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/TodoList.java new file mode 100644 index 0000000000..6f50009acd --- /dev/null +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/TodoList.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.examples.builder; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.wicket.examples.ajax.builtin.TodoList.TodoItem; +import org.apache.wicket.markup.head.CssHeaderItem; +import org.apache.wicket.markup.head.IHeaderResponse; +import org.apache.wicket.markup.html.WebMarkupContainer; +import org.apache.wicket.model.IModel; +import org.apache.wicket.model.Model; +import org.apache.wicket.request.resource.CssResourceReference; + +/** + * The todo list of {@link org.apache.wicket.examples.ajax.builtin.TodoList}, built. + */ +public class TodoList extends BasePage +{ + private static final long serialVersionUID = 1L; + + private final List<TodoItem> items = new ArrayList<>(); + + public TodoList() + { + IModel<Boolean> adding = Model.of(false); + IModel<Boolean> browsing = adding.map(a -> !a); + IModel<TodoItem> draft = Model.of(new TodoItem()); + + WebMarkupContainer showItems = container("showItems") + .outputMarkupId() + .body(container -> container.add(list("item", () -> items) + .each(row -> row.add( + ajaxCheckBox("check", row.bind(TodoItem::isChecked, TodoItem::setChecked)), + label("text", row.bind(TodoItem::getText)))))) + .build(); + add(showItems); + + add(ajaxLink("ajaxback").onClick(target -> setResponsePage(getPage())), + container("addItems") + .outputMarkupId() + .body(addItems -> addItems.add( + ajaxLink("link") + .visibleWhen(browsing) + .onClick(target -> { + adding.setObject(true); + target.add(addItems.self()); + }), + ajaxLink("remove") + .visibleWhen(browsing) + .onClick(target -> { + items.removeIf(TodoItem::isChecked); + target.add(addItems.self(), showItems); + }), + form("form", draft) + .visibleWhen(adding) + .body(form -> form.add( + textField("text", form.bind(TodoItem::getText, TodoItem::setText)), + ajaxButton("add").onSubmit(target -> { + items.add(new TodoItem(draft.getObject())); + draft.setObject(new TodoItem()); + adding.setObject(false); + target.add(addItems.self(), showItems); + }), + ajaxButton("cancel").onSubmit(target -> { + adding.setObject(false); + target.add(addItems.self()); + })))))); + } + + @Override + public void renderHead(IHeaderResponse response) + { + super.renderHead(response); + response.render(CssHeaderItem.forReference(new CssResourceReference( + org.apache.wicket.examples.ajax.builtin.TodoList.class, "todo.css"))); + } +} diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/builder/TodoList.properties b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/TodoList.properties new file mode 100644 index 0000000000..6078453043 --- /dev/null +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/TodoList.properties @@ -0,0 +1,16 @@ +# 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. +title=Todo list, built +description=the Ajax todo list rewritten with the builder vocabulary, with visibility driven by a model instead of overridden isVisible methods. diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/builder/Ui.java b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/Ui.java new file mode 100644 index 0000000000..f0478954e2 --- /dev/null +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/Ui.java @@ -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. + */ +package org.apache.wicket.examples.builder; + +import org.apache.wicket.examples.builder.spike.CoreUi; +import org.apache.wicket.examples.builder.spike.extensions.ExtensionsUi; + +/** + * The application's single entry point: every module it uses, combined. + */ +public interface Ui extends CoreUi, ExtensionsUi +{ +} diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/builder/spike/Builders.java b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/spike/Builders.java new file mode 100644 index 0000000000..087760a02d --- /dev/null +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/spike/Builders.java @@ -0,0 +1,400 @@ +/* + * 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.examples.builder.spike; + +import java.util.List; +import java.util.Locale; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.apache.wicket.MarkupContainer; +import org.apache.wicket.ajax.AjaxRequestTarget; +import org.apache.wicket.ajax.markup.html.AjaxLink; +import org.apache.wicket.ajax.markup.html.form.AjaxButton; +import org.apache.wicket.ajax.markup.html.form.AjaxCheckBox; +import org.apache.wicket.markup.html.WebMarkupContainer; +import org.apache.wicket.markup.html.form.Form; +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.model.IModel; +import org.apache.wicket.util.convert.ConversionException; +import org.apache.wicket.util.convert.IConverter; +import org.danekja.java.misc.serializable.SerializableRunnable; +import org.danekja.java.util.function.serializable.SerializableConsumer; +import org.danekja.java.util.function.serializable.SerializableFunction; + +/** + * SPIKE - throwaway. The builders behind {@link CoreUi}. + * <p> + * Every anonymous component subclass is created in a static method. An anonymous class created + * in an instance method keeps a reference to the builder, which would then be serialized with the + * page. + */ +public final class Builders +{ + private Builders() + { + } + + public static final class Simple<C extends org.apache.wicket.Component> + extends ComponentBuilder<C, Simple<C>> + { + private final Function<String, C> factory; + + public Simple(String id, Function<String, C> factory) + { + super(id); + this.factory = factory; + } + + @Override + protected C create(String id) + { + return factory.apply(id); + } + } + + public abstract static class AbstractContainerBuilder<C extends MarkupContainer, T, B extends AbstractContainerBuilder<C, T, B>> + extends ComponentBuilder<C, B> + { + private Consumer<Scope<T>> body = scope -> { + }; + + protected AbstractContainerBuilder(String id) + { + super(id); + } + + public B body(Consumer<Scope<T>> body) + { + this.body = body; + return self(); + } + + @Override + @SuppressWarnings("unchecked") + public C build() + { + C container = super.build(); + body.accept(new Scope<>(container, (IModel<T>)container.getDefaultModel())); + return container; + } + } + + public static final class ContainerBuilder<T> + extends AbstractContainerBuilder<WebMarkupContainer, T, ContainerBuilder<T>> + { + private final IModel<T> model; + + ContainerBuilder(String id, IModel<T> model) + { + super(id); + this.model = model; + } + + @Override + protected WebMarkupContainer create(String id) + { + return new WebMarkupContainer(id, model); + } + } + + public static final class FormBuilder<T> + extends AbstractContainerBuilder<Form<T>, T, FormBuilder<T>> + { + private final IModel<T> model; + + private SerializableConsumer<T> onSubmit = object -> { + }; + + FormBuilder(String id, IModel<T> model) + { + super(id); + this.model = model; + } + + public FormBuilder<T> onSubmit(SerializableConsumer<T> onSubmit) + { + this.onSubmit = onSubmit; + return this; + } + + @Override + protected Form<T> create(String id) + { + return newForm(id, model, onSubmit); + } + + private static <T> Form<T> newForm(String id, IModel<T> model, + SerializableConsumer<T> onSubmit) + { + return new Form<>(id, model) + { + private static final long serialVersionUID = 1L; + + @Override + protected void onSubmit() + { + onSubmit.accept(getModelObject()); + } + }; + } + } + + public static final class ListBuilder<T> extends ComponentBuilder<ListView<T>, ListBuilder<T>> + { + private final IModel<? extends List<T>> items; + + private SerializableConsumer<Scope<T>> each = row -> { + }; + + ListBuilder(String id, IModel<? extends List<T>> items) + { + super(id); + this.items = items; + } + + public ListBuilder<T> each(SerializableConsumer<Scope<T>> each) + { + this.each = each; + return this; + } + + @Override + protected ListView<T> create(String id) + { + return newListView(id, items, each); + } + + private static <T> ListView<T> newListView(String id, IModel<? extends List<T>> items, + SerializableConsumer<Scope<T>> each) + { + return new ListView<>(id, items) + { + private static final long serialVersionUID = 1L; + + @Override + protected void populateItem(ListItem<T> item) + { + each.accept(new Scope<>(item, item.getModel())); + } + }; + } + } + + public static final class LinkBuilder extends ComponentBuilder<Link<Void>, LinkBuilder> + { + private SerializableRunnable onClick = () -> { + }; + + LinkBuilder(String id) + { + super(id); + } + + public LinkBuilder onClick(SerializableRunnable onClick) + { + this.onClick = onClick; + return this; + } + + @Override + protected Link<Void> create(String id) + { + return newLink(id, onClick); + } + + private static Link<Void> newLink(String id, SerializableRunnable onClick) + { + return new Link<>(id) + { + private static final long serialVersionUID = 1L; + + @Override + public void onClick() + { + onClick.run(); + } + }; + } + } + + public static final class AjaxLinkBuilder extends ComponentBuilder<AjaxLink<Void>, AjaxLinkBuilder> + { + private SerializableConsumer<AjaxRequestTarget> onClick = target -> { + }; + + AjaxLinkBuilder(String id) + { + super(id); + } + + public AjaxLinkBuilder onClick(SerializableConsumer<AjaxRequestTarget> onClick) + { + this.onClick = onClick; + return this; + } + + @Override + protected AjaxLink<Void> create(String id) + { + return newAjaxLink(id, onClick); + } + + private static AjaxLink<Void> newAjaxLink(String id, + SerializableConsumer<AjaxRequestTarget> onClick) + { + return new AjaxLink<>(id) + { + private static final long serialVersionUID = 1L; + + @Override + public void onClick(AjaxRequestTarget target) + { + onClick.accept(target); + } + }; + } + } + + public static final class AjaxButtonBuilder extends ComponentBuilder<AjaxButton, AjaxButtonBuilder> + { + private SerializableConsumer<AjaxRequestTarget> onSubmit = target -> { + }; + + private SerializableConsumer<AjaxRequestTarget> onError = target -> { + }; + + AjaxButtonBuilder(String id) + { + super(id); + } + + public AjaxButtonBuilder onSubmit(SerializableConsumer<AjaxRequestTarget> onSubmit) + { + this.onSubmit = onSubmit; + return this; + } + + public AjaxButtonBuilder onError(SerializableConsumer<AjaxRequestTarget> onError) + { + this.onError = onError; + return this; + } + + @Override + protected AjaxButton create(String id) + { + return newAjaxButton(id, onSubmit, onError); + } + + private static AjaxButton newAjaxButton(String id, + SerializableConsumer<AjaxRequestTarget> onSubmit, + SerializableConsumer<AjaxRequestTarget> onError) + { + return new AjaxButton(id) + { + private static final long serialVersionUID = 1L; + + @Override + protected void onSubmit(AjaxRequestTarget target) + { + onSubmit.accept(target); + } + + @Override + protected void onError(AjaxRequestTarget target) + { + onError.accept(target); + } + }; + } + } + + public static final class AjaxCheckBoxBuilder + extends ComponentBuilder<AjaxCheckBox, AjaxCheckBoxBuilder> + { + private final IModel<Boolean> model; + + private SerializableConsumer<AjaxRequestTarget> onUpdate = target -> { + }; + + AjaxCheckBoxBuilder(String id, IModel<Boolean> model) + { + super(id); + this.model = model; + } + + public AjaxCheckBoxBuilder onUpdate(SerializableConsumer<AjaxRequestTarget> onUpdate) + { + this.onUpdate = onUpdate; + return this; + } + + @Override + protected AjaxCheckBox create(String id) + { + return newAjaxCheckBox(id, model, onUpdate); + } + + private static AjaxCheckBox newAjaxCheckBox(String id, IModel<Boolean> model, + SerializableConsumer<AjaxRequestTarget> onUpdate) + { + return new AjaxCheckBox(id, model) + { + private static final long serialVersionUID = 1L; + + @Override + protected void onUpdate(AjaxRequestTarget target) + { + onUpdate.accept(target); + } + }; + } + } + + static <T> IConverter<T> converter(SerializableFunction<String, T> parse, + SerializableFunction<T, String> format) + { + return new IConverter<>() + { + private static final long serialVersionUID = 1L; + + @Override + public T convertToObject(String value, Locale locale) throws ConversionException + { + try + { + return parse.apply(value); + } + catch (RuntimeException e) + { + throw new ConversionException(e).setSourceValue(value) + .setConverter(this) + .setLocale(locale); + } + } + + @Override + public String convertToString(T value, Locale locale) + { + return format.apply(value); + } + }; + } +} diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/builder/spike/ComponentBuilder.java b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/spike/ComponentBuilder.java new file mode 100644 index 0000000000..f803362af6 --- /dev/null +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/spike/ComponentBuilder.java @@ -0,0 +1,112 @@ +/* + * 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.examples.builder.spike; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import org.apache.wicket.Component; +import org.apache.wicket.behavior.Behavior; +import org.apache.wicket.model.IModel; +import org.apache.wicket.util.lang.Args; + +/** + * SPIKE - throwaway. Base of every builder: creates the component when it is added and then + * applies the configured steps in order. + * <p> + * A builder is used once, while the page is being constructed, and is never stored in the + * component tree. The steps are therefore plain, non-serializable consumers and may capture + * anything. Lambdas a builder hands to the component it creates, such as event handlers, are + * serializable and become part of the page. + * + * @param <C> + * the type of the component built + * @param <B> + * the type of the builder itself + */ +public abstract class ComponentBuilder<C extends Component, B extends ComponentBuilder<C, B>> +{ + private final String id; + + private final List<Consumer<? super C>> steps = new ArrayList<>(); + + protected ComponentBuilder(String id) + { + this.id = Args.notNull(id, "id"); + } + + protected abstract C create(String id); + + public B with(Behavior... behaviors) + { + return apply(component -> component.add(behaviors)); + } + + public B apply(Consumer<? super C> customizer) + { + steps.add(customizer); + return self(); + } + + public B outputMarkupId() + { + return apply(component -> component.setOutputMarkupId(true)); + } + + public B visibleWhen(IModel<Boolean> condition) + { + return with(new VisibleWhen(condition)); + } + + public C build() + { + C component = create(id); + steps.forEach(step -> step.accept(component)); + return component; + } + + @SuppressWarnings("unchecked") + protected final B self() + { + return (B)this; + } + + private static final class VisibleWhen extends Behavior + { + private static final long serialVersionUID = 1L; + + private final IModel<Boolean> condition; + + private VisibleWhen(IModel<Boolean> condition) + { + this.condition = Args.notNull(condition, "condition"); + } + + @Override + public void onConfigure(Component component) + { + component.setVisible(Boolean.TRUE.equals(condition.getObject())); + } + + @Override + public void detach(Component component) + { + condition.detach(); + } + } +} diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/builder/spike/CoreUi.java b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/spike/CoreUi.java new file mode 100644 index 0000000000..a23dff3688 --- /dev/null +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/spike/CoreUi.java @@ -0,0 +1,130 @@ +/* + * 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.examples.builder.spike; + +import java.util.List; + +import org.apache.wicket.MarkupContainer; +import org.apache.wicket.examples.builder.spike.Builders.AjaxButtonBuilder; +import org.apache.wicket.examples.builder.spike.Builders.AjaxCheckBoxBuilder; +import org.apache.wicket.examples.builder.spike.Builders.AjaxLinkBuilder; +import org.apache.wicket.examples.builder.spike.Builders.ContainerBuilder; +import org.apache.wicket.examples.builder.spike.Builders.FormBuilder; +import org.apache.wicket.examples.builder.spike.Builders.LinkBuilder; +import org.apache.wicket.examples.builder.spike.Builders.ListBuilder; +import org.apache.wicket.examples.builder.spike.Builders.Simple; +import org.apache.wicket.markup.html.basic.Label; +import org.apache.wicket.markup.html.basic.MultiLineLabel; +import org.apache.wicket.markup.html.form.TextArea; +import org.apache.wicket.markup.html.form.TextField; +import org.apache.wicket.markup.html.panel.FeedbackPanel; +import org.apache.wicket.model.IModel; +import org.apache.wicket.util.convert.IConverter; +import org.danekja.java.util.function.serializable.SerializableFunction; + +/** + * SPIKE - throwaway. The vocabulary wicket-core would ship: one entry point to its components, + * for a page or panel to implement, alone or combined with the vocabularies of other modules. + */ +public interface CoreUi +{ + default MarkupContainer add(ComponentBuilder<?, ?>... builders) + { + MarkupContainer container = (MarkupContainer)this; + for (ComponentBuilder<?, ?> builder : builders) + { + container.add(builder.build()); + } + return container; + } + + default Simple<Label> label(String id, IModel<?> model) + { + return new Simple<>(id, i -> new Label(i, model)); + } + + default Simple<MultiLineLabel> multiLineLabel(String id, IModel<?> model) + { + return new Simple<>(id, i -> new MultiLineLabel(i, model)); + } + + default <T> Simple<TextField<T>> textField(String id, IModel<T> model) + { + return new Simple<>(id, i -> new TextField<>(i, model)); + } + + default <T> Simple<TextArea<T>> textArea(String id, IModel<T> model) + { + return new Simple<>(id, i -> new TextArea<>(i, model)); + } + + default Simple<FeedbackPanel> feedback(String id) + { + return new Simple<>(id, FeedbackPanel::new); + } + + default ContainerBuilder<Void> container(String id) + { + return new ContainerBuilder<>(id, null); + } + + default <T> ContainerBuilder<T> container(String id, IModel<T> model) + { + return new ContainerBuilder<>(id, model); + } + + default FormBuilder<Void> form(String id) + { + return new FormBuilder<>(id, null); + } + + default <T> FormBuilder<T> form(String id, IModel<T> model) + { + return new FormBuilder<>(id, model); + } + + default <T> ListBuilder<T> list(String id, IModel<? extends List<T>> items) + { + return new ListBuilder<>(id, items); + } + + default LinkBuilder link(String id) + { + return new LinkBuilder(id); + } + + default AjaxLinkBuilder ajaxLink(String id) + { + return new AjaxLinkBuilder(id); + } + + default AjaxButtonBuilder ajaxButton(String id) + { + return new AjaxButtonBuilder(id); + } + + default AjaxCheckBoxBuilder ajaxCheckBox(String id, IModel<Boolean> model) + { + return new AjaxCheckBoxBuilder(id, model); + } + + default <T> IConverter<T> converter(SerializableFunction<String, T> parse, + SerializableFunction<T, String> format) + { + return Builders.converter(parse, format); + } +} diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/builder/spike/Scope.java b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/spike/Scope.java new file mode 100644 index 0000000000..1a8f4f176a --- /dev/null +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/spike/Scope.java @@ -0,0 +1,85 @@ +/* + * 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.examples.builder.spike; + +import org.apache.wicket.Component; +import org.apache.wicket.MarkupContainer; +import org.apache.wicket.model.IModel; +import org.apache.wicket.model.LambdaModel; +import org.apache.wicket.util.io.IClusterable; +import org.danekja.java.util.function.serializable.SerializableBiConsumer; +import org.danekja.java.util.function.serializable.SerializableFunction; + +/** + * SPIKE - throwaway. A container being filled, together with its model. + * <p> + * Children bind to the container's model with {@link #bind}: the getter and setter receive the + * model object as a parameter, so a method reference such as {@code Person::getName} captures + * nothing and the object itself stays in the model, where it is detached. + * + * @param <T> + * the type of the container's model object + */ +public final class Scope<T> implements IClusterable +{ + private static final long serialVersionUID = 1L; + + private final MarkupContainer container; + + private final IModel<T> model; + + Scope(MarkupContainer container, IModel<T> model) + { + this.container = container; + this.model = model; + } + + public MarkupContainer self() + { + return container; + } + + public IModel<T> model() + { + return model; + } + + public <R> IModel<R> bind(SerializableFunction<T, R> getter) + { + return model.map(getter); + } + + public <R> IModel<R> bind(SerializableFunction<T, R> getter, SerializableBiConsumer<T, R> setter) + { + return LambdaModel.of(model, getter, setter); + } + + public Scope<T> add(ComponentBuilder<?, ?>... builders) + { + for (ComponentBuilder<?, ?> builder : builders) + { + container.add(builder.build()); + } + return this; + } + + public Scope<T> add(Component... components) + { + container.add(components); + return this; + } +} diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/builder/spike/extensions/EditableLabelBuilder.java b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/spike/extensions/EditableLabelBuilder.java new file mode 100644 index 0000000000..1dc818e4d7 --- /dev/null +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/spike/extensions/EditableLabelBuilder.java @@ -0,0 +1,208 @@ +/* + * 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.examples.builder.spike.extensions; + +import java.util.List; + +import org.apache.wicket.ajax.AjaxRequestTarget; +import org.apache.wicket.examples.builder.spike.ComponentBuilder; +import org.apache.wicket.extensions.ajax.markup.html.AjaxEditableChoiceLabel; +import org.apache.wicket.extensions.ajax.markup.html.AjaxEditableLabel; +import org.apache.wicket.extensions.ajax.markup.html.AjaxEditableMultiLineLabel; +import org.apache.wicket.model.IModel; +import org.apache.wicket.util.convert.IConverter; +import org.apache.wicket.util.io.IClusterable; +import org.danekja.java.util.function.serializable.SerializableConsumer; + +/** + * SPIKE - throwaway. Builds the three in-place editors of wicket-extensions. + * <p> + * Each editor needs an anonymous subclass to hook into submit, error and conversion, so the hooks + * travel in a serializable {@link Hooks} object that the subclass consults. + * + * @param <T> + * the type of the edited value + */ +public final class EditableLabelBuilder<T> + extends ComponentBuilder<AjaxEditableLabel<T>, EditableLabelBuilder<T>> +{ + private enum Kind + { + SINGLE_LINE, MULTI_LINE, CHOICE + } + + private static final class Hooks<T> implements IClusterable + { + private static final long serialVersionUID = 1L; + + private SerializableConsumer<AjaxRequestTarget> onSubmit = target -> { + }; + + private SerializableConsumer<AjaxRequestTarget> onError = target -> { + }; + + private IConverter<T> converter; + } + + private final Kind kind; + + private final IModel<T> model; + + private final List<? extends T> choices; + + private final Hooks<T> hooks = new Hooks<>(); + + private EditableLabelBuilder(String id, Kind kind, IModel<T> model, List<? extends T> choices) + { + super(id); + this.kind = kind; + this.model = model; + this.choices = choices; + } + + static <T> EditableLabelBuilder<T> singleLine(String id, IModel<T> model) + { + return new EditableLabelBuilder<>(id, Kind.SINGLE_LINE, model, null); + } + + static <T> EditableLabelBuilder<T> multiLine(String id, IModel<T> model) + { + return new EditableLabelBuilder<>(id, Kind.MULTI_LINE, model, null); + } + + static <T> EditableLabelBuilder<T> choice(String id, IModel<T> model, + List<? extends T> choices) + { + return new EditableLabelBuilder<>(id, Kind.CHOICE, model, choices); + } + + public EditableLabelBuilder<T> onSubmit(SerializableConsumer<AjaxRequestTarget> onSubmit) + { + hooks.onSubmit = onSubmit; + return this; + } + + public EditableLabelBuilder<T> onError(SerializableConsumer<AjaxRequestTarget> onError) + { + hooks.onError = onError; + return this; + } + + public EditableLabelBuilder<T> converter(IConverter<T> converter) + { + hooks.converter = converter; + return this; + } + + @Override + protected AjaxEditableLabel<T> create(String id) + { + return switch (kind) + { + case SINGLE_LINE -> newSingleLine(id, model, hooks); + case MULTI_LINE -> newMultiLine(id, model, hooks); + case CHOICE -> newChoice(id, model, choices, hooks); + }; + } + + private static <T> AjaxEditableLabel<T> newSingleLine(String id, IModel<T> model, + Hooks<T> hooks) + { + return new AjaxEditableLabel<>(id, model) + { + private static final long serialVersionUID = 1L; + + @Override + protected void onSubmit(AjaxRequestTarget target) + { + super.onSubmit(target); + hooks.onSubmit.accept(target); + } + + @Override + protected void onError(AjaxRequestTarget target) + { + super.onError(target); + hooks.onError.accept(target); + } + + @Override + protected IConverter<?> createConverter(Class<?> type) + { + return hooks.converter != null ? hooks.converter : super.createConverter(type); + } + }; + } + + private static <T> AjaxEditableLabel<T> newMultiLine(String id, IModel<T> model, + Hooks<T> hooks) + { + return new AjaxEditableMultiLineLabel<>(id, model) + { + private static final long serialVersionUID = 1L; + + @Override + protected void onSubmit(AjaxRequestTarget target) + { + super.onSubmit(target); + hooks.onSubmit.accept(target); + } + + @Override + protected void onError(AjaxRequestTarget target) + { + super.onError(target); + hooks.onError.accept(target); + } + + @Override + protected IConverter<?> createConverter(Class<?> type) + { + return hooks.converter != null ? hooks.converter : super.createConverter(type); + } + }; + } + + private static <T> AjaxEditableLabel<T> newChoice(String id, IModel<T> model, + List<? extends T> choices, Hooks<T> hooks) + { + return new AjaxEditableChoiceLabel<>(id, model, choices) + { + private static final long serialVersionUID = 1L; + + @Override + protected void onSubmit(AjaxRequestTarget target) + { + super.onSubmit(target); + hooks.onSubmit.accept(target); + } + + @Override + protected void onError(AjaxRequestTarget target) + { + super.onError(target); + hooks.onError.accept(target); + } + + @Override + protected IConverter<?> createConverter(Class<?> type) + { + return hooks.converter != null ? hooks.converter : super.createConverter(type); + } + }; + } +} diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/builder/spike/extensions/ExtensionsUi.java b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/spike/extensions/ExtensionsUi.java new file mode 100644 index 0000000000..9620c5d4a4 --- /dev/null +++ b/wicket-examples/src/main/java/org/apache/wicket/examples/builder/spike/extensions/ExtensionsUi.java @@ -0,0 +1,44 @@ +/* + * 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.examples.builder.spike.extensions; + +import java.util.List; + +import org.apache.wicket.model.IModel; + +/** + * SPIKE - throwaway. The vocabulary wicket-extensions would ship. Core does not know it exists; an + * application combines the two in an interface of its own. + */ +public interface ExtensionsUi +{ + default <T> EditableLabelBuilder<T> editableLabel(String id, IModel<T> model) + { + return EditableLabelBuilder.singleLine(id, model); + } + + default <T> EditableLabelBuilder<T> editableMultiLineLabel(String id, IModel<T> model) + { + return EditableLabelBuilder.multiLine(id, model); + } + + default <T> EditableLabelBuilder<T> editableChoiceLabel(String id, IModel<T> model, + List<? extends T> choices) + { + return EditableLabelBuilder.choice(id, model, choices); + } +} diff --git a/wicket-examples/src/main/resources/org/apache/wicket/examples/homepage/HomePage.html b/wicket-examples/src/main/resources/org/apache/wicket/examples/homepage/HomePage.html index f5fe22d008..17224ff845 100644 --- a/wicket-examples/src/main/resources/org/apache/wicket/examples/homepage/HomePage.html +++ b/wicket-examples/src/main/resources/org/apache/wicket/examples/homepage/HomePage.html @@ -30,6 +30,7 @@ <tr><td align="right"><a href="captchas">Captcha</a></td><td> - Telling humans from spammers.</td></tr> <tr><td align="right"><a href="datetime">Dates and times</a></td><td> - Date components example.</td></tr> + <tr class="section"><td align="right"><a href="builder">Builder API spike</a></td><td> - Guestbook, todo list and editable labels rewritten with a fluent builder vocabulary.</td></tr> <tr class="section"><td align="right"><a href="guestbook">Guestbook</a></td><td> - A blog-like multi-user guestbook.</td></tr> <tr><td align="right"><a href="hangman">Hangman</a></td><td> - The game of hangman.</td></tr> <tr><td align="right"><a href="library">Library</a></td><td> - A simple application.</td></tr> diff --git a/wicket-examples/src/main/webapp/WEB-INF/web.xml b/wicket-examples/src/main/webapp/WEB-INF/web.xml index c11a872f60..9d1a34d176 100644 --- a/wicket-examples/src/main/webapp/WEB-INF/web.xml +++ b/wicket-examples/src/main/webapp/WEB-INF/web.xml @@ -149,6 +149,15 @@ </init-param> </filter> + <filter> + <filter-name>BuilderApplication</filter-name> + <filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class> + <init-param> + <param-name>applicationClassName</param-name> + <param-value>org.apache.wicket.examples.builder.BuilderApplication</param-value> + </init-param> + </filter> + <filter> <filter-name>GuestBookApplication</filter-name> <filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class> @@ -627,6 +636,13 @@ <dispatcher>INCLUDE</dispatcher> </filter-mapping> + <filter-mapping> + <filter-name>BuilderApplication</filter-name> + <url-pattern>/builder/*</url-pattern> + <dispatcher>REQUEST</dispatcher> + <dispatcher>INCLUDE</dispatcher> + </filter-mapping> + <filter-mapping> <filter-name>GuestBookApplication</filter-name> <url-pattern>/guestbook/*</url-pattern> diff --git a/wicket-examples/src/test/java/org/apache/wicket/examples/builder/BuilderSpikeTest.java b/wicket-examples/src/test/java/org/apache/wicket/examples/builder/BuilderSpikeTest.java new file mode 100644 index 0000000000..f149d123cf --- /dev/null +++ b/wicket-examples/src/test/java/org/apache/wicket/examples/builder/BuilderSpikeTest.java @@ -0,0 +1,140 @@ +/* + * 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.examples.builder; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; + +import org.apache.wicket.Page; +import org.apache.wicket.ajax.markup.html.form.AjaxCheckBox; +import org.apache.wicket.core.util.objects.checker.AbstractObjectChecker; +import org.apache.wicket.core.util.objects.checker.CheckingObjectOutputStream; +import org.apache.wicket.core.util.objects.checker.NotDetachedModelChecker; +import org.apache.wicket.core.util.objects.checker.ObjectSerializationChecker; +import org.apache.wicket.examples.builder.spike.ComponentBuilder; +import org.apache.wicket.util.tester.FormTester; +import org.apache.wicket.util.tester.WicketTestCase; +import org.junit.jupiter.api.Test; + +/** + * SPIKE - throwaway. Drives the built pages and reports how they serialize next to the originals. + */ +class BuilderSpikeTest extends WicketTestCase +{ + @Test + void guestBook() throws IOException + { + tester.startPage(GuestBook.class); + FormTester form = tester.newFormTester("commentForm"); + form.setValue("text", "first"); + form.submit(); + tester.assertModelValue("comments:0:text", "first"); + + form = tester.newFormTester("commentForm"); + form.setValue("text", "spam"); + form.setValue("comment", "filled"); + form.submit(); + tester.assertErrorMessages("Caught a spammer!!!"); + tester.assertModelValue("comments:0:text", "first"); + + check(tester.getLastRenderedPage()); + tester.startPage(org.apache.wicket.examples.guestbook.GuestBook.class); + report("GuestBook", tester.getLastRenderedPage(), GuestBook.class); + } + + @Test + void todoList() throws IOException + { + tester.startPage(TodoList.class); + tester.assertInvisible("addItems:form"); + + tester.clickLink("addItems:link", true); + tester.assertVisible("addItems:form"); + tester.assertInvisible("addItems:link"); + + FormTester form = tester.newFormTester("addItems:form"); + form.setValue("text", "write the spike"); + tester.executeAjaxEvent("addItems:form:add", "click"); + tester.assertModelValue("showItems:item:0:text", "write the spike"); + tester.assertInvisible("addItems:form"); + + AjaxCheckBox check = (AjaxCheckBox)tester.getComponentFromLastRenderedPage( + "showItems:item:0:check"); + check.setModelObject(true); + tester.clickLink("addItems:remove", true); + tester.assertModelValue("showItems:item", List.of()); + + check(tester.getLastRenderedPage()); + tester.startPage(org.apache.wicket.examples.ajax.builtin.TodoList.class); + report("TodoList", tester.getLastRenderedPage(), TodoList.class); + } + + @Test + void editableLabels() throws IOException + { + tester.startPage(EditableLabelPage.class); + tester.assertContains("0000000042"); + + tester.clickLink("form:refresh-link"); + tester.assertLabel("form:refresh-counter", "1"); + + check(tester.getLastRenderedPage()); + tester.startPage(org.apache.wicket.examples.ajax.builtin.EditableLabelPage.class); + report("EditableLabelPage", tester.getLastRenderedPage(), EditableLabelPage.class); + } + + private void check(Page page) throws IOException + { + page.detach(); + try (CheckingObjectOutputStream out = new CheckingObjectOutputStream( + new ByteArrayOutputStream(), new ObjectSerializationChecker(), + new NotDetachedModelChecker(), new NoBuilderChecker())) + { + out.writeObject(page); + } + } + + private void report(String name, Page original, Class<? extends Page> built) + { + Page builtPage = tester.startPage(built); + byte[] originalBytes = serialize(original); + byte[] builtBytes = serialize(builtPage); + System.out.printf("SPIKE %-18s original %6d bytes, built %6d bytes%n", name, + originalBytes.length, builtBytes.length); + assertTrue(builtBytes.length > 0); + } + + private static final class NoBuilderChecker extends AbstractObjectChecker + { + @Override + protected Result doCheck(Object object) + { + return object instanceof ComponentBuilder + ? new Result(Result.Status.FAILURE, "a builder ended up in the page") + : Result.SUCCESS; + } + } + + private byte[] serialize(Page page) + { + page.detach(); + return tester.getApplication().getFrameworkSettings().getSerializer().serialize(page); + } +}
