This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch docs
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/docs by this push:
new 6657da9a07 Docs: 10.0.0 breaking-changes release note +
placeholder-convention updates
6657da9a07 is described below
commit 6657da9a079dc0dceac1329e7d75c95416c82e4f
Author: James Bognar <[email protected]>
AuthorDate: Thu Jul 16 08:30:11 2026 -0400
Docs: 10.0.0 breaking-changes release note + placeholder-convention updates
Document the printf-only f()/fs() migration and mf()/mfs() split as a 10.0.0
breaking change, and update the affected topic pages to reflect the new
placeholder convention (%s printf vs {0} MessageFormat):
- release-notes/10.0.0: add breaking-change entry.
- topics: JuneauCommonsUtils, JuneauCommonsLang, JuneauCommonsLogging,
JuneauBeanCommon, CustomErrorMessages, V10MigrationGuide.
Co-authored-by: Cursor <[email protected]>
---
pages/release-notes/10.0.0.md | 6 +++++
pages/topics/02.01.JuneauCommonsUtils.md | 37 +++++++++++++++++-----------
pages/topics/02.03.JuneauCommonsLang.md | 9 +++++++
pages/topics/02.15.JuneauCommonsLogging.md | 2 +-
pages/topics/05.05.JuneauBeanCommon.md | 4 +--
pages/topics/07.02.01.CustomErrorMessages.md | 22 ++++++++---------
pages/topics/27.V10MigrationGuide.md | 12 +++++++++
7 files changed, 63 insertions(+), 29 deletions(-)
diff --git a/pages/release-notes/10.0.0.md b/pages/release-notes/10.0.0.md
index d97dee2453..3f0c862c24 100644
--- a/pages/release-notes/10.0.0.md
+++ b/pages/release-notes/10.0.0.md
@@ -622,6 +622,12 @@ _TBD — to be filled in as development continues._
- **Microservice demo resources moved out of the shipping jars into a new
unpublished example module.** Seven low-value demo `@Rest` resources —
`LogsResource`, `LogParser`, `DirectoryResource`, `ShutdownResource`,
`SampleRootResource`, `ConfigResource` (formerly
`org.apache.juneau.microservice.resources` in `juneau-microservice`) and
`DebugResource` (formerly `org.apache.juneau.microservice.jetty.resources` in
`juneau-microservice-jetty`) — have relocated to the new **`org.apache.juneau
[...]
+- **String formatting helpers `f()` / `fs()` (and `StringUtils.format`) are
now printf-only.** The general-purpose formatting helpers on
`org.apache.juneau.commons.utils.Shorts` — `f(String,Object...)` and
`fs(String,Object...)` — together with `StringUtils.format(...)`, previously
accepted **both** `java.util.Formatter` printf syntax (`%s`, `%d`, `%1$s`)
**and** `java.text.MessageFormat` syntax (`{0}`, `{1,number}`, `''`-quoting).
As of 10.0.0 they are **printf-only**: a pattern is pass [...]
+ - **Drop-in MessageFormat replacement:** new `mf(String,Object...)` /
`mfs(String,Object...)` helpers on `Shorts` (backed by
`StringUtils.mformat(...)`) render the full MessageFormat grammar (`{0}`,
`{1,number}`, `{2,date}`, `''`-quoting). Any caller that genuinely relied on
MessageFormat behavior should migrate `f(...)` → `mf(...)` and `fs(...)` →
`mfs(...)`.
+ - **Migration:** for the common case, change MessageFormat patterns to
printf — `f("Value {0} of {1}", a, b)` → `f("Value %s of %s", a, b)`; for
out-of-order/reused indices use positional printf — `{1} {0}` → `%2$s %1$s`,
`{0}…{0}` → `%1$s…%1$s`. Any literal `%` in a pattern now needs escaping as
`%%`. Where you truly need the MessageFormat engine (typed
`{0,number}`/`{0,date}`, externally-authored i18n patterns), switch that call
to `mf()`/`mfs()`.
+ - **i18n unaffected:** `Messages.getString(key, args)` (resource-bundle
i18n) and `LogRecord.getMessage()` (logging) continue to render
MessageFormat-style `.properties`/log patterns — they now route through `mf()`
internally, so `{0}`-style entries in your resource bundles keep working
exactly as before. Juneau's `Logger` MessageFormat-style logging is likewise
unchanged.
+ - The dual-syntax `StringFormat` engine still exists and remains directly
usable (`StringFormat.of(...)`) for callers that want both grammars in one
pattern; only the `f()`/`fs()`/`StringUtils.format` entry points were narrowed
to printf.
+
_Other entries TBD — to be filled in before release. See also the major
version bump note above._
### Deprecations
diff --git a/pages/topics/02.01.JuneauCommonsUtils.md
b/pages/topics/02.01.JuneauCommonsUtils.md
index 4f1be224de..65ee4c209c 100644
--- a/pages/topics/02.01.JuneauCommonsUtils.md
+++ b/pages/topics/02.01.JuneauCommonsUtils.md
@@ -53,8 +53,10 @@ import static org.apache.juneau.commons.utils.Shorts.*;
String name = or(nickname, username, "anonymous");
String port = or(configuredPort, "8080"); // 2-arg form of the same varargs
coalesce
-// Formatting shorthand (MessageFormat-style placeholders)
-String msg = f("User {0} has {1} items", "Alice", 10);
+// Formatting shorthand (printf-style placeholders — String.format semantics)
+String msg = f("User %s has %s items", "Alice", 10);
+// For MessageFormat-style {0} placeholders (e.g. i18n / .properties
patterns), use mf():
+String msg2 = mf("User {0} has {1} items", "Alice", 10);
// Comparisons
cmp("apple", "banana"); // < 0
@@ -91,9 +93,10 @@ Reusable string utility methods for formatting, parsing, and
manipulation.
```java
import static org.apache.juneau.commons.utils.StringUtils.*;
-// Format strings (supports both MessageFormat and printf styles)
+// Format strings (printf-style — String.format semantics)
format("Hello %s, you have %d items", "John", 5);
-format("Hello {0}, you have {1} items", "John", 5);
+// For MessageFormat-style {0} placeholders, use mformat():
+mformat("Hello {0}, you have {1} items", "John", 5);
// Case-insensitive comparison
equalsIgnoreCase("Hello", "hello"); // true
@@ -311,8 +314,8 @@ String value = assertArgNotNull("value", obj);
// Assert not null or blank (returns the value, throws
IllegalArgumentException otherwise)
String str = assertArgNotNullOrBlank("str", input);
-// Assert an arbitrary condition (MessageFormat-style placeholders)
-assertArg(count > 0, "count must be positive, was {0}", count);
+// Assert an arbitrary condition (printf-style placeholders)
+assertArg(count > 0, "count must be positive, was %s", count);
```
### <java-class><a
href="/site/apidocs/org/apache/juneau/commons/utils/PredicateUtils.html"
target="_blank">PredicateUtils</a></java-class>
@@ -364,13 +367,13 @@ methods — they do **not** delegate to `ThrowableUtils`:
```java
import static org.apache.juneau.commons.utils.Shorts.*;
-throw iaex("Value ''{0}'' is not valid", value); //
IllegalArgumentException
-throw isex("Component must be initialized before {0}", m); //
IllegalStateException
-throw rex("Unexpected error processing {0}", input); //
RuntimeException
-throw uoex("Method not supported for type {0}", type); //
UnsupportedOperationException
+throw iaex("Value '%s' is not valid", value); //
IllegalArgumentException
+throw isex("Component must be initialized before %s", m); //
IllegalStateException
+throw rex("Unexpected error processing %s", input); //
RuntimeException
+throw uoex("Method not supported for type %s", type); //
UnsupportedOperationException
throw brex(MyBean.class, "Failed to create instance"); //
BeanRuntimeException
-throw exex(cause, "Error invoking method {0}", method); //
ExecutableException
-throw ioex("Failed to read file {0}", path); // IOException
+throw exex(cause, "Error invoking method %s", method); //
ExecutableException
+throw ioex("Failed to read file %s", path); // IOException
```
:::
@@ -391,7 +394,7 @@ Settings.get().setGlobal("juneau.enableVerboseExceptions",
"true");
import static org.apache.juneau.commons.utils.Shorts.*;
import static org.apache.juneau.commons.utils.ThrowableUtils.log;
-throw log(iaex("Value ''{0}'' is not valid", value));
+throw log(iaex("Value '%s' is not valid", value));
```
This is particularly useful when debugging swallowed exceptions or unexpected
code paths where you need to identify exactly where an exception was created.
@@ -429,8 +432,12 @@ List<String> list = listBuilder(String.class)
```java
import static org.apache.juneau.commons.utils.StringUtils.*;
-// Mixed format styles
-String result = format("User {0} has %d items", "Alice", 10);
+// printf-style formatting (String.format semantics)
+String result = format("User %s has %d items", "Alice", 10);
+// Returns: "User Alice has 10 items"
+
+// For MessageFormat-style {0} placeholders (i18n / .properties), use
mformat():
+String result2 = mformat("User {0} has {1} items", "Alice", 10);
// Returns: "User Alice has 10 items"
```
diff --git a/pages/topics/02.03.JuneauCommonsLang.md
b/pages/topics/02.03.JuneauCommonsLang.md
index 3a27ebe37b..f801ed4681 100644
--- a/pages/topics/02.03.JuneauCommonsLang.md
+++ b/pages/topics/02.03.JuneauCommonsLang.md
@@ -121,6 +121,15 @@ String result3 = fmt3.format("Alice", "Bob", "Charlie");
// Returns: "Alice loves Bob, and Alice also loves Charlie"
```
+:::note
+The `StringFormat` engine itself still understands **both** grammars in a
single pattern (as shown above).
+However, as of 10.0.0 the terse formatting helpers `Shorts.f()` /
`Shorts.fs()` and `StringUtils.format(...)`
+are **printf-only** — they route the pattern straight through `String.format`
semantics, so `{0}`-style
+placeholders render literally. Use `Shorts.mf()` / `Shorts.mfs()` /
`StringUtils.mformat(...)` (or a direct
+`StringFormat.of(...)`) when you need MessageFormat-style `{0}` /
`''`-quoting. See the
+[10.0.0 release notes](/docs/release-notes/10.0.0) for migration details.
+:::
+
### <java-class><a
href="/site/apidocs/org/apache/juneau/commons/lang/HashCode.html"
target="_blank">HashCode</a></java-class>
Utility class for generating integer hash codes.
diff --git a/pages/topics/02.15.JuneauCommonsLogging.md
b/pages/topics/02.15.JuneauCommonsLogging.md
index f01c07d7de..224207a309 100644
--- a/pages/topics/02.15.JuneauCommonsLogging.md
+++ b/pages/topics/02.15.JuneauCommonsLogging.md
@@ -17,7 +17,7 @@ logger.warning("Low disk space");
```
### <java-class><a
href="/site/apidocs/org/apache/juneau/commons/logging/LogRecord.html"
target="_blank">LogRecord</a></java-class>
-A `java.util.logging.LogRecord` subclass that supports lazy message formatting
— the message string is only built (via `StringUtils.format(String,
Object...)`) if the record is actually published.
+A `java.util.logging.LogRecord` subclass that supports lazy message formatting
— the message string is only built (via `StringUtils.mformat(String,
Object...)`, using `MessageFormat`-style `{0}` placeholders to match the
`java.util.logging` `LogRecord` contract) if the record is actually published.
## Test Support
diff --git a/pages/topics/05.05.JuneauBeanCommon.md
b/pages/topics/05.05.JuneauBeanCommon.md
index ef02e623cd..d8b416fadd 100644
--- a/pages/topics/05.05.JuneauBeanCommon.md
+++ b/pages/topics/05.05.JuneauBeanCommon.md
@@ -33,10 +33,10 @@ LinkString link = new LinkString("Apache Juneau",
"https://juneau.apache.org");
String html = HtmlSerializer.DEFAULT.serialize(link);
```
-The `uri` accepts `MessageFormat`-style arguments for parameterized links:
+The `uri` accepts `printf`-style (`String.format`) arguments for parameterized
links:
```java
-LinkString view = new LinkString("View", "/items/{0}", itemId);
+LinkString view = new LinkString("View", "/items/%s", itemId);
```
## ResultSetList
diff --git a/pages/topics/07.02.01.CustomErrorMessages.md
b/pages/topics/07.02.01.CustomErrorMessages.md
index 0e7b1b38b5..b43d459e23 100644
--- a/pages/topics/07.02.01.CustomErrorMessages.md
+++ b/pages/topics/07.02.01.CustomErrorMessages.md
@@ -40,19 +40,19 @@ import static org.apache.juneau.commons.utils.Shorts.fs;
// Single placeholder
String testName = "validateUser";
-assertBean(fs("Test {0} failed", testName),
+assertBean(fs("Test %s failed", testName),
result, "status", "SUCCESS");
// Multiple placeholders
String userName = "Alice";
int iteration = 5;
-assertBean(fs("User {0} validation failed on iteration {1}", userName,
iteration),
+assertBean(fs("User %s validation failed on iteration %s", userName,
iteration),
user, "isValid", "true");
// Contextual information
String orderId = "ORD-123";
String expectedStatus = "COMPLETED";
-assertBean(fs("Order {0} expected status {1}", orderId, expectedStatus),
+assertBean(fs("Order %s expected status %s", orderId, expectedStatus),
order, "status", expectedStatus);
```
@@ -95,7 +95,7 @@ void testMultipleOrders() {
for (int i = 0; i < orders.size(); i++) {
Order order = orders.get(i);
- assertBean(fs("Order validation failed at index {0}", i),
+ assertBean(fs("Order validation failed at index %s", i),
order, "status,total", "PENDING,99.99");
}
}
@@ -114,7 +114,7 @@ void testUsersByRole() {
String role = entry.getKey();
User user = entry.getValue();
- assertBean(fs("User validation failed for role: {0}", role),
+ assertBean(fs("User validation failed for role: %s", role),
user, "role,isActive", role + ",true");
}
}
@@ -143,7 +143,7 @@ void testConfiguration() {
Config config = loadConfig();
String environment = System.getProperty("env", "unknown");
- assertBean(fs("Config validation failed in environment: {0}", environment),
+ assertBean(fs("Config validation failed in environment: %s", environment),
config, "database.host,database.port", "localhost,5432");
}
```
@@ -206,7 +206,7 @@ void setUp() {
}
// Use custom message with the converter
-assertBean(fs("Date validation failed for user {0}", userId),
+assertBean(fs("Date validation failed for user %s", userId),
user, "birthDate", "1990-01-15");
@AfterEach
@@ -225,7 +225,7 @@ import static org.apache.juneau.commons.utils.Shorts.fs;
void testUserEmails(String email) {
User user = findUserByEmail(email);
- assertBean(fs("User validation failed for email: {0}", email),
+ assertBean(fs("User validation failed for email: %s", email),
user, "email,isVerified", email + ",true");
}
```
@@ -241,7 +241,7 @@ Stream<DynamicTest> testOrders() {
.map(order -> DynamicTest.dynamicTest(
"Test Order #" + order.getId(),
() -> assertBean(
- fs("Order {0} validation failed", order.getId()),
+ fs("Order %s validation failed", order.getId()),
order, "status", "PENDING")));
}
```
@@ -340,7 +340,7 @@ void testBatchProcessing() {
processOrder(order);
assertBean(fs(
- "Batch processing failed at index {0} of {1}, Order ID: {2}",
+ "Batch processing failed at index %s of %s, Order ID: %s",
i, orders.size(), order.getId()),
order, "status", "PROCESSED");
}
@@ -358,7 +358,7 @@ void testUserPermissions() {
String expectedRole = user.isAdmin() ? "ADMIN" : "USER";
assertBean(fs(
- "Permission check failed for {0} user (expected role: {1})",
+ "Permission check failed for %s user (expected role: %s)",
user.isAdmin() ? "admin" : "regular",
expectedRole),
user, "role,hasAccess", expectedRole + ",true");
diff --git a/pages/topics/27.V10MigrationGuide.md
b/pages/topics/27.V10MigrationGuide.md
index 49ede32a93..a131890d5c 100644
--- a/pages/topics/27.V10MigrationGuide.md
+++ b/pages/topics/27.V10MigrationGuide.md
@@ -810,6 +810,18 @@ domain classes directly (`ObjectUtils.isNotNull(x)`,
`StringUtils.format(...)`,
`CollectionUtils.array(...)`, etc.) — the `Shorts` aliases and the canonical
methods are
interchangeable.
+:::caution
+**`f()` / `fs()` / `StringUtils.format(...)` are now printf-only.** These
helpers previously accepted
+**both** printf (`%s`) and `MessageFormat` (`{0}`) placeholder styles. As of
10.0.0 they route straight
+through `String.format` semantics, so `{0}`-style placeholders render
**literally** and doubled single
+quotes (`''`) are no longer collapsed. Migrate MessageFormat patterns to
printf (`{0}`→`%s`; out-of-order
+`{1} {0}`→`%2$s %1$s`; escape any literal `%` as `%%`), or switch to the new
`Shorts.mf()` / `mfs()` /
+`StringUtils.mformat(...)` helpers, which own the full `MessageFormat`
grammar. The i18n path
+(`Messages.getString`, `.properties` bundles) and `Logger` / `LogRecord`
MessageFormat logging are
+unaffected — they route through `mf()` internally. See the
+[10.0.0 release notes](/docs/release-notes/10.0.0) for the full breakdown.
+:::
+
## Miscellaneous Utility Removals
| Old | New | Notes |