This is an automated email from the ASF dual-hosted git repository. jamesbognar pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/juneau.git
commit 99fc927e9bfb28cc11d777657be453dcccd9a190 Author: James Bognar <[email protected]> AuthorDate: Thu Aug 20 11:35:18 2026 -0400 TODO-445a: Add juneau-rest-server-widgets module with ActionBar, ActionRef, and SafeAction Extract a reusable widget toolkit so views can compose row-detail action bars without inlining markup. --- juneau-bom/pom.xml | 1 + juneau-rest/juneau-rest-server-widgets/pom.xml | 122 +++++++++++++++++++++ .../juneau/rest/server/widgets/ActionBar.java | 74 +++++++++++++ .../juneau/rest/server/widgets/ActionBarItem.java | 26 +++++ .../juneau/rest/server/widgets/ActionRef.java | 49 +++++++++ .../juneau/rest/server/widgets/SafeAction.java | 59 ++++++++++ .../apache/juneau/rest/server/widgets/Widget.java | 37 +++++++ .../juneau/rest/server/widgets/package-info.java | 35 ++++++ .../juneau/rest/server/widgets/ActionBar_Test.java | 78 +++++++++++++ juneau-rest/pom.xml | 1 + 10 files changed, 482 insertions(+) diff --git a/juneau-bom/pom.xml b/juneau-bom/pom.xml index 4c0468308c..8530277776 100644 --- a/juneau-bom/pom.xml +++ b/juneau-bom/pom.xml @@ -62,6 +62,7 @@ <dependency><groupId>org.apache.juneau</groupId><artifactId>juneau-rest-server-tracing-otel</artifactId><version>${project.version}</version></dependency> <dependency><groupId>org.apache.juneau</groupId><artifactId>juneau-rest-server-management-logging</artifactId><version>${project.version}</version></dependency> <dependency><groupId>org.apache.juneau</groupId><artifactId>juneau-rest-server-datatables</artifactId><version>${project.version}</version></dependency> + <dependency><groupId>org.apache.juneau</groupId><artifactId>juneau-rest-server-widgets</artifactId><version>${project.version}</version></dependency> <dependency><groupId>org.apache.juneau</groupId><artifactId>juneau-rest-server-views</artifactId><version>${project.version}</version></dependency> <dependency><groupId>org.apache.juneau</groupId><artifactId>juneau-rest-server-console-ui</artifactId><version>${project.version}</version></dependency> <dependency><groupId>org.apache.juneau</groupId><artifactId>juneau-rest-server-console-ui-freemarker</artifactId><version>${project.version}</version></dependency> diff --git a/juneau-rest/juneau-rest-server-widgets/pom.xml b/juneau-rest/juneau-rest-server-widgets/pom.xml new file mode 100644 index 0000000000..650890f2ab --- /dev/null +++ b/juneau-rest/juneau-rest-server-widgets/pom.xml @@ -0,0 +1,122 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + 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. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.apache.juneau</groupId> + <artifactId>juneau-rest</artifactId> + <version>10.0.0-SNAPSHOT</version> + </parent> + + <artifactId>juneau-rest-server-widgets</artifactId> + <name>Apache Juneau REST Server Widgets</name> + <description>Apache Juneau REST Server - reusable widget primitives (ActionBar and shared Widget contract). Table-specific types stay in juneau-rest-server-views; this module never depends on views.</description> + <packaging>bundle</packaging> + + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> + + <dependencies> + <dependency> + <groupId>org.apache.juneau</groupId> + <artifactId>juneau-commons</artifactId> + <version>${project.version}</version> + </dependency> + + <!-- Test scope --> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter</artifactId> + <version>${junit.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.juneau</groupId> + <artifactId>juneau-test-utils</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.juneau</groupId> + <artifactId>juneau-test</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-source-plugin</artifactId> + <executions> + <execution> + <id>attach-sources</id> + <phase>verify</phase> + <goals> + <goal>jar-no-fork</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <extensions>true</extensions> + <configuration> + <supportIncrementalBuild>true</supportIncrementalBuild> + </configuration> + <executions> + <execution> + <id>bundle-manifest</id> + <phase>process-classes</phase> + <goals> + <goal>manifest</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jar-plugin</artifactId> + </plugin> + <plugin> + <groupId>org.jacoco</groupId> + <artifactId>jacoco-maven-plugin</artifactId> + <executions> + <execution> + <id>default-prepare-agent</id> + <goals> + <goal>prepare-agent</goal> + </goals> + </execution> + <execution> + <id>default-report</id> + <phase>prepare-package</phase> + <goals> + <goal>report</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> diff --git a/juneau-rest/juneau-rest-server-widgets/src/main/java/org/apache/juneau/rest/server/widgets/ActionBar.java b/juneau-rest/juneau-rest-server-widgets/src/main/java/org/apache/juneau/rest/server/widgets/ActionBar.java new file mode 100644 index 0000000000..4f9f422b3f --- /dev/null +++ b/juneau-rest/juneau-rest-server-widgets/src/main/java/org/apache/juneau/rest/server/widgets/ActionBar.java @@ -0,0 +1,74 @@ +/* + * 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.juneau.rest.server.widgets; + +import static org.apache.juneau.commons.utils.Shorts.*; + +import java.util.*; + +/** + * An ordered bar of {@link ActionRef} ids and {@link SafeAction}s. + * + * <p> + * Holds ids only — it does <b>not</b> import any views-module write-action type. CSRF, confirm, and + * write-result handling stay on the enclosing view's action catalog. + * + * @since 10.0.0 + */ +public class ActionBar implements Widget { + + /** The frozen contract version for this widget. */ + public static final String CONTRACT_VERSION = "1"; + + /** The ordered items; omitted / empty means no bar. */ + public List<ActionBarItem> items; + + /** + * Creates an empty action bar. + * + * @return A new {@link ActionBar}. + */ + public static ActionBar create() { + return new ActionBar(); + } + + /** + * Sets the ordered items. + * + * @param value The items, in display order. Must not be <jk>null</jk>. + * @return This object. + */ + public ActionBar items(ActionBarItem...value) { + items = l(value); + return this; + } + + @Override /* Widget */ + public void validate() { + if (items == null) + return; + for (var item : items) { + if (item == null) + throw iaex("ActionBar item must not be null."); + if (item instanceof ActionRef ar) { + if (ar.id == null || ar.id.isBlank()) + throw iaex("ActionRef id must not be null or blank."); + } else if (!(item instanceof SafeAction)) + throw iaex("ActionBar item must be ActionRef or SafeAction."); + } + } +} diff --git a/juneau-rest/juneau-rest-server-widgets/src/main/java/org/apache/juneau/rest/server/widgets/ActionBarItem.java b/juneau-rest/juneau-rest-server-widgets/src/main/java/org/apache/juneau/rest/server/widgets/ActionBarItem.java new file mode 100644 index 0000000000..d2cf7c3555 --- /dev/null +++ b/juneau-rest/juneau-rest-server-widgets/src/main/java/org/apache/juneau/rest/server/widgets/ActionBarItem.java @@ -0,0 +1,26 @@ +/* + * 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.juneau.rest.server.widgets; + +/** + * One item in an {@link ActionBar}: either an {@link ActionRef} (opaque id naming a write action on the + * enclosing view) or a {@link SafeAction} (client-only, no endpoint). + * + * @since 10.0.0 + */ +public interface ActionBarItem { +} diff --git a/juneau-rest/juneau-rest-server-widgets/src/main/java/org/apache/juneau/rest/server/widgets/ActionRef.java b/juneau-rest/juneau-rest-server-widgets/src/main/java/org/apache/juneau/rest/server/widgets/ActionRef.java new file mode 100644 index 0000000000..b0755c6225 --- /dev/null +++ b/juneau-rest/juneau-rest-server-widgets/src/main/java/org/apache/juneau/rest/server/widgets/ActionRef.java @@ -0,0 +1,49 @@ +/* + * 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.juneau.rest.server.widgets; + +import static org.apache.juneau.commons.utils.Shorts.*; + +/** + * An opaque id naming a write action on the enclosing view. + * + * <p> + * This type deliberately does <b>not</b> import any views-module type. Existence of the named action on the + * enclosing view's action catalog is checked in views ({@code RowDetailDef}/{@code ViewDef.validate()}), never + * here. The bar does not grow its own write protocol (no endpoint / method / csrf fields). + * + * @since 10.0.0 + */ +public class ActionRef implements ActionBarItem { + + /** The opaque action id. Must not be blank. */ + public String id; + + /** + * Creates an action reference with the specified id. + * + * @param id The action id. Must not be <jk>null</jk> or blank. + * @return A new {@link ActionRef}. + */ + public static ActionRef of(String id) { + if (id == null || id.isBlank()) + throw iaex("ActionRef id must not be null or blank."); + var a = new ActionRef(); + a.id = id; + return a; + } +} diff --git a/juneau-rest/juneau-rest-server-widgets/src/main/java/org/apache/juneau/rest/server/widgets/SafeAction.java b/juneau-rest/juneau-rest-server-widgets/src/main/java/org/apache/juneau/rest/server/widgets/SafeAction.java new file mode 100644 index 0000000000..aa17e10c06 --- /dev/null +++ b/juneau-rest/juneau-rest-server-widgets/src/main/java/org/apache/juneau/rest/server/widgets/SafeAction.java @@ -0,0 +1,59 @@ +/* + * 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.juneau.rest.server.widgets; + +/** + * A client-only {@link ActionBar} item with no endpoint. + * + * <p> + * This slice ships {@link #COLLAPSE} (collapse the child row). Unlike an {@link ActionRef}, a safe action is + * enabled immediately — including during an in-flight expand GET and after a failed GET — so the + * user can dismiss a loading or error panel. + * + * @since 10.0.0 + */ +public enum SafeAction implements ActionBarItem { + + /** Collapse the expanded child row. */ + COLLAPSE("collapse", "Collapse"); + + private final String wire; + private final String label; + + SafeAction(String wire, String label) { + this.wire = wire; + this.label = label; + } + + /** + * Returns the wire token stamped onto {@code data-juneau-safe}. + * + * @return The wire token (e.g. {@code "collapse"}). + */ + public String wire() { + return wire; + } + + /** + * Returns the built-in button label, painted with {@code textContent}. + * + * @return The label. + */ + public String label() { + return label; + } +} diff --git a/juneau-rest/juneau-rest-server-widgets/src/main/java/org/apache/juneau/rest/server/widgets/Widget.java b/juneau-rest/juneau-rest-server-widgets/src/main/java/org/apache/juneau/rest/server/widgets/Widget.java new file mode 100644 index 0000000000..e4f5c18736 --- /dev/null +++ b/juneau-rest/juneau-rest-server-widgets/src/main/java/org/apache/juneau/rest/server/widgets/Widget.java @@ -0,0 +1,37 @@ +/* + * 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.juneau.rest.server.widgets; + +/** + * Marker for a reusable widget primitive. + * + * <p> + * Each concrete type owns a {@code public static final String CONTRACT_VERSION} (starting at {@code "1"}) and + * implements {@link #validate()} as a fail-closed bean-level check. Serving-path call sites (for example a + * table emitter) must invoke {@code validate()} rather than treating it as documentation-only. + * + * @since 10.0.0 + */ +public interface Widget { + + /** + * Fail-closed bean validation. + * + * @throws IllegalArgumentException If this widget is not well-formed. + */ + void validate(); +} diff --git a/juneau-rest/juneau-rest-server-widgets/src/main/java/org/apache/juneau/rest/server/widgets/package-info.java b/juneau-rest/juneau-rest-server-widgets/src/main/java/org/apache/juneau/rest/server/widgets/package-info.java new file mode 100644 index 0000000000..e84bd72f99 --- /dev/null +++ b/juneau-rest/juneau-rest-server-widgets/src/main/java/org/apache/juneau/rest/server/widgets/package-info.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. + */ + +/** + * Reusable widget primitives for the Juneau REST server toolkit. + * + * <p> + * This optional module holds the shared {@link org.apache.juneau.rest.server.widgets.Widget} marker and the first + * concrete primitive, {@link org.apache.juneau.rest.server.widgets.ActionBar}. Table-specific types + * (row-detail defs, row actions) stay in {@code juneau-rest-server-views}; this module has <b>no</b> dependency + * on views, so an {@link org.apache.juneau.rest.server.widgets.ActionBar} can never import a row-action type. + * + * <h5 class='section'>See Also:</h5> + * <ul> + * <li class='jc'>{@link org.apache.juneau.rest.server.widgets.Widget} + * <li class='jc'>{@link org.apache.juneau.rest.server.widgets.ActionBar} + * </ul> + * + * @since 10.0.0 + */ +package org.apache.juneau.rest.server.widgets; diff --git a/juneau-rest/juneau-rest-server-widgets/src/test/java/org/apache/juneau/rest/server/widgets/ActionBar_Test.java b/juneau-rest/juneau-rest-server-widgets/src/test/java/org/apache/juneau/rest/server/widgets/ActionBar_Test.java new file mode 100644 index 0000000000..8ad06c95cf --- /dev/null +++ b/juneau-rest/juneau-rest-server-widgets/src/test/java/org/apache/juneau/rest/server/widgets/ActionBar_Test.java @@ -0,0 +1,78 @@ +/* + * 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.juneau.rest.server.widgets; + +import static org.apache.juneau.test.bct.BctAssertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.file.*; + +import org.apache.juneau.*; +import org.junit.jupiter.api.*; + +/** + * {@link ActionBar} / {@link ActionRef} / {@link SafeAction} bean contract, including the views-module isolation + * rule (this module must never import {@code RowAction}). + */ +class ActionBar_Test extends TestBase { + + @Test void a01_contractVersion_isOne() { + assertEquals("1", ActionBar.CONTRACT_VERSION); + } + + @Test void a02_items_roundTrip() { + var bar = ActionBar.create().items(ActionRef.of("ack"), SafeAction.COLLAPSE); + assertSize(2, bar.items); + assertEquals("ack", ((ActionRef) bar.items.get(0)).id); + assertEquals(SafeAction.COLLAPSE, bar.items.get(1)); + bar.validate(); + } + + @Test void a03_blankActionRef_rejectedAtFactory() { + assertThrows(IllegalArgumentException.class, () -> ActionRef.of(null)); + assertThrows(IllegalArgumentException.class, () -> ActionRef.of(" ")); + } + + @Test void a04_validate_rejectsBlankActionRefId() { + var bar = ActionBar.create(); + var blank = new ActionRef(); + blank.id = " "; + bar.items = java.util.List.of(blank); + var e = assertThrows(IllegalArgumentException.class, bar::validate); + assertTrue(e.getMessage().contains("ActionRef"), e::getMessage); + } + + @Test void a05_safeAction_collapseWireAndLabel() { + assertEquals("collapse", SafeAction.COLLAPSE.wire()); + assertEquals("Collapse", SafeAction.COLLAPSE.label()); + } + + @Test void a06_sources_doNotImportRowAction() throws Exception { + var root = Path.of("").toAbsolutePath(); + var src = root; + if (!Files.isDirectory(src.resolve("src/main/java"))) + src = root.resolve("juneau-rest/juneau-rest-server-widgets"); + assertTrue(Files.isDirectory(src.resolve("src/main/java")), src::toString); + try (var walk = Files.walk(src.resolve("src/main/java"))) { + for (var f : walk.filter(p -> p.toString().endsWith(".java")).toList()) { + var text = Files.readString(f); + assertFalse(text.contains("import org.apache.juneau.rest.server.views.RowAction"), + () -> "widgets module must not import RowAction: " + f); + } + } + } +} diff --git a/juneau-rest/pom.xml b/juneau-rest/pom.xml index 47781c6716..0d8a964dc3 100644 --- a/juneau-rest/pom.xml +++ b/juneau-rest/pom.xml @@ -47,6 +47,7 @@ <module>juneau-rest-server-tracing-otel</module> <module>juneau-rest-server-management-logging</module> <module>juneau-rest-server-datatables</module> + <module>juneau-rest-server-widgets</module> <module>juneau-rest-server-views</module> <module>juneau-rest-server-views-markdown</module> <module>juneau-rest-server-console-ui</module>
