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 
  24 /*
  25  * @test
  26  * @bug     8140364
  27  * @author  danielfuchs
  28  * @summary JDK implementation specific unit test for JDK internal artifacts.
  29  *          Tests the consistency of the LoggerFinder and JDK extensions.
  30  * @modules java.base/sun.util.logging
  31  *          java.base/jdk.internal.logger
  32  * @run  main LoggerFinderAPITest
  33  */
  34 
  35 
  36 import java.lang.reflect.Method;
  37 import java.lang.reflect.Modifier;
  38 import java.util.ArrayList;
  39 import java.util.Arrays;
  40 import java.util.Collection;
  41 import java.util.Collections;
  42 import java.util.Enumeration;
  43 import java.util.HashMap;
  44 import java.util.LinkedHashMap;
  45 import java.util.LinkedHashSet;
  46 import java.util.List;
  47 import java.util.Map;
  48 import java.util.ResourceBundle;
  49 import java.util.function.Supplier;
  50 import java.util.logging.ConsoleHandler;
  51 import java.util.logging.Handler;
  52 import java.util.logging.LogRecord;
  53 import java.util.logging.Logger;
  54 import java.util.regex.Matcher;
  55 import java.util.regex.Pattern;
  56 import java.util.stream.Collectors;
  57 import java.util.stream.Stream;
  58 import sun.util.logging.PlatformLogger;
  59 
  60 public class LoggerFinderAPITest {
  61 
  62     static final Class<java.lang.System.Logger> spiLoggerClass
  63             = java.lang.System.Logger.class;
  64     static final Class<java.lang.System.Logger> jdkLoggerClass
  65             = java.lang.System.Logger.class;
  66     static final Class<sun.util.logging.PlatformLogger.Bridge> bridgeLoggerClass
  67             = sun.util.logging.PlatformLogger.Bridge.class;
  68     static final Class<java.util.logging.Logger> julLoggerClass
  69             = java.util.logging.Logger.class;
  70     static final Class<sun.util.logging.PlatformLogger.Bridge> julLogProducerClass
  71             = PlatformLogger.Bridge.class;
  72     static final Pattern julLogNames = Pattern.compile(
  73             "^((log(p|rb)?)|severe|warning|info|config|fine|finer|finest|isLoggable)$");
  74     static final Collection<Method> julLoggerIgnores;
  75     static {
  76         List<Method> ignores = new ArrayList<>();
  77         try {
  78             ignores.add(julLoggerClass.getDeclaredMethod("log", LogRecord.class));
  79         } catch (NoSuchMethodException | SecurityException ex) {
  80             throw new ExceptionInInitializerError(ex);
  81         }
  82         julLoggerIgnores = Collections.unmodifiableList(ignores);
  83     }
  84 
  85 
  86 
  87     // Don't require LoggerBridge to have a body for those methods
  88     interface LoggerBridgeMethodsWithNoBody extends
  89         PlatformLogger.Bridge, java.lang.System.Logger {
  90 
  91         @Override
  92         public default String getName() {
  93             throw new UnsupportedOperationException("Not supported yet.");
  94         }
  95 
  96         @Override
  97         public default boolean isLoggable(PlatformLogger.Level level) {
  98             throw new UnsupportedOperationException("Not supported yet.");
  99         }
 100 
 101         @Override
 102         public default void log(sun.util.logging.PlatformLogger.Level level,
 103                          String msg, Throwable thrown) {
 104         }
 105         @Override
 106         public default void log(sun.util.logging.PlatformLogger.Level level,
 107                          Throwable thrown, Supplier<String> msgSupplier) {
 108         }
 109         @Override
 110         public default void log(sun.util.logging.PlatformLogger.Level level,
 111                          Supplier<String> msgSupplier) {
 112         }
 113         @Override
 114         public default void log(sun.util.logging.PlatformLogger.Level level, String msg) {
 115         }
 116         @Override
 117         public default void log(sun.util.logging.PlatformLogger.Level level,
 118                          String format, Object... params) {
 119         }
 120         @Override
 121         public default void logrb(sun.util.logging.PlatformLogger.Level level,
 122                          ResourceBundle bundle, String key, Throwable thrown) {
 123         }
 124         @Override
 125         public default void logrb(sun.util.logging.PlatformLogger.Level level,
 126                          ResourceBundle bundle, String format, Object... params) {
 127         }
 128 
 129         @Override
 130         public default void logrb(PlatformLogger.Level level,
 131                          String sourceClass, String sourceMethod,
 132                          ResourceBundle bundle, String msg, Throwable thrown) {
 133         }
 134 
 135         @Override
 136         public default void logrb(PlatformLogger.Level level, String sourceClass,
 137                          String sourceMethod, ResourceBundle bundle, String msg,
 138                          Object... params) {
 139         }
 140 
 141         @Override
 142         public default void logp(PlatformLogger.Level level, String sourceClass,
 143                          String sourceMethod, Supplier<String> msgSupplier) {
 144         }
 145 
 146         @Override
 147         public default void logp(PlatformLogger.Level level, String sourceClass,
 148                          String sourceMethod, String msg, Object... params) {
 149         }
 150 
 151         @Override
 152         public default void logp(PlatformLogger.Level level, String sourceClass,
 153                          String sourceMethod, String msg, Throwable thrown) {
 154         }
 155 
 156         @Override
 157         public default void logp(PlatformLogger.Level level, String sourceClass,
 158                          String sourceMethod, String msg) {
 159         }
 160 
 161         @Override
 162         public default void logp(PlatformLogger.Level level, String sourceClass,
 163                          String sourceMethod, Throwable thrown,
 164                          Supplier<String> msgSupplier) {
 165         }
 166 
 167         static boolean requiresDefaultBodyFor(Method m) {
 168             try {
 169                 Method m2 = LoggerBridgeMethodsWithNoBody.class
 170                         .getDeclaredMethod(m.getName(),
 171                         m.getParameterTypes());
 172                 return !m2.isDefault();
 173             } catch (NoSuchMethodException x) {
 174                 return true;
 175             }
 176         }
 177     }
 178 
 179     final boolean warnDuplicateMappings;
 180     public LoggerFinderAPITest(boolean verbose) {
 181         this.warnDuplicateMappings = verbose;
 182         for (Handler h : Logger.getLogger("").getHandlers()) {
 183             if (h instanceof ConsoleHandler) {
 184                 Logger.getLogger("").removeHandler(h);
 185             }
 186         }
 187         Logger.getLogger("").addHandler( new Handler() {
 188             @Override
 189             public void publish(LogRecord record) {
 190                 StringBuilder builder = new StringBuilder();
 191                 builder.append("GOT LogRecord: ")
 192                         .append(record.getLevel().getLocalizedName())
 193                         .append(": [").append(record.getLoggerName())
 194                         .append("] ").append(record.getSourceClassName())
 195                         .append('.')
 196                         .append(record.getSourceMethodName()).append(" -> ")
 197                         .append(record.getMessage())
 198                         .append(' ')
 199                         .append(record.getParameters() == null ? ""
 200                                 : Arrays.toString(record.getParameters()))
 201                         ;
 202                 System.out.println(builder);
 203                 if (record.getThrown() != null) {
 204                     record.getThrown().printStackTrace(System.out);
 205                 }
 206             }
 207             @Override public void flush() {}
 208             @Override public void close() {}
 209         });
 210     }
 211 
 212     public Stream<Method> getJulLogMethodStream(Class<?> loggerClass) {
 213 
 214         return Stream.of(loggerClass.getMethods()).filter((x) -> {
 215             final Matcher m = julLogNames.matcher(x.getName());
 216             return m.matches() ? x.getAnnotation(Deprecated.class) == null : false;
 217         });
 218     }
 219 
 220     /**
 221      * Tells whether a method invocation of 'origin' can be transformed in a
 222      * method invocation of 'target'.
 223      * This method only look at the parameter signatures, it doesn't look at
 224      * the name, nor does it look at the return types.
 225      * <p>
 226      * Example:
 227      * <ul>
 228      *     <li>java.util.logging.Logger.log(Level, String, Object) can be invoked as<br>
 229          java.util.logging.spi.Logger.log(Level, String, Object...) because the
 230          last parameter in 'target' is a varargs.</li>
 231      *     <li>java.util.logging.Logger.log(Level, String) can also be invoked as<br>
 232          java.util.logging.spi.Logger.log(Level, String, Object...) for the
 233          same reason.</li>
 234      * </ul>
 235      * <p>
 236      * The algorithm is tailored for our needs: when the last parameter in the
 237      * target is a vararg, and when origin & target have the same number of
 238      * parameters, then we consider that the types of the last parameter *must*
 239      * match.
 240      * <p>
 241      * Similarly - we do not consider that o(X x, Y y, Y y) matches t(X x, Y... y)
 242      * although strictly speaking, it should...
 243      *
 244      * @param origin The method in the original class
 245      * @param target The correspondent candidate in the target class
 246      * @return true if a method invocation of 'origin' can be transformed in a
 247      * method invocation of 'target'.
 248      */
 249     public boolean canBeInvokedAs(Method origin, Method target,
 250                                   Map<Class<?>,Class<?>> substitutes) {
 251         final Class<?>[] xParams = target.getParameterTypes();
 252         final Class<?>[] mParams = Stream.of(origin.getParameterTypes())
 253                 .map((x) -> substitutes.getOrDefault(x, x))
 254                 .collect(Collectors.toList()).toArray(new Class<?>[0]);
 255         if (Arrays.deepEquals(xParams, mParams)) return true;
 256         if (target.isVarArgs()) {
 257             if (xParams.length == mParams.length) {
 258                 if (xParams[xParams.length-1].isArray()) {
 259                     return mParams[mParams.length -1].equals(
 260                             xParams[xParams.length -1].getComponentType());
 261                 }
 262             } else if (xParams.length == mParams.length + 1) {
 263                 return Arrays.deepEquals(
 264                         Arrays.copyOfRange(xParams, 0, xParams.length-1), mParams);
 265             }
 266         }
 267         return false;
 268     }
 269 
 270     /**
 271      * Look whether {@code otherClass} has a public method similar to m
 272      * @param m
 273      * @param otherClass
 274      * @return
 275      */
 276     public Stream<Method> findInvokable(Method m, Class<?> otherClass) {
 277         final Map<Class<?>,Class<?>> substitues =
 278                 Collections.singletonMap(java.util.logging.Level.class,
 279                         sun.util.logging.PlatformLogger.Level.class);
 280         return Stream.of(otherClass.getMethods())
 281                 .filter((x) -> m.getName().equals(x.getName()))
 282                 .filter((x) -> canBeInvokedAs(m, x, substitues));
 283     }
 284 
 285     /**
 286      * Test that the concrete Logger implementation passed as parameter
 287      * overrides all the methods defined by its interface.
 288      * @param julLogger A concrete implementation of System.Logger
 289      *    whose backend is a JUL Logger.
 290      */
 291     StringBuilder testDefaultJULLogger(java.lang.System.Logger julLogger) {
 292         final StringBuilder errors = new StringBuilder();
 293         if (!bridgeLoggerClass.isInstance(julLogger)) {
 294             final String errorMsg =
 295                     "Logger returned by LoggerFactory.getLogger(\"foo\") is not a "
 296                     + bridgeLoggerClass + "\n\t" + julLogger;
 297             System.err.println(errorMsg);
 298             errors.append(errorMsg).append('\n');
 299         }
 300         final Class<? extends java.lang.System.Logger> xClass = julLogger.getClass();
 301         List<Method> notOverridden =
 302                 Stream.of(bridgeLoggerClass.getDeclaredMethods()).filter((m) -> {
 303             try {
 304                 Method x = xClass.getDeclaredMethod(m.getName(), m.getParameterTypes());
 305                 return x == null;
 306             } catch (NoSuchMethodException ex) {
 307                 return !Modifier.isStatic(m.getModifiers());
 308             }
 309         }).collect(Collectors.toList());
 310         notOverridden.stream().filter((x) -> {
 311             boolean shouldOverride = true;
 312             try {
 313                 final Method m = xClass.getMethod(x.getName(), x.getParameterTypes());
 314                 Method m2 = null;
 315                 try {
 316                     m2 = jdkLoggerClass.getDeclaredMethod(x.getName(), x.getParameterTypes());
 317                 } catch (Exception e) {
 318 
 319                 }
 320                 shouldOverride = m.isDefault() || m2 == null;
 321             } catch (Exception e) {
 322                 // should override.
 323             }
 324             return shouldOverride;
 325         }).forEach(x -> {
 326             final String errorMsg = xClass.getName() + " should override\n\t" + x.toString();
 327             System.err.println(errorMsg);
 328             errors.append(errorMsg).append('\n');
 329         });
 330         if (notOverridden.isEmpty()) {
 331             System.out.println(xClass + " overrides all methods from " + bridgeLoggerClass);
 332         }
 333         return errors;
 334     }
 335 
 336     public static class ResourceBundeParam extends ResourceBundle {
 337         Map<String, String> map = Collections.synchronizedMap(new LinkedHashMap<>());
 338         @Override
 339         protected Object handleGetObject(String key) {
 340             map.putIfAbsent(key, "${"+key+"}");
 341             return map.get(key);
 342         }
 343 
 344         @Override
 345         public Enumeration<String> getKeys() {
 346             return Collections.enumeration(new LinkedHashSet<>(map.keySet()));
 347         }
 348 
 349     }
 350 
 351     final ResourceBundle bundleParam =
 352             ResourceBundle.getBundle(ResourceBundeParam.class.getName());
 353 
 354     public static class ResourceBundeLocalized extends ResourceBundle {
 355         Map<String, String> map = Collections.synchronizedMap(new LinkedHashMap<>());
 356         @Override
 357         protected Object handleGetObject(String key) {
 358             map.putIfAbsent(key, "Localized:${"+key+"}");
 359             return map.get(key);
 360         }
 361 
 362         @Override
 363         public Enumeration<String> getKeys() {
 364             return Collections.enumeration(new LinkedHashSet<>(map.keySet()));
 365         }
 366 
 367     }
 368 
 369     final static ResourceBundle bundleLocalized =
 370             ResourceBundle.getBundle(ResourceBundeLocalized.class.getName());
 371 
 372     final Map<Class<?>, Object> params = new HashMap<>();
 373     {
 374         params.put(String.class, "TestString");
 375         params.put(sun.util.logging.PlatformLogger.Level.class, sun.util.logging.PlatformLogger.Level.WARNING);
 376         params.put(java.lang.System.Logger.Level.class, java.lang.System.Logger.Level.WARNING);
 377         params.put(ResourceBundle.class, bundleParam);
 378         params.put(Throwable.class, new Throwable("TestThrowable (Please ignore it!)"));
 379         params.put(Object[].class, new Object[] {"One", "Two"});
 380         params.put(Object.class, new Object() {
 381             @Override public String toString() { return "I am an object!"; }
 382         });
 383     }
 384 
 385     public Object[] getParamsFor(Method m) {
 386         final Object[] res = new Object[m.getParameterCount()];
 387         final Class<?>[] sig = m.getParameterTypes();
 388         if (res.length == 0) {
 389             return res;
 390         }
 391         for (int i=0; i<res.length; i++) {
 392             Object p = params.get(sig[i]);
 393             if (p == null && sig[i].equals(Supplier.class)) {
 394                 final String msg = "SuppliedMsg["+i+"]";
 395                 p = (Supplier<String>) () -> msg;
 396             }
 397             if (p instanceof String) {
 398                 res[i] = String.valueOf(p)+"["+i+"]";
 399             } else {
 400                 res[i] = p;
 401             }
 402         }
 403         return res;
 404     }
 405 
 406     public void invokeOn(java.lang.System.Logger logger, Method m) {
 407         Object[] p = getParamsFor(m);
 408         try {
 409             m.invoke(logger, p);
 410         } catch (Exception e) {
 411             throw new RuntimeException("Failed to invoke "+m.toString(), e);
 412         }
 413     }
 414 
 415     public void testAllJdkExtensionMethods(java.lang.System.Logger logger) {
 416         Stream.of(jdkLoggerClass.getDeclaredMethods())
 417                 .filter(m -> !Modifier.isStatic(m.getModifiers()))
 418                 .forEach((m) -> invokeOn(logger, m));
 419     }
 420 
 421     public void testAllAPIMethods(java.lang.System.Logger logger) {
 422         Stream.of(spiLoggerClass.getDeclaredMethods())
 423                 .filter(m -> !Modifier.isStatic(m.getModifiers()))
 424                 .forEach((m) -> invokeOn(logger, m));
 425     }
 426 
 427     public void testAllBridgeMethods(java.lang.System.Logger logger) {
 428         Stream.of(bridgeLoggerClass.getDeclaredMethods())
 429                 .filter(m -> !Modifier.isStatic(m.getModifiers()))
 430                 .forEach((m) -> invokeOn(logger, m));
 431     }
 432 
 433     public void testAllLogProducerMethods(java.lang.System.Logger logger) {
 434         Stream.of(julLogProducerClass.getDeclaredMethods())
 435                 .filter(m -> !Modifier.isStatic(m.getModifiers()))
 436                 .forEach((m) -> invokeOn(logger, m));
 437     }
 438 
 439     public StringBuilder testGetLoggerOverriddenOnSpi() {
 440         final StringBuilder errors = new StringBuilder();
 441         Stream.of(jdkLoggerClass.getDeclaredMethods())
 442                 .filter(m -> Modifier.isStatic(m.getModifiers()))
 443                 .filter(m -> Modifier.isPublic(m.getModifiers()))
 444                 .filter(m -> !m.getName().equals("getLoggerFinder"))
 445                 .filter(m -> {
 446                     try {
 447                         final Method x = bridgeLoggerClass.getDeclaredMethod(m.getName(), m.getParameterTypes());
 448                         return x == null;
 449                     } catch (NoSuchMethodException ex) {
 450                         return true;
 451                     }
 452                 }).forEach(m -> {
 453                     final String errorMsg = bridgeLoggerClass.getName() + " should override\n\t" + m.toString();
 454                     System.err.println(errorMsg);
 455                     errors.append(errorMsg).append('\n');
 456                 });
 457         if (errors.length() == 0) {
 458             System.out.println(bridgeLoggerClass + " overrides all static methods from " + jdkLoggerClass);
 459         } else {
 460             if (errors.length() > 0) throw new RuntimeException(errors.toString());
 461         }
 462         return errors;
 463     }
 464 
 465     public static void main(String argv[]) throws Exception {
 466         final LoggerFinderAPITest test = new LoggerFinderAPITest(false);
 467         final StringBuilder errors = new StringBuilder();
 468         errors.append(test.testGetLoggerOverriddenOnSpi());
 469         java.lang.System.Logger julLogger =
 470                 java.lang.System.LoggerFinder.getLoggerFinder()
 471                         .getLogger("foo", LoggerFinderAPITest.class);
 472         errors.append(test.testDefaultJULLogger(julLogger));
 473         if (errors.length() > 0) throw new RuntimeException(errors.toString());
 474         java.lang.System.Logger julSystemLogger =
 475                 java.lang.System.LoggerFinder.getLoggerFinder()
 476                         .getLogger("bar", Thread.class);
 477         errors.append(test.testDefaultJULLogger(julSystemLogger));
 478         if (errors.length() > 0) throw new RuntimeException(errors.toString());
 479         java.lang.System.Logger julLocalizedLogger =
 480                 (java.lang.System.Logger)
 481                 System.getLogger("baz", bundleLocalized);
 482         java.lang.System.Logger julLocalizedSystemLogger =
 483                 java.lang.System.LoggerFinder.getLoggerFinder()
 484                         .getLocalizedLogger("oof", bundleLocalized, Thread.class);
 485         final String error = errors.toString();
 486         if (!error.isEmpty()) throw new RuntimeException(error);
 487         for (java.lang.System.Logger logger : new java.lang.System.Logger[] {
 488             julLogger, julSystemLogger, julLocalizedLogger, julLocalizedSystemLogger
 489         }) {
 490             test.testAllJdkExtensionMethods(logger);
 491             test.testAllAPIMethods(logger);
 492             test.testAllBridgeMethods(logger);
 493             test.testAllLogProducerMethods(logger);
 494         }
 495     }
 496 
 497 }