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.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.
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
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