1/*
2 * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.
8 *
9 * This code is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 * version 2 for more details (a copy is included in the LICENSE file that
13 * accompanied this code).
14 *
15 * You should have received a copy of the GNU General Public License version
16 * 2 along with this work; if not, write to the Free Software Foundation,
17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18 *
19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20 * or visit www.oracle.com if you need additional information or have any
21 * questions.
22 */
23import java.io.ByteArrayInputStream;
24import java.io.ByteArrayOutputStream;
25import java.io.IOException;
26import java.io.ObjectInputStream;
27import java.io.ObjectOutputStream;
28import java.time.ZoneId;
29import java.util.Base64;
30import java.util.Locale;
31import java.util.TimeZone;
32import java.util.logging.Level;
33import java.util.logging.LogRecord;
34import java.util.logging.SimpleFormatter;
35import java.util.regex.Matcher;
36import java.util.regex.Pattern;
37import java.util.stream.Stream;
38
39/**
40 * @test
41 * @bug 8072645
42 * @summary tests the compatibility of LogRecord serial form between
43 *          JDK 8 and JDK 9. Ideally this test should be run on both platforms.
44 *          (It is designed to run on both).
45 * @run main/othervm SerializeLogRecord
46 * @author danielfuchs
47 */
48public class SerializeLogRecord {
49
50    /**
51     * Serializes a log record, encode the serialized bytes in base 64, and
52     * prints pseudo java code that can be cut and pasted into this test.
53     * @param record the log record to serialize, encode in base 64, and for
54     *               which test data will be generated.
55     * @return A string containing the generated pseudo java code.
56     * @throws IOException Unexpected.
57     * @throws ClassNotFoundException  Unexpected.
58     */
59    public static String generate(LogRecord record) throws IOException, ClassNotFoundException {
60
61        // Format the given logRecord using the SimpleFormatter
62        SimpleFormatter formatter = new SimpleFormatter();
63        String str = formatter.format(record);
64
65        // Serialize the given LogRecord
66        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
67        final ObjectOutputStream oos = new ObjectOutputStream(baos);
68        oos.writeObject(record);
69        oos.flush();
70        oos.close();
71
72        // Now we're going to perform a number of smoke tests before
73        // generating the Java pseudo code.
74        //
75        // First checks that the log record can be deserialized
76        final ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
77        final ObjectInputStream ois = new ObjectInputStream(bais);
78        final LogRecord record2 = (LogRecord)ois.readObject();
79
80        // Format the deserialized LogRecord using the SimpleFormatter, and
81        // check that the string representation obtained matches the string
82        // representation of the original LogRecord
83        String str2 = formatter.format(record2);
84        if (!str.equals(str2)) throw new RuntimeException("Unexpected values in deserialized object:"
85                + "\n\tExpected:  " + str
86                + "\n\tRetrieved: "+str);
87
88        // Now get a Base64 string representation of the serialized bytes.
89        final String base64 = Base64.getEncoder().encodeToString(baos.toByteArray());
90
91        // Check that we can deserialize a log record from the Base64 string
92        // representation we just computed.
93        final ByteArrayInputStream bais2 = new ByteArrayInputStream(Base64.getDecoder().decode(base64));
94        final ObjectInputStream ois2 = new ObjectInputStream(bais2);
95        final LogRecord record3 = (LogRecord)ois2.readObject();
96
97        // Format the new deserialized LogRecord using the SimpleFormatter, and
98        // check that the string representation obtained matches the string
99        // representation of the original LogRecord
100        String str3 = formatter.format(record3);
101        if (!str.equals(str3)) throw new RuntimeException("Unexpected values in deserialized object:"
102                + "\n\tExpected:  " + str
103                + "\n\tRetrieved: "+str);
104        //System.out.println(base64);
105        //System.out.println();
106
107        // Generates the Java Pseudo code that can be cut & pasted into
108        // this test (see Jdk8SerializedLog and Jdk9SerializedLog below)
109        final StringBuilder sb = new StringBuilder();
110        sb.append("    /**").append('\n');
111        sb.append("     * Base64 encoded string for LogRecord object.").append('\n');
112        sb.append("     * Java version: ").append(System.getProperty("java.version")).append('\n');
113        sb.append("     **/").append('\n');
114        sb.append("    final String base64 = ").append("\n          ");
115        final int last = base64.length() - 1;
116        for (int i=0; i<base64.length();i++) {
117            if (i%64 == 0) sb.append("\"");
118            sb.append(base64.charAt(i));
119            if (i%64 == 63 || i == last) {
120                sb.append("\"");
121                if (i == last) sb.append(";\n");
122                else sb.append("\n        + ");
123            }
124        }
125        sb.append('\n');
126        sb.append("    /**").append('\n');
127        sb.append("     * SimpleFormatter output for LogRecord object.").append('\n');
128        sb.append("     * Java version: ").append(System.getProperty("java.version")).append('\n');
129        sb.append("     **/").append('\n');
130        sb.append("    final String str = ").append("\n          ");
131        sb.append("\"").append(str.replace("\n", "\\n")).append("\";\n");
132        return sb.toString();
133    }
134
135    /**
136     * An abstract class to test that a log record previously serialized on a
137     * different java version can be deserialized in the current java version.
138     * (see Jdk8SerializedLog and Jdk9SerializedLog below)
139     */
140    public abstract static class SerializedLog {
141        public abstract String getBase64();
142        public abstract String getString();
143
144        /**
145         * Deserializes the Base64 encoded string returned by {@link
146         * #getBase64()}, format the obtained LogRecord using a
147         * SimpleFormatter, and checks that the string representation obtained
148         * matches the original string representation returned by {@link
149         * #getString()}.
150         */
151        protected void dotest() {
152            try {
153                final String base64 = getBase64();
154                final ByteArrayInputStream bais =
155                        new ByteArrayInputStream(Base64.getDecoder().decode(base64));
156                final ObjectInputStream ois = new ObjectInputStream(bais);
157                final LogRecord record = (LogRecord)ois.readObject();
158                final SimpleFormatter formatter = new SimpleFormatter();
159                String expected = getString();
160                String str2 = formatter.format(record);
161                check(expected, str2);
162                System.out.println(str2);
163                System.out.println("PASSED: "+this.getClass().getName()+"\n");
164            } catch (IOException | ClassNotFoundException x) {
165                throw new RuntimeException(x);
166            }
167        }
168        /**
169         * Check that the actual String representation obtained matches the
170         * expected String representation.
171         * @param expected Expected String representation, as returned by
172         *                 {@link #getString()}.
173         * @param actual   Actual String representation obtained by formatting
174         *                 the LogRecord obtained by the deserialization of the
175         *                 bytes encoded in {@link #getBase64()}.
176         */
177        protected void check(String expected, String actual) {
178            if (!expected.equals(actual)) {
179                throw new RuntimeException(this.getClass().getName()
180                    + " - Unexpected values in deserialized object:"
181                    + "\n\tExpected:  " + expected
182                    + "\n\tRetrieved: "+ actual);
183            }
184        }
185    }
186
187    public static class Jdk8SerializedLog extends SerializedLog {
188
189        // Generated by generate() on JDK 8.
190        // --------------------------------
191        // BEGIN
192
193        /**
194         * Base64 encoded string for LogRecord object.
195         * Java version: 1.8.0_11
196         **/
197        final String base64 =
198              "rO0ABXNyABtqYXZhLnV0aWwubG9nZ2luZy5Mb2dSZWNvcmRKjVk982lRlgMACkoA"
199            + "Bm1pbGxpc0oADnNlcXVlbmNlTnVtYmVySQAIdGhyZWFkSURMAAVsZXZlbHQAGUxq"
200            + "YXZhL3V0aWwvbG9nZ2luZy9MZXZlbDtMAApsb2dnZXJOYW1ldAASTGphdmEvbGFu"
201            + "Zy9TdHJpbmc7TAAHbWVzc2FnZXEAfgACTAAScmVzb3VyY2VCdW5kbGVOYW1lcQB+"
202            + "AAJMAA9zb3VyY2VDbGFzc05hbWVxAH4AAkwAEHNvdXJjZU1ldGhvZE5hbWVxAH4A"
203            + "AkwABnRocm93bnQAFUxqYXZhL2xhbmcvVGhyb3dhYmxlO3hwAAABSjUCgo0AAAAA"
204            + "AAAAAAAAAAFzcgAXamF2YS51dGlsLmxvZ2dpbmcuTGV2ZWyOiHETUXM2kgIAA0kA"
205            + "BXZhbHVlTAAEbmFtZXEAfgACTAAScmVzb3VyY2VCdW5kbGVOYW1lcQB+AAJ4cAAA"
206            + "AyB0AARJTkZPdAAic3VuLnV0aWwubG9nZ2luZy5yZXNvdXJjZXMubG9nZ2luZ3QA"
207            + "BHRlc3R0ABFKYXZhIFZlcnNpb246IHswfXBwcHB3BgEAAAAAAXQACDEuOC4wXzEx"
208            + "eA==";
209
210        /**
211         * SimpleFormatter output for LogRecord object.
212         * Java version: 1.8.0_11
213         **/
214        final String str =
215              "Dec 10, 2014 4:22:44.621000000 PM test - INFO: Java Version: 1.8.0_11";
216              //                    ^^^
217              // Notice the milli second resolution above...
218
219        // END
220        // --------------------------------
221
222        @Override
223        public String getBase64() {
224            return base64;
225        }
226
227        @Override
228        public String getString() {
229            return str;
230        }
231
232        public static void test() {
233            new Jdk8SerializedLog().dotest();
234        }
235    }
236
237    public static class Jdk9SerializedLog extends SerializedLog {
238
239        // Generated by generate() on JDK 9.
240        // --------------------------------
241        // BEGIN
242
243        /**
244         * Base64 encoded string for LogRecord object.
245         * Java version: 1.9.0-internal
246         **/
247        final String base64 =
248              "rO0ABXNyABtqYXZhLnV0aWwubG9nZ2luZy5Mb2dSZWNvcmRKjVk982lRlgMAC0oA"
249            + "Bm1pbGxpc0kADm5hbm9BZGp1c3RtZW50SgAOc2VxdWVuY2VOdW1iZXJJAAh0aHJl"
250            + "YWRJREwABWxldmVsdAAZTGphdmEvdXRpbC9sb2dnaW5nL0xldmVsO0wACmxvZ2dl"
251            + "ck5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztMAAdtZXNzYWdlcQB+AAJMABJyZXNv"
252            + "dXJjZUJ1bmRsZU5hbWVxAH4AAkwAD3NvdXJjZUNsYXNzTmFtZXEAfgACTAAQc291"
253            + "cmNlTWV0aG9kTmFtZXEAfgACTAAGdGhyb3dudAAVTGphdmEvbGFuZy9UaHJvd2Fi"
254            + "bGU7eHAAAAFLl3u6OAAOU/gAAAAAAAAAAAAAAAFzcgAXamF2YS51dGlsLmxvZ2dp"
255            + "bmcuTGV2ZWyOiHETUXM2kgIAA0kABXZhbHVlTAAEbmFtZXEAfgACTAAScmVzb3Vy"
256            + "Y2VCdW5kbGVOYW1lcQB+AAJ4cAAAAyB0AARJTkZPdAAic3VuLnV0aWwubG9nZ2lu"
257            + "Zy5yZXNvdXJjZXMubG9nZ2luZ3QABHRlc3R0ABFKYXZhIFZlcnNpb246IHswfXBw"
258            + "cHB3BgEAAAAAAXQADjEuOS4wLWludGVybmFseA==";
259
260        /**
261         * SimpleFormatter output for LogRecord object.
262         * Java version: 1.9.0-internal
263         **/
264        final String str =
265              "Feb 17, 2015 12:20:43.192939000 PM test - INFO: Java Version: 1.9.0-internal";
266              //                       ^^^
267              // Notice the micro second resolution above...
268
269        // END
270        // --------------------------------
271
272        @Override
273        public String getBase64() {
274            return base64;
275        }
276
277        @Override
278        public String getString() {
279            return str;
280        }
281
282        @Override
283        protected void check(String expected, String actual) {
284            if (System.getProperty("java.version").startsWith("1.8")) {
285                // If we are in JDK 8 and print a log record serialized in JDK 9,
286                // then we won't be able to print anything below the millisecond
287                // precision, since that hasn't been implemented in JDK 8.
288                // Therefore - we need to replace anything below millseconds by
289                // zeroes in the expected string (which was generated on JDK 9).
290                Pattern pattern = Pattern.compile("^"
291                        + "(.*\\.[0-9][0-9][0-9])" // group1: everything up to milliseconds
292                        + "([0-9][0-9][0-9][0-9][0-9][0-9])" // group 2: micros and nanos
293                        + "(.* - .*)$"); // group three: all the rest...
294                Matcher matcher = pattern.matcher(expected);
295                if (matcher.matches()) {
296                    expected = matcher.group(1) + "000000" + matcher.group(3);
297                }
298            }
299            super.check(expected, actual);
300        }
301
302        public static void test() {
303            new Jdk9SerializedLog().dotest();
304        }
305    }
306
307    public static void generate() {
308        try {
309            LogRecord record = new LogRecord(Level.INFO, "Java Version: {0}");
310            record.setLoggerName("test");
311            record.setParameters(new Object[] {System.getProperty("java.version")});
312            System.out.println(generate(record));
313        } catch (IOException | ClassNotFoundException x) {
314            throw new RuntimeException(x);
315        }
316    }
317
318    static enum TestCase { GENERATE, TESTJDK8, TESTJDK9 };
319
320    public static void main(String[] args) {
321        // Set the locale and time zone to make sure we won't depend on the
322        // test env - in particular we don't want to depend on the
323        // time zone in which the test machine might be located.
324        // So we're gong to use Locale English and Time Zone UTC for this test.
325        // (Maybe that should be Locale.ROOT?)
326        Locale.setDefault(Locale.ENGLISH);
327        TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("UTC")));
328
329        // Set the format property to make sure we always have the nanos, and
330        // to make sure it's the same format than what we used when
331        // computing the formatted string for Jdk8SerializedLog and
332        // Jdk9SerializedLog above.
333        //
334        // If you change the formatting, then you will need to regenerate
335        // the data for Jdk8SerializedLog and Jdk9SerializedLog.
336        //
337        // To do that - just run this test on JDK 8, and cut & paste the
338        // pseudo code printed by generate() into Jdk8SerializedLog.
339        // Then run this test again on JDK 9, and cut & paste the
340        // pseudo code printed by generate() into Jdk9SerializedLog.
341        // [Note: you can pass GENERATE as single arg to main() to avoid
342        //        running the actual test]
343        // Finally run the test again to check that it still passes after
344        // your modifications.
345        //
346        System.setProperty("java.util.logging.SimpleFormatter.format",
347                "%1$tb %1$td, %1$tY %1$tl:%1$tM:%1$tS.%1$tN %1$Tp %2$s - %4$s: %5$s%6$s");
348
349        // If no args, then run everything....
350        if (args == null || args.length == 0) {
351            args = new String[] { "GENERATE", "TESTJDK8", "TESTJDK9" };
352        }
353
354        // Run the specified test case(s)
355        Stream.of(args).map(x -> TestCase.valueOf(x)).forEach((x) -> {
356            switch(x) {
357                case GENERATE: generate(); break;
358                case TESTJDK8: Jdk8SerializedLog.test(); break;
359                case TESTJDK9: Jdk9SerializedLog.test(); break;
360            }
361        });
362    }
363}
364