/* * Copyright (c) 2015, 2016, 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. */ /* * @test * @bug 8140364 * @author danielfuchs * @summary JDK implementation specific unit test for JDK internal artifacts. * Tests the consistency of the LoggerFinder and JDK extensions. * @modules java.base/sun.util.logging * java.base/jdk.internal.logger * java.logging * @run main LoggerFinderAPITest */ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.ResourceBundle; import java.util.function.Supplier; import java.util.logging.ConsoleHandler; import java.util.logging.Handler; import java.util.logging.LogRecord; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; import sun.util.logging.PlatformLogger; public class LoggerFinderAPITest { static final Class<java.lang.System.Logger> spiLoggerClass = java.lang.System.Logger.class; static final Class<java.lang.System.Logger> jdkLoggerClass = java.lang.System.Logger.class; static final Class<sun.util.logging.PlatformLogger.Bridge> bridgeLoggerClass = sun.util.logging.PlatformLogger.Bridge.class; static final Class<java.util.logging.Logger> julLoggerClass = java.util.logging.Logger.class; static final Class<sun.util.logging.PlatformLogger.Bridge> julLogProducerClass = PlatformLogger.Bridge.class; static final Pattern julLogNames = Pattern.compile( "^((log(p|rb)?)|severe|warning|info|config|fine|finer|finest|isLoggable)$"); static final Collection<Method> julLoggerIgnores; static { List<Method> ignores = new ArrayList<>(); try { ignores.add(julLoggerClass.getDeclaredMethod("log", LogRecord.class)); } catch (NoSuchMethodException | SecurityException ex) { throw new ExceptionInInitializerError(ex); } julLoggerIgnores = Collections.unmodifiableList(ignores); } // Don't require LoggerBridge to have a body for those methods interface LoggerBridgeMethodsWithNoBody extends PlatformLogger.Bridge, java.lang.System.Logger { @Override public default String getName() { throw new UnsupportedOperationException("Not supported yet."); } @Override public default boolean isLoggable(PlatformLogger.Level level) { throw new UnsupportedOperationException("Not supported yet."); } @Override public default void log(sun.util.logging.PlatformLogger.Level level, String msg, Throwable thrown) { } @Override public default void log(sun.util.logging.PlatformLogger.Level level, Throwable thrown, Supplier<String> msgSupplier) { } @Override public default void log(sun.util.logging.PlatformLogger.Level level, Supplier<String> msgSupplier) { } @Override public default void log(sun.util.logging.PlatformLogger.Level level, String msg) { } @Override public default void log(sun.util.logging.PlatformLogger.Level level, String format, Object... params) { } @Override public default void logrb(sun.util.logging.PlatformLogger.Level level, ResourceBundle bundle, String key, Throwable thrown) { } @Override public default void logrb(sun.util.logging.PlatformLogger.Level level, ResourceBundle bundle, String format, Object... params) { } @Override public default void logrb(PlatformLogger.Level level, String sourceClass, String sourceMethod, ResourceBundle bundle, String msg, Throwable thrown) { } @Override public default void logrb(PlatformLogger.Level level, String sourceClass, String sourceMethod, ResourceBundle bundle, String msg, Object... params) { } @Override public default void logp(PlatformLogger.Level level, String sourceClass, String sourceMethod, Supplier<String> msgSupplier) { } @Override public default void logp(PlatformLogger.Level level, String sourceClass, String sourceMethod, String msg, Object... params) { } @Override public default void logp(PlatformLogger.Level level, String sourceClass, String sourceMethod, String msg, Throwable thrown) { } @Override public default void logp(PlatformLogger.Level level, String sourceClass, String sourceMethod, String msg) { } @Override public default void logp(PlatformLogger.Level level, String sourceClass, String sourceMethod, Throwable thrown, Supplier<String> msgSupplier) { } static boolean requiresDefaultBodyFor(Method m) { try { Method m2 = LoggerBridgeMethodsWithNoBody.class .getDeclaredMethod(m.getName(), m.getParameterTypes()); return !m2.isDefault(); } catch (NoSuchMethodException x) { return true; } } } final boolean warnDuplicateMappings; public LoggerFinderAPITest(boolean verbose) { this.warnDuplicateMappings = verbose; for (Handler h : Logger.getLogger("").getHandlers()) { if (h instanceof ConsoleHandler) { Logger.getLogger("").removeHandler(h); } } Logger.getLogger("").addHandler( new Handler() { @Override public void publish(LogRecord record) { StringBuilder builder = new StringBuilder(); builder.append("GOT LogRecord: ") .append(record.getLevel().getLocalizedName()) .append(": [").append(record.getLoggerName()) .append("] ").append(record.getSourceClassName()) .append('.') .append(record.getSourceMethodName()).append(" -> ") .append(record.getMessage()) .append(' ') .append(record.getParameters() == null ? "" : Arrays.toString(record.getParameters())) ; System.out.println(builder); if (record.getThrown() != null) { record.getThrown().printStackTrace(System.out); } } @Override public void flush() {} @Override public void close() {} }); } public Stream<Method> getJulLogMethodStream(Class<?> loggerClass) { return Stream.of(loggerClass.getMethods()).filter((x) -> { final Matcher m = julLogNames.matcher(x.getName()); return m.matches() ? x.getAnnotation(Deprecated.class) == null : false; }); } /** * Tells whether a method invocation of 'origin' can be transformed in a * method invocation of 'target'. * This method only look at the parameter signatures, it doesn't look at * the name, nor does it look at the return types. * <p> * Example: * <ul> * <li>java.util.logging.Logger.log(Level, String, Object) can be invoked as<br> java.util.logging.spi.Logger.log(Level, String, Object...) because the last parameter in 'target' is a varargs.</li> * <li>java.util.logging.Logger.log(Level, String) can also be invoked as<br> java.util.logging.spi.Logger.log(Level, String, Object...) for the same reason.</li> * </ul> * <p> * The algorithm is tailored for our needs: when the last parameter in the * target is a vararg, and when origin & target have the same number of * parameters, then we consider that the types of the last parameter *must* * match. * <p> * Similarly - we do not consider that o(X x, Y y, Y y) matches t(X x, Y... y) * although strictly speaking, it should... * * @param origin The method in the original class * @param target The correspondent candidate in the target class * @return true if a method invocation of 'origin' can be transformed in a * method invocation of 'target'. */ public boolean canBeInvokedAs(Method origin, Method target, Map<Class<?>,Class<?>> substitutes) { final Class<?>[] xParams = target.getParameterTypes(); final Class<?>[] mParams = Stream.of(origin.getParameterTypes()) .map((x) -> substitutes.getOrDefault(x, x)) .collect(Collectors.toList()).toArray(new Class<?>[0]); if (Arrays.deepEquals(xParams, mParams)) return true; if (target.isVarArgs()) { if (xParams.length == mParams.length) { if (xParams[xParams.length-1].isArray()) { return mParams[mParams.length -1].equals( xParams[xParams.length -1].getComponentType()); } } else if (xParams.length == mParams.length + 1) { return Arrays.deepEquals( Arrays.copyOfRange(xParams, 0, xParams.length-1), mParams); } } return false; } /** * Look whether {@code otherClass} has a public method similar to m * @param m * @param otherClass * @return */ public Stream<Method> findInvokable(Method m, Class<?> otherClass) { final Map<Class<?>,Class<?>> substitues = Collections.singletonMap(java.util.logging.Level.class, sun.util.logging.PlatformLogger.Level.class); return Stream.of(otherClass.getMethods()) .filter((x) -> m.getName().equals(x.getName())) .filter((x) -> canBeInvokedAs(m, x, substitues)); } /** * Test that the concrete Logger implementation passed as parameter * overrides all the methods defined by its interface. * @param julLogger A concrete implementation of System.Logger * whose backend is a JUL Logger. */ StringBuilder testDefaultJULLogger(java.lang.System.Logger julLogger) { final StringBuilder errors = new StringBuilder(); if (!bridgeLoggerClass.isInstance(julLogger)) { final String errorMsg = "Logger returned by LoggerFactory.getLogger(\"foo\") is not a " + bridgeLoggerClass + "\n\t" + julLogger; System.err.println(errorMsg); errors.append(errorMsg).append('\n'); } final Class<? extends java.lang.System.Logger> xClass = julLogger.getClass(); List<Method> notOverridden = Stream.of(bridgeLoggerClass.getDeclaredMethods()).filter((m) -> { try { Method x = xClass.getDeclaredMethod(m.getName(), m.getParameterTypes()); return x == null; } catch (NoSuchMethodException ex) { return !Modifier.isStatic(m.getModifiers()); } }).collect(Collectors.toList()); notOverridden.stream().filter((x) -> { boolean shouldOverride = true; try { final Method m = xClass.getMethod(x.getName(), x.getParameterTypes()); Method m2 = null; try { m2 = jdkLoggerClass.getDeclaredMethod(x.getName(), x.getParameterTypes()); } catch (Exception e) { } shouldOverride = m.isDefault() || m2 == null; } catch (Exception e) { // should override. } return shouldOverride; }).forEach(x -> { final String errorMsg = xClass.getName() + " should override\n\t" + x.toString(); System.err.println(errorMsg); errors.append(errorMsg).append('\n'); }); if (notOverridden.isEmpty()) { System.out.println(xClass + " overrides all methods from " + bridgeLoggerClass); } return errors; } public static class ResourceBundeParam extends ResourceBundle { Map<String, String> map = Collections.synchronizedMap(new LinkedHashMap<>()); @Override protected Object handleGetObject(String key) { map.putIfAbsent(key, "${"+key+"}"); return map.get(key); } @Override public Enumeration<String> getKeys() { return Collections.enumeration(new LinkedHashSet<>(map.keySet())); } } final ResourceBundle bundleParam = ResourceBundle.getBundle(ResourceBundeParam.class.getName()); public static class ResourceBundeLocalized extends ResourceBundle { Map<String, String> map = Collections.synchronizedMap(new LinkedHashMap<>()); @Override protected Object handleGetObject(String key) { map.putIfAbsent(key, "Localized:${"+key+"}"); return map.get(key); } @Override public Enumeration<String> getKeys() { return Collections.enumeration(new LinkedHashSet<>(map.keySet())); } } final static ResourceBundle bundleLocalized = ResourceBundle.getBundle(ResourceBundeLocalized.class.getName()); final Map<Class<?>, Object> params = new HashMap<>(); { params.put(String.class, "TestString"); params.put(sun.util.logging.PlatformLogger.Level.class, sun.util.logging.PlatformLogger.Level.WARNING); params.put(java.lang.System.Logger.Level.class, java.lang.System.Logger.Level.WARNING); params.put(ResourceBundle.class, bundleParam); params.put(Throwable.class, new Throwable("TestThrowable (Please ignore it!)")); params.put(Object[].class, new Object[] {"One", "Two"}); params.put(Object.class, new Object() { @Override public String toString() { return "I am an object!"; } }); } public Object[] getParamsFor(Method m) { final Object[] res = new Object[m.getParameterCount()]; final Class<?>[] sig = m.getParameterTypes(); if (res.length == 0) { return res; } for (int i=0; i<res.length; i++) { Object p = params.get(sig[i]); if (p == null && sig[i].equals(Supplier.class)) { final String msg = "SuppliedMsg["+i+"]"; p = (Supplier<String>) () -> msg; } if (p instanceof String) { res[i] = String.valueOf(p)+"["+i+"]"; } else { res[i] = p; } } return res; } public void invokeOn(java.lang.System.Logger logger, Method m) { Object[] p = getParamsFor(m); try { m.invoke(logger, p); } catch (Exception e) { throw new RuntimeException("Failed to invoke "+m.toString(), e); } } public void testAllJdkExtensionMethods(java.lang.System.Logger logger) { Stream.of(jdkLoggerClass.getDeclaredMethods()) .filter(m -> !Modifier.isStatic(m.getModifiers())) .forEach((m) -> invokeOn(logger, m)); } public void testAllAPIMethods(java.lang.System.Logger logger) { Stream.of(spiLoggerClass.getDeclaredMethods()) .filter(m -> !Modifier.isStatic(m.getModifiers())) .forEach((m) -> invokeOn(logger, m)); } public void testAllBridgeMethods(java.lang.System.Logger logger) { Stream.of(bridgeLoggerClass.getDeclaredMethods()) .filter(m -> !Modifier.isStatic(m.getModifiers())) .forEach((m) -> invokeOn(logger, m)); } public void testAllLogProducerMethods(java.lang.System.Logger logger) { Stream.of(julLogProducerClass.getDeclaredMethods()) .filter(m -> !Modifier.isStatic(m.getModifiers())) .forEach((m) -> invokeOn(logger, m)); } public StringBuilder testGetLoggerOverriddenOnSpi() { final StringBuilder errors = new StringBuilder(); Stream.of(jdkLoggerClass.getDeclaredMethods()) .filter(m -> Modifier.isStatic(m.getModifiers())) .filter(m -> Modifier.isPublic(m.getModifiers())) .filter(m -> !m.getName().equals("getLoggerFinder")) .filter(m -> { try { final Method x = bridgeLoggerClass.getDeclaredMethod(m.getName(), m.getParameterTypes()); return x == null; } catch (NoSuchMethodException ex) { return true; } }).forEach(m -> { final String errorMsg = bridgeLoggerClass.getName() + " should override\n\t" + m.toString(); System.err.println(errorMsg); errors.append(errorMsg).append('\n'); }); if (errors.length() == 0) { System.out.println(bridgeLoggerClass + " overrides all static methods from " + jdkLoggerClass); } else { if (errors.length() > 0) throw new RuntimeException(errors.toString()); } return errors; } public static void main(String argv[]) throws Exception { final LoggerFinderAPITest test = new LoggerFinderAPITest(false); final StringBuilder errors = new StringBuilder(); errors.append(test.testGetLoggerOverriddenOnSpi()); java.lang.System.Logger julLogger = java.lang.System.LoggerFinder.getLoggerFinder() .getLogger("foo", LoggerFinderAPITest.class.getModule()); errors.append(test.testDefaultJULLogger(julLogger)); if (errors.length() > 0) throw new RuntimeException(errors.toString()); java.lang.System.Logger julSystemLogger = java.lang.System.LoggerFinder.getLoggerFinder() .getLogger("bar", Thread.class.getModule()); errors.append(test.testDefaultJULLogger(julSystemLogger)); if (errors.length() > 0) throw new RuntimeException(errors.toString()); java.lang.System.Logger julLocalizedLogger = (java.lang.System.Logger) System.getLogger("baz", bundleLocalized); java.lang.System.Logger julLocalizedSystemLogger = java.lang.System.LoggerFinder.getLoggerFinder() .getLocalizedLogger("oof", bundleLocalized, Thread.class.getModule()); final String error = errors.toString(); if (!error.isEmpty()) throw new RuntimeException(error); for (java.lang.System.Logger logger : new java.lang.System.Logger[] { julLogger, julSystemLogger, julLocalizedLogger, julLocalizedSystemLogger }) { test.testAllJdkExtensionMethods(logger); test.testAllAPIMethods(logger); test.testAllBridgeMethods(logger); test.testAllLogProducerMethods(logger); } } }