8231640: (prop) Canonical property storage
Reviewed-by: rriggs, smarks, dfuchs, ihse
This commit is contained in:
parent
ddc262746a
commit
af50772d39
@ -46,6 +46,7 @@ import java.util.function.BiConsumer;
|
|||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import jdk.internal.util.StaticProperty;
|
||||||
import sun.nio.cs.ISO_8859_1;
|
import sun.nio.cs.ISO_8859_1;
|
||||||
import sun.nio.cs.UTF_8;
|
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 #}
|
* If the comments argument is not null, then an ASCII {@code #}
|
||||||
* character, the comments string, and a line separator are first written
|
* character, the comments string, and a line separator are first written
|
||||||
* to the output stream. Thus, the {@code comments} can serve as an
|
* to the output stream. Thus, the {@code comments} can serve as an
|
||||||
* identifying comment. Any one of a line feed ('\n'), a carriage
|
* identifying comment. Any one of a line feed ({@code \n}), a carriage
|
||||||
* return ('\r'), or a carriage return followed immediately by a line feed
|
* return ({@code \r}), or a carriage return followed immediately by a line feed
|
||||||
* in comments is replaced by a line separator generated by the {@code Writer}
|
* ({@code \r\n}) in comments is replaced by a
|
||||||
* and if the next character in comments is not character {@code #} or
|
* {@link System#lineSeparator() line separator} and if the next
|
||||||
* character {@code !} then an ASCII {@code #} is written out
|
* character in comments is not character {@code #} or character {@code !} then
|
||||||
* after that line separator.
|
* an ASCII {@code #} is written out after that line separator.
|
||||||
* <p>
|
* <p>
|
||||||
* Next, a comment line is always written, consisting of an ASCII
|
* If the {@systemProperty java.properties.date} is set on the command line
|
||||||
* {@code #} character, the current date and time (as if produced
|
* and is non-empty (as determined by {@link String#isEmpty() String.isEmpty}),
|
||||||
* by the {@code toString} method of {@code Date} for the
|
* a comment line is written as follows.
|
||||||
* current time), and a line separator as generated by the {@code Writer}.
|
* 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>
|
* <p>
|
||||||
* Then every entry in this {@code Properties} table is
|
* Then every entry in this {@code Properties} table is
|
||||||
* written out, one per line. For each entry the key string 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.
|
* After the entries have been written, the output stream is flushed.
|
||||||
* The output stream remains open after this method returns.
|
* 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 writer an output character stream writer.
|
||||||
* @param comments a description of the property list.
|
* @param comments a description of the property list.
|
||||||
* @throws IOException if writing this property list to the specified
|
* @throws IOException if writing this property list to the specified
|
||||||
@ -903,12 +916,25 @@ public class Properties extends Hashtable<Object,Object> {
|
|||||||
if (comments != null) {
|
if (comments != null) {
|
||||||
writeComments(bw, comments);
|
writeComments(bw, comments);
|
||||||
}
|
}
|
||||||
bw.write("#" + new Date().toString());
|
writeDateComment(bw);
|
||||||
bw.newLine();
|
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
for (Map.Entry<Object, Object> e : entrySet()) {
|
@SuppressWarnings("unchecked")
|
||||||
String key = (String)e.getKey();
|
Collection<Map.Entry<String, String>> entries = (Set<Map.Entry<String, String>>) (Set) entrySet();
|
||||||
String val = (String)e.getValue();
|
// 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);
|
key = saveConvert(key, true, escUnicode);
|
||||||
/* No need to escape embedded and trailing spaces for value, hence
|
/* No need to escape embedded and trailing spaces for value, hence
|
||||||
* pass false to flag.
|
* pass false to flag.
|
||||||
@ -921,6 +947,19 @@ public class Properties extends Hashtable<Object,Object> {
|
|||||||
bw.flush();
|
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
|
* Loads all of the properties represented by the XML document on the
|
||||||
* specified input stream into this properties table.
|
* specified input stream into this properties table.
|
||||||
|
@ -51,6 +51,7 @@ public final class StaticProperty {
|
|||||||
private static final String JAVA_IO_TMPDIR;
|
private static final String JAVA_IO_TMPDIR;
|
||||||
private static final String NATIVE_ENCODING;
|
private static final String NATIVE_ENCODING;
|
||||||
private static final String FILE_ENCODING;
|
private static final String FILE_ENCODING;
|
||||||
|
private static final String JAVA_PROPERTIES_DATE;
|
||||||
|
|
||||||
private StaticProperty() {}
|
private StaticProperty() {}
|
||||||
|
|
||||||
@ -67,6 +68,7 @@ public final class StaticProperty {
|
|||||||
JDK_SERIAL_FILTER_FACTORY = getProperty(props, "jdk.serialFilterFactory", null);
|
JDK_SERIAL_FILTER_FACTORY = getProperty(props, "jdk.serialFilterFactory", null);
|
||||||
NATIVE_ENCODING = getProperty(props, "native.encoding");
|
NATIVE_ENCODING = getProperty(props, "native.encoding");
|
||||||
FILE_ENCODING = getProperty(props, "file.encoding");
|
FILE_ENCODING = getProperty(props, "file.encoding");
|
||||||
|
JAVA_PROPERTIES_DATE = getProperty(props, "java.properties.date", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getProperty(Properties props, String key) {
|
private static String getProperty(Properties props, String key) {
|
||||||
@ -227,4 +229,16 @@ public final class StaticProperty {
|
|||||||
public static String fileEncoding() {
|
public static String fileEncoding() {
|
||||||
return FILE_ENCODING;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
275
test/jdk/java/util/Properties/PropertiesStoreTest.java
Normal file
275
test/jdk/java/util/Properties/PropertiesStoreTest.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
494
test/jdk/java/util/Properties/StoreReproducibilityTest.java
Normal file
494
test/jdk/java/util/Properties/StoreReproducibilityTest.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user