18
18
19
19
import java .io .IOException ;
20
20
import java .io .InputStream ;
21
- import java .io .InputStreamReader ;
22
21
import java .io .OutputStream ;
23
22
import java .nio .charset .StandardCharsets ;
24
23
import java .util .ArrayList ;
24
+ import java .util .Arrays ;
25
25
import java .util .HashMap ;
26
26
import java .util .Iterator ;
27
27
import java .util .List ;
28
28
import java .util .Map ;
29
+ import java .util .Set ;
30
+ import java .util .TreeSet ;
29
31
30
32
import org .springframework .boot .configurationprocessor .json .JSONArray ;
31
33
import org .springframework .boot .configurationprocessor .json .JSONObject ;
32
34
import org .springframework .boot .configurationprocessor .metadata .ItemMetadata .ItemType ;
33
35
34
36
/**
35
- * Marshaller to write {@link ConfigurationMetadata} as JSON.
37
+ * Marshaller to read and write {@link ConfigurationMetadata} as JSON.
36
38
*
37
39
* @author Stephane Nicoll
38
40
* @author Phillip Webb
41
+ * @author Moritz Halbritter
39
42
* @since 1.2.0
40
43
*/
41
44
public class JsonMarshaller {
42
45
43
- private static final int BUFFER_SIZE = 4098 ;
44
-
45
46
public void write (ConfigurationMetadata metadata , OutputStream outputStream ) throws IOException {
46
47
try {
47
48
JSONObject object = new JSONObject ();
@@ -65,42 +66,53 @@ public void write(ConfigurationMetadata metadata, OutputStream outputStream) thr
65
66
public ConfigurationMetadata read (InputStream inputStream ) throws Exception {
66
67
ConfigurationMetadata metadata = new ConfigurationMetadata ();
67
68
JSONObject object = new JSONObject (toString (inputStream ));
69
+ JsonPath path = JsonPath .root ();
70
+ checkAllowedKeys (object , path , "groups" , "properties" , "hints" );
68
71
JSONArray groups = object .optJSONArray ("groups" );
69
72
if (groups != null ) {
70
73
for (int i = 0 ; i < groups .length (); i ++) {
71
- metadata .add (toItemMetadata ((JSONObject ) groups .get (i ), ItemType .GROUP ));
74
+ metadata
75
+ .add (toItemMetadata ((JSONObject ) groups .get (i ), path .resolve ("groups" ).index (i ), ItemType .GROUP ));
72
76
}
73
77
}
74
78
JSONArray properties = object .optJSONArray ("properties" );
75
79
if (properties != null ) {
76
80
for (int i = 0 ; i < properties .length (); i ++) {
77
- metadata .add (toItemMetadata ((JSONObject ) properties .get (i ), ItemType .PROPERTY ));
81
+ metadata .add (toItemMetadata ((JSONObject ) properties .get (i ), path .resolve ("properties" ).index (i ),
82
+ ItemType .PROPERTY ));
78
83
}
79
84
}
80
85
JSONArray hints = object .optJSONArray ("hints" );
81
86
if (hints != null ) {
82
87
for (int i = 0 ; i < hints .length (); i ++) {
83
- metadata .add (toItemHint ((JSONObject ) hints .get (i )));
88
+ metadata .add (toItemHint ((JSONObject ) hints .get (i ), path . resolve ( "hints" ). index ( i ) ));
84
89
}
85
90
}
86
91
return metadata ;
87
92
}
88
93
89
- private ItemMetadata toItemMetadata (JSONObject object , ItemType itemType ) throws Exception {
94
+ private ItemMetadata toItemMetadata (JSONObject object , JsonPath path , ItemType itemType ) throws Exception {
95
+ switch (itemType ) {
96
+ case GROUP -> checkAllowedKeys (object , path , "name" , "type" , "description" , "sourceType" , "sourceMethod" );
97
+ case PROPERTY -> checkAllowedKeys (object , path , "name" , "type" , "description" , "sourceType" , "defaultValue" ,
98
+ "deprecation" , "deprecated" );
99
+ }
90
100
String name = object .getString ("name" );
91
101
String type = object .optString ("type" , null );
92
102
String description = object .optString ("description" , null );
93
103
String sourceType = object .optString ("sourceType" , null );
94
104
String sourceMethod = object .optString ("sourceMethod" , null );
95
105
Object defaultValue = readItemValue (object .opt ("defaultValue" ));
96
- ItemDeprecation deprecation = toItemDeprecation (object );
106
+ ItemDeprecation deprecation = toItemDeprecation (object , path );
97
107
return new ItemMetadata (itemType , name , null , type , sourceType , sourceMethod , description , defaultValue ,
98
108
deprecation );
99
109
}
100
110
101
- private ItemDeprecation toItemDeprecation (JSONObject object ) throws Exception {
111
+ private ItemDeprecation toItemDeprecation (JSONObject object , JsonPath path ) throws Exception {
102
112
if (object .has ("deprecation" )) {
103
113
JSONObject deprecationJsonObject = object .getJSONObject ("deprecation" );
114
+ checkAllowedKeys (deprecationJsonObject , path .resolve ("deprecation" ), "level" , "reason" , "replacement" ,
115
+ "since" );
104
116
ItemDeprecation deprecation = new ItemDeprecation ();
105
117
deprecation .setLevel (deprecationJsonObject .optString ("level" , null ));
106
118
deprecation .setReason (deprecationJsonObject .optString ("reason" , null ));
@@ -111,32 +123,35 @@ private ItemDeprecation toItemDeprecation(JSONObject object) throws Exception {
111
123
return object .optBoolean ("deprecated" ) ? new ItemDeprecation () : null ;
112
124
}
113
125
114
- private ItemHint toItemHint (JSONObject object ) throws Exception {
126
+ private ItemHint toItemHint (JSONObject object , JsonPath path ) throws Exception {
127
+ checkAllowedKeys (object , path , "name" , "values" , "providers" );
115
128
String name = object .getString ("name" );
116
129
List <ItemHint .ValueHint > values = new ArrayList <>();
117
130
if (object .has ("values" )) {
118
131
JSONArray valuesArray = object .getJSONArray ("values" );
119
132
for (int i = 0 ; i < valuesArray .length (); i ++) {
120
- values .add (toValueHint ((JSONObject ) valuesArray .get (i )));
133
+ values .add (toValueHint ((JSONObject ) valuesArray .get (i ), path . resolve ( "values" ). index ( i ) ));
121
134
}
122
135
}
123
136
List <ItemHint .ValueProvider > providers = new ArrayList <>();
124
137
if (object .has ("providers" )) {
125
138
JSONArray providersObject = object .getJSONArray ("providers" );
126
139
for (int i = 0 ; i < providersObject .length (); i ++) {
127
- providers .add (toValueProvider ((JSONObject ) providersObject .get (i )));
140
+ providers .add (toValueProvider ((JSONObject ) providersObject .get (i ), path . resolve ( "providers" ). index ( i ) ));
128
141
}
129
142
}
130
143
return new ItemHint (name , values , providers );
131
144
}
132
145
133
- private ItemHint .ValueHint toValueHint (JSONObject object ) throws Exception {
146
+ private ItemHint .ValueHint toValueHint (JSONObject object , JsonPath path ) throws Exception {
147
+ checkAllowedKeys (object , path , "value" , "description" );
134
148
Object value = readItemValue (object .get ("value" ));
135
149
String description = object .optString ("description" , null );
136
150
return new ItemHint .ValueHint (value , description );
137
151
}
138
152
139
- private ItemHint .ValueProvider toValueProvider (JSONObject object ) throws Exception {
153
+ private ItemHint .ValueProvider toValueProvider (JSONObject object , JsonPath path ) throws Exception {
154
+ checkAllowedKeys (object , path , "name" , "parameters" );
140
155
String name = object .getString ("name" );
141
156
Map <String , Object > parameters = new HashMap <>();
142
157
if (object .has ("parameters" )) {
@@ -162,14 +177,48 @@ private Object readItemValue(Object value) throws Exception {
162
177
}
163
178
164
179
private String toString (InputStream inputStream ) throws IOException {
165
- StringBuilder out = new StringBuilder ();
166
- InputStreamReader reader = new InputStreamReader (inputStream , StandardCharsets .UTF_8 );
167
- char [] buffer = new char [BUFFER_SIZE ];
168
- int bytesRead ;
169
- while ((bytesRead = reader .read (buffer )) != -1 ) {
170
- out .append (buffer , 0 , bytesRead );
171
- }
172
- return out .toString ();
180
+ return new String (inputStream .readAllBytes (), StandardCharsets .UTF_8 );
181
+ }
182
+
183
+ @ SuppressWarnings ("unchecked" )
184
+ private void checkAllowedKeys (JSONObject object , JsonPath path , String ... allowedKeys ) {
185
+ Set <String > availableKeys = new TreeSet <>();
186
+ object .keys ().forEachRemaining ((key ) -> availableKeys .add ((String ) key ));
187
+ Arrays .stream (allowedKeys ).forEach (availableKeys ::remove );
188
+ if (!availableKeys .isEmpty ()) {
189
+ throw new IllegalStateException ("Expected only keys %s, but found additional keys %s. Path: %s"
190
+ .formatted (new TreeSet <>(Arrays .asList (allowedKeys )), availableKeys , path ));
191
+ }
192
+ }
193
+
194
+ private static final class JsonPath {
195
+
196
+ private final String path ;
197
+
198
+ private JsonPath (String path ) {
199
+ this .path = path ;
200
+ }
201
+
202
+ JsonPath resolve (String path ) {
203
+ if (this .path .endsWith ("." )) {
204
+ return new JsonPath (this .path + path );
205
+ }
206
+ return new JsonPath (this .path + "." + path );
207
+ }
208
+
209
+ JsonPath index (int index ) {
210
+ return resolve ("[%d]" .formatted (index ));
211
+ }
212
+
213
+ @ Override
214
+ public String toString () {
215
+ return this .path ;
216
+ }
217
+
218
+ static JsonPath root () {
219
+ return new JsonPath ("." );
220
+ }
221
+
173
222
}
174
223
175
224
}
0 commit comments