From c39c4211df15500996142a265d7afcc31c23c6f8 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 28 May 2019 11:29:23 +0200 Subject: [PATCH] Introduce support for sorted Properties This commit introduces an internal SortedProperties class that is a specialization of java.util.Properties which sorts properties alphanumerically based on their keys. This can be useful when storing a java.util.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. An instance of SortedProperties can be created via two new createSortedProperties() factory methods in org.springframework.core.CollectionFactory. Closes gh-23018 --- .../core/CollectionFactory.java | 44 ++++ .../core/SortedProperties.java | 161 +++++++++++++ .../core/SortedPropertiesTests.java | 213 ++++++++++++++++++ 3 files changed, 418 insertions(+) create mode 100644 spring-core/src/main/java/org/springframework/core/SortedProperties.java create mode 100644 spring-core/src/test/java/org/springframework/core/SortedPropertiesTests.java diff --git a/spring-core/src/main/java/org/springframework/core/CollectionFactory.java b/spring-core/src/main/java/org/springframework/core/CollectionFactory.java index 4923464910..6df7b4daac 100644 --- a/spring-core/src/main/java/org/springframework/core/CollectionFactory.java +++ b/spring-core/src/main/java/org/springframework/core/CollectionFactory.java @@ -346,6 +346,50 @@ public final class CollectionFactory { }; } + /** + * Create a variant of {@link java.util.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. + * + * @param omitComments {@code true} if comments should be omitted when + * storing properties in a file + * @return a new {@code Properties} instance + * @since 5.2 + * @see #createSortedProperties(Properties, boolean) + */ + public static Properties createSortedProperties(boolean omitComments) { + return new SortedProperties(omitComments); + } + + /** + * Create a variant of {@link java.util.Properties} that sorts properties + * alphanumerically based on their keys. + * + *

This can be useful when storing the {@code 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. + * + *

The returned {@code Properties} instance will be populated with + * properties from the supplied {@code properties} object, but 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 + * @return a new {@code Properties} instance + * @since 5.2 + * @see #createSortedProperties(boolean) + */ + public static Properties createSortedProperties(Properties properties, boolean omitComments) { + return new SortedProperties(properties, omitComments); + } + /** * Cast the given type to a subtype of {@link Enum}. * @param enumType the enum type, never {@code null} diff --git a/spring-core/src/main/java/org/springframework/core/SortedProperties.java b/spring-core/src/main/java/org/springframework/core/SortedProperties.java new file mode 100644 index 0000000000..f1c2e8417b --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/SortedProperties.java @@ -0,0 +1,161 @@ +/* + * 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.core; + +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; + +import org.springframework.util.StringUtils; + +/** + * 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.getProperty("line.separator"); + + 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 : StringUtils.tokenizeToStringArray(contents, 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 : StringUtils.tokenizeToStringArray(contents, 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-core/src/test/java/org/springframework/core/SortedPropertiesTests.java b/spring-core/src/test/java/org/springframework/core/SortedPropertiesTests.java new file mode 100644 index 0000000000..8cd96facb6 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/SortedPropertiesTests.java @@ -0,0 +1,213 @@ +/* + * 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.core; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Properties; + +import org.junit.Test; + +import static java.util.Arrays.stream; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * Unit tests for {@link SortedProperties}. + * + * @author Sam Brannen + * @since 5.2 + */ +public class SortedPropertiesTests { + + @Test + public void keys() { + assertKeys(createSortedProps()); + } + + @Test + public void keysFromPrototype() { + assertKeys(createSortedPropsFromPrototype()); + } + + @Test + public void keySet() { + assertKeySet(createSortedProps()); + } + + @Test + public void keySetFromPrototype() { + assertKeySet(createSortedPropsFromPrototype()); + } + + @Test + public void entrySet() { + assertEntrySet(createSortedProps()); + } + + @Test + public void entrySetFromPrototype() { + assertEntrySet(createSortedPropsFromPrototype()); + } + + @Test + public void sortsPropertiesUsingOutputStream() throws IOException { + SortedProperties sortedProperties = createSortedProps(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + sortedProperties.store(baos, "custom comment"); + + String[] lines = lines(baos); + assertThat(lines).hasSize(7); + assertThat(lines[0]).isEqualTo("#custom comment"); + assertThat(lines[1]).as("timestamp").startsWith("#"); + + assertPropsAreSorted(lines); + } + + @Test + public void sortsPropertiesUsingWriter() throws IOException { + SortedProperties sortedProperties = createSortedProps(); + + StringWriter writer = new StringWriter(); + sortedProperties.store(writer, "custom comment"); + + String[] lines = lines(writer); + assertThat(lines).hasSize(7); + assertThat(lines[0]).isEqualTo("#custom comment"); + assertThat(lines[1]).as("timestamp").startsWith("#"); + + assertPropsAreSorted(lines); + } + + @Test + public void sortsPropertiesAndOmitsCommentsUsingOutputStream() throws IOException { + SortedProperties sortedProperties = createSortedProps(true); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + sortedProperties.store(baos, "custom comment"); + + String[] lines = lines(baos); + assertThat(lines).hasSize(5); + + assertPropsAreSorted(lines); + } + + @Test + public void sortsPropertiesAndOmitsCommentsUsingWriter() throws IOException { + SortedProperties sortedProperties = createSortedProps(true); + + StringWriter writer = new StringWriter(); + sortedProperties.store(writer, "custom comment"); + + String[] lines = lines(writer); + assertThat(lines).hasSize(5); + + assertPropsAreSorted(lines); + } + + @Test + public void storingAsXmlSortsPropertiesAndOmitsComments() throws IOException { + SortedProperties sortedProperties = createSortedProps(true); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + sortedProperties.storeToXML(baos, "custom comment"); + + String[] lines = lines(baos); + + assertThat(lines).containsExactly( // + "", // + "", // + "", // + "blue", // + "sweet", // + "apple", // + "medium", // + "car", // + "" // + ); + } + + private SortedProperties createSortedProps() { + return createSortedProps(false); + } + + private SortedProperties createSortedProps(boolean omitComments) { + SortedProperties sortedProperties = new SortedProperties(omitComments); + populateProperties(sortedProperties); + return sortedProperties; + } + + private SortedProperties createSortedPropsFromPrototype() { + Properties properties = new Properties(); + populateProperties(properties); + return new SortedProperties(properties, false); + } + + private void populateProperties(Properties properties) { + properties.setProperty("color", "blue"); + properties.setProperty("fragrance", "sweet"); + properties.setProperty("fruit", "apple"); + properties.setProperty("size", "medium"); + properties.setProperty("vehicle", "car"); + } + + private String[] lines(ByteArrayOutputStream baos) { + return lines(new String(baos.toByteArray(), StandardCharsets.ISO_8859_1)); + } + + private String[] lines(StringWriter writer) { + return lines(writer.toString()); + } + + private String[] lines(String input) { + return input.trim().split(SortedProperties.EOL); + } + + private void assertKeys(Properties properties) { + assertThat(Collections.list(properties.keys())) // + .containsExactly("color", "fragrance", "fruit", "size", "vehicle"); + } + + private void assertKeySet(Properties properties) { + assertThat(properties.keySet()).containsExactly("color", "fragrance", "fruit", "size", "vehicle"); + } + + private void assertEntrySet(Properties properties) { + assertThat(properties.entrySet()).containsExactly( // + entry("color", "blue"), // + entry("fragrance", "sweet"), // + entry("fruit", "apple"), // + entry("size", "medium"), // + entry("vehicle", "car") // + ); + } + + private void assertPropsAreSorted(String[] lines) { + assertThat(stream(lines).filter(s -> !s.startsWith("#"))).containsExactly( // + "color=blue", // + "fragrance=sweet", // + "fruit=apple", // + "size=medium", // + "vehicle=car"// + ); + } + +}