Skip to content

Commit 46c8ebb

Browse files
committed
Fix parameters parsing in JobOperator and MetaDataInstanceFactory
Before this commit, the parsing of job parameters in JobOperator#start and MetaDataInstanceFactory#createJobExecution was accepting the comma separated key=value pairs format, which is incompatible with the new job parameters format introduced in v5. This commit updates the contract as well as the implementation of those APIs to be compatible with v5. Resolves #4253 Resolves #4301
1 parent 0dfa737 commit 46c8ebb

File tree

5 files changed

+82
-182
lines changed

5 files changed

+82
-182
lines changed

spring-batch-core/src/main/java/org/springframework/batch/core/launch/JobOperator.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ default JobInstance getJobInstance(String jobName, JobParameters jobParameters)
9191
Set<Long> getRunningExecutions(String jobName) throws NoSuchJobException;
9292

9393
/**
94-
* Get the {@link JobParameters} as an easily readable String.
94+
* Get the {@link JobParameters} as a human readable String (new line separated
95+
* key=value pairs).
9596
* @param executionId the id of an existing {@link JobExecution}
9697
* @return the job parameters that were used to launch the associated instance
9798
* @throws NoSuchJobExecutionException if the id was not associated with any
@@ -102,8 +103,8 @@ default JobInstance getJobInstance(String jobName, JobParameters jobParameters)
102103
/**
103104
* Start a new instance of a job with the parameters specified.
104105
* @param jobName the name of the {@link Job} to launch
105-
* @param parameters the parameters to launch it with (comma or newline separated
106-
* name=value pairs)
106+
* @param parameters the parameters to launch it with (new line separated key=value
107+
* pairs)
107108
* @return the id of the {@link JobExecution} that is launched
108109
* @throws NoSuchJobException if there is no {@link Job} with the specified name
109110
* @throws JobInstanceAlreadyExistsException if a job instance with this name and

spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/SimpleJobOperator.java

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2006-2022 the original author or authors.
2+
* Copyright 2006-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -60,9 +60,9 @@
6060
import org.springframework.batch.core.step.tasklet.StoppableTasklet;
6161
import org.springframework.batch.core.step.tasklet.Tasklet;
6262
import org.springframework.batch.core.step.tasklet.TaskletStep;
63+
import org.springframework.batch.support.PropertiesConverter;
6364
import org.springframework.beans.factory.InitializingBean;
6465
import org.springframework.lang.Nullable;
65-
import org.springframework.transaction.annotation.Transactional;
6666
import org.springframework.util.Assert;
6767

6868
/**
@@ -222,11 +222,7 @@ public String getParameters(long executionId) throws NoSuchJobExecutionException
222222

223223
Properties properties = this.jobParametersConverter.getProperties(jobExecution.getJobParameters());
224224

225-
List<String> keyValuePairs = new ArrayList<>();
226-
for (Map.Entry<Object, Object> entry : properties.entrySet()) {
227-
keyValuePairs.add(entry.getKey() + "=" + entry.getValue());
228-
}
229-
return String.join(" ", keyValuePairs);
225+
return PropertiesConverter.propertiesToString(properties);
230226
}
231227

232228
/*
@@ -319,14 +315,7 @@ public Long start(String jobName, String parameters)
319315
logger.info("Checking status of job with name=" + jobName);
320316
}
321317

322-
Properties properties = new Properties();
323-
if (!parameters.isEmpty()) {
324-
String[] keyValuePairs = parameters.split(" ");
325-
for (String string : keyValuePairs) {
326-
String[] keyValuePair = string.split("=");
327-
properties.setProperty(keyValuePair[0], keyValuePair[1]);
328-
}
329-
}
318+
Properties properties = PropertiesConverter.stringToProperties(parameters);
330319
JobParameters jobParameters = jobParametersConverter.getJobParameters(properties);
331320

332321
if (jobRepository.isJobInstanceExists(jobName, jobParameters)) {
Lines changed: 38 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2006-2007 the original author or authors.
2+
* Copyright 2006-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,122 +16,74 @@
1616

1717
package org.springframework.batch.support;
1818

19-
import java.io.IOException;
20-
import java.io.StringReader;
21-
import java.io.StringWriter;
22-
import java.util.Arrays;
19+
import java.util.ArrayList;
2320
import java.util.List;
21+
import java.util.Map;
2422
import java.util.Properties;
2523

26-
import org.springframework.util.DefaultPropertiesPersister;
27-
import org.springframework.util.PropertiesPersister;
24+
import org.springframework.lang.NonNull;
25+
import org.springframework.util.Assert;
2826
import org.springframework.util.StringUtils;
2927

3028
/**
31-
* Utility to convert a Properties object to a String and back. Ideally this utility
32-
* should have been used to convert to string in order to convert that string back to a
33-
* Properties Object. Attempting to convert a string obtained by calling
34-
* Properties.toString() will return an invalid Properties object. The format of
35-
* Properties is that used by {@link PropertiesPersister} from the Spring Core, so a
36-
* String in the correct format for a Spring property editor is fine (key=value pairs
37-
* separated by new lines).
29+
* Utility to convert a Properties object to a String and back. The format of properties
30+
* is new line separated key=value pairs.
3831
*
3932
* @author Lucas Ward
4033
* @author Dave Syer
41-
* @see PropertiesPersister
34+
* @author Mahmoud Ben Hassine
4235
*/
4336
public final class PropertiesConverter {
4437

45-
private static final PropertiesPersister propertiesPersister = new DefaultPropertiesPersister();
46-
47-
private static final String LINE_SEPARATOR = System.getProperty("line.separator");
38+
private static final String LINE_SEPARATOR = "\n";
4839

4940
// prevents the class from being instantiated
5041
private PropertiesConverter() {
5142
}
5243

5344
/**
54-
* Parse a String to a Properties object. If string is null, an empty Properties
55-
* object will be returned. The input String is a set of name=value pairs, delimited
56-
* by either newline or comma (for brevity). If the input String contains a newline it
57-
* is assumed that the separator is newline, otherwise comma.
58-
* @param stringToParse String to parse.
59-
* @return Properties parsed from each string.
60-
* @see PropertiesPersister
45+
* Parse a String to a Properties object. If string is empty, an empty Properties
46+
* object will be returned. The input String should be a set of key=value pairs,
47+
* separated by a new line.
48+
* @param stringToParse String to parse. Must not be {@code null}.
49+
* @return Properties parsed from each key=value pair.
6150
*/
62-
public static Properties stringToProperties(String stringToParse) {
63-
64-
if (stringToParse == null) {
51+
public static Properties stringToProperties(@NonNull String stringToParse) {
52+
Assert.notNull(stringToParse, "stringToParse must not be null");
53+
if (!StringUtils.hasText(stringToParse)) {
6554
return new Properties();
6655
}
67-
68-
if (!contains(stringToParse, "\n")) {
69-
stringToParse = StringUtils
70-
.arrayToDelimitedString(StringUtils.commaDelimitedListToStringArray(stringToParse), "\n");
71-
}
72-
73-
StringReader stringReader = new StringReader(stringToParse);
74-
7556
Properties properties = new Properties();
76-
77-
try {
78-
propertiesPersister.load(properties, stringReader);
79-
// Exception is only thrown by StringReader after it is closed,
80-
// so never in this case.
81-
}
82-
catch (IOException ex) {
83-
throw new IllegalStateException(
84-
"Error while trying to parse String to java.util.Properties," + " given String: " + properties);
57+
String[] keyValuePairs = stringToParse.split(LINE_SEPARATOR);
58+
for (String string : keyValuePairs) {
59+
if (!string.contains("=")) {
60+
throw new IllegalArgumentException(string + "is not a valid key=value pair");
61+
}
62+
String[] keyValuePair = string.split("=");
63+
properties.setProperty(keyValuePair[0], keyValuePair[1]);
8564
}
86-
8765
return properties;
8866
}
8967

9068
/**
91-
* Convert Properties object to String. This is only necessary for compatibility with
92-
* converting the String back to a properties object. If an empty properties object is
93-
* passed in, a blank string is returned, otherwise it's string representation is
94-
* returned.
95-
* @param propertiesToParse contains the properties be converted.
96-
* @return String representation of properties object
69+
* Convert a Properties object to a String. This is only necessary for compatibility
70+
* with converting the String back to a properties object. If an empty properties
71+
* object is passed in, a blank string is returned, otherwise it's string
72+
* representation is returned.
73+
* @param propertiesToParse contains the properties to be converted. Must not be
74+
* {@code null}.
75+
* @return String representation of the properties object
9776
*/
98-
public static String propertiesToString(Properties propertiesToParse) {
99-
100-
// If properties is empty, return a blank string.
101-
if (propertiesToParse == null || propertiesToParse.size() == 0) {
77+
public static String propertiesToString(@NonNull Properties propertiesToParse) {
78+
Assert.notNull(propertiesToParse, "propertiesToParse must not be null");
79+
if (propertiesToParse.isEmpty()) {
10280
return "";
10381
}
104-
105-
StringWriter stringWriter = new StringWriter();
106-
107-
try {
108-
propertiesPersister.store(propertiesToParse, stringWriter, null);
109-
}
110-
catch (IOException ex) {
111-
// Exception is never thrown by StringWriter
112-
throw new IllegalStateException("Error while trying to convert properties to string");
113-
}
114-
115-
// If the value is short enough (and doesn't contain commas), convert to
116-
// comma-separated...
117-
String value = stringWriter.toString();
118-
if (value.length() < 160) {
119-
List<String> list = Arrays
120-
.asList(StringUtils.delimitedListToStringArray(value, LINE_SEPARATOR, LINE_SEPARATOR));
121-
String shortValue = StringUtils.collectionToCommaDelimitedString(list.subList(1, list.size()));
122-
int count = StringUtils.countOccurrencesOf(shortValue, ",");
123-
if (count == list.size() - 2) {
124-
value = shortValue;
125-
}
126-
if (value.endsWith(",")) {
127-
value = value.substring(0, value.length() - 1);
128-
}
82+
List<String> keyValuePairs = new ArrayList<>();
83+
for (Map.Entry<Object, Object> entry : propertiesToParse.entrySet()) {
84+
keyValuePairs.add(entry.getKey() + "=" + entry.getValue());
12985
}
130-
return value;
131-
}
132-
133-
private static boolean contains(String str, String searchStr) {
134-
return str.indexOf(searchStr) != -1;
86+
return String.join(LINE_SEPARATOR, keyValuePairs);
13587
}
13688

13789
}

spring-batch-infrastructure/src/test/java/org/springframework/batch/support/PropertiesConverterTests.java

Lines changed: 34 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2006-2022 the original author or authors.
2+
* Copyright 2006-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,121 +16,79 @@
1616

1717
package org.springframework.batch.support;
1818

19-
import static org.junit.jupiter.api.Assertions.assertEquals;
20-
import static org.junit.jupiter.api.Assertions.assertNotNull;
21-
import static org.junit.jupiter.api.Assertions.assertTrue;
22-
2319
import java.util.Properties;
2420

21+
import org.junit.jupiter.api.Assertions;
2522
import org.junit.jupiter.api.Test;
23+
2624
import org.springframework.util.StringUtils;
2725

26+
import static org.junit.jupiter.api.Assertions.assertEquals;
27+
import static org.junit.jupiter.api.Assertions.assertTrue;
28+
2829
/**
29-
* Unit tests for {@link PropertiesConverter}
30+
* Unit tests for {@link PropertiesConverter}.
3031
*
3132
* @author Robert Kasanicky
33+
* @author Mahmoud Ben Hassine
3234
*/
3335
class PropertiesConverterTests {
3436

35-
// convenience attributes for storing results of conversions
36-
private Properties props = null;
37-
38-
/**
39-
* Check that Properties can be converted to String and back correctly.
40-
*/
4137
@Test
42-
void testTwoWayRegularConversion() {
43-
44-
Properties storedProps = new Properties();
45-
storedProps.setProperty("key1", "value1");
46-
storedProps.setProperty("key2", "value2");
38+
void testStringToPropertiesConversion() {
39+
String stringToParse = "key1=value1\nkey2=value2";
40+
Properties expectedProperties = new Properties();
41+
expectedProperties.setProperty("key1", "value1");
42+
expectedProperties.setProperty("key2", "value2");
4743

48-
props = PropertiesConverter.stringToProperties(PropertiesConverter.propertiesToString(storedProps));
44+
Properties props = PropertiesConverter.stringToProperties(stringToParse);
4945

50-
assertEquals(storedProps, props);
46+
assertEquals(expectedProperties, props);
5147
}
5248

53-
/**
54-
* Check that Properties can be comma delimited.
55-
*/
5649
@Test
57-
void testRegularConversionWithComma() {
58-
59-
Properties storedProps = new Properties();
60-
storedProps.setProperty("key1", "value1");
61-
storedProps.setProperty("key2", "value2");
50+
void testPropertiesToStringConversion() {
51+
Properties properties = new Properties();
52+
properties.setProperty("key1", "value1");
53+
properties.setProperty("key2", "value2");
6254

63-
props = PropertiesConverter.stringToProperties("key1=value1,key2=value2");
55+
String value = PropertiesConverter.propertiesToString(properties);
6456

65-
assertEquals(storedProps, props);
57+
assertTrue(value.contains("key1=value1"), "Wrong value: " + value);
58+
assertTrue(value.contains("key2=value2"), "Wrong value: " + value);
59+
assertEquals(1, StringUtils.countOccurrencesOf(value, "\n"));
6660
}
6761

68-
/**
69-
* Check that Properties can be comma delimited with extra whitespace.
70-
*/
7162
@Test
72-
void testRegularConversionWithCommaAndWhitespace() {
73-
63+
void testTwoWayRegularConversion() {
7464
Properties storedProps = new Properties();
7565
storedProps.setProperty("key1", "value1");
7666
storedProps.setProperty("key2", "value2");
7767

78-
props = PropertiesConverter.stringToProperties("key1=value1, key2=value2");
68+
Properties props = PropertiesConverter.stringToProperties(PropertiesConverter.propertiesToString(storedProps));
7969

8070
assertEquals(storedProps, props);
8171
}
8272

83-
/**
84-
* Check that Properties can be comma delimited with extra whitespace.
85-
*/
8673
@Test
87-
void testShortConversionWithCommas() {
88-
89-
Properties storedProps = new Properties();
90-
storedProps.setProperty("key1", "value1");
91-
storedProps.setProperty("key2", "value2");
92-
93-
String value = PropertiesConverter.propertiesToString(storedProps);
94-
95-
assertTrue(value.contains("key1=value1"), "Wrong value: " + value);
96-
assertTrue(value.contains("key2=value2"), "Wrong value: " + value);
97-
assertEquals(1, StringUtils.countOccurrencesOf(value, ","));
74+
void nullStringShouldNotBeAccepted() {
75+
Assertions.assertThrows(IllegalArgumentException.class, () -> PropertiesConverter.stringToProperties(null));
9876
}
9977

100-
/**
101-
* Check that Properties can be newline delimited.
102-
*/
10378
@Test
104-
void testRegularConversionWithCommaAndNewline() {
105-
106-
Properties storedProps = new Properties();
107-
storedProps.setProperty("key1", "value1");
108-
storedProps.setProperty("key2", "value2");
109-
110-
props = PropertiesConverter.stringToProperties("key1=value1\n key2=value2");
111-
112-
assertEquals(storedProps, props);
79+
void emptyStringShouldBeConvertedToEmptyProperties() {
80+
Properties properties = PropertiesConverter.stringToProperties("");
81+
Assertions.assertTrue(properties.isEmpty());
11382
}
11483

115-
/**
116-
* Null String should be converted to empty Properties
117-
*/
11884
@Test
119-
void testStringToPropertiesNull() {
120-
props = PropertiesConverter.stringToProperties(null);
121-
assertNotNull(props);
122-
assertEquals(0, props.size(), "properties are empty");
85+
void nullPropertiesShouldNotBeAccepted() {
86+
Assertions.assertThrows(IllegalArgumentException.class, () -> PropertiesConverter.propertiesToString(null));
12387
}
12488

125-
/**
126-
* Null or empty properties should be converted to empty String
127-
*/
12889
@Test
129-
void testPropertiesToStringNull() {
130-
String string = PropertiesConverter.propertiesToString(null);
131-
assertEquals("", string);
132-
133-
string = PropertiesConverter.propertiesToString(new Properties());
90+
void emptyPropertiesShouldBeConvertedToEmptyString() {
91+
String string = PropertiesConverter.propertiesToString(new Properties());
13492
assertEquals("", string);
13593
}
13694

0 commit comments

Comments
 (0)