8231640: (prop) Canonical property storage

Reviewed-by: rriggs, smarks, dfuchs, ihse
This commit is contained in:
Jaikiran Pai 2021-09-28 05:17:31 +00:00
parent ddc262746a
commit af50772d39
4 changed files with 837 additions and 15 deletions

View File

@ -46,6 +46,7 @@ import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import jdk.internal.util.StaticProperty;
import sun.nio.cs.ISO_8859_1;
import sun.nio.cs.UTF_8;
@ -807,17 +808,25 @@ public class Properties extends Hashtable<Object,Object> {
* If the comments argument is not null, then an ASCII {@code #}
* character, the comments string, and a line separator are first written
* to the output stream. Thus, the {@code comments} can serve as an
* identifying comment. Any one of a line feed ('\n'), a carriage
* return ('\r'), or a carriage return followed immediately by a line feed
* in comments is replaced by a line separator generated by the {@code Writer}
* and if the next character in comments is not character {@code #} or
* character {@code !} then an ASCII {@code #} is written out
* after that line separator.
* identifying comment. Any one of a line feed ({@code \n}), a carriage
* return ({@code \r}), or a carriage return followed immediately by a line feed
* ({@code \r\n}) in comments is replaced by a
* {@link System#lineSeparator() line separator} and if the next
* character in comments is not character {@code #} or character {@code !} then
* an ASCII {@code #} is written out after that line separator.
* <p>
* Next, a comment line is always written, consisting of an ASCII
* {@code #} character, the current date and time (as if produced
* by the {@code toString} method of {@code Date} for the
* current time), and a line separator as generated by the {@code Writer}.
* If the {@systemProperty java.properties.date} is set on the command line
* and is non-empty (as determined by {@link String#isEmpty() String.isEmpty}),
* a comment line is written as follows.
* First, a {@code #} character is written, followed by the contents
* of the property, followed by a line separator. Any line terminator characters
* in the value of the system property are treated the same way as noted above
* for the comments argument.
* If the system property is not set or is empty, a comment line is written
* as follows.
* First, a {@code #} character is written, followed by the current date and time
* formatted as if by the {@link Date#toString() Date.toString} method,
* followed by a line separator.
* <p>
* Then every entry in this {@code Properties} table is
* written out, one per line. For each entry the key string is
@ -833,6 +842,10 @@ public class Properties extends Hashtable<Object,Object> {
* After the entries have been written, the output stream is flushed.
* The output stream remains open after this method returns.
*
* @implSpec The keys and elements are written in the natural sort order
* of the keys in the {@code entrySet()} unless {@code entrySet()} is
* overridden by a subclass to return a different value than {@code super.entrySet()}.
*
* @param writer an output character stream writer.
* @param comments a description of the property list.
* @throws IOException if writing this property list to the specified
@ -903,12 +916,25 @@ public class Properties extends Hashtable<Object,Object> {
if (comments != null) {
writeComments(bw, comments);
}
bw.write("#" + new Date().toString());
bw.newLine();
writeDateComment(bw);
synchronized (this) {
for (Map.Entry<Object, Object> e : entrySet()) {
String key = (String)e.getKey();
String val = (String)e.getValue();
@SuppressWarnings("unchecked")
Collection<Map.Entry<String, String>> entries = (Set<Map.Entry<String, String>>) (Set) entrySet();
// entrySet() can be overridden by subclasses. Here we check to see if
// the returned instance type is the one returned by the Properties.entrySet()
// implementation. If yes, then we sort those entries in the natural order
// of their key. Else, we consider that the subclassed implementation may
// potentially have returned a differently ordered entries and so we just
// use the iteration order of the returned instance.
if (entries instanceof Collections.SynchronizedSet<?> ss
&& ss.c instanceof EntrySet) {
entries = new ArrayList<>(entries);
((List<Map.Entry<String, String>>) entries).sort(Map.Entry.comparingByKey());
}
for (Map.Entry<String, String> e : entries) {
String key = e.getKey();
String val = e.getValue();
key = saveConvert(key, true, escUnicode);
/* No need to escape embedded and trailing spaces for value, hence
* pass false to flag.
@ -921,6 +947,19 @@ public class Properties extends Hashtable<Object,Object> {
bw.flush();
}
private static void writeDateComment(BufferedWriter bw) throws IOException {
// value of java.properties.date system property isn't sensitive
// and so doesn't need any security manager checks to make the value accessible
// to the callers
String sysPropVal = StaticProperty.javaPropertiesDate();
if (sysPropVal != null && !sysPropVal.isEmpty()) {
writeComments(bw, sysPropVal);
} else {
bw.write("#" + new Date());
bw.newLine();
}
}
/**
* Loads all of the properties represented by the XML document on the
* specified input stream into this properties table.

View File

@ -51,6 +51,7 @@ public final class StaticProperty {
private static final String JAVA_IO_TMPDIR;
private static final String NATIVE_ENCODING;
private static final String FILE_ENCODING;
private static final String JAVA_PROPERTIES_DATE;
private StaticProperty() {}
@ -67,6 +68,7 @@ public final class StaticProperty {
JDK_SERIAL_FILTER_FACTORY = getProperty(props, "jdk.serialFilterFactory", null);
NATIVE_ENCODING = getProperty(props, "native.encoding");
FILE_ENCODING = getProperty(props, "file.encoding");
JAVA_PROPERTIES_DATE = getProperty(props, "java.properties.date", null);
}
private static String getProperty(Properties props, String key) {
@ -227,4 +229,16 @@ public final class StaticProperty {
public static String fileEncoding() {
return FILE_ENCODING;
}
/**
* Return the {@code java.properties.date} system property.
*
* <strong>{@link SecurityManager#checkPropertyAccess} is NOT checked
* in this method.</strong>
*
* @return the {@code java.properties.date} system property
*/
public static String javaPropertiesDate() {
return JAVA_PROPERTIES_DATE;
}
}

View File

@ -0,0 +1,275 @@
/*
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
/*
* @test
* @summary tests the order in which the Properties.store() method writes out the properties
* @bug 8231640
* @run testng PropertiesStoreTest
*/
public class PropertiesStoreTest {
private static final String DATE_FORMAT_PATTERN = "EEE MMM dd HH:mm:ss zzz uuuu";
@DataProvider(name = "propsProvider")
private Object[][] createProps() {
final Properties simple = new Properties();
simple.setProperty("1", "one");
simple.setProperty("2", "two");
simple.setProperty("10", "ten");
simple.setProperty("02", "zero-two");
simple.setProperty("3", "three");
simple.setProperty("0", "zero");
simple.setProperty("00", "zero-zero");
simple.setProperty("0", "zero-again");
final Properties specialChars = new Properties();
// some special chars
simple.setProperty(" 1", "space-one");
simple.setProperty("\t 3 7 \n", "tab-space-three-space-seven-space-newline");
// add some simple chars
simple.setProperty("3", "three");
simple.setProperty("0", "zero");
final Properties overrideCallsSuper = new OverridesEntrySetCallsSuper();
overrideCallsSuper.putAll(simple);
final OverridesEntrySet overridesEntrySet = new OverridesEntrySet();
overridesEntrySet.putAll(simple);
final Properties doesNotOverrideEntrySet = new DoesNotOverrideEntrySet();
doesNotOverrideEntrySet.putAll(simple);
return new Object[][]{
{simple, naturalOrder(simple)},
{specialChars, naturalOrder(specialChars)},
{overrideCallsSuper, naturalOrder(overrideCallsSuper)},
{overridesEntrySet, overridesEntrySet.expectedKeyOrder()},
{doesNotOverrideEntrySet, naturalOrder(doesNotOverrideEntrySet)}
};
}
/**
* Tests that the {@link Properties#store(Writer, String)} API writes out the properties
* in the expected order
*/
@Test(dataProvider = "propsProvider")
public void testStoreWriterKeyOrder(final Properties props, final String[] expectedOrder) throws Exception {
// Properties.store(...) to a temp file
final Path tmpFile = Files.createTempFile("8231640", "props");
try (final Writer writer = Files.newBufferedWriter(tmpFile)) {
props.store(writer, null);
}
testStoreKeyOrder(props, tmpFile, expectedOrder);
}
/**
* Tests that the {@link Properties#store(OutputStream, String)} API writes out the properties
* in the expected order
*/
@Test(dataProvider = "propsProvider")
public void testStoreOutputStreamKeyOrder(final Properties props, final String[] expectedOrder) throws Exception {
// Properties.store(...) to a temp file
final Path tmpFile = Files.createTempFile("8231640", "props");
try (final OutputStream os = Files.newOutputStream(tmpFile)) {
props.store(os, null);
}
testStoreKeyOrder(props, tmpFile, expectedOrder);
}
/**
* {@link Properties#load(InputStream) Loads a Properties instance} from the passed
* {@code Path} and then verifies that:
* - the loaded properties instance "equals" the passed (original) "props" instance
* - the order in which the properties appear in the file represented by the path
* is the same as the passed "expectedOrder"
*/
private void testStoreKeyOrder(final Properties props, final Path storedProps,
final String[] expectedOrder) throws Exception {
// Properties.load(...) from that stored file and verify that the loaded
// Properties has expected content
final Properties loaded = new Properties();
try (final InputStream is = Files.newInputStream(storedProps)) {
loaded.load(is);
}
Assert.assertEquals(loaded, props, "Unexpected properties loaded from stored state");
// now read lines from the stored file and keep track of the order in which the keys were
// found in that file. Compare that order with the expected store order of the keys.
final List<String> actualOrder;
try (final BufferedReader reader = Files.newBufferedReader(storedProps)) {
actualOrder = readInOrder(reader);
}
Assert.assertEquals(actualOrder.size(), expectedOrder.length,
"Unexpected number of keys read from stored properties");
if (!Arrays.equals(actualOrder.toArray(new String[0]), expectedOrder)) {
Assert.fail("Unexpected order of stored property keys. Expected order: " + Arrays.toString(expectedOrder)
+ ", found order: " + actualOrder);
}
}
/**
* Tests that {@link Properties#store(Writer, String)} writes out a proper date comment
*/
@Test
public void testStoreWriterDateComment() throws Exception {
final Properties props = new Properties();
props.setProperty("a", "b");
final Path tmpFile = Files.createTempFile("8231640", "props");
try (final Writer writer = Files.newBufferedWriter(tmpFile)) {
props.store(writer, null);
}
testDateComment(tmpFile);
}
/**
* Tests that {@link Properties#store(OutputStream, String)} writes out a proper date comment
*/
@Test
public void testStoreOutputStreamDateComment() throws Exception {
final Properties props = new Properties();
props.setProperty("a", "b");
final Path tmpFile = Files.createTempFile("8231640", "props");
try (final Writer writer = Files.newBufferedWriter(tmpFile)) {
props.store(writer, null);
}
testDateComment(tmpFile);
}
/**
* Reads each line in the {@code file} and verifies that there is only one comment line
* and that comment line can be parsed into a {@link java.util.Date}
*/
private void testDateComment(Path file) throws Exception {
String comment = null;
try (final BufferedReader reader = Files.newBufferedReader(file)) {
String line = null;
while ((line = reader.readLine()) != null) {
if (line.startsWith("#")) {
if (comment != null) {
Assert.fail("More than one comment line found in the stored properties file " + file);
}
comment = line.substring(1);
}
}
}
if (comment == null) {
Assert.fail("No comment line found in the stored properties file " + file);
}
try {
DateTimeFormatter.ofPattern(DATE_FORMAT_PATTERN).parse(comment);
} catch (DateTimeParseException pe) {
Assert.fail("Unexpected date comment: " + comment, pe);
}
}
// returns the property keys in their natural order
private static String[] naturalOrder(final Properties props) {
return new TreeSet<>(props.stringPropertyNames()).toArray(new String[0]);
}
// reads each non-comment line and keeps track of the order in which the property key lines
// were read
private static List<String> readInOrder(final BufferedReader reader) throws IOException {
final List<String> readKeys = new ArrayList<>();
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith("#")) {
continue;
}
final String key = line.substring(0, line.indexOf("="));
// the Properties.store(...) APIs write out the keys in a specific format for certain
// special characters. Our test uses some of the keys which have those special characters.
// Here we handle such special character conversion (for only those characters that this test uses).
// replace the backslash character followed by the t character with the tab character
String replacedKey = key.replace("\\t", "\t");
// replace the backslash character followed by the n character with the newline character
replacedKey = replacedKey.replace("\\n", "\n");
// replace backslash character followed by the space character with the space character
replacedKey = replacedKey.replace("\\ ", " ");
readKeys.add(replacedKey);
}
return readKeys;
}
// Extends java.util.Properties and overrides entrySet() to return a reverse
// sorted entries set
private static class OverridesEntrySet extends Properties {
@Override
@SuppressWarnings("unchecked")
public Set<Map.Entry<Object, Object>> entrySet() {
// return a reverse sorted entries set
var entries = super.entrySet();
Comparator<Map.Entry<String, String>> comparator = Map.Entry.comparingByKey(Comparator.reverseOrder());
TreeSet<Map.Entry<String, String>> reverseSorted = new TreeSet<>(comparator);
reverseSorted.addAll((Set) entries);
return (Set) reverseSorted;
}
String[] expectedKeyOrder() {
// returns in reverse order of the property keys' natural ordering
var keys = new ArrayList<>(stringPropertyNames());
keys.sort(Comparator.reverseOrder());
return keys.toArray(new String[0]);
}
}
// Extends java.util.Properties and overrides entrySet() to just return "super.entrySet()"
private static class OverridesEntrySetCallsSuper extends Properties {
@Override
public Set<Map.Entry<Object, Object>> entrySet() {
return super.entrySet();
}
}
// Extends java.util.Properties but doesn't override entrySet() method
private static class DoesNotOverrideEntrySet extends Properties {
@Override
public String toString() {
return "DoesNotOverrideEntrySet - " + super.toString();
}
}
}

View File

@ -0,0 +1,494 @@
/*
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import jdk.test.lib.process.OutputAnalyzer;
import jdk.test.lib.process.ProcessTools;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.TimeZone;
/*
* @test
* @summary Tests that the Properties.store() APIs generate output that is reproducible
* @bug 8231640
* @library /test/lib
* @run driver StoreReproducibilityTest
*/
public class StoreReproducibilityTest {
private static final String DATE_FORMAT_PATTERN = "EEE MMM dd HH:mm:ss zzz uuuu";
private static final String SYS_PROP_JAVA_PROPERTIES_DATE = "java.properties.date";
private static final DateTimeFormatter reproducibleDateTimeFormatter = DateTimeFormatter.ofPattern(DATE_FORMAT_PATTERN)
.withLocale(Locale.ROOT).withZone(ZoneOffset.UTC);
public static void main(final String[] args) throws Exception {
// no security manager enabled
testWithoutSecurityManager();
// security manager enabled and security policy explicitly allows
// read permissions on java.properties.date system property
testWithSecMgrExplicitPermission();
// security manager enabled and no explicit permission on java.properties.date system property
testWithSecMgrNoSpecificPermission();
// free form non-date value for java.properties.date system property
testNonDateSysPropValue();
// blank value for java.properties.date system property
testBlankSysPropValue();
// empty value for java.properties.date system property
testEmptySysPropValue();
// value for java.properties.date system property contains line terminator characters
testMultiLineSysPropValue();
// value for java.properties.date system property contains backslash character
testBackSlashInSysPropValue();
}
/**
* Launches a Java program which is responsible for using Properties.store() to write out the
* properties to a file. The launched Java program is passed a value for the
* {@code java.properties.date} system property and the date comment written out
* to the file is expected to use this value.
* The program is launched multiple times with the same value for {@code java.properties.date}
* and the output written by each run of this program is verified to be exactly the same.
* Additionally, the date comment that's written out is verified to be the expected date that
* corresponds to the passed {@code java.properties.date}.
* The launched Java program is run without any security manager
*/
private static void testWithoutSecurityManager() throws Exception {
final List<Path> storedFiles = new ArrayList<>();
final String sysPropVal = reproducibleDateTimeFormatter.format(Instant.ofEpochSecond(243535322));
for (int i = 0; i < 5; i++) {
final Path tmpFile = Files.createTempFile("8231640", ".props");
storedFiles.add(tmpFile);
final ProcessBuilder processBuilder = ProcessTools.createJavaProcessBuilder(
"-D" + SYS_PROP_JAVA_PROPERTIES_DATE + "=" + sysPropVal,
StoreTest.class.getName(),
tmpFile.toString(),
i % 2 == 0 ? "--use-outputstream" : "--use-writer");
executeJavaProcess(processBuilder);
assertExpectedComment(tmpFile, sysPropVal);
if (!StoreTest.propsToStore.equals(loadProperties(tmpFile))) {
throw new RuntimeException("Unexpected properties stored in " + tmpFile);
}
}
assertAllFileContentsAreSame(storedFiles, sysPropVal);
}
/**
* Launches a Java program which is responsible for using Properties.store() to write out the
* properties to a file. The launched Java program is passed a value for the
* {@code java.properties.date} system property and the date comment written out to the file
* is expected to use this value.
* The launched Java program is run with the default security manager and is granted
* a {@code read} permission on {@code java.properties.date}.
* The program is launched multiple times with the same value for {@code java.properties.date}
* and the output written by each run of this program is verified to be exactly the same.
* Additionally, the date comment that's written out is verified to be the expected date that
* corresponds to the passed {@code java.properties.date}.
*/
private static void testWithSecMgrExplicitPermission() throws Exception {
final Path policyFile = Files.createTempFile("8231640", ".policy");
Files.write(policyFile, Collections.singleton("""
grant {
// test writes/stores to a file, so FilePermission
permission java.io.FilePermission "<<ALL FILES>>", "read,write";
// explicitly grant read permission on java.properties.date system property
// to verify store() APIs work fine
permission java.util.PropertyPermission "java.properties.date", "read";
};
"""));
final List<Path> storedFiles = new ArrayList<>();
final String sysPropVal = reproducibleDateTimeFormatter.format(Instant.ofEpochSecond(1234342423));
for (int i = 0; i < 5; i++) {
final Path tmpFile = Files.createTempFile("8231640", ".props");
storedFiles.add(tmpFile);
final ProcessBuilder processBuilder = ProcessTools.createJavaProcessBuilder(
"-D" + SYS_PROP_JAVA_PROPERTIES_DATE + "=" + sysPropVal,
"-Djava.security.manager",
"-Djava.security.policy=" + policyFile.toString(),
StoreTest.class.getName(),
tmpFile.toString(),
i % 2 == 0 ? "--use-outputstream" : "--use-writer");
executeJavaProcess(processBuilder);
assertExpectedComment(tmpFile, sysPropVal);
if (!StoreTest.propsToStore.equals(loadProperties(tmpFile))) {
throw new RuntimeException("Unexpected properties stored in " + tmpFile);
}
}
assertAllFileContentsAreSame(storedFiles, sysPropVal);
}
/**
* Launches a Java program which is responsible for using Properties.store() to write out the
* properties to a file. The launched Java program is passed a value for the
* {@code java.properties.date} system property and the date comment written out to the file
* is expected to use this value.
* The launched Java program is run with the default security manager and is NOT granted
* any explicit permission for {@code java.properties.date} system property.
* The program is launched multiple times with the same value for {@code java.properties.date}
* and the output written by each run of this program is verified to be exactly the same.
* Additionally, the date comment that's written out is verified to be the expected date that
* corresponds to the passed {@code java.properties.date}.
*/
private static void testWithSecMgrNoSpecificPermission() throws Exception {
final Path policyFile = Files.createTempFile("8231640", ".policy");
Files.write(policyFile, Collections.singleton("""
grant {
// test writes/stores to a file, so FilePermission
permission java.io.FilePermission "<<ALL FILES>>", "read,write";
// no other grants, not even "read" java.properties.date system property.
// test should still work fine and the date comment should correspond to the value of
// java.properties.date system property.
};
"""));
final List<Path> storedFiles = new ArrayList<>();
final String sysPropVal = reproducibleDateTimeFormatter.format(Instant.ofEpochSecond(1234342423));
for (int i = 0; i < 5; i++) {
final Path tmpFile = Files.createTempFile("8231640", ".props");
storedFiles.add(tmpFile);
final ProcessBuilder processBuilder = ProcessTools.createJavaProcessBuilder(
"-D" + SYS_PROP_JAVA_PROPERTIES_DATE + "=" + sysPropVal,
"-Djava.security.manager",
"-Djava.security.policy=" + policyFile.toString(),
StoreTest.class.getName(),
tmpFile.toString(),
i % 2 == 0 ? "--use-outputstream" : "--use-writer");
executeJavaProcess(processBuilder);
assertExpectedComment(tmpFile, sysPropVal);
if (!StoreTest.propsToStore.equals(loadProperties(tmpFile))) {
throw new RuntimeException("Unexpected properties stored in " + tmpFile);
}
}
assertAllFileContentsAreSame(storedFiles, sysPropVal);
}
/**
* Launches a Java program which is responsible for using Properties.store() to write out the
* properties to a file. The launched Java program is passed a {@link String#isBlank() blank} value
* for the {@code java.properties.date} system property.
* It is expected and verified in this test that such a value for the system property
* will cause a comment line to be written out with only whitespaces.
* The launched program is expected to complete without any errors.
*/
private static void testBlankSysPropValue() throws Exception {
final List<Path> storedFiles = new ArrayList<>();
final String sysPropVal = " \t";
for (int i = 0; i < 2; i++) {
final Path tmpFile = Files.createTempFile("8231640", ".props");
storedFiles.add(tmpFile);
final ProcessBuilder processBuilder = ProcessTools.createJavaProcessBuilder(
"-D" + SYS_PROP_JAVA_PROPERTIES_DATE + "=" + sysPropVal,
StoreTest.class.getName(),
tmpFile.toString(),
i % 2 == 0 ? "--use-outputstream" : "--use-writer");
executeJavaProcess(processBuilder);
if (!StoreTest.propsToStore.equals(loadProperties(tmpFile))) {
throw new RuntimeException("Unexpected properties stored in " + tmpFile);
}
String blankCommentLine = findNthComment(tmpFile, 2);
if (blankCommentLine == null) {
throw new RuntimeException("Comment line representing the value of "
+ SYS_PROP_JAVA_PROPERTIES_DATE + " system property is missing in file " + tmpFile);
}
if (!blankCommentLine.isBlank()) {
throw new RuntimeException("Expected comment line to be blank but was " + blankCommentLine);
}
}
assertAllFileContentsAreSame(storedFiles, sysPropVal);
}
/**
* Launches a Java program which is responsible for using Properties.store() to write out the
* properties to a file. The launched Java program is passed a {@link String#isEmpty() empty} value
* for the {@code java.properties.date} system property.
* It is expected and verified in this test that such a value for the system property
* will cause the current date and time to be written out as a comment.
* The launched program is expected to complete without any errors.
*/
private static void testEmptySysPropValue() throws Exception {
for (int i = 0; i < 2; i++) {
final Path tmpFile = Files.createTempFile("8231640", ".props");
final ProcessBuilder processBuilder = ProcessTools.createJavaProcessBuilder(
"-D" + SYS_PROP_JAVA_PROPERTIES_DATE + "=" + "",
StoreTest.class.getName(),
tmpFile.toString(),
i % 2 == 0 ? "--use-outputstream" : "--use-writer");
Date launchedAt = new Date();
// wait for a second before launching so that we can then expect
// the date written out by the store() APIs to be "after" this launch date
Thread.sleep(1000);
executeJavaProcess(processBuilder);
if (!StoreTest.propsToStore.equals(loadProperties(tmpFile))) {
throw new RuntimeException("Unexpected properties stored in " + tmpFile);
}
assertCurrentDate(tmpFile, launchedAt);
}
}
/**
* Launches a Java program which is responsible for using Properties.store() to write out the
* properties to a file. The launched Java program is passed the {@code java.properties.date}
* system property with a value that doesn't represent a formatted date.
* It is expected and verified in this test that such a value for the system property
* will cause the comment to use that value verbatim. The launched program is expected to complete
* without any errors.
*/
private static void testNonDateSysPropValue() throws Exception {
final String sysPropVal = "foo-bar";
final List<Path> storedFiles = new ArrayList<>();
for (int i = 0; i < 2; i++) {
final Path tmpFile = Files.createTempFile("8231640", ".props");
storedFiles.add(tmpFile);
final ProcessBuilder processBuilder = ProcessTools.createJavaProcessBuilder(
"-D" + SYS_PROP_JAVA_PROPERTIES_DATE + "=" + sysPropVal,
StoreTest.class.getName(),
tmpFile.toString(),
i % 2 == 0 ? "--use-outputstream" : "--use-writer");
executeJavaProcess(processBuilder);
if (!StoreTest.propsToStore.equals(loadProperties(tmpFile))) {
throw new RuntimeException("Unexpected properties stored in " + tmpFile);
}
assertExpectedComment(tmpFile, sysPropVal);
}
assertAllFileContentsAreSame(storedFiles, sysPropVal);
}
/**
* Launches a Java program which is responsible for using Properties.store() to write out the
* properties to a file. The launched Java program is passed the {@code java.properties.date}
* system property with a value that has line terminator characters.
* It is expected and verified in this test that such a value for the system property
* will cause the comment written out to be multiple separate comments. The launched program is expected
* to complete without any errors.
*/
private static void testMultiLineSysPropValue() throws Exception {
final String[] sysPropVals = {"hello-world\nc=d", "hello-world\rc=d", "hello-world\r\nc=d"};
for (final String sysPropVal : sysPropVals) {
final List<Path> storedFiles = new ArrayList<>();
for (int i = 0; i < 2; i++) {
final Path tmpFile = Files.createTempFile("8231640", ".props");
storedFiles.add(tmpFile);
final ProcessBuilder processBuilder = ProcessTools.createJavaProcessBuilder(
"-D" + SYS_PROP_JAVA_PROPERTIES_DATE + "=" + sysPropVal,
StoreTest.class.getName(),
tmpFile.toString(),
i % 2 == 0 ? "--use-outputstream" : "--use-writer");
executeJavaProcess(processBuilder);
if (!StoreTest.propsToStore.equals(loadProperties(tmpFile))) {
throw new RuntimeException("Unexpected properties stored in " + tmpFile);
}
// verify this results in 2 separate comment lines in the stored file
String commentLine1 = findNthComment(tmpFile, 2);
String commentLine2 = findNthComment(tmpFile, 3);
if (commentLine1 == null || commentLine2 == null) {
throw new RuntimeException("Did not find the expected multi-line comments in " + tmpFile);
}
if (!commentLine1.equals("hello-world")) {
throw new RuntimeException("Unexpected comment line " + commentLine1 + " in " + tmpFile);
}
if (!commentLine2.equals("c=d")) {
throw new RuntimeException("Unexpected comment line " + commentLine2 + " in " + tmpFile);
}
}
assertAllFileContentsAreSame(storedFiles, sysPropVal);
}
}
/**
* Launches a Java program which is responsible for using Properties.store() to write out the
* properties to a file. The launched Java program is passed the {@code java.properties.date}
* system property with a value that has backslash character.
* It is expected and verified in this test that such a value for the system property
* will not cause any malformed comments or introduce any new properties in the stored content.
* The launched program is expected to complete without any errors.
*/
private static void testBackSlashInSysPropValue() throws Exception {
final String[] sysPropVals = {"\\hello-world", "hello-world\\", "hello-world\\c=d",
"newline-plus-backslash\\\nc=d"};
for (final String sysPropVal : sysPropVals) {
final List<Path> storedFiles = new ArrayList<>();
for (int i = 0; i < 2; i++) {
final Path tmpFile = Files.createTempFile("8231640", ".props");
storedFiles.add(tmpFile);
final ProcessBuilder processBuilder = ProcessTools.createJavaProcessBuilder(
"-D" + SYS_PROP_JAVA_PROPERTIES_DATE + "=" + sysPropVal,
StoreTest.class.getName(),
tmpFile.toString(),
i % 2 == 0 ? "--use-outputstream" : "--use-writer");
executeJavaProcess(processBuilder);
if (!StoreTest.propsToStore.equals(loadProperties(tmpFile))) {
throw new RuntimeException("Unexpected properties stored in " + tmpFile);
}
String commentLine1 = findNthComment(tmpFile, 2);
if (commentLine1 == null) {
throw new RuntimeException("Did not find the expected comment line in " + tmpFile);
}
if (sysPropVal.contains("newline-plus-backslash")) {
if (!commentLine1.equals("newline-plus-backslash\\")) {
throw new RuntimeException("Unexpected comment line " + commentLine1 + " in " + tmpFile);
}
// we expect this specific system property value to be written out into 2 separate comment lines
String commentLine2 = findNthComment(tmpFile, 3);
if (commentLine2 == null) {
throw new RuntimeException(sysPropVal + " was expected to be split into 2 comment line, " +
"but wasn't, in " + tmpFile);
}
if (!commentLine2.equals("c=d")) {
throw new RuntimeException("Unexpected comment line " + commentLine2 + " in " + tmpFile);
}
} else {
if (!commentLine1.equals(sysPropVal)) {
throw new RuntimeException("Unexpected comment line " + commentLine1 + " in " + tmpFile);
}
}
}
assertAllFileContentsAreSame(storedFiles, sysPropVal);
}
}
// launches the java process and waits for it to exit. throws an exception if exit value is non-zero
private static void executeJavaProcess(ProcessBuilder pb) throws Exception {
final OutputAnalyzer outputAnalyzer = ProcessTools.executeProcess(pb);
try {
outputAnalyzer.shouldHaveExitValue(0);
} finally {
// print out any stdout/err that was generated in the launched program
outputAnalyzer.reportDiagnosticSummary();
}
}
// Properties.load() from the passed file and return the loaded Properties instance
private static Properties loadProperties(final Path file) throws IOException {
final Properties props = new Properties();
props.load(Files.newBufferedReader(file));
return props;
}
/**
* Verifies that the comment in the {@code destFile} is same as {@code expectedComment},
* instead of the default date comment.
*/
private static void assertExpectedComment(final Path destFile,
final String expectedComment) throws Exception {
final String actualComment = findNthComment(destFile, 2);
if (actualComment == null) {
throw new RuntimeException("Comment \"" + expectedComment + "\" not found in stored properties " + destFile);
}
if (!expectedComment.equals(actualComment)) {
throw new RuntimeException("Expected comment \"" + expectedComment + "\" but found \"" + actualComment + "\" " +
"in stored properties " + destFile);
}
}
/**
* Verifies that the date comment in the {@code destFile} can be parsed using the
* "EEE MMM dd HH:mm:ss zzz uuuu" format and the time represented by it is {@link Date#after(Date) after}
* the passed {@code date}
*/
private static void assertCurrentDate(final Path destFile, final Date date) throws Exception {
final String dateComment = findNthComment(destFile, 2);
if (dateComment == null) {
throw new RuntimeException("Date comment not found in stored properties " + destFile);
}
System.out.println("Found date comment " + dateComment + " in file " + destFile);
final Date parsedDate;
try {
Instant instant = Instant.from(DateTimeFormatter.ofPattern(DATE_FORMAT_PATTERN).parse(dateComment));
parsedDate = new Date(instant.toEpochMilli());
} catch (DateTimeParseException pe) {
throw new RuntimeException("Unexpected date " + dateComment + " in stored properties " + destFile);
}
if (!parsedDate.after(date)) {
throw new RuntimeException("Expected date comment " + dateComment + " to be after " + date
+ " but was " + parsedDate);
}
}
// returns the "Nth" comment from the file. Comment index starts from 1.
private static String findNthComment(Path file, int commentIndex) throws IOException {
List<String> comments = new ArrayList<>();
try (final BufferedReader reader = Files.newBufferedReader(file)) {
String line = null;
while ((line = reader.readLine()) != null) {
if (line.startsWith("#")) {
comments.add(line.substring(1));
if (comments.size() == commentIndex) {
return comments.get(commentIndex - 1);
}
}
}
}
return null;
}
// verifies the byte equality of the contents in each of the files
private static void assertAllFileContentsAreSame(final List<Path> files,
final String sysPropVal) throws Exception {
final byte[] file1Contents = Files.readAllBytes(files.get(0));
for (int i = 1; i < files.size(); i++) {
final byte[] otherFileContents = Files.readAllBytes(files.get(i));
if (!Arrays.equals(file1Contents, otherFileContents)) {
throw new RuntimeException("Properties.store() did not generate reproducible content when " +
SYS_PROP_JAVA_PROPERTIES_DATE + " was set to " + sysPropVal);
}
}
}
static class StoreTest {
private static final Properties propsToStore = new Properties();
static {
propsToStore.setProperty("a", "b");
}
/**
* Uses Properties.store() APIs to store the properties into file
*/
public static void main(final String[] args) throws Exception {
final Path destFile = Path.of(args[0]);
final String comment = "some user specified comment";
System.out.println("Current default timezone is " + TimeZone.getDefault());
if (args[1].equals("--use-outputstream")) {
try (var os = Files.newOutputStream(destFile)) {
propsToStore.store(os, comment);
}
} else {
try (var br = Files.newBufferedWriter(destFile)) {
propsToStore.store(br, comment);
}
}
}
}
}