From 3abbf7ac734a3a04365197c0168e2f8215d2dc9d Mon Sep 17 00:00:00 2001 From: Matteo Merli Date: Tue, 12 May 2026 13:27:02 -0700 Subject: [PATCH 1/2] Drop jibx-tools dependency to remove transitive log4j 1.x jibx-tools was only used in Util.plural / Util.singular to derive accessor names for `repeated` fields (addItem / getItemsCount / etc.). It transitively pulled in log4j 1.2.17 (multiple CVEs) and ~10 ancient Eclipse JDT jars. The relevant pluralize/depluralize logic from JiBX NameUtilities is ~40 lines of rule-based suffix manipulation with no irregular-plural handling, so vendor it directly into Util (BSD 3-clause, attribution in comment). Generated output is byte-identical; all 262 existing tests pass unchanged. Add UtilTest covering every branch of plural / singular, including the case-sensitivity quirk in jibx where "ANY" pluralizes to "ANYs". --- code-generator/pom.xml | 11 ++- .../lightproto/generator/Util.java | 38 ++++++-- .../lightproto/generator/UtilTest.java | 92 +++++++++++++++++++ pom.xml | 7 -- 4 files changed, 127 insertions(+), 21 deletions(-) create mode 100644 code-generator/src/test/java/io/streamnative/lightproto/generator/UtilTest.java diff --git a/code-generator/pom.xml b/code-generator/pom.xml index f317bb0..ad623f1 100644 --- a/code-generator/pom.xml +++ b/code-generator/pom.xml @@ -46,11 +46,6 @@ guava - - org.jibx - jibx-tools - - org.jboss.forge.roaster roaster-api @@ -60,6 +55,12 @@ roaster-jdt runtime + + + org.junit.jupiter + junit-jupiter + test + diff --git a/code-generator/src/main/java/io/streamnative/lightproto/generator/Util.java b/code-generator/src/main/java/io/streamnative/lightproto/generator/Util.java index d5151ce..1c407cf 100644 --- a/code-generator/src/main/java/io/streamnative/lightproto/generator/Util.java +++ b/code-generator/src/main/java/io/streamnative/lightproto/generator/Util.java @@ -17,9 +17,6 @@ import java.io.PrintWriter; -import org.jibx.schema.codegen.extend.DefaultNameConverter; -import org.jibx.schema.codegen.extend.NameConverter; - import static com.google.common.base.CaseFormat.LOWER_CAMEL; import static com.google.common.base.CaseFormat.LOWER_UNDERSCORE; @@ -66,14 +63,37 @@ public static String upperCase(String... parts) { return sb.toString().toUpperCase(); } - private static final NameConverter nameTools = new DefaultNameConverter(); - - public static String plural(String s) { - return nameTools.pluralize(s); + // pluralize/singular rules vendored from JiBX NameUtilities + // (Copyright (c) 2008-2010, Dennis M. Sosnoski, BSD 3-clause license). + public static String plural(String name) { + if (name.endsWith("List") || (name.endsWith("s") && !name.endsWith("ss"))) { + return name; + } + if (name.endsWith("y") && !name.endsWith("ay") && !name.endsWith("ey") && !name.endsWith("iy") + && !name.endsWith("oy") && !name.endsWith("uy")) { + if (name.equalsIgnoreCase("any")) { + return name; + } + return name.substring(0, name.length() - 1) + "ies"; + } else if (name.endsWith("ss")) { + return name + "es"; + } else { + return name + 's'; + } } - public static String singular(String s) { - return nameTools.depluralize(s); + public static String singular(String name) { + if (name.endsWith("ies")) { + return name.substring(0, name.length() - 3) + 'y'; + } else if (name.endsWith("sses")) { + return name.substring(0, name.length() - 2); + } else if (name.endsWith("s") && !name.endsWith("ss")) { + return name.substring(0, name.length() - 1); + } else if (name.endsWith("List")) { + return name.substring(0, name.length() - 4); + } else { + return name; + } } public static void writeJavadoc(PrintWriter w, String doc, String indent) { diff --git a/code-generator/src/test/java/io/streamnative/lightproto/generator/UtilTest.java b/code-generator/src/test/java/io/streamnative/lightproto/generator/UtilTest.java new file mode 100644 index 0000000..f63fbaa --- /dev/null +++ b/code-generator/src/test/java/io/streamnative/lightproto/generator/UtilTest.java @@ -0,0 +1,92 @@ +/** + * Copyright 2026 StreamNative + * + * Licensed 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 io.streamnative.lightproto.generator; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +/** + * Locks the pluralize/singular behavior inherited from JiBX NameUtilities. + * These functions feed into generated public API names (e.g. {@code addItem}, + * {@code getItemsCount}), so changing their output is an ABI break. + */ +class UtilTest { + + @ParameterizedTest + @CsvSource({ + // default case: append 's' + "item, items", + "book, books", + "Message, Messages", + // ends with 'ss': append 'es' + "address, addresses", + "class, classes", + // ends with consonant+'y': replace with 'ies' + "city, cities", + "category, categories", + "company, companies", + // ends with vowel+'y': default rule, append 's' + "day, days", + "key, keys", + "boy, boys", + "guy, guys", + "way, ways", + // 'any' is a hardcoded exception (case-insensitive match), but + // only reached when endsWith("y") matches, which is case-sensitive. + // So "ANY" slips through to the default rule. + "any, any", + "Any, Any", + "ANY, ANYs", + // already plural ('s' but not 'ss'): unchanged + "items, items", + "books, books", + // ends with 'List': treated as already plural + "itemList, itemList", + "userList, userList", + }) + void pluralizes(String input, String expected) { + assertEquals(expected, Util.plural(input)); + } + + @ParameterizedTest + @CsvSource({ + // ends with 'ies': replace with 'y' + "cities, city", + "categories, category", + "companies, company", + // ends with 'sses': strip 'es' + "addresses, address", + "classes, class", + // ends with 's' (not 'ss'): strip 's' + "items, item", + "books, book", + "days, day", + // ends with 'List': strip suffix + "itemList, item", + "userList, user", + // ends with 'ss': unchanged (not a plural) + "address, address", + "class, class", + // no recognized plural marker: unchanged + "item, item", + "any, any", + }) + void singularizes(String input, String expected) { + assertEquals(expected, Util.singular(input)); + } +} diff --git a/pom.xml b/pom.xml index bee3e9f..ce61a10 100644 --- a/pom.xml +++ b/pom.xml @@ -68,7 +68,6 @@ 5.7.0 32.0.0-jre 4.1.131.Final - 1.3.3 2.22.2.Final 1.68.0 @@ -177,12 +176,6 @@ ${protobuf.version} - - org.jibx - jibx-tools - ${jibx.version} - - org.openjdk.jmh jmh-core From 1483e4a66fbc89ef53a68657c6174e59afd9651f Mon Sep 17 00:00:00 2001 From: Matteo Merli Date: Tue, 12 May 2026 13:35:57 -0700 Subject: [PATCH 2/2] Drop guava dependency guava was used in the code generator only for three small utilities: - Joiner.on('/').join(arr) -> String.join("/", arr) - Maps.newHashMap() -> new HashMap<>() - CaseFormat.LOWER_UNDERSCORE.to(LOWER_CAMEL, s) and the inverse, used in Util.camelCase / Util.upperCase The CaseFormat conversions were replaced by lowerUnderscoreToLowerCamel and lowerCamelToLowerUnderscore helpers in Util. Their behavior was verified against guava's CaseFormat across every input shape that appears in the codebase (proto field names, gRPC method names, short prefixes like "get"/"set", and the embedded-camel "_msgSize" input where guava's normalization lowercases internal uppercase letters). Generated source is byte-identical; all 262 integration tests pass. Removes guava and its 6 transitive jars (failureaccess, listenablefuture, jsr305, checker-qual, error_prone_annotations, j2objc-annotations) from the code-generator runtime tree. --- code-generator/pom.xml | 5 -- .../generator/LightProtoGenerator.java | 5 +- .../generator/LightProtoNumberField.java | 5 +- .../lightproto/generator/Util.java | 49 +++++++++++++++++-- .../lightproto/generator/UtilTest.java | 49 +++++++++++++++++++ pom.xml | 6 --- 6 files changed, 97 insertions(+), 22 deletions(-) diff --git a/code-generator/pom.xml b/code-generator/pom.xml index ad623f1..9e33b25 100644 --- a/code-generator/pom.xml +++ b/code-generator/pom.xml @@ -41,11 +41,6 @@ slf4j-api - - com.google.guava - guava - - org.jboss.forge.roaster roaster-api diff --git a/code-generator/src/main/java/io/streamnative/lightproto/generator/LightProtoGenerator.java b/code-generator/src/main/java/io/streamnative/lightproto/generator/LightProtoGenerator.java index 5689d1b..c8f4a2a 100644 --- a/code-generator/src/main/java/io/streamnative/lightproto/generator/LightProtoGenerator.java +++ b/code-generator/src/main/java/io/streamnative/lightproto/generator/LightProtoGenerator.java @@ -15,7 +15,6 @@ */ package io.streamnative.lightproto.generator; -import com.google.common.base.Joiner; import org.jboss.forge.roaster.Roaster; import org.jboss.forge.roaster.model.source.JavaClassSource; import org.slf4j.Logger; @@ -79,7 +78,7 @@ public static List generate(List descriptors, File ou String outerClassName = Util.camelCaseFirstUpper(classPrefix, fileWithoutExtension); String javaPackageName = proto.getJavaPackageName(); - String javaDir = Joiner.on('/').join(javaPackageName.split("\\.")); + String javaDir = String.join("/", javaPackageName.split("\\.")); Path targetDir = Paths.get(String.format("%s/%s", outputDirectory, javaDir)); LightProto lightProto = new LightProto(proto, fileName, outerClassName, useOuterClass, generateTextFormat, generateJson); @@ -94,7 +93,7 @@ public static List generate(List descriptors, File ou JavaClassSource codecClass = (JavaClassSource) Roaster.parse(is); codecClass.setPackage(javaPackage); - String javaDir = Joiner.on('/').join(javaPackage.split("\\.")); + String javaDir = String.join("/", javaPackage.split("\\.")); Path codecFile = Paths.get(String.format("%s/%s/LightProtoCodec.java", outputDirectory, javaDir)); try (Writer w = Files.newBufferedWriter(codecFile)) { w.write(String.format( diff --git a/code-generator/src/main/java/io/streamnative/lightproto/generator/LightProtoNumberField.java b/code-generator/src/main/java/io/streamnative/lightproto/generator/LightProtoNumberField.java index b0242dc..da164db 100644 --- a/code-generator/src/main/java/io/streamnative/lightproto/generator/LightProtoNumberField.java +++ b/code-generator/src/main/java/io/streamnative/lightproto/generator/LightProtoNumberField.java @@ -15,9 +15,8 @@ */ package io.streamnative.lightproto.generator; -import com.google.common.collect.Maps; - import java.io.PrintWriter; +import java.util.HashMap; import java.util.Map; import static io.streamnative.lightproto.generator.Util.camelCase; @@ -25,7 +24,7 @@ public class LightProtoNumberField extends LightProtoField { - private static final Map typeToTag = Maps.newHashMap(); + private static final Map typeToTag = new HashMap<>(); static { typeToTag.put("double", "LightProtoCodec.WIRETYPE_FIXED64"); diff --git a/code-generator/src/main/java/io/streamnative/lightproto/generator/Util.java b/code-generator/src/main/java/io/streamnative/lightproto/generator/Util.java index 1c407cf..ff7b453 100644 --- a/code-generator/src/main/java/io/streamnative/lightproto/generator/Util.java +++ b/code-generator/src/main/java/io/streamnative/lightproto/generator/Util.java @@ -17,9 +17,6 @@ import java.io.PrintWriter; -import static com.google.common.base.CaseFormat.LOWER_CAMEL; -import static com.google.common.base.CaseFormat.LOWER_UNDERSCORE; - public class Util { public static String camelCase(String... parts) { @@ -30,7 +27,7 @@ public static String camelCase(String... parts) { continue; } if (s.contains("_")) { - s = LOWER_UNDERSCORE.to(LOWER_CAMEL, s); + s = lowerUnderscoreToLowerCamel(s); } if (i != 0) { @@ -52,7 +49,7 @@ public static String camelCaseFirstUpper(String... parts) { public static String upperCase(String... parts) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < parts.length; i++) { - String s = LOWER_CAMEL.to(LOWER_UNDERSCORE, parts[i]); + String s = lowerCamelToLowerUnderscore(parts[i]); if (i != 0) { sb.append('_'); } @@ -63,6 +60,48 @@ public static String upperCase(String... parts) { return sb.toString().toUpperCase(); } + // Mirrors Guava's CaseFormat.LOWER_UNDERSCORE.to(LOWER_CAMEL, s): + // splits on '_', lowercases everything, capitalizes the first char of + // each non-leading segment. + static String lowerUnderscoreToLowerCamel(String s) { + if (s.indexOf('_') < 0) { + return s; + } + StringBuilder sb = new StringBuilder(s.length()); + boolean upperNext = false; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '_') { + upperNext = true; + } else if (upperNext) { + sb.append(Character.toUpperCase(c)); + upperNext = false; + } else { + sb.append(Character.toLowerCase(c)); + } + } + return sb.toString(); + } + + // Mirrors Guava's CaseFormat.LOWER_CAMEL.to(LOWER_UNDERSCORE, s): + // inserts '_' before every uppercase letter (except a leading one) and + // lowercases all letters. + static String lowerCamelToLowerUnderscore(String s) { + StringBuilder sb = new StringBuilder(s.length() + 4); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (Character.isUpperCase(c)) { + if (i > 0) { + sb.append('_'); + } + sb.append(Character.toLowerCase(c)); + } else { + sb.append(c); + } + } + return sb.toString(); + } + // pluralize/singular rules vendored from JiBX NameUtilities // (Copyright (c) 2008-2010, Dennis M. Sosnoski, BSD 3-clause license). public static String plural(String name) { diff --git a/code-generator/src/test/java/io/streamnative/lightproto/generator/UtilTest.java b/code-generator/src/test/java/io/streamnative/lightproto/generator/UtilTest.java index f63fbaa..7e8b7c7 100644 --- a/code-generator/src/test/java/io/streamnative/lightproto/generator/UtilTest.java +++ b/code-generator/src/test/java/io/streamnative/lightproto/generator/UtilTest.java @@ -63,6 +63,55 @@ void pluralizes(String input, String expected) { assertEquals(expected, Util.plural(input)); } + @ParameterizedTest + @CsvSource({ + // typical snake_case + "first_name, firstName", + "user_id, userId", + "foo_bar_baz, fooBarBaz", + "snake_case_name, snakeCaseName", + // no underscore: returned as-is (does NOT lowercase) + "foo, foo", + "MyService, MyService", + // numerics + "abc_123, abc123", + // double underscore + "foo__bar, fooBar", + // leading underscore capitalizes the next char + "_leading, Leading", + "_msgSize, Msgsize", + // trailing underscore is dropped + "trailing_, trailing", + // embedded uppercase is normalized to lowercase (matches Guava) + "ALL_CAPS, allCaps", + "Foo_Bar, fooBar", + "FOO_BAR_BAZ, fooBarBaz", + }) + void snakeToCamel(String input, String expected) { + assertEquals(expected, Util.lowerUnderscoreToLowerCamel(input)); + } + + @ParameterizedTest + @CsvSource({ + // typical camelCase + "fooBar, foo_bar", + "fooBarBaz, foo_bar_baz", + "myService, my_service", + // leading uppercase: lowercased without leading underscore + "MyService, my_service", + "X, x", + // no uppercase: unchanged + "foo, foo", + "first_name, first_name", + // numerics + "abc123, abc123", + // already snake-ish stays put + "foo_bar_baz, foo_bar_baz", + }) + void camelToSnake(String input, String expected) { + assertEquals(expected, Util.lowerCamelToLowerUnderscore(input)); + } + @ParameterizedTest @CsvSource({ // ends with 'ies': replace with 'y' diff --git a/pom.xml b/pom.xml index ce61a10..10f5ad6 100644 --- a/pom.xml +++ b/pom.xml @@ -66,7 +66,6 @@ 1.7.25 5.7.0 - 32.0.0-jre 4.1.131.Final 2.22.2.Final 1.68.0 @@ -158,11 +157,6 @@ slf4j-api ${slf4j.version} - - com.google.guava - guava - ${guava.version} - io.netty netty-bom