Skip to content

Commit 9e9eb90

Browse files
mbhavephilwebb
andcommitted
Add multi-document properties file support
Update `OriginTrackedPropertiesLoader` so that it can support multi-document properties files. These are similar to multi-document YAML files but use `#---` as the separator. Closes gh-22495 Co-authored-by: Phillip Webb <[email protected]>
1 parent 945e5b9 commit 9e9eb90

File tree

7 files changed

+203
-63
lines changed

7 files changed

+203
-63
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedPropertiesLoader.java

Lines changed: 76 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2020 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.
@@ -21,8 +21,11 @@
2121
import java.io.InputStreamReader;
2222
import java.io.LineNumberReader;
2323
import java.nio.charset.StandardCharsets;
24+
import java.util.ArrayList;
2425
import java.util.LinkedHashMap;
26+
import java.util.List;
2527
import java.util.Map;
28+
import java.util.function.BooleanSupplier;
2629

2730
import org.springframework.boot.origin.Origin;
2831
import org.springframework.boot.origin.OriginTrackedValue;
@@ -59,7 +62,7 @@ class OriginTrackedPropertiesLoader {
5962
* @return the loaded properties
6063
* @throws IOException on read error
6164
*/
62-
Map<String, OriginTrackedValue> load() throws IOException {
65+
List<Document> load() throws IOException {
6366
return load(true);
6467
}
6568

@@ -70,18 +73,30 @@ Map<String, OriginTrackedValue> load() throws IOException {
7073
* @return the loaded properties
7174
* @throws IOException on read error
7275
*/
73-
Map<String, OriginTrackedValue> load(boolean expandLists) throws IOException {
76+
List<Document> load(boolean expandLists) throws IOException {
77+
List<Document> result = new ArrayList<>();
78+
Document document = new Document();
7479
try (CharacterReader reader = new CharacterReader(this.resource)) {
75-
Map<String, OriginTrackedValue> result = new LinkedHashMap<>();
7680
StringBuilder buffer = new StringBuilder();
7781
while (reader.read()) {
82+
if (reader.getCharacter() == '#') {
83+
if (isNewDocument(reader)) {
84+
if (!document.isEmpty()) {
85+
result.add(document);
86+
}
87+
document = new Document();
88+
}
89+
else {
90+
reader.skipComment();
91+
}
92+
}
7893
String key = loadKey(buffer, reader).trim();
7994
if (expandLists && key.endsWith("[]")) {
8095
key = key.substring(0, key.length() - 2);
8196
int index = 0;
8297
do {
8398
OriginTrackedValue value = loadValue(buffer, reader, true);
84-
put(result, key + "[" + (index++) + "]", value);
99+
document.put(key + "[" + (index++) + "]", value);
85100
if (!reader.isEndOfLine()) {
86101
reader.read();
87102
}
@@ -90,17 +105,15 @@ Map<String, OriginTrackedValue> load(boolean expandLists) throws IOException {
90105
}
91106
else {
92107
OriginTrackedValue value = loadValue(buffer, reader, false);
93-
put(result, key, value);
108+
document.put(key, value);
94109
}
95110
}
96-
return result;
97-
}
98-
}
99111

100-
private void put(Map<String, OriginTrackedValue> result, String key, OriginTrackedValue value) {
101-
if (!key.isEmpty()) {
102-
result.put(key, value);
103112
}
113+
if (!document.isEmpty() && !result.contains(document)) {
114+
result.add(document);
115+
}
116+
return result;
104117
}
105118

106119
private String loadKey(StringBuilder buffer, CharacterReader reader) throws IOException {
@@ -136,6 +149,20 @@ private OriginTrackedValue loadValue(StringBuilder buffer, CharacterReader reade
136149
return OriginTrackedValue.of(buffer.toString(), origin);
137150
}
138151

152+
boolean isNewDocument(CharacterReader reader) throws IOException {
153+
boolean result = reader.isPoundCharacter();
154+
result = result && readAndExpect(reader, reader::isHyphenCharacter);
155+
result = result && readAndExpect(reader, reader::isHyphenCharacter);
156+
result = result && readAndExpect(reader, reader::isHyphenCharacter);
157+
result = result && readAndExpect(reader, reader::isEndOfLine);
158+
return result;
159+
}
160+
161+
private boolean readAndExpect(CharacterReader reader, BooleanSupplier check) throws IOException {
162+
reader.read();
163+
return check.getAsBoolean();
164+
}
165+
139166
/**
140167
* Reads characters from the source resource, taking care of skipping comments,
141168
* handling multi-line values and tracking {@code '\'} escapes.
@@ -173,7 +200,9 @@ boolean read(boolean wrappedLine) throws IOException {
173200
if (this.columnNumber == 0) {
174201
skipLeadingWhitespace();
175202
if (!wrappedLine) {
176-
skipComment();
203+
if (this.character == '!') {
204+
skipComment();
205+
}
177206
}
178207
}
179208
if (this.character == '\\') {
@@ -194,13 +223,10 @@ private void skipLeadingWhitespace() throws IOException {
194223
}
195224

196225
private void skipComment() throws IOException {
197-
if (this.character == '#' || this.character == '!') {
198-
while (this.character != '\n' && this.character != -1) {
199-
this.character = this.reader.read();
200-
}
201-
this.columnNumber = -1;
202-
read();
226+
while (this.character != '\n' && this.character != -1) {
227+
this.character = this.reader.read();
203228
}
229+
this.columnNumber = -1;
204230
}
205231

206232
private void readEscaped() throws IOException {
@@ -265,6 +291,37 @@ Location getLocation() {
265291
return new Location(this.reader.getLineNumber(), this.columnNumber);
266292
}
267293

294+
boolean isPoundCharacter() {
295+
return this.character == '#';
296+
}
297+
298+
boolean isHyphenCharacter() {
299+
return this.character == '-';
300+
}
301+
302+
}
303+
304+
/**
305+
* A single document within the properties file.
306+
*/
307+
static class Document {
308+
309+
private final Map<String, OriginTrackedValue> values = new LinkedHashMap<>();
310+
311+
void put(String key, OriginTrackedValue value) {
312+
if (!key.isEmpty()) {
313+
this.values.put(key, value);
314+
}
315+
}
316+
317+
boolean isEmpty() {
318+
return this.values.isEmpty();
319+
}
320+
321+
Map<String, OriginTrackedValue> asMap() {
322+
return this.values;
323+
}
324+
268325
}
269326

270327
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/PropertiesPropertySourceLoader.java

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2020 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.
@@ -17,10 +17,12 @@
1717
package org.springframework.boot.env;
1818

1919
import java.io.IOException;
20+
import java.util.ArrayList;
2021
import java.util.Collections;
2122
import java.util.List;
2223
import java.util.Map;
2324

25+
import org.springframework.boot.env.OriginTrackedPropertiesLoader.Document;
2426
import org.springframework.core.env.PropertySource;
2527
import org.springframework.core.io.Resource;
2628
import org.springframework.core.io.support.PropertiesLoaderUtils;
@@ -44,21 +46,31 @@ public String[] getFileExtensions() {
4446

4547
@Override
4648
public List<PropertySource<?>> load(String name, Resource resource) throws IOException {
47-
Map<String, ?> properties = loadProperties(resource);
49+
List<Map<String, ?>> properties = loadProperties(resource);
4850
if (properties.isEmpty()) {
4951
return Collections.emptyList();
5052
}
51-
return Collections
52-
.singletonList(new OriginTrackedMapPropertySource(name, Collections.unmodifiableMap(properties), true));
53+
List<PropertySource<?>> propertySources = new ArrayList<>(properties.size());
54+
for (int i = 0; i < properties.size(); i++) {
55+
String documentNumber = (properties.size() != 1) ? " (document #" + i + ")" : "";
56+
propertySources.add(new OriginTrackedMapPropertySource(name + documentNumber,
57+
Collections.unmodifiableMap(properties.get(i)), true));
58+
}
59+
return propertySources;
5360
}
5461

5562
@SuppressWarnings({ "unchecked", "rawtypes" })
56-
private Map<String, ?> loadProperties(Resource resource) throws IOException {
63+
private List<Map<String, ?>> loadProperties(Resource resource) throws IOException {
5764
String filename = resource.getFilename();
65+
List<Map<String, ?>> result = new ArrayList<>();
5866
if (filename != null && filename.endsWith(XML_FILE_EXTENSION)) {
59-
return (Map) PropertiesLoaderUtils.loadProperties(resource);
67+
result.add((Map) PropertiesLoaderUtils.loadProperties(resource));
68+
}
69+
else {
70+
List<Document> documents = new OriginTrackedPropertiesLoader(resource).load();
71+
documents.forEach((document) -> result.add(document.asMap()));
6072
}
61-
return new OriginTrackedPropertiesLoader(resource).load();
73+
return result;
6274
}
6375

6476
}

0 commit comments

Comments
 (0)