| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| package freemarker.log; |
| |
| import java.security.AccessControlException; |
| import java.security.AccessController; |
| import java.security.PrivilegedAction; |
| import java.util.HashMap; |
| import java.util.Map; |
| |
| /** |
| * Delegates logger creation to an actual logging library. By default it looks for logger libraries in this order (in |
| * FreeMarker 2.3.x): Log4J, Avalon LogKit, JUL (i.e., {@code java.util.logging}). Prior to FreeMarker 2.4, SLF4J and |
| * Apache Commons Logging aren't searched automatically due to backward compatibility constraints. But if you have |
| * {@code log4j-over-slf4j} properly installed (means, you have no real Log4j in your class path, and SLF4J has a |
| * backing implementation like {@code logback-classic}), then FreeMarker will use SLF4J directly instead of Log4j (since |
| * FreeMarker 2.3.22). |
| * |
| * <p> |
| * If the auto detection sequence describet above doesn't give you the result that you want, see |
| * {@link #SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY}. |
| */ |
| public abstract class Logger { |
| |
| /** |
| * The {@value #SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY} system property is used to select a logger library explicitly, |
| * rather than relying on automatic selection. You meant to set this system property where the JVM is started, like |
| * {@code java ... -Dorg.freemarker.loggerLibrary=SLF4j}. Setting it from Java code isn't reliable, also it can |
| * cause confusion if you override the value set by others with {@code -D}. |
| * |
| * <p> |
| * If the requested logging library is not available, an error will be printed to the stderr, then logging will be |
| * disabled. |
| * |
| * <p> |
| * The supported values are: |
| * <ul> |
| * <li>{@value #LIBRARY_NAME_SLF4J}: Use SLF4J (recommended)</li> |
| * <li>{@value #LIBRARY_NAME_COMMONS_LOGGING}: Use Apache Commons Logging.</li> |
| * <li>{@value #LIBRARY_NAME_LOG4J}: Use Log4j.</li> |
| * <li>{@value #LIBRARY_NAME_JUL}: Use {@code java.util.logging}.</li> |
| * <li>{@value #LIBRARY_NAME_AVALON}: Use Avalon LogKit (deprecated)</li> |
| * <li>{@value #LIBRARY_NAME_AUTO}: Use automatic selection (default behavior). See {@link Logger} class |
| * documentation for more.</li> |
| * <li>{@value #LIBRARY_NAME_NONE}: Don't log.</li> |
| * </ul> |
| * |
| * @since 2.3.22 |
| */ |
| public static final String SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY = "org.freemarker.loggerLibrary"; |
| |
| /** |
| * Enum value used for {@link #selectLoggerLibrary(int)}; indicates auto-selection as described in the class |
| * documentation of {@link #Logger()}. |
| */ |
| public static final int LIBRARY_AUTO = -1; |
| private static final int MIN_LIBRARY_ENUM = LIBRARY_AUTO; |
| |
| /** |
| * {@value #SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY} property value; indicates auto-selection as described in the class |
| * documentation of {@link #Logger()}. @since 2.3.22 |
| */ |
| public static final String LIBRARY_NAME_AUTO = "auto"; |
| |
| /** |
| * Enum value used for {@link #selectLoggerLibrary(int)}; indicates that no logging should occur. |
| */ |
| public static final int LIBRARY_NONE = 0; |
| |
| /** |
| * {@value #SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY} property value; indicates that no logging should occur. @since |
| * 2.3.22 |
| */ |
| public static final String LIBRARY_NAME_NONE = "none"; |
| |
| /** |
| * Enum value used for {@link #selectLoggerLibrary(int)}; indicates that {@code java.util.logging} should be used. |
| */ |
| public static final int LIBRARY_JAVA = 1; |
| |
| /** |
| * {@value #SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY} property value; indicates that {@code java.util.logging} should be |
| * used. @since 2.3.22 |
| */ |
| public static final String LIBRARY_NAME_JUL = "JUL"; |
| |
| /** |
| * Enum value used for {@link #selectLoggerLibrary(int)}; indicates that Avalon LogKit should be used. |
| * |
| * @deprecated Avalon LogKit support will be removed sometimes in the future. |
| */ |
| @Deprecated |
| public static final int LIBRARY_AVALON = 2; |
| |
| /** |
| * {@value #SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY} property value; indicates that Avalon LogKit should be used. @since |
| * 2.3.22 |
| * |
| * @deprecated Avalon LogKit support will be removed sometimes in the future. |
| */ |
| // This value is also used as part of the factory class name. |
| @Deprecated |
| public static final String LIBRARY_NAME_AVALON = "Avalon"; |
| |
| /** |
| * Enum value used for {@link #selectLoggerLibrary(int)}; indicates that Log4J should be used. |
| */ |
| public static final int LIBRARY_LOG4J = 3; |
| |
| /** |
| * {@value #SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY} property value; indicates that Log4J should be used. @since 2.3.22 |
| */ |
| // This value is also used as part of the factory class name. |
| public static final String LIBRARY_NAME_LOG4J = "Log4j"; |
| |
| /** |
| * Enum value used for {@link #selectLoggerLibrary(int)}; indicates that Apache commons-logging should be used. |
| */ |
| public static final int LIBRARY_COMMONS = 4; |
| |
| /** |
| * {@value #SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY} property value; indicates that Apache commons-logging should be |
| * used. @since 2.3.22 |
| */ |
| // This value is also used as part of the factory class name. |
| public static final String LIBRARY_NAME_COMMONS_LOGGING = "CommonsLogging"; |
| |
| /** |
| * Constant used with {@link #selectLoggerLibrary(int)}; indicates that SLF4J should be used. |
| */ |
| public static final int LIBRARY_SLF4J = 5; |
| |
| /** |
| * {@value #SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY} property value; indicates that SLF4J should be used. @since 2.3.22 |
| */ |
| // This value is also used as part of the factory class name. |
| public static final String LIBRARY_NAME_SLF4J = "SLF4J"; |
| private static final int MAX_LIBRARY_ENUM = LIBRARY_SLF4J; |
| |
| private static final String REAL_LOG4J_PRESENCE_CLASS = "org.apache.log4j.FileAppender"; |
| private static final String LOG4J_OVER_SLF4J_TESTER_CLASS = "freemarker.log._Log4jOverSLF4JTester"; |
| |
| /** |
| * Order matters! Starts with the lowest priority. |
| */ |
| private static final String[] LIBRARIES_BY_PRIORITY = { |
| null, LIBRARY_NAME_JUL, |
| "org.apache.log.Logger", LIBRARY_NAME_AVALON, |
| "org.apache.log4j.Logger", LIBRARY_NAME_LOG4J, |
| /* In 2.3.x this two is skipped by LIBRARY_AUTO: */ |
| "org.apache.commons.logging.Log", LIBRARY_NAME_COMMONS_LOGGING, |
| "org.slf4j.Logger", LIBRARY_NAME_SLF4J, |
| }; |
| |
| private static String getAvailabilityCheckClassName(int libraryEnum) { |
| if (libraryEnum == LIBRARY_AUTO || libraryEnum == LIBRARY_NONE) { |
| // Statically linked |
| return null; |
| } |
| return LIBRARIES_BY_PRIORITY[(libraryEnum - 1) * 2]; |
| } |
| |
| static { |
| if (LIBRARIES_BY_PRIORITY.length / 2 != MAX_LIBRARY_ENUM) { |
| throw new AssertionError(); |
| } |
| } |
| |
| private static String getLibraryName(int libraryEnum) { |
| if (libraryEnum == LIBRARY_AUTO) { |
| return LIBRARY_NAME_AUTO; |
| } |
| if (libraryEnum == LIBRARY_NONE) { |
| return LIBRARY_NAME_NONE; |
| } |
| return LIBRARIES_BY_PRIORITY[(libraryEnum - 1) * 2 + 1]; |
| } |
| |
| private static boolean isAutoDetected(int libraryEnum) { |
| // 2.4: Remove libraryEnum == LIBRARY_SLF4J || libraryEnum == LIBRARY_COMMONS |
| return !(libraryEnum == LIBRARY_AUTO || libraryEnum == LIBRARY_NONE |
| || libraryEnum == LIBRARY_SLF4J || libraryEnum == LIBRARY_COMMONS); |
| } |
| |
| private static int libraryEnum; |
| private static LoggerFactory loggerFactory; |
| private static boolean initializedFromSystemProperty; |
| |
| private static String categoryPrefix = ""; |
| |
| private static final Map loggersByCategory = new HashMap(); |
| |
| /** |
| * Selects the logger library to use, unless it's already specified by the |
| * {@value #SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY} system property. Call this early in application initialization |
| * phase, before calling any other FreeMarker API-s, since once parts of the FreeMarker library bind to the logging |
| * subsystem, the change in this value will have no effect on them. |
| * |
| * @param libraryEnum |
| * One of {@code LIBRARY_...} constants. By default, {@link #LIBRARY_AUTO} is used. |
| * |
| * @throws ClassNotFoundException |
| * if an explicit logging library is asked for (that is, not {@value #LIBRARY_AUTO} or |
| * {@value #LIBRARY_NONE}) and it's not found in the classpath. |
| * |
| * @deprecated This method isn't reliable, unless you can somehow ensure that you access the FreeMarker classes |
| * first; use the {@value #SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY} Java system property instead, like |
| * {@code java ... -Dorg.freemarker.loggerLibrary=slf4j}. See |
| * {@link #SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY} for more. |
| */ |
| @Deprecated |
| public static void selectLoggerLibrary(int libraryEnum) throws ClassNotFoundException { |
| if (libraryEnum < MIN_LIBRARY_ENUM || libraryEnum > MAX_LIBRARY_ENUM) { |
| throw new IllegalArgumentException("Library enum value out of range"); |
| } |
| |
| synchronized (Logger.class) { |
| final boolean loggerFactoryWasAlreadySet = loggerFactory != null; |
| if (!loggerFactoryWasAlreadySet || libraryEnum != Logger.libraryEnum) { |
| // Creates the factory only if it can be done based on system property: |
| ensureLoggerFactorySet(true); |
| |
| // The system property has precedence because this method was deprecated by it: |
| if (!initializedFromSystemProperty || loggerFactory == null) { |
| int replacedLibraryEnum = Logger.libraryEnum; |
| setLibrary(libraryEnum); |
| loggersByCategory.clear(); |
| if (loggerFactoryWasAlreadySet) { |
| logWarnInLogger("Logger library was already set earlier to \"" |
| + getLibraryName(replacedLibraryEnum) + "\"; " |
| + "change to \"" + getLibraryName(libraryEnum) + "\" won't effect loggers created " |
| + "earlier."); |
| } |
| } else if (libraryEnum != Logger.libraryEnum) { |
| logWarnInLogger("Ignored " + Logger.class.getName() + ".selectLoggerLibrary(\"" |
| + getLibraryName(libraryEnum) |
| + "\") call, because the \"" + SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY |
| + "\" system property is set to \"" + getLibraryName(Logger.libraryEnum) + "\"."); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Sets a category prefix. This prefix is prepended to any logger category name. This makes it possible to have |
| * different FreeMarker logger categories on a per-application basis (better said, per-classloader basis). By |
| * default the category prefix is the empty string. If you set a non-empty category prefix, be sure to include the |
| * trailing separator dot (i.e. "MyApp.") If you want to change the default setting, do it early in application |
| * initialization phase, before calling any other FreeMarker API since once various parts of the FreeMarker library |
| * bind to the logging subsystem, the change in this value will have no effect on them. |
| * |
| * @deprecated This wasn't reliable, unless you can somehow ensure that you access the FreeMarker classes first. As |
| * it's not known to be useful for users, consider it removed. |
| */ |
| @Deprecated |
| public static void setCategoryPrefix(String prefix) { |
| synchronized (Logger.class) { |
| if (prefix == null) { |
| throw new IllegalArgumentException(); |
| } |
| categoryPrefix = prefix; |
| } |
| } |
| |
| /** |
| * Logs a debugging message. |
| */ |
| public abstract void debug(String message); |
| |
| /** |
| * Logs a debugging message with accompanying throwable. |
| */ |
| public abstract void debug(String message, Throwable t); |
| |
| /** |
| * Logs an informational message. |
| */ |
| public abstract void info(String message); |
| |
| /** |
| * Logs an informational message with accompanying throwable. |
| */ |
| public abstract void info(String message, Throwable t); |
| |
| /** |
| * Logs a warning message. |
| */ |
| public abstract void warn(String message); |
| |
| /** |
| * Logs a warning message with accompanying throwable. |
| */ |
| public abstract void warn(String message, Throwable t); |
| |
| /** |
| * Logs an error message. |
| */ |
| public abstract void error(String message); |
| |
| /** |
| * Logs an error message with accompanying throwable. |
| */ |
| public abstract void error(String message, Throwable t); |
| |
| /** |
| * Returns true if this logger will log debug messages. |
| */ |
| public abstract boolean isDebugEnabled(); |
| |
| /** |
| * Returns true if this logger will log informational messages. |
| */ |
| public abstract boolean isInfoEnabled(); |
| |
| /** |
| * Returns true if this logger will log warning messages. |
| */ |
| public abstract boolean isWarnEnabled(); |
| |
| /** |
| * Returns true if this logger will log error messages. |
| */ |
| public abstract boolean isErrorEnabled(); |
| |
| /** |
| * Returns true if this logger will log fatal error messages. |
| */ |
| public abstract boolean isFatalEnabled(); |
| |
| /** |
| * Returns a logger for the specified category. |
| * |
| * @param category |
| * a dot separated hierarchical category name. If a category prefix is in effect, it's prepended to the |
| * category name. |
| */ |
| public static Logger getLogger(String category) { |
| if (categoryPrefix.length() != 0) { |
| category = categoryPrefix + category; |
| } |
| synchronized (loggersByCategory) { |
| Logger logger = (Logger) loggersByCategory.get(category); |
| if (logger == null) { |
| ensureLoggerFactorySet(false); |
| logger = loggerFactory.getLogger(category); |
| loggersByCategory.put(category, logger); |
| } |
| return logger; |
| } |
| } |
| |
| private static void ensureLoggerFactorySet(boolean onlyIfCanBeSetFromSysProp) { |
| if (loggerFactory != null) return; |
| synchronized (Logger.class) { |
| if (loggerFactory != null) return; |
| |
| String sysPropVal = getSystemProperty(SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY); |
| |
| final int libraryEnum; |
| if (sysPropVal != null) { |
| sysPropVal = sysPropVal.trim(); |
| |
| boolean foundMatch = false; |
| int matchedEnum = MIN_LIBRARY_ENUM; |
| do { |
| if (sysPropVal.equalsIgnoreCase(getLibraryName(matchedEnum))) { |
| foundMatch = true; |
| } else { |
| matchedEnum++; |
| } |
| } while (matchedEnum <= MAX_LIBRARY_ENUM && !foundMatch); |
| |
| if (!foundMatch) { |
| logWarnInLogger("Ignored invalid \"" + SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY |
| + "\" system property value: \"" + sysPropVal + "\""); |
| if (onlyIfCanBeSetFromSysProp) return; |
| } |
| |
| libraryEnum = foundMatch ? matchedEnum : LIBRARY_AUTO; |
| } else { |
| if (onlyIfCanBeSetFromSysProp) return; |
| libraryEnum = LIBRARY_AUTO; |
| } |
| |
| try { |
| setLibrary(libraryEnum); |
| if (sysPropVal != null) { |
| initializedFromSystemProperty = true; |
| } |
| } catch (Throwable e) { |
| final boolean disableLogging = !(onlyIfCanBeSetFromSysProp && sysPropVal != null); |
| logErrorInLogger( |
| "Couldn't set up logger for \"" + getLibraryName(libraryEnum) + "\"" |
| + (disableLogging ? "; logging disabled" : "."), e); |
| if (disableLogging) { |
| try { |
| setLibrary(LIBRARY_NONE); |
| } catch (ClassNotFoundException e2) { |
| throw new RuntimeException("Bug", e2); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * @throws ClassNotFoundException |
| * If the requested logger library is not available. Never thrown for {@link #LIBRARY_AUTO}. |
| */ |
| private static LoggerFactory createLoggerFactory(int libraryEnum) throws ClassNotFoundException { |
| if (libraryEnum == LIBRARY_AUTO) { |
| for (int libraryEnumToTry = MAX_LIBRARY_ENUM; libraryEnumToTry >= MIN_LIBRARY_ENUM; libraryEnumToTry--) { |
| if (!isAutoDetected(libraryEnumToTry)) continue; |
| if (libraryEnumToTry == LIBRARY_LOG4J && hasLog4LibraryThatDelegatesToWorkingSLF4J()) { |
| libraryEnumToTry = LIBRARY_SLF4J; |
| } |
| |
| try { |
| return createLoggerFactoryForNonAuto(libraryEnumToTry); |
| } catch (ClassNotFoundException e) { |
| // Expected, intentionally suppressed. |
| } catch (Throwable e) { |
| logErrorInLogger( |
| "Unexpected error when initializing logging for \"" |
| + getLibraryName(libraryEnumToTry) + "\".", |
| e); |
| } |
| } |
| logWarnInLogger("Auto detecton couldn't set up any logger libraries; FreeMarker logging suppressed."); |
| return new _NullLoggerFactory(); |
| } else { |
| return createLoggerFactoryForNonAuto(libraryEnum); |
| } |
| } |
| |
| /** |
| * @throws ClassNotFoundException |
| * If the required logger library is not available. |
| */ |
| private static LoggerFactory createLoggerFactoryForNonAuto(int libraryEnum) throws ClassNotFoundException { |
| final String availabilityCheckClassName = getAvailabilityCheckClassName(libraryEnum); |
| if (availabilityCheckClassName != null) { // Dynamically created factory |
| Class.forName(availabilityCheckClassName); |
| String libraryName = getLibraryName(libraryEnum); |
| try { |
| return (LoggerFactory) Class.forName( |
| "freemarker.log._" + libraryName + "LoggerFactory").newInstance(); |
| } catch (Exception e) { |
| throw new RuntimeException( |
| "Unexpected error when creating logger factory for \"" + libraryName + "\".", e); |
| } |
| } else { // Non-dynamically created factory |
| if (libraryEnum == LIBRARY_JAVA) { |
| return new _JULLoggerFactory(); |
| } else if (libraryEnum == LIBRARY_NONE) { |
| return new _NullLoggerFactory(); |
| } else { |
| throw new RuntimeException("Bug"); |
| } |
| } |
| } |
| |
| private static boolean hasLog4LibraryThatDelegatesToWorkingSLF4J() { |
| try { |
| Class.forName(getAvailabilityCheckClassName(LIBRARY_LOG4J)); |
| Class.forName(getAvailabilityCheckClassName(LIBRARY_SLF4J)); |
| } catch (Throwable e) { |
| return false; |
| } |
| try { |
| Class.forName(REAL_LOG4J_PRESENCE_CLASS); |
| return false; |
| } catch (ClassNotFoundException e) { |
| try { |
| Object r = Class.forName(LOG4J_OVER_SLF4J_TESTER_CLASS) |
| .getMethod("test", new Class[] {}).invoke(null); |
| return ((Boolean) r).booleanValue(); |
| } catch (Throwable e2) { |
| return false; |
| } |
| } |
| } |
| |
| private synchronized static void setLibrary(int libraryEnum) throws ClassNotFoundException { |
| loggerFactory = createLoggerFactory(libraryEnum); |
| Logger.libraryEnum = libraryEnum; |
| } |
| |
| private static void logWarnInLogger(String message) { |
| logInLogger(false, message, null); |
| } |
| |
| private static void logErrorInLogger(String message, Throwable exception) { |
| logInLogger(true, message, exception); |
| } |
| |
| private static void logInLogger(boolean error, String message, Throwable exception) { |
| boolean canUseRealLogger; |
| synchronized (Logger.class) { |
| canUseRealLogger = loggerFactory != null && !(loggerFactory instanceof _NullLoggerFactory); |
| } |
| |
| if (canUseRealLogger) { |
| try { |
| final Logger logger = Logger.getLogger("freemarker.logger"); |
| if (error) { |
| logger.error(message); |
| } else { |
| logger.warn(message); |
| } |
| } catch (Throwable e) { |
| canUseRealLogger = false; |
| } |
| } |
| |
| if (!canUseRealLogger) { |
| System.err.println((error ? "ERROR" : "WARN") + " " |
| + LoggerFactory.class.getName() + ": " + message); |
| if (exception != null) { |
| System.err.println("\tException: " + tryToString(exception)); |
| while (exception.getCause() != null) { |
| exception = exception.getCause(); |
| System.err.println("\tCaused by: " + tryToString(exception)); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Don't use {@link freemarker.template.utility.SecurityUtilities#getSystemProperty(String, String)} here, as it |
| * (might) depends on the logger, hence interfering with the initialization. |
| */ |
| private static String getSystemProperty(final String key) { |
| try { |
| return (String) AccessController.doPrivileged( |
| new PrivilegedAction() { |
| @Override |
| public Object run() { |
| return System.getProperty(key, null); |
| } |
| }); |
| } catch (AccessControlException e) { |
| logWarnInLogger("Insufficient permissions to read system property \"" + key + "\"."); |
| return null; |
| } catch (Throwable e) { |
| logErrorInLogger("Failed to read system property \"" + key + "\".", e); |
| return null; |
| } |
| } |
| |
| /** |
| * Don't use {@link freemarker.template.utility.StringUtil#tryToString(Object)} here, as it might depends on the |
| * logger, hence interfering with the initialization. |
| */ |
| private static String tryToString(Object object) { |
| if (object == null) return null; |
| try { |
| return object.toString(); |
| } catch (Throwable e) { |
| return object.getClass().getName(); |
| } |
| } |
| |
| } |