Skip to content

Commit 0493355

Browse files
committed
Use deterministic order for configuration properties metadata
This commit updates the annotation processor to write metadata in a consistent way. Groups, properties and hints are written and each item is ordered alphabetically based on its name. Also, deprecated items are written last. Closes gh-14347
1 parent d3ecd02 commit 0493355

File tree

6 files changed

+118
-61
lines changed

6 files changed

+118
-61
lines changed

spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/json-shade/java/org/springframework/boot/configurationprocessor/json/JSONObject.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818

1919
import java.util.ArrayList;
2020
import java.util.Collection;
21-
import java.util.HashMap;
2221
import java.util.Iterator;
22+
import java.util.LinkedHashMap;
2323
import java.util.Map;
2424

2525
// Note: this class was written without inspecting the non-free org.json source code.
@@ -111,7 +111,7 @@ public String toString() {
111111
* Creates a {@code JSONObject} with no name/value mappings.
112112
*/
113113
public JSONObject() {
114-
this.nameValuePairs = new HashMap<>();
114+
this.nameValuePairs = new LinkedHashMap<>();
115115
}
116116

117117
/**

spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JSONOrderedObject.java

Lines changed: 0 additions & 48 deletions
This file was deleted.

spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonConverter.java

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818

1919
import java.lang.reflect.Array;
2020
import java.util.Collection;
21+
import java.util.Comparator;
22+
import java.util.List;
2123
import java.util.Map;
24+
import java.util.stream.Collectors;
2225

2326
import org.springframework.boot.configurationprocessor.json.JSONArray;
2427
import org.springframework.boot.configurationprocessor.json.JSONObject;
@@ -32,10 +35,15 @@
3235
*/
3336
class JsonConverter {
3437

38+
private static final ItemMetadataComparator ITEM_COMPARATOR = new ItemMetadataComparator();
39+
3540
public JSONArray toJsonArray(ConfigurationMetadata metadata, ItemType itemType)
3641
throws Exception {
3742
JSONArray jsonArray = new JSONArray();
38-
for (ItemMetadata item : metadata.getItems()) {
43+
List<ItemMetadata> items = metadata.getItems().stream()
44+
.filter((item) -> item.isOfItemType(itemType)).sorted(ITEM_COMPARATOR)
45+
.collect(Collectors.toList());
46+
for (ItemMetadata item : items) {
3947
if (item.isOfItemType(itemType)) {
4048
jsonArray.put(toJsonObject(item));
4149
}
@@ -52,7 +60,7 @@ public JSONArray toJsonArray(Collection<ItemHint> hints) throws Exception {
5260
}
5361

5462
public JSONObject toJsonObject(ItemMetadata item) throws Exception {
55-
JSONObject jsonObject = new JSONOrderedObject();
63+
JSONObject jsonObject = new JSONObject();
5664
jsonObject.put("name", item.getName());
5765
putIfPresent(jsonObject, "type", item.getType());
5866
putIfPresent(jsonObject, "description", item.getDescription());
@@ -81,7 +89,7 @@ public JSONObject toJsonObject(ItemMetadata item) throws Exception {
8189
}
8290

8391
private JSONObject toJsonObject(ItemHint hint) throws Exception {
84-
JSONObject jsonObject = new JSONOrderedObject();
92+
JSONObject jsonObject = new JSONObject();
8593
jsonObject.put("name", hint.getName());
8694
if (!hint.getValues().isEmpty()) {
8795
jsonObject.put("values", getItemHintValues(hint));
@@ -101,7 +109,7 @@ private JSONArray getItemHintValues(ItemHint hint) throws Exception {
101109
}
102110

103111
private JSONObject getItemHintValue(ItemHint.ValueHint value) throws Exception {
104-
JSONObject result = new JSONOrderedObject();
112+
JSONObject result = new JSONObject();
105113
putHintValue(result, value.getValue());
106114
putIfPresent(result, "description", value.getDescription());
107115
return result;
@@ -117,10 +125,10 @@ private JSONArray getItemHintProviders(ItemHint hint) throws Exception {
117125

118126
private JSONObject getItemHintProvider(ItemHint.ValueProvider provider)
119127
throws Exception {
120-
JSONObject result = new JSONOrderedObject();
128+
JSONObject result = new JSONObject();
121129
result.put("name", provider.getName());
122130
if (provider.getParameters() != null && !provider.getParameters().isEmpty()) {
123-
JSONObject parameters = new JSONOrderedObject();
131+
JSONObject parameters = new JSONObject();
124132
for (Map.Entry<String, Object> entry : provider.getParameters().entrySet()) {
125133
parameters.put(entry.getKey(), extractItemValue(entry.getValue()));
126134
}
@@ -160,4 +168,34 @@ private Object extractItemValue(Object value) {
160168
return defaultValue;
161169
}
162170

171+
private static class ItemMetadataComparator implements Comparator<ItemMetadata> {
172+
173+
@Override
174+
public int compare(ItemMetadata o1, ItemMetadata o2) {
175+
if (o1.isOfItemType(ItemType.GROUP)) {
176+
return compareGroup(o1, o2);
177+
}
178+
return compareProperty(o1, o2);
179+
}
180+
181+
private int compareGroup(ItemMetadata o1, ItemMetadata o2) {
182+
return o1.getName().compareTo(o2.getName());
183+
}
184+
185+
private int compareProperty(ItemMetadata o1, ItemMetadata o2) {
186+
if (isDeprecated(o1) && !isDeprecated(o2)) {
187+
return 1;
188+
}
189+
if (isDeprecated(o2) && !isDeprecated(o1)) {
190+
return -1;
191+
}
192+
return o1.getName().compareTo(o2.getName());
193+
}
194+
195+
private boolean isDeprecated(ItemMetadata item) {
196+
return item.getDeprecation() != null;
197+
}
198+
199+
}
200+
163201
}

spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshaller.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public class JsonMarshaller {
4545
public void write(ConfigurationMetadata metadata, OutputStream outputStream)
4646
throws IOException {
4747
try {
48-
JSONObject object = new JSONOrderedObject();
48+
JSONObject object = new JSONObject();
4949
JsonConverter converter = new JsonConverter();
5050
object.put("groups", converter.toJsonArray(metadata, ItemType.GROUP));
5151
object.put("properties", converter.toJsonArray(metadata, ItemType.PROPERTY));

spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import java.time.Duration;
2323
import java.util.Arrays;
2424
import java.util.Collections;
25+
import java.util.List;
26+
import java.util.stream.Collectors;
2527

2628
import org.junit.Before;
2729
import org.junit.Rule;
@@ -822,9 +824,26 @@ public void mergeExistingPropertyWithSeveralCandidates() throws Exception {
822824
ConfigurationMetadata metadata = compile(SimpleProperties.class,
823825
SimpleConflictingProperties.class);
824826
assertThat(metadata.getItems()).hasSize(6);
825-
assertThat(metadata).has(Metadata.withProperty("simple.flag", Boolean.class)
826-
.fromSource(SimpleProperties.class).withDescription("A simple flag.")
827-
.withDeprecation(null, null).withDefaultValue(true));
827+
List<ItemMetadata> items = metadata.getItems().stream()
828+
.filter((item) -> item.getName().equals("simple.flag"))
829+
.collect(Collectors.toList());
830+
assertThat(items).hasSize(2);
831+
ItemMetadata matchingProperty = items.stream()
832+
.filter((item) -> item.getType().equals(Boolean.class.getName()))
833+
.findFirst().orElse(null);
834+
assertThat(matchingProperty).isNotNull();
835+
assertThat(matchingProperty.getDefaultValue()).isEqualTo(true);
836+
assertThat(matchingProperty.getSourceType())
837+
.isEqualTo(SimpleProperties.class.getName());
838+
assertThat(matchingProperty.getDescription()).isEqualTo("A simple flag.");
839+
ItemMetadata nonMatchingProperty = items.stream()
840+
.filter((item) -> item.getType().equals(String.class.getName()))
841+
.findFirst().orElse(null);
842+
assertThat(nonMatchingProperty).isNotNull();
843+
assertThat(nonMatchingProperty.getDefaultValue()).isEqualTo("hello");
844+
assertThat(nonMatchingProperty.getSourceType())
845+
.isEqualTo(SimpleConflictingProperties.class.getName());
846+
assertThat(nonMatchingProperty.getDescription()).isNull();
828847
}
829848

830849
@Test

spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshallerTests.java

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2017 the original author or authors.
2+
* Copyright 2012-2018 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.
@@ -18,6 +18,7 @@
1818

1919
import java.io.ByteArrayInputStream;
2020
import java.io.ByteArrayOutputStream;
21+
import java.io.IOException;
2122
import java.io.InputStream;
2223
import java.util.Arrays;
2324
import java.util.Collections;
@@ -82,4 +83,51 @@ public void marshallAndUnmarshal() throws Exception {
8283
.withProvider("second"));
8384
}
8485

86+
@Test
87+
public void marshallOrderItems() throws IOException {
88+
ConfigurationMetadata metadata = new ConfigurationMetadata();
89+
metadata.add(ItemHint.newHint("fff"));
90+
metadata.add(ItemHint.newHint("eee"));
91+
metadata.add(ItemMetadata.newProperty("com.example.bravo", "bbb", null, null,
92+
null, null, null, null));
93+
metadata.add(ItemMetadata.newProperty("com.example.bravo", "aaa", null, null,
94+
null, null, null, null));
95+
metadata.add(ItemMetadata.newProperty("com.example.alpha", "ddd", null, null,
96+
null, null, null, null));
97+
metadata.add(ItemMetadata.newProperty("com.example.alpha", "ccc", null, null,
98+
null, null, null, null));
99+
metadata.add(ItemMetadata.newGroup("com.acme.bravo",
100+
"com.example.AnotherTestProperties", null, null));
101+
metadata.add(ItemMetadata.newGroup("com.acme.alpha", "com.example.TestProperties",
102+
null, null));
103+
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
104+
JsonMarshaller marshaller = new JsonMarshaller();
105+
marshaller.write(metadata, outputStream);
106+
String json = new String(outputStream.toByteArray());
107+
assertThat(json).containsSubsequence("\"groups\"", "\"com.acme.alpha\"",
108+
"\"com.acme.bravo\"", "\"properties\"", "\"com.example.alpha.ccc\"",
109+
"\"com.example.alpha.ddd\"", "\"com.example.bravo.aaa\"",
110+
"\"com.example.bravo.bbb\"", "\"hints\"", "\"eee\"", "\"fff\"");
111+
}
112+
113+
@Test
114+
public void marshallPutDeprecatedItemsAtTheEnd() throws IOException {
115+
ConfigurationMetadata metadata = new ConfigurationMetadata();
116+
metadata.add(ItemMetadata.newProperty("com.example.bravo", "bbb", null, null,
117+
null, null, null, null));
118+
metadata.add(ItemMetadata.newProperty("com.example.bravo", "aaa", null, null,
119+
null, null, null, new ItemDeprecation(null, null, "warning")));
120+
metadata.add(ItemMetadata.newProperty("com.example.alpha", "ddd", null, null,
121+
null, null, null, null));
122+
metadata.add(ItemMetadata.newProperty("com.example.alpha", "ccc", null, null,
123+
null, null, null, new ItemDeprecation(null, null, "warning")));
124+
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
125+
JsonMarshaller marshaller = new JsonMarshaller();
126+
marshaller.write(metadata, outputStream);
127+
String json = new String(outputStream.toByteArray());
128+
assertThat(json).containsSubsequence("\"properties\"",
129+
"\"com.example.alpha.ddd\"", "\"com.example.bravo.bbb\"",
130+
"\"com.example.alpha.ccc\"", "\"com.example.bravo.aaa\"");
131+
}
132+
85133
}

0 commit comments

Comments
 (0)