Skip to content

Commit 04dd7ad

Browse files
committed
Fix command line parameter parsing to support values containing commas
The parameter format is: key=value,type,identifying Previously, the parser split on ALL commas, which broke when values contained commas. For example, "items=apple,banana,orange" would be incorrectly parsed, treating "banana" as a type name. This commit adds intelligent parsing that: 1. Checks if the second-to-last token looks like a class name 2. If yes, treats everything before it as the value (which may contain commas) 3. If no, treats the entire string as the value with commas included The heuristic "looks like a class name" checks for: - Fully qualified class names (contain dots like java.lang.String) - Primitive types (int, long, double, etc.) This allows values containing commas to work correctly while maintaining backward compatibility with existing parameter formats. Fixes #5329
1 parent 258da33 commit 04dd7ad

File tree

2 files changed

+103
-5
lines changed

2 files changed

+103
-5
lines changed

spring-batch-core/src/main/java/org/springframework/batch/core/converter/DefaultJobParametersConverter.java

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,20 +175,68 @@ private String parseValue(String encodedJobParameter) {
175175
if (tokens.length == 0) {
176176
return "";
177177
}
178-
return tokens[0];
178+
if (tokens.length == 1) {
179+
return tokens[0];
180+
}
181+
// For 2+ tokens, figure out which part is the type
182+
int typeIndex = -1;
183+
if (tokens.length == 2) {
184+
// Could be: value,type OR value,value (both parts of value)
185+
if (looksLikeClassName(tokens[1])) {
186+
typeIndex = 1;
187+
}
188+
}
189+
else if (tokens.length >= 3) {
190+
// Could be: value,type,identifying OR value,value,value (all parts of value)
191+
// Check if second-to-last looks like a class
192+
if (looksLikeClassName(tokens[tokens.length - 2])) {
193+
typeIndex = tokens.length - 2;
194+
}
195+
}
196+
197+
if (typeIndex == -1) {
198+
// No valid type found, all commas are part of the value
199+
return encodedJobParameter;
200+
}
201+
202+
// Reconstruct value from all tokens before the type
203+
StringBuilder value = new StringBuilder(tokens[0]);
204+
for (int i = 1; i < typeIndex; i++) {
205+
value.append(",").append(tokens[i]);
206+
}
207+
return value.toString();
179208
}
180209

181210
private Class<?> parseType(String encodedJobParameter) {
182211
String[] tokens = StringUtils.commaDelimitedListToStringArray(encodedJobParameter);
183212
if (tokens.length <= 1) {
184213
return String.class;
185214
}
215+
// For 2+ tokens, figure out which part is the type
216+
int typeIndex = -1;
217+
if (tokens.length == 2) {
218+
// Could be: value,type OR value,value (both parts of value)
219+
if (looksLikeClassName(tokens[1])) {
220+
typeIndex = 1;
221+
}
222+
}
223+
else if (tokens.length >= 3) {
224+
// Could be: value,type,identifying OR value,value,value (all parts of value)
225+
// Check if second-to-last looks like a class
226+
if (looksLikeClassName(tokens[tokens.length - 2])) {
227+
typeIndex = tokens.length - 2;
228+
}
229+
}
230+
231+
if (typeIndex == -1) {
232+
return String.class;
233+
}
234+
186235
try {
187-
Class<?> type = Class.forName(tokens[1]);
188-
return type;
236+
return Class.forName(tokens[typeIndex]);
189237
}
190238
catch (ClassNotFoundException e) {
191-
throw new JobParametersConversionException("Unable to parse job parameter " + encodedJobParameter, e);
239+
return String.class;
192240
}
193241
}
194242

@@ -197,7 +245,35 @@ private boolean parseIdentifying(String encodedJobParameter) {
197245
if (tokens.length <= 2) {
198246
return true;
199247
}
200-
return Boolean.parseBoolean(tokens[2]);
248+
// Check if the last token is a boolean (identifying flag)
249+
String lastToken = tokens[tokens.length - 1];
250+
if ("true".equalsIgnoreCase(lastToken) || "false".equalsIgnoreCase(lastToken)) {
251+
// And the second-to-last token looks like a class name
252+
String potentialType = tokens[tokens.length - 2];
253+
if (looksLikeClassName(potentialType)) {
254+
return Boolean.parseBoolean(lastToken);
255+
}
256+
}
257+
return true;
258+
}
259+
260+
/**
261+
* Simple heuristic to check if a string looks like a class name. Class names
262+
* typically contain dots (package names) or are primitive types.
263+
*/
264+
private boolean looksLikeClassName(String className) {
265+
if (className.isEmpty()) {
266+
return false;
267+
}
268+
// Check for primitive types
269+
if (className.matches("^(int|long|double|float|boolean|char|byte|short|void)$")) {
270+
return true;
271+
}
272+
// Check for fully qualified class name (contains dots)
273+
if (className.contains(".")) {
274+
return true;
275+
}
276+
return false;
201277
}
202278

203279
}

spring-batch-core/src/test/java/org/springframework/batch/core/converter/DefaultJobParametersConverterTests.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,4 +340,26 @@ void testRoundTripWithOffsetDateTime() {
340340
props.getProperty("schedule.offsetDateTime"));
341341
}
342342

343+
@Test
344+
void testValueWithComma() {
345+
// Test that values containing commas are handled correctly
346+
String[] args = new String[] { "items=apple,banana,orange" };
347+
348+
JobParameters parameters = factory.getJobParameters(StringUtils.splitArrayElementsIntoProperties(args, "="));
349+
assertEquals("apple,banana,orange", parameters.getString("items"));
350+
}
351+
352+
@Test
353+
void testValueWithCommaRoundTrip() {
354+
// Test round-trip encoding/decoding of values with commas
355+
JobParametersBuilder builder = new JobParametersBuilder();
356+
builder.addString("items", "apple,banana,orange");
357+
JobParameters originalParameters = builder.toJobParameters();
358+
359+
Properties props = factory.getProperties(originalParameters);
360+
JobParameters decodedParameters = factory.getJobParameters(props);
361+
362+
assertEquals("apple,banana,orange", decodedParameters.getString("items"));
363+
}
364+
343365
}

0 commit comments

Comments
 (0)