diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/PropertiesMarshaller.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/PropertiesMarshaller.java index d1fdc09367..936591b548 100644 --- a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/PropertiesMarshaller.java +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/PropertiesMarshaller.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -33,7 +33,7 @@ import java.util.Set; abstract class PropertiesMarshaller { public static void write(CandidateComponentsMetadata metadata, OutputStream out) throws IOException { - Properties props = new Properties(); + Properties props = new SortedProperties(true); metadata.getItems().forEach(m -> props.put(m.getType(), String.join(",", m.getStereotypes()))); props.store(out, ""); } diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/SortedProperties.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/SortedProperties.java new file mode 100644 index 0000000000..412e2bfb6b --- /dev/null +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/SortedProperties.java @@ -0,0 +1,159 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.index.processor; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.StringWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.Set; +import java.util.TreeSet; + +/** + * Specialization of {@link Properties} that sorts properties alphanumerically + * based on their keys. + * + *

This can be useful when storing the {@link Properties} instance in a + * properties file, since it allows such files to be generated in a repeatable + * manner with consistent ordering of properties. + * + *

Comments in generated properties files can also be optionally omitted. + * + * @author Sam Brannen + * @since 5.2 + * @see java.util.Properties + */ +@SuppressWarnings("serial") +class SortedProperties extends Properties { + + static final String EOL = System.lineSeparator(); + + private static final Comparator keyComparator = // + (key1, key2) -> String.valueOf(key1).compareTo(String.valueOf(key2)); + + private static final Comparator> entryComparator = // + Entry.comparingByKey(keyComparator); + + private final boolean omitComments; + + + /** + * Construct a new {@code SortedProperties} instance that honors the supplied + * {@code omitComments} flag. + * + * @param omitComments {@code true} if comments should be omitted when + * storing properties in a file + */ + SortedProperties(boolean omitComments) { + this.omitComments = omitComments; + } + + /** + * Construct a new {@code SortedProperties} instance with properties populated + * from the supplied {@link Properties} object and honoring the supplied + * {@code omitComments} flag. + * + *

Default properties from the supplied {@code Properties} object will + * not be copied. + * + * @param properties the {@code Properties} object from which to copy the + * initial properties + * @param omitComments {@code true} if comments should be omitted when + * storing properties in a file + */ + SortedProperties(Properties properties, boolean omitComments) { + this(omitComments); + putAll(properties); + } + + @Override + public void store(OutputStream out, String comments) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + super.store(baos, (this.omitComments ? null : comments)); + String contents = new String(baos.toByteArray(), StandardCharsets.ISO_8859_1); + for (String line : contents.split(EOL)) { + if (!this.omitComments || !line.startsWith("#")) { + out.write((line + EOL).getBytes(StandardCharsets.ISO_8859_1)); + } + } + } + + @Override + public void store(Writer writer, String comments) throws IOException { + StringWriter stringWriter = new StringWriter(); + super.store(stringWriter, (this.omitComments ? null : comments)); + String contents = stringWriter.toString(); + for (String line : contents.split(EOL)) { + if (!this.omitComments || !line.startsWith("#")) { + writer.write(line + EOL); + } + } + } + + @Override + public void storeToXML(OutputStream out, String comments) throws IOException { + super.storeToXML(out, (this.omitComments ? null : comments)); + } + + @Override + public void storeToXML(OutputStream out, String comments, String encoding) throws IOException { + super.storeToXML(out, (this.omitComments ? null : comments), encoding); + } + + /** + * Return a sorted enumeration of the keys in this {@link Properties} object. + * @see #keySet() + */ + @Override + public synchronized Enumeration keys() { + return Collections.enumeration(keySet()); + } + + /** + * Return a sorted set of the keys in this {@link Properties} object. + *

The keys will be converted to strings if necessary using + * {@link String#valueOf(Object)} and sorted alphanumerically according to + * the natural order of strings. + */ + @Override + public Set keySet() { + Set sortedKeys = new TreeSet<>(keyComparator); + sortedKeys.addAll(super.keySet()); + return Collections.synchronizedSet(sortedKeys); + } + + /** + * Return a sorted set of the entries in this {@link Properties} object. + *

The entries will be sorted based on their keys, and the keys will be + * converted to strings if necessary using {@link String#valueOf(Object)} + * and compared alphanumerically according to the natural order of strings. + */ + @Override + public Set> entrySet() { + Set> sortedEntries = new TreeSet<>(entryComparator); + sortedEntries.addAll(super.entrySet()); + return Collections.synchronizedSet(sortedEntries); + } + +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/processor/PropertiesMarshallerTests.java b/spring-context-indexer/src/test/java/org/springframework/context/index/processor/PropertiesMarshallerTests.java index 3116aecdb8..7a7a8211ff 100644 --- a/spring-context-indexer/src/test/java/org/springframework/context/index/processor/PropertiesMarshallerTests.java +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/processor/PropertiesMarshallerTests.java @@ -19,6 +19,7 @@ package org.springframework.context.index.processor; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.HashSet; @@ -27,11 +28,11 @@ import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; - /** * Tests for {@link PropertiesMarshaller}. * * @author Stephane Nicoll + * @author Vedran Pavic */ public class PropertiesMarshallerTests { @@ -50,6 +51,19 @@ public class PropertiesMarshallerTests { assertThat(readMetadata.getItems()).hasSize(2); } + @Test + public void metadataIsWrittenDeterministically() throws IOException { + CandidateComponentsMetadata metadata = new CandidateComponentsMetadata(); + metadata.add(createItem("com.b", "type")); + metadata.add(createItem("com.c", "type")); + metadata.add(createItem("com.a", "type")); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + PropertiesMarshaller.write(metadata, outputStream); + String contents = new String(outputStream.toByteArray(), StandardCharsets.ISO_8859_1); + assertThat(contents.split(System.lineSeparator())).containsExactly("com.a=type", "com.b=type", "com.c=type"); + } + private static ItemMetadata createItem(String type, String... stereotypes) { return new ItemMetadata(type, new HashSet<>(Arrays.asList(stereotypes))); }