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  */
  23 import java.io.ByteArrayInputStream;
  24 import java.io.ByteArrayOutputStream;
  25 import java.io.IOException;
  26 import java.io.ObjectInputStream;
  27 import java.io.ObjectOutputStream;
  28 import java.time.ZoneId;
  29 import java.util.Base64;
  30 import java.util.Locale;
  31 import java.util.TimeZone;
  32 import java.util.logging.Level;
  33 import java.util.logging.LogRecord;
  34 import java.util.logging.SimpleFormatter;
  35 import java.util.regex.Matcher;
  36 import java.util.regex.Pattern;
  37 import 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  */
  48 public 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 static abstract 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 }