org.glassfish.pfl.basic.logex.WrapperGenerator Maven / Gradle / Ivy
/*
* Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0, which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.glassfish.pfl.basic.logex;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.TreeMap;
import java.util.logging.Formatter;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import org.glassfish.pfl.basic.algorithm.AnnotationAnalyzer;
import org.glassfish.pfl.basic.proxy.CompositeInvocationHandler;
import org.glassfish.pfl.basic.proxy.CompositeInvocationHandlerImpl;
/** Given an annotated interface, return a Proxy that implements that interface.
* Interface must be annotated with @ExceptionWrapper( String idPrefix, String loggerName ).
* id prefix defaults to empty, loggerName defaults to the package name of the annotated
* class.
*
* Also, note that this returned wrapper will always implement the MessageInfo
* interface, which provides a way to capture all of the messages and IDs used
* in the interface. This is used to generate resource bundles. In order for
* this to work, it is required that the interface declare a field
*
* public static final [class name] self = ExceptionWrapper.makeWrapper( ... ) ;
*
* This is necessary because the extension mechanism allows the construction
* of message IDs that cannot be predicted based on the annotations alone.
*
* The behavior of the implementation of each method on the interface is determined
* in part by its return type as follows:
*
* - void. Such a method can only log a message. Must have @Log, @Message is
* optional.
* - String. Such a method may log a message, and also returns the message.
* Both @Log and @Message are optional.
* - A subclass of Exception. Such a method may log a message, and also returns
* an exception containing the message.
* Both @Log and @Message are optional.
*
*
* Each method may be annotated as follows:
*
*
* - @Message( String value ). This defines the message to be placed in a resource
* bundle (generated at build time by a separate tool). The key to the resource
* bundle is
.. The message is prepended with the
* idPrefix and the id from the @Log annotation (if @Log is present, otherwise nothing
* is prepended to the message). If this annotation is not present, a default message
* is created from the method name and the arguments.
* - @Log( LogLevel level, int id ). The presence of this annotation indicates that
* a log record must be generated, and logger IF the appropriate logger is enabled at
* the given level (note that LogLevel is an enum used for the annotation, each member
* of which returns the java.util.logging.Level from a getLevel() method).
*
*
* In addition, the @Chain annotation may be used on a method parameter (whose type
* must be a subclass of Throwable) of a method that returns an exception
* to indicate that the parameter should be the cause of the returned exception.
* All other method parameters are used as arguments in formatting the message.
*
* @author ken
*/
public class WrapperGenerator {
// XXX check the CORBA version of logex to see if optimizations are in the
// pfl version.
/** Hidden interface implemented by the result of the makeWrapper call.
* This is needed in the resource file generation tool.
*/
public interface MessageInfo {
/** Return a map from message ID to message for all exceptions
* defined in a @ExceptionWrapper interface.
* The key in the result is the message ID, and the value is the
* message string (defined in @Message).
* @return map from message ID to message.
*/
Map getMessageInfo() ;
}
/** Extension API available to override the default behavior of the
* WrapperGenerator.
*/
public interface Extension {
/** Get a message id for this log.
*
* @param method The method defining this log.
* @return The message id.
*/
String getLogId( Method method ) ;
/** Construct an exception from the message and the exception type.
* The method provides access to any additional annotations that may
* be needed.
*
* @param msg The message to use in the exception.
* @param method The method creating the exception.
*/
Throwable makeException( String msg, Method method ) ;
/** Modify the default logger name if needed.
*
* @param cls The standard logger name
* @return A possibly updated logger name
*/
String getLoggerName( Class> cls );
}
/** Convenience base class for implementations of Extension that don't
* need to override every method.
*/
public static abstract class ExtensionBase implements Extension {
@Override
public String getLogId(Method method) {
return WrapperGenerator.getStandardLogId(method) ;
}
@Override
public Throwable makeException(String msg, Method method) {
return WrapperGenerator.makeStandardException(msg, method) ;
}
@Override
public String getLoggerName(Class> cls) {
return WrapperGenerator.getStandardLoggerName( cls ) ;
}
}
/** Expose the standard log ID for the method. This is simply
* the annotated value in the @Log annotation: it is not processed in
* any way.
*
* @param method The method for which the ID is requested.
* @return The ID (as a string), or null if no @Log annotation is present.
*/
public static String getStandardLogId( Method method ) {
final Class> cls = method.getDeclaringClass() ;
final ExceptionWrapper ew = cls.getAnnotation( ExceptionWrapper.class ) ;
final String idPrefix = ew.idPrefix() ;
final Log log = aa.getAnnotation( method, Log.class ) ;
if (log == null) {
return null ;
}
final String result = String.format( "%s%05d", idPrefix, log.id() ) ;
return result ;
}
static Throwable makeStandardException( final String msg,
final Method method ) {
Throwable result ;
final Class> rtype = method.getReturnType() ;
try {
@SuppressWarnings("unchecked")
final Constructor cons =
(Constructor)rtype.getConstructor(String.class);
result = cons.newInstance(msg);
} catch (InstantiationException ex) {
throw new RuntimeException( ex ) ;
} catch (IllegalAccessException ex) {
throw new RuntimeException( ex ) ;
} catch (IllegalArgumentException ex) {
throw new RuntimeException( ex ) ;
} catch (InvocationTargetException ex) {
throw new RuntimeException( ex ) ;
} catch (NoSuchMethodException ex) {
throw new RuntimeException( ex ) ;
} catch (SecurityException ex) {
throw new RuntimeException( ex ) ;
}
return result ;
}
static String getStandardLoggerName( Class> cls ) {
final ExceptionWrapper ew = aa.getAnnotation( cls, ExceptionWrapper.class ) ;
String str = ew.loggerName() ;
if (str.length() == 0) {
str = cls.getPackage().getName() ;
}
return str ;
}
// Used whenever there is no user-supplied Extension.
static final Extension stdExtension = new ExtensionBase() {} ;
// Used to handle inheritance of annotations for methods.
static final AnnotationAnalyzer aa = new AnnotationAnalyzer() ;
private WrapperGenerator() {}
// Find the outer index in pannos for which the element array
// contains an annotation of type cls.
static int findAnnotatedParameter( Annotation[][] pannos,
Class extends Annotation> cls ) {
for (int ctr1=0; ctr1= 0) {
Object[] result = new Object[args.length-1] ;
int rindex = 0 ;
for (int ctr=0; ctr getMessageMap( Class> cls,
Extension extension ) {
final Map result = new TreeMap() ;
final ExceptionWrapper ew = aa.getAnnotation( cls, ExceptionWrapper.class ) ;
final String loggerName = ew.loggerName() ;
final String idPrefix = ew.idPrefix() ;
// A message is defined for every method, even if no annotations are
// present!
for (Method method : cls.getMethods()) {
final String key = getMsgKey( loggerName, method ) ;
final String msg = getMessage( method, idPrefix, extension ) ;
result.put( key, msg ) ;
}
return result ;
}
// Used to construct the message map, and in case no ResourceBundle is
// available.
static String getMessage( Method method,
String prefix, Extension extension ) {
final Message message = aa.getAnnotation( method, Message.class ) ;
final StringBuilder sb = new StringBuilder() ;
if (prefix != null) {
sb.append( prefix ) ;
sb.append( ": " ) ;
}
if (message == null) {
sb.append( method.getName() ) ;
sb.append( ' ' ) ;
for (int ctr=0; ctr0) {
sb.append( ", " ) ;
}
sb.append( "arg" ) ;
sb.append( ctr ) ;
sb.append("={").append(ctr).append( "}") ;
}
} else {
sb.append( message.value() ) ;
}
return sb.toString() ;
}
static String getMessageOrKey( Logger logger, Method method,
Extension extension ) {
final String prefix = extension.getLogId(method) ;
final ResourceBundle catalog = logger.getResourceBundle() ;
String transMsg = null ;
if (catalog != null) {
final String msgKey = getMsgKey( logger.getName(), method ) ;
transMsg = catalog.getString( msgKey ) ;
if (transMsg != null || transMsg.equals( msgKey )) {
transMsg = null ;
}
}
if (transMsg == null) {
// if there is no ResourceBundle, we use the message
// from the method.
transMsg = getMessage( method, prefix, extension ) ;
}
return transMsg ;
}
static String handleMessageOnly( Method method, Extension extension,
Logger logger, Object[] messageParams ) {
final String prefix = extension.getLogId(method) ;
final ResourceBundle catalog = logger.getResourceBundle() ;
final String transMsg = getMessageOrKey( logger, method, extension ) ;
String result ;
if (transMsg.indexOf( "{0" ) >= 0 ) {
result = java.text.MessageFormat.format( transMsg, messageParams ) ;
} else {
result = transMsg ;
}
// XXX OperationContext?
return result ;
}
enum ReturnType { EXCEPTION, STRING, NULL } ;
static ReturnType classifyReturnType( Method method ) {
Class> rtype = method.getReturnType() ;
if (rtype.equals( void.class ) ) {
return ReturnType.NULL ;
} else if (rtype.equals( String.class)) {
return ReturnType.STRING ;
} else if (Throwable.class.isAssignableFrom(rtype)) {
return ReturnType.EXCEPTION ;
} else {
throw new RuntimeException( "Method " + method
+ " has an illegal return type" ) ;
}
}
static LogRecord makeLogRecord( Level level, String key,
Object[] args, Logger logger ) {
LogRecord result = new LogRecord( level, key ) ;
if (args != null && args.length > 0) {
result.setParameters( args ) ;
}
result.setLoggerName( logger.getName() ) ;
result.setResourceBundle( logger.getResourceBundle() ) ;
return result ;
}
// Note: This is used ONLY to format the message used in the method
// result, not in the actual log handler.
// We define this class simply to re-use the code in formatMessage.
static class ShortFormatter extends Formatter {
@Override
public String format( LogRecord record ) {
StringBuilder sb = new StringBuilder() ;
sb.append(record.getLevel().getLocalizedName());
sb.append(": ");
String message = formatMessage( record ) ;
sb.append(message);
return sb.toString() ;
}
}
static final String cihiName =
CompositeInvocationHandlerImpl.class.getName() ;
static void trimStackTrace( Throwable exc, LogRecord lrec ) {
// Massage exception into appropriate form, and get the caller's
// class and method.
final StackTraceElement[] st = exc.getStackTrace() ;
final List filtered =
new ArrayList() ;
boolean skipping = true ;
for (StackTraceElement ste : st) {
if (skipping) {
if (ste.getClassName().equals( cihiName )
&& ste.getMethodName().equals( "invoke" )) {
skipping = false ;
}
} else {
filtered.add( ste ) ;
}
}
exc.setStackTrace( filtered.toArray(
new StackTraceElement[filtered.size()] ) ) ;
// First stack element we want is the Proxy$n.exceptionMethod
// from the exception interface. Second gives us the
// caller class and method name.
StackTraceElement caller = filtered.get(1) ;
lrec.setSourceClassName( caller.getClassName() );
lrec.setSourceMethodName( caller.getMethodName() );
}
static boolean isMajorLevel( Level level ) {
return level.intValue() > Level.INFO.intValue() ;
}
static boolean needStackTrace( Level level, Method method ) {
// Determine whether or not we need to add a stack trace to
// the log record.
final Class> mcls = method.getDeclaringClass() ;
final boolean hasClassST = aa.getAnnotation(mcls,
StackTrace.class ) != null ;
final boolean hasClassNST = aa.getAnnotation(mcls,
NoStackTrace.class ) != null ;
final boolean hasMethodST = aa.getAnnotation(method,
StackTrace.class ) != null ;
final boolean hasMethodNST = aa.getAnnotation(method,
NoStackTrace.class ) != null ;
final boolean highLevel = isMajorLevel( level ) ;
// How to interpret flags:
// xST and xNST (for x=class or method) should not both be true at
// the same time (but if they are, assume xST)
// hasClassST hasClassNST hasMethodST hasMethodNST level above info result
// F F F F F F
// F F F F T T
// F F F T - F
// F F T - - T
// F T F - - F
// F T T - - T
// T - - F - T
// T - - T - F
boolean useST = false ;
if (hasClassST) {
useST = !hasMethodNST ;
} else if (hasClassNST) {
useST = hasMethodST ;
} else if (hasMethodST) {
useST = true ;
} else if (hasMethodNST) {
useST = false ;
} else {
useST = highLevel ;
}
return useST ;
}
static Object handleFullLogging( Log log, Method method,
ReturnType rtype, Logger logger,
String idPrefix, Object[] messageParams, Throwable cause,
Extension extension ) {
final Level level = log.level().getLevel() ;
final boolean useST = needStackTrace( level, method ) ;
final String msgKey = getMessageOrKey( logger, method, extension ) ;
final LogRecord lrec = makeLogRecord( level, msgKey,
messageParams, logger ) ;
final ShortFormatter formatter = new ShortFormatter() ; // fix GLASSFISH-18351
final String message = formatter.format( lrec ) ;
Throwable exc = null ;
if (rtype == ReturnType.EXCEPTION) {
exc = extension.makeException( message, method ) ;
if (exc != null) {
trimStackTrace( exc, lrec);
if (cause != null) {
exc.initCause( cause ) ;
}
}
} else {
// Just do this to correctly set the source class and method name
// in the log record.
trimStackTrace( new Throwable(), lrec ) ;
}
if (exc != null) {
if (useST) {
lrec.setThrown( exc ) ;
}
}
/* XXX This is a problem, because we don't control the message string.
* We need to do something like add another {n} argument to the end
* of the message string that goes into the ResourceBundle.
* Do we need yet another annotation (@OperationContext) to control
* this?
*
// XXX need to centralize isLoggable checks.
if (logger.isLoggable(level) && isMajorLevel(level)) {
final String context = OperationTracer.getAsString() ;
String newMsg = msgKey ;
// FIXME: doesn't work with resource bundle.
if (context.length() > 0) {
newMsg += "\nCONTEXT:" + context ;
lrec.setMessage( newMsg ) ;
}
logger.log( lrec ) ;
}
*/
switch (rtype) {
case EXCEPTION : return exc ;
case STRING : return message ;
default : return null ;
}
}
/** Given an interface annotated with @ExceptionWrapper, return a proxy
* implementing the interface.
*
* @param The annotated interface type.
* @param cls The class of the annotated interface.
* @return An instance of the interface.
*/
public static T makeWrapper( final Class cls ) {
return makeWrapper(cls, stdExtension ) ;
}
/** Given an interface annotated with @ExceptionWrapper, return a proxy
* implementing the interface.
*
* @param The annotated interface type.
* @param cls The class of the annotated interface.
* @param extension The extension instance used to override the default
* behavior.
* @return An instance of the interface.
*/
@SuppressWarnings({"unchecked", "unchecked"})
public static T makeWrapper( final Class cls,
final Extension extension ) {
try {
// Must have an interface to use a Proxy.
if (!cls.isInterface()) {
throw new IllegalArgumentException( "Class " + cls +
"is not an interface" ) ;
}
final ExceptionWrapper ew = aa.getAnnotation( cls, ExceptionWrapper.class ) ;
final String idPrefix = ew.idPrefix() ;
final String name = extension.getLoggerName( cls );
// Get the logger with the resource bundle if it is available,
// otherwise without it. This is needed because sometimes
// when we load a class to generate a .properties file, the
// ResourceBundle is (obviously!) not availabe, and a static
// initializer must initialize a log wrapper WITHOUT a
// ResourceBundle, in order to generate a properties file which
// implements the ResourceBundle.
// Issue GLASSFISH-14269: Get the logger outside of the
// construction of the InvocationHandler, because Logger.getLogger
// is an expensive synchronized call.
Logger lg = null ;
try {
lg = Logger.getLogger( name, name ) ;
} catch (MissingResourceException exc) {
lg = Logger.getLogger( name ) ;
}
final Logger logger = lg ;
InvocationHandler inh = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
final ReturnType rtype = classifyReturnType(method) ;
final Log log = aa.getAnnotation( method, Log.class ) ;
// Issue GLASSFISH-14852: If there is no message and no logging
// needed, return early and avoid unneeded computation.
if (rtype == ReturnType.NULL) {
if (log == null) {
return null ;
} else {
final LogLevel level = log.level() ;
// XXX need to centralize isLoggable checks.
// This may be a new extension point.
if (!logger.isLoggable(level.getLevel())) {
return null ;
}
}
}
final Annotation[][] pannos =
method.getParameterAnnotations() ;
final int chainIndex = findAnnotatedParameter( pannos,
Chain.class ) ;
final Object[] messageParams = getWithSkip( args,
chainIndex ) ;
if (log == null) {
if (rtype != ReturnType.STRING) {
throw new IllegalArgumentException(
"No @Log annotation present on "
+ cls.getName() + "." + method.getName() ) ;
}
return handleMessageOnly( method, extension, logger,
messageParams ) ;
} else {
Throwable cause = null ;
if (chainIndex >= 0) {
cause = (Throwable)args[chainIndex] ;
}
return handleFullLogging( log, method, rtype, logger,
idPrefix, messageParams, cause, extension ) ;
}
}
} ;
InvocationHandler inhmi = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if (method.getName().equals( "getMessageInfo")) {
return getMessageMap( cls, extension ) ;
}
throw new RuntimeException( "Unexpected method " + method ) ;
}
} ;
final CompositeInvocationHandler cih =
new CompositeInvocationHandlerImpl() {
private static final long serialVersionUID =
3086904407674824236L;
@Override
public String toString() {
return "ExceptionWrapper[" + cls.getName() + "]" ;
}
} ;
cih.addInvocationHandler( cls, inh ) ;
cih.addInvocationHandler( MessageInfo.class, inhmi) ;
// Load the Proxy using the same ClassLoader that loaded the
// interface
ClassLoader loader = cls.getClassLoader() ;
Class>[] classes = { cls, MessageInfo.class } ;
return (T)Proxy.newProxyInstance(loader, classes, cih ) ;
} catch (Throwable thr) {
// This method must NEVER throw an exception, because it is usually
// called from a static initializer, and uncaught exceptions in
// static initializers are VERY hard to debug.
Logger.getLogger( WrapperGenerator.class.getName()).log(Level.SEVERE,
"Error in makeWrapper for " + cls, thr );
return null ;
}
}
}