Skip to content

Commit b51e553

Browse files
vpavicsbrannen
authored andcommitted
Ensure indexer output is deterministic and repeatable
Closes gh-22383
1 parent a5828ca commit b51e553

File tree

3 files changed

+176
-3
lines changed

3 files changed

+176
-3
lines changed

spring-context-indexer/src/main/java/org/springframework/context/index/processor/PropertiesMarshaller.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 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.
@@ -33,7 +33,7 @@
3333
abstract class PropertiesMarshaller {
3434

3535
public static void write(CandidateComponentsMetadata metadata, OutputStream out) throws IOException {
36-
Properties props = new Properties();
36+
Properties props = new SortedProperties(true);
3737
metadata.getItems().forEach(m -> props.put(m.getType(), String.join(",", m.getStereotypes())));
3838
props.store(out, "");
3939
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Copyright 2002-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.context.index.processor;
18+
19+
import java.io.ByteArrayOutputStream;
20+
import java.io.IOException;
21+
import java.io.OutputStream;
22+
import java.io.StringWriter;
23+
import java.io.Writer;
24+
import java.nio.charset.StandardCharsets;
25+
import java.util.Collections;
26+
import java.util.Comparator;
27+
import java.util.Enumeration;
28+
import java.util.Map.Entry;
29+
import java.util.Properties;
30+
import java.util.Set;
31+
import java.util.TreeSet;
32+
33+
/**
34+
* Specialization of {@link Properties} that sorts properties alphanumerically
35+
* based on their keys.
36+
*
37+
* <p>This can be useful when storing the {@link Properties} instance in a
38+
* properties file, since it allows such files to be generated in a repeatable
39+
* manner with consistent ordering of properties.
40+
*
41+
* <p>Comments in generated properties files can also be optionally omitted.
42+
*
43+
* @author Sam Brannen
44+
* @since 5.2
45+
* @see java.util.Properties
46+
*/
47+
@SuppressWarnings("serial")
48+
class SortedProperties extends Properties {
49+
50+
static final String EOL = System.lineSeparator();
51+
52+
private static final Comparator<Object> keyComparator = //
53+
(key1, key2) -> String.valueOf(key1).compareTo(String.valueOf(key2));
54+
55+
private static final Comparator<Entry<Object, Object>> entryComparator = //
56+
Entry.comparingByKey(keyComparator);
57+
58+
private final boolean omitComments;
59+
60+
61+
/**
62+
* Construct a new {@code SortedProperties} instance that honors the supplied
63+
* {@code omitComments} flag.
64+
*
65+
* @param omitComments {@code true} if comments should be omitted when
66+
* storing properties in a file
67+
*/
68+
SortedProperties(boolean omitComments) {
69+
this.omitComments = omitComments;
70+
}
71+
72+
/**
73+
* Construct a new {@code SortedProperties} instance with properties populated
74+
* from the supplied {@link Properties} object and honoring the supplied
75+
* {@code omitComments} flag.
76+
*
77+
* <p>Default properties from the supplied {@code Properties} object will
78+
* not be copied.
79+
*
80+
* @param properties the {@code Properties} object from which to copy the
81+
* initial properties
82+
* @param omitComments {@code true} if comments should be omitted when
83+
* storing properties in a file
84+
*/
85+
SortedProperties(Properties properties, boolean omitComments) {
86+
this(omitComments);
87+
putAll(properties);
88+
}
89+
90+
@Override
91+
public void store(OutputStream out, String comments) throws IOException {
92+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
93+
super.store(baos, (this.omitComments ? null : comments));
94+
String contents = new String(baos.toByteArray(), StandardCharsets.ISO_8859_1);
95+
for (String line : contents.split(EOL)) {
96+
if (!this.omitComments || !line.startsWith("#")) {
97+
out.write((line + EOL).getBytes(StandardCharsets.ISO_8859_1));
98+
}
99+
}
100+
}
101+
102+
@Override
103+
public void store(Writer writer, String comments) throws IOException {
104+
StringWriter stringWriter = new StringWriter();
105+
super.store(stringWriter, (this.omitComments ? null : comments));
106+
String contents = stringWriter.toString();
107+
for (String line : contents.split(EOL)) {
108+
if (!this.omitComments || !line.startsWith("#")) {
109+
writer.write(line + EOL);
110+
}
111+
}
112+
}
113+
114+
@Override
115+
public void storeToXML(OutputStream out, String comments) throws IOException {
116+
super.storeToXML(out, (this.omitComments ? null : comments));
117+
}
118+
119+
@Override
120+
public void storeToXML(OutputStream out, String comments, String encoding) throws IOException {
121+
super.storeToXML(out, (this.omitComments ? null : comments), encoding);
122+
}
123+
124+
/**
125+
* Return a sorted enumeration of the keys in this {@link Properties} object.
126+
* @see #keySet()
127+
*/
128+
@Override
129+
public synchronized Enumeration<Object> keys() {
130+
return Collections.enumeration(keySet());
131+
}
132+
133+
/**
134+
* Return a sorted set of the keys in this {@link Properties} object.
135+
* <p>The keys will be converted to strings if necessary using
136+
* {@link String#valueOf(Object)} and sorted alphanumerically according to
137+
* the natural order of strings.
138+
*/
139+
@Override
140+
public Set<Object> keySet() {
141+
Set<Object> sortedKeys = new TreeSet<>(keyComparator);
142+
sortedKeys.addAll(super.keySet());
143+
return Collections.synchronizedSet(sortedKeys);
144+
}
145+
146+
/**
147+
* Return a sorted set of the entries in this {@link Properties} object.
148+
* <p>The entries will be sorted based on their keys, and the keys will be
149+
* converted to strings if necessary using {@link String#valueOf(Object)}
150+
* and compared alphanumerically according to the natural order of strings.
151+
*/
152+
@Override
153+
public Set<Entry<Object, Object>> entrySet() {
154+
Set<Entry<Object, Object>> sortedEntries = new TreeSet<>(entryComparator);
155+
sortedEntries.addAll(super.entrySet());
156+
return Collections.synchronizedSet(sortedEntries);
157+
}
158+
159+
}

spring-context-indexer/src/test/java/org/springframework/context/index/processor/PropertiesMarshallerTests.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.io.ByteArrayInputStream;
2020
import java.io.ByteArrayOutputStream;
2121
import java.io.IOException;
22+
import java.nio.charset.StandardCharsets;
2223
import java.util.Arrays;
2324
import java.util.HashSet;
2425

@@ -27,11 +28,11 @@
2728
import static org.assertj.core.api.Assertions.assertThat;
2829

2930

30-
3131
/**
3232
* Tests for {@link PropertiesMarshaller}.
3333
*
3434
* @author Stephane Nicoll
35+
* @author Vedran Pavic
3536
*/
3637
public class PropertiesMarshallerTests {
3738

@@ -50,6 +51,19 @@ public void readWrite() throws IOException {
5051
assertThat(readMetadata.getItems()).hasSize(2);
5152
}
5253

54+
@Test
55+
public void metadataIsWrittenDeterministically() throws IOException {
56+
CandidateComponentsMetadata metadata = new CandidateComponentsMetadata();
57+
metadata.add(createItem("com.b", "type"));
58+
metadata.add(createItem("com.c", "type"));
59+
metadata.add(createItem("com.a", "type"));
60+
61+
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
62+
PropertiesMarshaller.write(metadata, outputStream);
63+
String contents = new String(outputStream.toByteArray(), StandardCharsets.ISO_8859_1);
64+
assertThat(contents.split(System.lineSeparator())).containsExactly("com.a=type", "com.b=type", "com.c=type");
65+
}
66+
5367
private static ItemMetadata createItem(String type, String... stereotypes) {
5468
return new ItemMetadata(type, new HashSet<>(Arrays.asList(stereotypes)));
5569
}

0 commit comments

Comments
 (0)