Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
6dd51f4
Backend: Spring-style beans, transactions, scheduling, metrics and MC…
shai-almog Sep 26, 2026
e31da12
Backend: fix review findings in sessions, tasks, MCP, metrics and wiring
shai-almog Sep 26, 2026
28cad30
Backend: per-server sessions and registries, bean lifecycle fixes; sp…
shai-almog Sep 26, 2026
d4d761e
Backend: per-server executors, bounded shutdown, lifecycle and DST fixes
shai-almog Sep 27, 2026
1ce6092
Backend: drain virtual tasks at shutdown, harden JDBC sessions and wi…
shai-almog Sep 27, 2026
5315e2c
Backend: finish abandoned tasks, order request beans before the sessi…
shai-almog Sep 27, 2026
aac2cc4
Backend: roll back failed statements as Spring does; per-server logs …
shai-almog Sep 27, 2026
51929fe
Backend: per-server tracers and gauges, dependency-ordered destruction
shai-almog Sep 27, 2026
b9ab538
Backend: safer start-up failure, init order, session replacement
shai-almog Sep 27, 2026
b7ea87b
Backend: clean up after Errors at start-up; tighten sessions, MCP, me…
shai-almog Sep 27, 2026
941e52e
Backend: guard post-start hooks, protect in-use session beans, scope …
shai-almog Sep 27, 2026
ec07ddb
Backend: test that expiry spares session beans a request is using
shai-almog Sep 27, 2026
3a382d1
Backend: database-time locks, atomic session rotation, per-server cal…
shai-almog Sep 27, 2026
42ae64f
Backend: spare in-use sessions in the store, finish sessions on Errors
shai-almog Sep 27, 2026
496ee4d
Merge master: hold the Spring-style backend to the backend API gates
shai-almog Sep 27, 2026
1bfafbd
Backend: contain tracer Errors, honour lost rotations and leases, che…
shai-almog Sep 27, 2026
e681885
Backend: spare in-use sessions on lookup, check the whole bean graph …
shai-almog Sep 27, 2026
88d4c8e
Backend: undo a stale memory rotation, finish every session, bound th…
shai-almog Sep 27, 2026
be4c4cd
Backend: forget found session ids, run notifications, check executor …
shai-almog Sep 27, 2026
df9743b
Backend: histogram shapes, empty batches, untraced servers, one metri…
shai-almog Sep 27, 2026
80f3ddc
Backend: tokenless MCP on loopback, JSON-RPC 2.0 only, DST folds, bri…
shai-almog Sep 27, 2026
c8053ea
Backend: spare pre-rotation ids, answer malformed MCP, drain before m…
shai-almog Sep 27, 2026
3c23684
Backend: strict loopback, JSON-RPC id types, @Async scope, savepoint …
shai-almog Sep 28, 2026
70b26f2
Backend: retire shared session beans, drain gauge reads, bound-addres…
shai-almog Sep 28, 2026
cf0746d
Backend: per-source gauge drains, executor kinds, unannounced rotatio…
shai-almog Sep 28, 2026
f0cc877
Backend: one cron run per repeated time, strict MCP args, masking, De…
shai-almog Sep 28, 2026
2804983
Backend: tear down after an in-flight stop caller, one server per bui…
shai-almog Sep 28, 2026
a6b2aec
Backend: one transaction one pool, unwind overrunning tasks, session …
shai-almog Sep 28, 2026
b654de9
Backend: conditional @Primary fallbacks, scoped factory owners, metri…
shai-almog Sep 28, 2026
7e7cf54
Backend: joined failures mark rollback-only, AUTO decided per task, n…
shai-almog Sep 28, 2026
5c3926d
Backend: namespaced JDBC sessions, exporter transport check, start-up…
shai-almog Sep 28, 2026
34ffd0f
Backend: align with Spring on handler Errors, scope checks and shared…
shai-almog Sep 28, 2026
c5079bb
Backend: undo failed rotations, text-keyed histogram series, early JS…
shai-almog Sep 28, 2026
4bd7142
Backend: saturated schedules, active-only routes, one setter per prop…
shai-almog Sep 28, 2026
98e65a7
Backend: non-blocking chained futures, handshake stop, tracer before …
shai-almog Sep 28, 2026
1d43b28
Backend: only the rotating request announces a session rotation; expo…
shai-almog Sep 28, 2026
c821e65
Backend: Prometheus-folded aspect metric names, replaced shared gauge…
shai-almog Sep 29, 2026
035be30
Backend: old session id retired at rotation, request beans drained fu…
shai-almog Sep 29, 2026
5b45f82
Backend: one backend per process; conditional deps checked at start a…
shai-almog Sep 29, 2026
b518fdf
Merge remote-tracking branch 'origin/master' into backend-spring-model
shai-almog Sep 29, 2026
3acab79
Backend: class-specific woven bodies, virtual-task stop, per-bean job…
shai-almog Sep 29, 2026
f930488
Backend: injective support names, discarded file responses closed, re…
shai-almog Sep 29, 2026
030a2fe
Backend build: injective support names, per-bean stand-ins, lifecycle…
shai-almog Sep 29, 2026
4cdef68
Backend review fixes; recalibrate the Windows AMD perf row; GcSteadyS…
shai-almog Sep 29, 2026
3b667ad
Backend docs: drop unsupported hand-written mains, promote Future, db…
shai-almog Sep 30, 2026
d307f82
Backend: management and MCP linked only on request; settings as annot…
shai-almog Sep 30, 2026
0e6549b
Backend: controllers return and accept the application's own classes …
shai-almog Sep 30, 2026
f8bb219
Backend: outbound I/O parks a virtual thread instead of blocking its …
shai-almog Sep 30, 2026
903a18f
Backend: clear the PMD report; native checks for the JSON codec runtime
shai-almog Sep 30, 2026
8a41512
Backend review: codec value dispatch, TreeSet, long bounds, MCP CORS …
shai-almog Sep 30, 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
18 changes: 17 additions & 1 deletion .github/scripts/build_javadocs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,25 @@ done < <("$BACKEND_DIR/shared-sources.sh")
# server as for an app. The website's doclet marks the whole tree shared with
# --shared-sources.
BACKEND_SOURCES_ARGFILE="$CN1_DIR/build/backend-javadoc-sources.txt"
# The vm/JavaAPI classes the backend's public API exposes beyond the CLDC set --
# @Async's Future and what its get() throws, Config's Properties. The backend
# compiles against vm/JavaAPI, so these work there; listing them here is what makes
# them documented, supported API rather than an accident of the class library.
# check-backend-jdk-surface.py fails the build when the backend exposes a JDK type
# that is neither CLDC nor in this list. Keep the two lists in step.
BACKEND_PROMOTED_JDK="java/util/Properties.java
java/util/concurrent/CancellationException.java
java/util/concurrent/ExecutionException.java
java/util/concurrent/Future.java
java/util/concurrent/TimeUnit.java
java/util/concurrent/TimeoutException.java"
python3 "$ROOT_DIR/scripts/check-backend-jdk-surface.py"
{
find "$BACKEND_STAGE" -name "*.java" | grep -v '/com/codename1/impl/'
find "$ROOT_DIR/Ports/CLDC11/src" -name "*.java"
echo "$BACKEND_PROMOTED_JDK" | while IFS= read -r promoted; do
echo "$ROOT_DIR/vm/JavaAPI/src/$promoted"
done
} | LC_ALL=C sort > "$BACKEND_SOURCES_ARGFILE"

