Declarative router + deep links + bytecode annotation framework#5037
Open
shai-almog wants to merge 10 commits into
Open
Declarative router + deep links + bytecode annotation framework#5037shai-almog wants to merge 10 commits into
shai-almog wants to merge 10 commits into
Conversation
A new com.codename1.router package layers URL-based navigation, deep
links, route guards, and a per-tab nav shell on top of existing Form
infrastructure. The runtime is opt-in: existing Form.show() / showBack()
code continues to work unchanged.
Core runtime (CodenameOne/src/com/codename1/router/):
- DeepLink — tolerant URL parser (scheme/host/path/segments/query/fragment).
- Router — fluent route(), redirect(), guard(), notFound(), start();
push/pop/replace with a navigation stack, location listeners, and a
pluggable BrowserHistoryBridge for the JavaScript port.
- PopGuard / PopReason — Flutter-PopScope analogue; intercepts hardware
back, toolbar back, and Router.pop() before user code runs.
- RouteContext / RouteBuilder / RouteGuard — typed handoff between a
matched URL and the Form constructor.
- TabsForm — Form whose tabs each keep their own navigation stack.
- AasaBuilder / AssetLinksBuilder — JSON generators for iOS Universal
Links and Android App Links.
- web/JsRouterBootstrap + cn1-router-history.js — browser-history bridge
for the JS port.
Display gains setDeepLinkHandler(LinkHandler) / dispatchDeepLink(url).
Display.setProperty("AppArg", url) now routes URL-shaped values through
the deep-link handler, so iOS/Android ports need no native changes.
Form gains setPopGuard(PopGuard) and checkPopGuard(reason); guards are
honored from MenuBar.keyReleased (hardware back) and Button.fireActionEvent
(toolbar back). Sheet gains showForResult() returning AsyncResource<T>
with auto-cancel on dismiss.
Build-time annotation framework (maven/codenameone-maven-plugin/):
- A reusable AnnotationProcessor SPI under com.codename1.maven.annotations
(AnnotatedClass, MethodInfo, FieldInfo, AnnotationValues, ProcessorContext,
ClassScanner). Processors register via ServiceLoader.
- Two new Mojos:
cn1:generate-annotation-stubs (generate-sources) — writes compile-time
stubs each processor declares.
cn1:process-annotations (process-classes) — ASM-scans target/classes,
dispatches to every registered processor, fail-fast aborts with a
combined error list when validation fails.
- RouteAnnotationProcessor — the first concrete processor. Bytecode-based
(not source regex): validates @route classes (extends Form, non-empty
path starting with /, accessible constructor, no duplicate patterns),
emits the RoutesIndex + RoutesIndex$Builder bytecode directly via ASM.
Prefers a (RouteContext) constructor over a no-arg one when both exist.
Adding more annotations is now a matter of dropping a new processor in
META-INF/services — the orchestrator picks them up unchanged.
Tests:
- 46 new core unit tests for DeepLink, RouteMatch, Router, PopGuard,
AasaBuilder, AssetLinksBuilder (all 2608 core tests still pass).
- 11 new plugin tests: ClassScanner picks annotations off real .class
files; RouteAnnotationProcessor compiles fixtures with javac, runs
the processor, loads the generated bytecode in a child classloader,
and invokes RoutesIndex.register() against a stub Router that records
every call. Negative tests cover non-Form @route, empty pattern,
missing leading slash, duplicate pattern, and abstract class targets.
Maven plugin pom: bumped maven-surefire-plugin from 2.22.1 to 3.2.5 and
added junit-vintage-engine. The old surefire silently skipped every JUnit
test in this module under JDK 8; the bump matches the parent reactor's
existing pin.
Docs:
- Routing-And-Deep-Links.asciidoc — reference page covering every part of
the API plus iOS Universal Links / Android App Links setup.
- Tutorial-Routing-And-Deep-Links.asciidoc — end-to-end tutorial from an
empty project to a working deep-linkable app with @route, guards, and
a tab shell.
- Both pages included from developer-guide.asciidoc; asciidoctor lint
passes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Codename One JavaScript port's bundler scans the build output for any *.js file and importScripts() each one into the parparvm Web Worker. cn1-router-history.js was authored as a browser-main-thread shim and crashed the worker with `ReferenceError: document is not defined` on import, which broke the javascript-screenshots CI run. Guard the body of the shim with an explicit feature test for document / addEventListener / history so the same file safely round-trips through the worker import without doing anything, while still installing the browser-history bridge when loaded into the main page. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Hugo-rendered website flattens each .asciidoc into its own page and doesn't translate the AsciiDoc link: macro into the resulting HTML, so both `link:Routing-And-Deep-Links.asciidoc[...]` and `link:Maven-Getting-Started.adoc[...]` in the tutorial pointed at non-existent files in the build output. Lychee correctly flagged them. Replace with inline references — the developer guide is a single include-stitched book, so cross-page links between included files were always cosmetic. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collaborator
Author
|
Compared 110 screenshots: 110 matched. Native Android coverage
✅ Native Android screenshot tests passed. Native Android coverage
Benchmark ResultsDetailed Performance Metrics
|
Contributor
|
Developer Guide build artifacts are available for download from this workflow run:
Developer Guide quality checks: |
Vale reported 5 issues against the routing chapters: - Two Microsoft.HeadingColons violations on === headings whose post-colon word started lowercase. - Three Microsoft.Contractions hits for spelling out 'it is', 'does not', and 'they are' in the tutorial prose. LanguageTool flagged 'parparvm' and 'assetlinks' as misspellings. Both are technical identifiers; add them to the LanguageTool accept list under a new comment block. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collaborator
Author
|
Compared 20 screenshots: 20 matched. |
Contributor
Cloudflare Preview
|
Collaborator
Author
|
Compared 110 screenshots: 110 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
Collaborator
Author
|
Compared 110 screenshots: 110 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
The legacy Ant build of CodenameOne core compiles against the CLDC11 bootclasspath, which doesn't expose java.util.regex.Pattern / Matcher — that's why the rest of CN1's core uses com.codename1.util.regex.RE. Under JDK 21 the Ant build (build-test (21)) broke with `package java.util.regex does not exist`. Switch RouteMatch to RE. Two engine differences vs java.util.regex that the rewrite has to account for: 1. RE.match(s) is find-style, not full-match. The pattern was already anchored with ^ and $, but we also assert getParenStart(0) == 0 and getParenEnd(0) == path.length() as belt-and-braces. 2. RE.getParenCount() counts groups the matcher actually visited. The catch-all wildcard `**` is implemented as an alternation `(?:|/(.*))` where the suffix capture lives on the right branch; when the left (empty) branch wins, the inner capture group isn't reported at all. Always populate the param map with empty string in that case so callers don't have to null-check. Refactor only — no public API change. All 46 router unit tests still pass, including the new bare-prefix catch-all assertion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two CI gates needed cleanup:
1. build-test (8): the PR-CI quality-report aggregates PMD findings and
fails the build when any forbidden rule fires in changed code. The
routing PR introduced 88 violations split across:
66 ControlStatementBraces (single-line `if (x) stmt;`)
10 MissingOverride (anonymous classes implementing interfaces)
5 ForLoopCanBeForeach (index-based for over a List)
4 LiteralsFirstInComparisons (`seg.equals("X")`)
1 SingularField (compiledRegex used only at construction)
1 UnnecessaryImport (java.util.HashMap leftover after refactor)
1 UnnecessaryFullyQualifiedName (com.codename1.io.Log.e)
Re-formatted every flagged line; PMD now reports zero forbidden
violations from this PR.
2. build-test (17): the legacy Android Ant build compiles with the
platform-default encoding (US-ASCII) and rejected the em-dashes,
en-dashes, and smart quotes that had crept into javadoc comments.
Replaced every non-ASCII character in the new and modified .java
files with its ASCII equivalent (-- for em-dash, ' for curly quote,
etc.). Verified there are zero codepoints > 0x7F in any touched
.java file.
Tests still green: 46 router core tests + 11 plugin tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
build-test (8) failed at the quality-report Checkstyle gate: the
project's checkstyle.xml uses the default LeftCurly option (`eol`),
which rejects any `{` that has code after it on the same line. Inline
forms like `if (x) { return; }`, `private RoutesIndex() { }`, and
`public String getRaw() { return raw; }` all violated.
Expand every flagged occurrence into the canonical three-line form:
if (x) {
return;
}
PMD's ControlStatementBraces rule is satisfied by the braces; Checkstyle
is now happy that the brace ends its line. The two gates pull in the
same direction once you commit to multi-line bodies.
Affects only the new routing package and the surface I touched on
Display/Form/Sheet/MenuBar/Button. No behavior change. All 46 router
core tests + 11 plugin tests still pass; local Checkstyle severity-Error
count drops from 99 to 0 across the touched files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
✅ Continuous Quality ReportTest & Coverage
Static Analysis
Generated automatically by the PR CI workflow. |
…files The new files I introduced inherited the Oracle "All rights reserved" copyright header from the surrounding sources I was copying boilerplate from. That header is correct on Oracle-origin files (the J2ME fork ancestry) but wrong on newly authored code, which belongs to Codename One. Swap every header on the 44 new files for the canonical CN1 header. Touched files in CodenameOne/src/com/codename1/router, maven/codenameone-maven-plugin, the test stubs, and the new core unit tests. Also strip every `#### Since 8.0` block from doc comments. We're not at 8.0 yet and the version pinning was speculative; the public API surface is stable across the touched packages and the per-method version markers were noise. No code or test changes; all 46 router unit tests + 11 plugin tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Combine the two non-overlapping accept-list blocks (router & JS port identifiers from this PR, WebAuthn / passkey terms from master's #5039) into one section.
cn1-router-history.js was wrongly living under
CodenameOne/src/com/codename1/router/web/, which is the Java source
tree. That's exactly why it got swept into the parparvm worker bundle
by the JS bundler (which scans `*.js` in the build output and
importScripts()'s them) and crashed with `ReferenceError: document is
not defined` — a class of bug that only existed because the file sat
in the wrong place.
Move to Ports/JavaScriptPort/src/main/webapp/cn1-router-history.js
alongside port.js and sw.js. Those files are served as static webapp
assets and never imported into the worker, so:
- the bundler doesn't pick the shim up at all
- the worker-context guard I added earlier becomes unnecessary and is
removed (the shim now unconditionally installs the popstate /
history.pushState bridge it always wanted to)
Also flatten the now-empty com.codename1.router.web subpackage:
JsRouterBootstrap moves up to com.codename1.router. It was the only
class in the subpackage and is itself a single-method install() helper
that doesn't justify its own namespace.
Doc updates to match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
✅ ByteCodeTranslator Quality ReportTest & Coverage
Benchmark Results
Static Analysis
Generated automatically by the PR CI workflow. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
com.codename1.router(URL-based navigation, route guards, redirects, per-tab navigation shell, location listeners) layered on top of the existingForminfrastructure —Form.show()/showBack()continues to work unchanged.Display.setDeepLinkHandler(LinkHandler)receiving a normalizedDeepLink(scheme/host/path/segments/query/fragment); auto-invoked for cold + warm launches. iOS / Android need no port changes —Display.setProperty("AppArg", url)now routes URL-shaped values through the handler.AnnotationProcessorSPI + two new Mojos (cn1:generate-annotation-stubs,cn1:process-annotations). ASM-scanstarget/classes, dispatches to every registered processor, fails-fast with a combined error list. Adding more annotations later is a matter of dropping a new processor intoMETA-INF/services— no orchestrator changes.RouteAnnotationProcessorvalidates@Route("/path")classes (extendsForm, non-empty path starting with/, accessible constructor, no duplicate patterns) and emits aRoutesIndex+RoutesIndex$Builderdirectly via ASM. No source-text regex; no runtime reflection.Form.setPopGuard(PopGuard)(Flutter-PopScopeanalogue) honored from hardware back, toolbar back, andRouter.pop().Sheet.showForResult()returning anAsyncResource<T>that auto-cancels withnullon dismiss.TabsForm— aFormwhose tabs each keep their own navigation stack.AasaBuilder,AssetLinksBuilder).window.historybridge:JsRouterBootstrap+ standalonecn1-router-history.jsshim.Docs
Two new pages under
docs/developer-guide/, both wired into the masterdeveloper-guide.asciidoc:Routing-And-Deep-Links.asciidoc— reference covering every part of the API plus iOS Universal Links / Android App Links setup and the extension recipe for adding new annotation processors.Tutorial-Routing-And-Deep-Links.asciidoc— end-to-end tutorial from an empty project to a working deep-linkable app with@Route, guards, and a tab shell.Asciidoctor lint passes locally with
--failure-level WARN.Test plan
DeepLink,RouteMatch,Router,PopGuard,AasaBuilder,AssetLinksBuilder. All 2608 core tests pass against the local-dev-javase profile.@Route-annotated fixtures withjavac, runsRouteAnnotationProcessor, loads the generatedRoutesIndexbytecode in a child classloader, and invokesregister()against a stubRouterthat records every call. Negative tests cover non-Form targets, empty pattern, missing leading slash, duplicate pattern, and abstract class targets.mvn verifyoncore-unittests(SpotBugs / PMD / Checkstyle) and oncodenameone-maven-plugin(SpotBugs): zero new SpotBugs findings on the touched modules.Notes on the build-time scanner
The earlier source-regex prototype was scrapped — the user pushed for a fail-fast bytecode-based scanner sharing infrastructure with the existing ASM passes (
BytecodeComplianceMojo's String.split rewrite is the prior art). The new framework runs inprocess-classes, so it sees the JVM's view of the project rather than a textual approximation, and any validation issue aborts the build with class + reason before any generated.classlands on disk.A small compile-time stub source (a no-op
RoutesIndexwithregister()) is emitted undertarget/generated-sources/cn1-annotations/duringgenerate-sourcesso application code that referencesRoutesIndex.register()resolves at compile time.process-classesoverwrites the stub's.classwith the real registrations.Bumped surefire from 2.22.1 to 3.2.5 in the plugin pom
The older surefire silently skipped every JUnit test in this module under JDK 8 (
Tests run: 0even thoughAppTestwas present). The bump matches the version already pinned by the parent reactor pom. Vintage engine is included so existing JUnit 4 tests in the module keep working.🤖 Generated with Claude Code