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