Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9683cf8
Add declarative router, deep-link API, and bytecode annotation framework
shai-almog May 24, 2026
d84233a
Make cn1-router-history.js a no-op in non-DOM contexts
shai-almog May 24, 2026
eac9191
Drop link: cross-references that broke the website lychee check
shai-almog May 24, 2026
29d113f
Fix developer-guide quality gates for the new routing docs
shai-almog May 24, 2026
1f277e3
RouteMatch: use com.codename1.util.regex.RE instead of java.util.regex
shai-almog May 24, 2026
a21fcd2
Fix forbidden PMD violations and strip non-ASCII chars from routing PR
shai-almog May 24, 2026
accff3f
Expand one-line braced bodies to satisfy Checkstyle LeftCurly rule
shai-almog May 24, 2026
720e83c
Use Codename One copyright header and drop Since tags on new routing …
shai-almog May 25, 2026
ddefce3
Merge master into feat/declarative-router-and-deep-links
shai-almog May 25, 2026
e780289
Put the JS shim in Ports/JavaScriptPort/src/main/webapp where it belongs
shai-almog May 25, 2026
edc86f8
Reduce public API to @Route + @RouteParam, build-time-generated dispa…
shai-almog May 25, 2026
28d1863
Add classloader diagnostics to RouteAnnotationProcessorTest (temp)
shai-almog May 25, 2026
f6f5a42
Merge master into feat/declarative-router-and-deep-links
shai-almog May 25, 2026
79b4308
Add Navigation API: in-app routing on top of @Route table
shai-almog May 25, 2026
76cf9e5
Compile @Route fixtures against real cn1-core via surefire test class…
shai-almog May 25, 2026
8052c48
Avoid SpotBugs DE_MIGHT_IGNORE in JavaSourceCompiler URL->File fallback
shai-almog May 25, 2026
d96b786
Clear PMD forbidden-rule hits on routing PR
shai-almog May 25, 2026
f77604d
Stop shipping the Routes stub in cn1-core; load it reflectively
shai-almog May 26, 2026
820ecd1
Use Class.newInstance for Routes bootstrap (CLDC11 lacks getMethod)
shai-almog May 26, 2026
fe7f66c
Satisfy Vale on the routing-doc paragraph added in the previous commit
shai-almog May 26, 2026
7de8fbf
Bind Routes via the per-build application stub, not Class.forName
shai-almog May 26, 2026
f6caa2d
Wire annotation Mojo into archetype + simulator dynamic Route load
shai-almog May 26, 2026
2770cdb
Reword routing-doc sentence to satisfy LanguageTool ATD_VERBS_TO_COLL…
shai-almog May 26, 2026
bf3aed7
Skip Routes stub binding for legacy projects without the annotation Mojo
shai-almog May 26, 2026
d84438d
Detect project Routes via sourceZip, not the post-unzip classes dir
shai-almog May 26, 2026
aeb1afb
Invalidate cached cn1-built artifact when maven plugin source changes
shai-almog May 26, 2026
5c04763
Route plumbing cleanup per review
shai-almog May 26, 2026
ff1f1a7
Drop generate-annotation-stubs Mojo from project poms; revert initializr
shai-almog May 26, 2026
0269486
Merge remote-tracking branch 'origin/master' into feat/declarative-ro…
shai-almog May 26, 2026
772dfc3
Lift Routes-binding helper into Executor; drop per-subclass duplicates
shai-almog May 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/_build-ios-port.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ jobs:
run: |
set -euo pipefail
SRC_HASH=$(find CodenameOne/src Ports/iOSPort vm/JavaAPI vm/ByteCodeTranslator Themes native-themes \
maven/codenameone-maven-plugin/src/main \
-type f \( -name '*.java' -o -name '*.m' -o -name '*.h' -o -name '*.xml' -o -name '*.properties' -o -name '*.css' \) 2>/dev/null \
| sort | xargs shasum -a 256 | shasum -a 256 | awk '{print $1}')
POM_HASH=$(find . -name 'pom.xml' -not -path './scripts/*' 2>/dev/null \
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/input-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ jobs:
run: |
set -euo pipefail
SRC_HASH=$(find CodenameOne/src Ports/iOSPort vm/JavaAPI vm/ByteCodeTranslator Themes native-themes \
maven/codenameone-maven-plugin/src/main \
-type f \( -name '*.java' -o -name '*.m' -o -name '*.h' -o -name '*.xml' -o -name '*.properties' -o -name '*.css' \) 2>/dev/null \
| sort | xargs shasum -a 256 | shasum -a 256 | awk '{print $1}')
POM_HASH=$(find . -name 'pom.xml' -not -path './scripts/*' 2>/dev/null \
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ios-packaging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ jobs:
run: |
set -euo pipefail
SRC_HASH=$(find CodenameOne/src Ports/iOSPort vm/JavaAPI vm/ByteCodeTranslator Themes native-themes \
maven/codenameone-maven-plugin/src/main \
-type f \( -name '*.java' -o -name '*.m' -o -name '*.h' -o -name '*.xml' -o -name '*.properties' -o -name '*.css' \) 2>/dev/null \
| sort | xargs shasum -a 256 | shasum -a 256 | awk '{print $1}')
POM_HASH=$(find . -name 'pom.xml' -not -path './scripts/*' 2>/dev/null \
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/scripts-ios-native.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ jobs:
run: |
set -euo pipefail
SRC_HASH=$(find CodenameOne/src Ports/iOSPort vm/JavaAPI vm/ByteCodeTranslator Themes native-themes \
maven/codenameone-maven-plugin/src/main \
-type f \( -name '*.java' -o -name '*.m' -o -name '*.h' -o -name '*.xml' -o -name '*.properties' -o -name '*.css' \) 2>/dev/null \
| sort | xargs shasum -a 256 | shasum -a 256 | awk '{print $1}')
POM_HASH=$(find . -name 'pom.xml' -not -path './scripts/*' 2>/dev/null \
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/scripts-ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ jobs:
run: |
set -euo pipefail
SRC_HASH=$(find CodenameOne/src Ports/iOSPort vm/JavaAPI vm/ByteCodeTranslator Themes native-themes \
maven/codenameone-maven-plugin/src/main \
-type f \( -name '*.java' -o -name '*.m' -o -name '*.h' -o -name '*.xml' -o -name '*.properties' -o -name '*.css' \) 2>/dev/null \
| sort | xargs shasum -a 256 | shasum -a 256 | awk '{print $1}')
POM_HASH=$(find . -name 'pom.xml' -not -path './scripts/*' 2>/dev/null \
Expand Down Expand Up @@ -263,6 +264,7 @@ jobs:
run: |
set -euo pipefail
SRC_HASH=$(find CodenameOne/src Ports/iOSPort vm/JavaAPI vm/ByteCodeTranslator Themes native-themes \
maven/codenameone-maven-plugin/src/main \
-type f \( -name '*.java' -o -name '*.m' -o -name '*.h' -o -name '*.xml' -o -name '*.properties' -o -name '*.css' \) 2>/dev/null \
| sort | xargs shasum -a 256 | shasum -a 256 | awk '{print $1}')
POM_HASH=$(find . -name 'pom.xml' -not -path './scripts/*' 2>/dev/null \
Expand Down
88 changes: 88 additions & 0 deletions CodenameOne/src/com/codename1/annotations/Route.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/// Binds a `Form` class -- or a static method that returns a `Form` -- to a
/// URL path so the framework can show it in response to a deep link.
///
/// `@Route` is the only annotation an application needs in order to make a
/// form reachable from a Universal Link, an Android App Link, a custom-scheme
/// URL, a push-notification payload, or any other URL the platform delivers
/// to the app. Path variables flow into constructor or method parameters
/// through `RouteParam`.
///
/// ```java
/// @Route("/users/:id")
/// public class ProfileForm extends Form {
/// public ProfileForm(@RouteParam("id") String id) { ... }
/// }
///
/// public class Routes {
/// @Route("/home")
/// public static Form home() {
/// return new HomeForm();
/// }
///
/// @Route("/users/:id")
/// public static Form profile(@RouteParam("id") String id) {
/// return new ProfileForm(id);
/// }
/// }
/// ```
///
/// **At build time** the Codename One Maven plugin scans the project's
/// compiled bytecode, validates every `@Route` (extends `Form`, accessible
/// constructor or static factory, no duplicate patterns, every parameter
/// bound), then generates an internal dispatch class that the framework wires
/// to the platform's deep-link plumbing under the hood. There is no
/// reflection at runtime and no router API for the application to call --
/// `new MyForm().show()` is still the way to navigate inside the app; URL
/// routing only handles links coming from outside.
///
/// #### Path syntax
///
/// - **Literal segments** -- `/about`
/// - **Named parameters** -- `/users/:id`, bound via `@RouteParam("id")`
/// - **Single-segment wildcard** -- `/files/*`
/// - **Catch-all wildcard** -- `/files/**`
@Retention(RetentionPolicy.CLASS)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface Route {

/// The path pattern. Always starts with `/`. Required.
String value();

/// Container annotation for binding several path patterns to the same
/// target. Use it when a single Form should be reachable from multiple
/// URLs without repeating the body.
@Retention(RetentionPolicy.CLASS)
@Target({ ElementType.TYPE, ElementType.METHOD })
@interface Routes {
Route[] value();
}
}
61 changes: 61 additions & 0 deletions CodenameOne/src/com/codename1/annotations/RouteParam.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/// Binds a constructor or static-factory parameter to a path variable or query
/// parameter from an incoming deep link.
///
/// Used together with `Route`. The build-time route processor inspects each
/// annotated parameter and generates dispatch code that pulls the value out of
/// the matched URL before invoking the constructor or factory.
///
/// ```java
/// @Route("/users/:id")
/// public class ProfileForm extends Form {
/// public ProfileForm(@RouteParam("id") String id) { ... }
/// }
///
/// @Route("/search")
/// public static Form search(@RouteParam("q") String query,
/// @RouteParam(value = "page", required = false) String page) { ... }
/// ```
///
/// The `value` is matched first against named path variables (`:name`) and then
/// against query-string keys. The annotation is required on every parameter the
/// framework should bind; unannotated parameters are an error at build time.
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.PARAMETER)
public @interface RouteParam {

/// The name of the path variable or query parameter to bind. Required.
String value();

/// When true (the default) the build fails if the deep link cannot supply a
/// value. When false a missing value is passed in as null.
boolean required() default true;
}
193 changes: 193 additions & 0 deletions CodenameOne/src/com/codename1/router/Navigation.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.router;

import com.codename1.ui.Display;
import com.codename1.ui.Form;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/// In-app navigation API on top of the declarative `@Route` table.
///
/// `Navigation` is the imperative counterpart to the `Route` annotation:
/// declare your forms with `@Route("/users/:id")` once, then trigger
/// navigation from anywhere with `Navigation.navigate("/users/42")`. The same
/// route table that handles deep links is reused, so there is exactly one
/// place that knows how `/users/:id` maps to a form.
///
/// The class also exposes the navigation stack so applications can render
/// breadcrumb UIs without maintaining a parallel history:
///
/// ```java
/// Container breadcrumbs = new Container(BoxLayout.x());
/// for (final NavigationEntry e : Navigation.getStack()) {
/// Button crumb = new Button(e.getTitle());
/// crumb.addActionListener(evt -> Navigation.popTo(e));
/// breadcrumbs.add(crumb);
/// }
/// ```
///
/// The surface is intentionally tiny -- five static methods and one value
/// type. Applications that prefer raw `Form#show` / `Form#showBack` keep
/// working unchanged; the `Navigation` stack only records URL-driven
/// navigations.
///
/// All methods must be called on the EDT.
public final class Navigation {

private static RouteDispatcher dispatcher;
private static final List<NavigationEntry> stack = new ArrayList<NavigationEntry>();

private Navigation() {
}

// ------------------------------------------------------------------------
// Internal: dispatcher installation
// ------------------------------------------------------------------------

/// Installs the build-time-generated route dispatcher. Invoked once by
/// `com.codename1.router.generated.Routes#bootstrap` during framework
/// initialization. Application code should not call this.
public static void setDispatcher(RouteDispatcher d) {
dispatcher = d;
}

// ------------------------------------------------------------------------
// Public API
// ------------------------------------------------------------------------

/// Navigate to a path. Looks the URL up in the route table generated from
/// `@Route` annotations, builds the matching `Form`, pushes it onto the
/// navigation stack, and shows it.
///
/// Accepts either a bare path (`/users/42`), a full URL with scheme +
/// host (`https://example.com/users/42`), or a custom-scheme URL. Scheme
/// and host are ignored -- only the path + query are matched.
///
/// Returns `true` when a route matched and the form was shown, `false`
/// when no route matched.
public static boolean navigate(String path) {
RouteDispatcher d = dispatcher;
if (d == null || path == null) {
return false;
}
Form f;
try {
f = d.dispatch(path);
} catch (Throwable t) {
com.codename1.io.Log.e(t);
return false;
}
if (f == null) {
return false;
}
stack.add(new NavigationEntry(path, f));
f.show();
return true;
}

/// Pop the top entry off the navigation stack and return to the previous
/// one. Uses `Form#showBack` so the transition runs in reverse. Returns
/// `true` when a frame was popped, `false` when the stack had at most one
/// entry (already at the root, nothing to go back to).
public static boolean back() {
if (stack.size() <= 1) {
return false;
}
stack.remove(stack.size() - 1);
NavigationEntry now = stack.get(stack.size() - 1);
now.getForm().showBack();
return true;
}

/// The current entry (top of stack), or null when the stack is empty.
public static NavigationEntry getCurrent() {
return stack.isEmpty() ? null : stack.get(stack.size() - 1);
}

/// Unmodifiable snapshot of the navigation stack, oldest entry first
/// (breadcrumb order). The list is a copy: mutating navigations after
/// the call do not affect it.
public static List<NavigationEntry> getStack() {
return Collections.unmodifiableList(new ArrayList<NavigationEntry>(stack));
}

/// Pop entries until `entry` is on top, then show its form via
/// `Form#showBack`. Returns `true` when the entry was on the stack and
/// we navigated back to it, `false` when the entry is not on the stack.
/// Calling with the current entry is a no-op that returns `true`.
public static boolean popTo(NavigationEntry entry) {
if (entry == null) {
return false;
}
// NavigationEntry doesn't override equals, so entry.equals(other) is
// reference equality -- which is what we want here. Two navigations to
// the same path are independent stack frames.
int idx = -1;
for (int i = 0; i < stack.size(); i++) {
if (entry.equals(stack.get(i))) {
idx = i;
break;
}
}
if (idx < 0) {
return false;
}
if (idx == stack.size() - 1) {
return true;
}
while (stack.size() > idx + 1) {
stack.remove(stack.size() - 1);
}
entry.getForm().showBack();
return true;
}

// ------------------------------------------------------------------------
// Internal: framework-side entry point invoked by Display when the
// platform delivers a deep link through `AppArg`.
// ------------------------------------------------------------------------

/// Dispatch a URL delivered by the platform. Invoked by
/// `com.codename1.ui.Display#setProperty(String, String)` for URL-shaped
/// `AppArg` values; applications should call `#navigate(String)` instead.
public static boolean dispatchExternalUrl(String url) {
if (url == null || url.length() == 0) {
return false;
}
if (Display.getInstance().isEdt()) {
return navigate(url);
}
final String captured = url;
final boolean[] holder = new boolean[1];
Display.getInstance().callSeriallyAndWait(new Runnable() {
@Override
public void run() {
holder[0] = navigate(captured);
}
});
return holder[0];
}
}
Loading
Loading