diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/external-config.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/external-config.adoc index 13a964897bf9..fb147302fd73 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/external-config.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/external-config.adoc @@ -498,7 +498,7 @@ For example, the following file has two logical documents: on-cloud-platform: "kubernetes" ---- -For `application.properties` files a special `#---` comment is used to mark the document splits: +For `application.properties` files a special `#---` or `!---` comment is used to mark the document splits: [source,properties,indent=0,subs="verbatim"] ---- @@ -509,7 +509,7 @@ For `application.properties` files a special `#---` comment is used to mark the ---- NOTE: Property file separators must not have any leading whitespace and must have exactly three hyphen characters. -The lines immediately before and after the separator must not be comments. +The lines immediately before and after the separator must not be same comment prefix. TIP: Multi-document property files are often used in conjunction with activation properties such as `spring.config.activate.on-profile`. See the <> for details. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedPropertiesLoader.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedPropertiesLoader.java index adf07a191b2f..4d6706920790 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedPropertiesLoader.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedPropertiesLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,6 +42,7 @@ * @author Madhura Bhave * @author Phillip Webb * @author Thiago Hirata + * @author Guirong Hu */ class OriginTrackedPropertiesLoader { @@ -78,7 +79,8 @@ List load(boolean expandLists) throws IOException { StringBuilder buffer = new StringBuilder(); try (CharacterReader reader = new CharacterReader(this.resource)) { while (reader.read()) { - if (reader.isPoundCharacter()) { + if (reader.isCommentPrefixCharacter()) { + char commentPrefixCharacter = reader.getCharacter(); if (isNewDocument(reader)) { if (!document.isEmpty()) { documents.add(document); @@ -89,12 +91,12 @@ List load(boolean expandLists) throws IOException { if (document.isEmpty() && !documents.isEmpty()) { document = documents.remove(documents.size() - 1); } - reader.setLastLineComment(true); + reader.setLastLineCommentPrefixCharacter(commentPrefixCharacter); reader.skipComment(); } } else { - reader.setLastLineComment(false); + reader.setLastLineCommentPrefixCharacter(-1); loadKeyAndValue(expandLists, document, reader, buffer); } } @@ -161,10 +163,10 @@ private OriginTrackedValue loadValue(StringBuilder buffer, CharacterReader reade } private boolean isNewDocument(CharacterReader reader) throws IOException { - if (reader.isLastLineComment()) { + if (reader.isSameLastLineCommentPrefix()) { return false; } - boolean result = reader.getLocation().getColumn() == 0 && reader.isPoundCharacter(); + boolean result = reader.getLocation().getColumn() == 0; result = result && readAndExpect(reader, reader::isHyphenCharacter); result = result && readAndExpect(reader, reader::isHyphenCharacter); result = result && readAndExpect(reader, reader::isHyphenCharacter); @@ -196,7 +198,7 @@ private static class CharacterReader implements Closeable { private int character; - private boolean lastLineComment; + private int lastLineCommentPrefixCharacter; CharacterReader(Resource resource) throws IOException { this.reader = new LineNumberReader( @@ -209,20 +211,11 @@ public void close() throws IOException { } boolean read() throws IOException { - return read(false); - } - - boolean read(boolean wrappedLine) throws IOException { this.escaped = false; this.character = this.reader.read(); this.columnNumber++; if (this.columnNumber == 0) { skipWhitespace(); - if (!wrappedLine) { - if (this.character == '!') { - skipComment(); - } - } } if (this.character == '\\') { this.escaped = true; @@ -241,12 +234,8 @@ private void skipWhitespace() throws IOException { } } - private void setLastLineComment(boolean lastLineComment) { - this.lastLineComment = lastLineComment; - } - - private boolean isLastLineComment() { - return this.lastLineComment; + private void setLastLineCommentPrefixCharacter(int lastLineCommentPrefixCharacter) { + this.lastLineCommentPrefixCharacter = lastLineCommentPrefixCharacter; } private void skipComment() throws IOException { @@ -264,7 +253,7 @@ private void readEscaped() throws IOException { } else if (this.character == '\n') { this.columnNumber = -1; - read(true); + read(); } else if (this.character == 'u') { readUnicode(); @@ -318,8 +307,12 @@ Location getLocation() { return new Location(this.reader.getLineNumber(), this.columnNumber); } - boolean isPoundCharacter() { - return this.character == '#'; + boolean isSameLastLineCommentPrefix() { + return this.lastLineCommentPrefixCharacter == this.character; + } + + boolean isCommentPrefixCharacter() { + return this.character == '#' || this.character == '!'; } boolean isHyphenCharacter() { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/env/OriginTrackedPropertiesLoaderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/env/OriginTrackedPropertiesLoaderTests.java index 13d1af165ce4..5a591c4de28f 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/env/OriginTrackedPropertiesLoaderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/env/OriginTrackedPropertiesLoaderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -183,33 +183,70 @@ void getImmediateMultiline() { } @Test - void loadWhenMultiDocumentWithoutWhitespaceLoadsMultiDoc() throws IOException { + void loadWhenMultiDocumentWithPoundPrefixAndWithoutWhitespaceLoadsMultiDoc() throws IOException { String content = "a=a\n#---\nb=b"; List loaded = new OriginTrackedPropertiesLoader(new ByteArrayResource(content.getBytes())).load(); assertThat(loaded).hasSize(2); } @Test - void loadWhenMultiDocumentWithLeadingWhitespaceLoadsSingleDoc() throws IOException { + void loadWhenMultiDocumentWithExclamationPrefixAndWithoutWhitespaceLoadsMultiDoc() throws IOException { + String content = "a=a\n!---\nb=b"; + List loaded = new OriginTrackedPropertiesLoader(new ByteArrayResource(content.getBytes())).load(); + assertThat(loaded).hasSize(2); + } + + @Test + void loadWhenMultiDocumentWithPoundPrefixAndLeadingWhitespaceLoadsSingleDoc() throws IOException { String content = "a=a\n \t#---\nb=b"; List loaded = new OriginTrackedPropertiesLoader(new ByteArrayResource(content.getBytes())).load(); assertThat(loaded).hasSize(1); } @Test - void loadWhenMultiDocumentWithTrailingWhitespaceLoadsMultiDoc() throws IOException { + void loadWhenMultiDocumentWithExclamationPrefixAndLeadingWhitespaceLoadsSingleDoc() throws IOException { + String content = "a=a\n \t!---\nb=b"; + List loaded = new OriginTrackedPropertiesLoader(new ByteArrayResource(content.getBytes())).load(); + assertThat(loaded).hasSize(1); + } + + @Test + void loadWhenMultiDocumentWithPoundPrefixAndTrailingWhitespaceLoadsMultiDoc() throws IOException { String content = "a=a\n#--- \t \nb=b"; List loaded = new OriginTrackedPropertiesLoader(new ByteArrayResource(content.getBytes())).load(); assertThat(loaded).hasSize(2); } @Test - void loadWhenMultiDocumentWithTrailingCharsLoadsSingleDoc() throws IOException { + void loadWhenMultiDocumentWithExclamationPrefixAndTrailingWhitespaceLoadsMultiDoc() throws IOException { + String content = "a=a\n!--- \t \nb=b"; + List loaded = new OriginTrackedPropertiesLoader(new ByteArrayResource(content.getBytes())).load(); + assertThat(loaded).hasSize(2); + } + + @Test + void loadWhenMultiDocumentWithPoundPrefixAndTrailingCharsLoadsSingleDoc() throws IOException { String content = "a=a\n#--- \tcomment\nb=b"; List loaded = new OriginTrackedPropertiesLoader(new ByteArrayResource(content.getBytes())).load(); assertThat(loaded).hasSize(1); } + @Test + void loadWhenMultiDocumentWithExclamationPrefixAndTrailingCharsLoadsSingleDoc() throws IOException { + String content = "a=a\n!--- \tcomment\nb=b"; + List loaded = new OriginTrackedPropertiesLoader(new ByteArrayResource(content.getBytes())).load(); + assertThat(loaded).hasSize(1); + } + + @Test + void loadWhenMultiDocumentSeparatorPrefixDifferentFromCommentPrefixLoadsMultiDoc() throws IOException { + String[] contents = new String[] { "a=a\n# comment\n!---\nb=b", "a=a\n! comment\n#---\nb=b" }; + for (String content : contents) { + List loaded = new OriginTrackedPropertiesLoader(new ByteArrayResource(content.getBytes())).load(); + assertThat(loaded).hasSize(2); + } + } + @Test void getPropertyWithWhitespaceAfterKey() { OriginTrackedValue value = getFromFirst("bar"); diff --git a/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/env/existing-non-multi-document.properties b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/env/existing-non-multi-document.properties index b5e003745cb6..faab368e3723 100644 --- a/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/env/existing-non-multi-document.properties +++ b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/env/existing-non-multi-document.properties @@ -14,3 +14,19 @@ boot=bar #--- bar=ok + +!--- +! Test +!--- + +ok=well + +!--- +! Test + +well=hello + +! Test +!--- + +hello=world