# Held to the same doclint as the client API above. --release 8 matches the backend module's
Expand All @@ -185,7 +201,7 @@ BACKEND_SOURCES_ARGFILE="$CN1_DIR/build/backend-javadoc-sources.txt"
--add-script "$ROOT_DIR/maven/javadoc-resources/highlight.min.js" \
--add-script "$ROOT_DIR/maven/javadoc-resources/javadoc-highlight-init.js" \
--release 8 \
-sourcepath "$BACKEND_STAGE:$ROOT_DIR/Ports/CLDC11/src" \
-sourcepath "$BACKEND_STAGE:$ROOT_DIR/Ports/CLDC11/src:$ROOT_DIR/vm/JavaAPI/src" \
-Xdoclint:all,-missing \
-Xmaxerrs 10000 \
-Xmaxwarns 10000 \
Expand Down
5 changes: 5 additions & 0 deletions CodenameOne/src/com/codename1/annotations/JsonIgnore.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@

/// Excludes a `@Mapped` field from the JSON projection. The same field still
/// participates in XML mapping unless `@XmlTransient` is also present.
///
/// The server's generated codecs honour it too: a field that points back at its
/// owner is the usual candidate, since writing both ends of that loop never
/// finishes.
@com.codename1.impl.SharedWithBackend
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface JsonIgnore {
Expand Down
4 changes: 4 additions & 0 deletions CodenameOne/src/com/codename1/annotations/JsonProperty.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
/// Renames a `@Mapped` field in the JSON projection. The default JSON key is
/// the field name; `@JsonProperty` lets a field map to `snake_case` or any
/// alternative spelling without touching the Java identifier.
///
/// The server's generated codecs read it too, so a class shared between an app
/// and its backend has one JSON form on both sides.
@com.codename1.impl.SharedWithBackend
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface JsonProperty {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,9 @@ public final class DatabaseSnippets {
private DatabaseSnippets() {
}

public static List open() throws IOException {
public static List open(DataSource db) throws IOException {
// tag::backend-database[]
DataSource db = DataSource.open(System.getenv("DATABASE_URL")); // or ":memory:"

// db is the pool the server opened from cn1.datasource.url and injected
List rows = db.query("SELECT id, body FROM note WHERE id > ?",
new Object[] { Integer.valueOf(10) });
// end::backend-database[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,18 @@
import com.codename1.backend.annotations.ResponseStatus;
import com.codename1.backend.annotations.RestController;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

// tag::backend-first-server[]
@RestController
@RequestMapping("/notes")
public class Notes {
private final Map<Long, Map> store = new ConcurrentHashMap<Long, Map>();
private final Map<Long, Map> store =
Collections.synchronizedMap(new LinkedHashMap<Long, Map>());
private final AtomicLong nextId = new AtomicLong(1);

@GetMapping("/healthz")
Expand All @@ -58,11 +59,13 @@ public Map read(@PathVariable("id") long id) {
@GetMapping
public List list(@RequestParam(value = "limit", defaultValue = "20") int limit) {
List page = new ArrayList();
for (Map note : store.values()) {
if (page.size() >= limit) {
break;
synchronized (store) { // iterating needs the map's own lock
for (Map note : store.values()) {
if (page.size() >= limit) {
break;
}
page.add(note);
}
page.add(note);
}
return page;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@
*/
package com.codenameone.developerguide.backend;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

// tag::backend-contract-server[]
public class NotesEndpoint implements NotesApiServer {
private final Map<String, Note> notes = new ConcurrentHashMap<String, Note>();
private final Map<String, Note> notes =
Collections.synchronizedMap(new HashMap<String, Note>());

public Note note(String id) { // no callback: this IS the server
return notes.get(id);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright (c) 2012, 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.codenameone.developerguide.backend;

import com.codename1.backend.HttpServer;
import com.codename1.backend.annotations.DeleteMapping;
import com.codename1.backend.annotations.GetMapping;
import com.codename1.backend.annotations.PathVariable;
import com.codename1.backend.annotations.PostMapping;
import com.codename1.backend.annotations.RequestBody;
import com.codename1.backend.annotations.RequestHeader;
import com.codename1.backend.annotations.RequestMapping;
import com.codename1.backend.annotations.RequestParam;
import com.codename1.backend.annotations.ResponseStatus;
import com.codename1.backend.annotations.RestController;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

// tag::backend-web-mappings[]
@RestController
@RequestMapping("/api/products")
public class Products {
private final Map<Long, Map<String, Object>> products =
new LinkedHashMap<Long, Map<String, Object>>();

@GetMapping("/{id}") // GET /api/products/42
public synchronized Map<String, Object> get(@PathVariable("id") long id) {
return products.get(Long.valueOf(id)); // null answers 404
}

@GetMapping("/search") // GET /api/products/search?q=mug
public synchronized List<Map<String, Object>> search(
@RequestParam(value = "q", required = false) String query,
@RequestParam(value = "limit", defaultValue = "20") int limit) {
List<Map<String, Object>> out = new ArrayList<Map<String, Object>>();
for (Map<String, Object> p : products.values()) {
if (out.size() < limit
&& (query == null || String.valueOf(p.get("name")).contains(query))) {
out.add(p);
}
}
return out;
}

@PostMapping // POST /api/products
@ResponseStatus(201)
public synchronized Map<String, Object> create(
@RequestBody Map<String, Object> body,
@RequestHeader(value = "Idempotency-Key", required = false) String key) {
Long id = Long.valueOf(products.size() + 1);
Map<String, Object> product = new LinkedHashMap<String, Object>(body);
product.put("id", id);
products.put(id, product);
return product;
}

@DeleteMapping("/{id}") // void answers 204
public synchronized void delete(@PathVariable("id") long id) {
products.remove(Long.valueOf(id));
}

@GetMapping("/{id}/label")
public HttpServer.Response label(HttpServer.Request request, @PathVariable("id") long id) {
byte[] text = ("product " + id).getBytes();
return request.respond(200, "text/plain; charset=utf-8", text);
}
}
// end::backend-web-mappings[]
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@

import com.codename1.backend.Crypto;
import com.codename1.backend.HttpServer;
import com.codename1.backend.Json;
import com.codename1.backend.annotations.PostMapping;
import com.codename1.backend.annotations.RequestMapping;
import com.codename1.backend.annotations.RestController;
import com.codename1.io.JSONParser;
import com.codename1.backend.annotations.Value;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -57,25 +58,35 @@ void removeTargetAndMarkApplied(String eventKey, String provider, String target)
throws Exception;
}

/** Assigned once at start-up; there is no dependency injection here. */
static DeviceStore store;
private final DeviceStore store;

/** The signing secret shown in Push > Settings. Read it from configuration. */
private static final String SECRET = System.getenv("CN1_PUSH_CALLBACK_SECRET");
/** The signing secret shown in Push > Settings. */
private final String secret;

/**
* Both are injected: the store is your bean implementing DeviceStore, and
* the secret is read from cn1.push.callbackSecret, which the environment
* variable CN1_PUSH_CALLBACKSECRET overrides.
*/
public PushFeedback(DeviceStore store,
@Value("${cn1.push.callbackSecret}") String secret) {
this.store = store;
this.secret = secret;
}

/** Reject a digest whose timestamp is older than this, to bound replay. */
private static final long MAX_AGE_MS = 5 * 60 * 1000L;

@PostMapping("/feedback")
public HttpServer.Response feedback(HttpServer.Request request) throws Exception {
String body = request.getBody();
if (!verified(request.getHeader("X-CN1-Signature"), body)) {
if (!verified(request.getHeader("X-CN1-Signature"), body, secret)) {
// Anything but 2xx keeps the window at the sender and resends it,
// which is what you want while a secret rotation is half-applied.
return new HttpServer.Response(401, "text/plain",
"bad signature".getBytes(StandardCharsets.UTF_8));
}
Map digest = JSONParser.parseJSON(body);
Map digest = Json.parseObject(body);
List events = (List) digest.get("events");
if (events != null) {
for (Object entry : events) {
Expand Down Expand Up @@ -118,8 +129,9 @@ public HttpServer.Response feedback(HttpServer.Request request) throws Exception

// throws, because String.getBytes(Charset) is a CHECKED throw in the
// ParparVM class library even though it is not one on a JVM.
private static boolean verified(String header, String body) throws Exception {
if (header == null || SECRET == null) {
private static boolean verified(String header, String body, String secret)
throws Exception {
if (header == null || secret == null || secret.length() == 0) {
return false;
}
long timestamp = 0;
Expand Down Expand Up @@ -156,7 +168,7 @@ private static boolean verified(String header, String body) throws Exception {
// no JCE. equalsConstantTime is here for the same reason a hand-written
// loop would be -- an early exit on the first differing byte lets a MAC
// be forged one byte at a time.
byte[] expected = Crypto.hmacSha256(SECRET.getBytes(StandardCharsets.UTF_8),
byte[] expected = Crypto.hmacSha256(secret.getBytes(StandardCharsets.UTF_8),
(timestamp + "." + body).getBytes(StandardCharsets.UTF_8));
return Crypto.equalsConstantTime(expected, decodeHex(provided));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,10 @@
*/
package com.codenameone.developerguide.backend;

import com.codename1.backend.Config;
import com.codename1.backend.Tracing;
import com.codename1.backend.annotations.GetMapping;
import com.codename1.backend.annotations.OpenTelemetry;
import com.codename1.backend.annotations.PathVariable;
import com.codename1.backend.annotations.RestController;
import com.codename1.backend.otel.OtlpTracer;

/// The Backend chapter's tracing examples, compiled so they cannot drift. This
/// module runs no annotation processing, which is what lets `@OpenTelemetry` appear
Expand All @@ -51,10 +48,4 @@ public String note(@PathVariable("id") String id) {
// end::backend-otel-annotation[]
}

public static void installByHand() throws Exception {
// tag::backend-otel-install[]
Tracing.install(OtlpTracer.open(Config.load(), "notes"));
// end::backend-otel-install[]
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@
*/
package com.codenameone.developerguide.backend;

import com.codename1.backend.Backend;
import com.codename1.backend.DataSource;
import com.codename1.backend.orm.EntityManager;
import com.codename1.backend.HttpServer;
import com.codename1.backend.WebSocket;
import com.codename1.backend.WebSocketSession;
import com.codename1.backend.annotations.WebSocketMapping;
Expand Down Expand Up @@ -58,19 +54,6 @@ public void onBinary(WebSocketSession session, byte[] message, int offset, int l
}
// end::backend-websocket-echo[]

// tag::backend-websocket-register[]
public static void main(String[] args) throws Exception {
Backend.builder()
.webSockets(new Backend.WebSocketEndpoints() {
public void register(HttpServer.WebSocketRegistry registry,
DataSource dataSource, EntityManager entities) {
registry.route("/echo", new Echo());
}
})
.run();
}
// end::backend-websocket-register[]

// tag::backend-websocket-annotated[]
@WebSocketMapping("/chat")
public static final class ChatEndpoint implements WebSocket {
Expand Down Expand Up @@ -143,18 +126,4 @@ public void onBinary(WebSocketSession session, byte[] message, int offset, int l
}
// end::backend-websocket-subprotocol[]

// tag::backend-websocket-raw[]
public static void serveForever() throws Exception {
HttpServer server = HttpServer.start(null, 8080, 512, 16, new HttpServer.Handler() {
public HttpServer.Response handle(HttpServer.Request request) {
return HttpServer.Response.text(200, "ok");
}
}, null, new HttpServer.WebSocketRoutes() {
public void register(HttpServer.WebSocketRegistry registry) {
registry.route("/echo", new Echo());
}
});
server.awaitTermination();
}
// end::backend-websocket-raw[]
}
Loading
Loading