| /* | |
| * Copyright (c) 2021, 2024, 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 8282023 8316540 | |
| * @comment The test launches several processes and in the presence of -Xcomp it's too slow | |
| * and thus causes timeouts | |
| * @requires vm.compMode != "Xcomp" | |
| * @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 FORMATTER = DateTimeFormatter.ofPattern(DATE_FORMAT_PATTERN, Locale.ROOT) | |
| .withZone(ZoneOffset.UTC); | |
| public static void main(final String[] args) throws Exception { | |
| // test that store method uses the java.properties.date system property | |
| testStoreUsesPropValue(); | |
| // 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}. | |
| */ | |
| private static void testStoreUsesPropValue() throws Exception { | |
| final List<Path> storedFiles = new ArrayList<>(); | |
| final String sysPropVal = FORMATTER.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.createTestJavaProcessBuilder( | |
| "-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 {@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.createTestJavaProcessBuilder( | |
| "-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.createTestJavaProcessBuilder( | |
| "-D" + SYS_PROP_JAVA_PROPERTIES_DATE + "=", | |
| "-Duser.timezone=UTC", | |
| 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.createTestJavaProcessBuilder( | |
| "-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.createTestJavaProcessBuilder( | |
| "-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.createTestJavaProcessBuilder( | |
| "-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} | |
| * The JVM runtime to invoke this method should set the time zone to UTC, i.e, specify | |
| * "-Duser.timezone=UTC" at the command line. Otherwise, it will fail with some time | |
| * zones that have ambiguous short names, such as "IST" | |
| */ | |
| 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(FORMATTER.parse(dateComment)); | |
| parsedDate = new Date(instant.toEpochMilli()); | |
| } catch (DateTimeParseException pe) { | |
| throw new RuntimeException("Unexpected date " + dateComment + " in stored properties " + destFile, pe); | |
| } | |
| 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; | |
| 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); | |
| } | |
| } | |
| } | |
| } | |
| } |