001package apps.util;
002
003import java.awt.GraphicsEnvironment;
004
005import apps.SystemConsole;
006
007import java.io.*;
008import java.nio.file.Files;
009import java.util.*;
010
011import javax.annotation.CheckForNull;
012import javax.annotation.Nonnull;
013
014import jmri.util.FileUtil;
015import jmri.util.exceptionhandler.UncaughtExceptionHandler;
016import jmri.util.swing.JmriJOptionPane;
017
018import org.apache.logging.log4j.Level;
019import org.apache.logging.log4j.Logger;
020import org.apache.logging.log4j.LogManager;
021import org.apache.logging.log4j.core.Appender;
022import org.apache.logging.log4j.core.appender.FileAppender;
023import org.apache.logging.log4j.core.appender.RollingFileAppender;
024import org.apache.logging.log4j.core.config.Configurator;
025
026/**
027 * Common utility methods for working with Log4J.
028 * <p>
029 * Two system properties influence how logging is configured in JMRI:
030 * <dl>
031 * <dt>jmri.log</dt><dd>The logging control file. If this file is not an
032 * absolute path, this file is searched for in the following order:<ol>
033 * <li>JMRI settings directory</li>
034 * <li>JMRI installation (program) directory</li>
035 * </ol>
036 * If this property is not specified, the logging control file
037 * <i>default_lcf.xml</i> is used, following the above search order to find it.
038 * </dd>
039 * <dt>jmri.log.path</dt><dd>The directory for storing logs. If not specified,
040 * logs are stored in the JMRI preferences directory.</dd>
041 * </dl>
042 * <p>
043 * See also jmri.util.TestingLoggerConfiguration in the Test code for 
044 * Tests Logging Setup.
045 * @author Bob Jacobsen Copyright 2009, 2010
046 * @author Randall Wood Copyright 2014, 2020
047 */
048public class Log4JUtil {
049
050    public static final String DEFAULT_LCF_NAME = "default_lcf.xml";
051    public static final String SYS_PROP_LCF_LOCATION = "jmri.log";
052    public static final String SYS_PROP_LOG_PATH =  "jmri.log.path";
053
054    private static final String LOG_HEADER = "****** JMRI log *******";
055
056    /**
057     * Initialize logging from a default control file.
058     * <p>
059     * Primary functions:
060     * <ul>
061     * <li>Initialize the JMRI System Console.
062     * <li>Set up the slf4j j.u.logging to log4J bridge.
063     * <li>Start log4j.
064     * <li>Initialize a default exception handler.
065     * </ul>
066     * 
067     */
068    static public void initLogging() {
069        initLogging(System.getProperty(SYS_PROP_LCF_LOCATION, DEFAULT_LCF_NAME));
070    }
071
072    /**
073     * Initialize logging, specifying a control file.
074     * <p>
075     * Generally, only used for unit testing. Much better to use allow this
076     * class to find the control file using a set of conventions.
077     *
078     * @param controlfile the logging control file
079     */
080    static public void initLogging(@Nonnull String controlfile) {
081        initLog4J(controlfile);
082    }
083
084    /**
085     * Initialize Log4J.
086     * <p>
087     * Use the logging control file specified in the <i>jmri.log</i> property
088     * or, if none, the default_lcf.xml file. If the file is absolute and cannot be
089     * found, look for the file first in the settings directory and then in the
090     * installation directory.
091     *
092     * @param logFile the logging control file
093     * @see jmri.util.FileUtil#getPreferencesPath()
094     * @see jmri.util.FileUtil#getProgramPath()
095     */
096    static void initLog4J(@Nonnull String logFile) {
097        Logger logger = LogManager.getLogger();
098        Map<String, Appender> appenderMap = ((org.apache.logging.log4j.core.Logger) logger).getAppenders();
099        if ( appenderMap.size() > 1 ) {
100            log.debug("initLog4J already initialized!");
101            return;
102        }
103        // Initialise JMRI System Console
104        // Need to do this before initialising log4j so that the new
105        // stdout and stderr streams are set up and usable by the ConsoleAppender
106        if (!GraphicsEnvironment.isHeadless()) {
107            SystemConsole.create();
108        }
109
110        // initialize the java.util.logging to log4j bridge
111        initializeJavaUtilLogging();
112
113        // initialize log4j - from logging control file (lcf) only
114        String loggingControlFileLocation = getLoggingConfig(logFile);
115        if ( loggingControlFileLocation != null ) {
116            configureLogging(loggingControlFileLocation);
117        } else {
118            Configurator.reconfigure();
119            Configurator.setRootLevel(Level.INFO);
120            log.error("Unable to load Configuration {}", logFile);
121            if (!GraphicsEnvironment.isHeadless()) {
122                JmriJOptionPane.showMessageDialog(null,
123                    "Could not locate Logging Configuration file " + logFile,
124                    "Could not Locate Logging Configuration File",
125                    JmriJOptionPane.ERROR_MESSAGE);
126            }
127        }
128        // install default exception handler so uncaught exceptions are logged, not printed
129        Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler());
130    }
131
132    @CheckForNull
133    public static String getLoggingConfig(@Nonnull String logFileLocation) {
134        if (new File(logFileLocation).isAbsolute() && new File(logFileLocation).canRead()) {
135            return logFileLocation;
136        } else if ( new File(FileUtil.getPreferencesPath() + logFileLocation).canRead()) {
137            return FileUtil.getPreferencesPath() + logFileLocation;
138        } else if ( new File(FileUtil.getProgramPath() + logFileLocation).canRead()) {
139            return FileUtil.getProgramPath() + logFileLocation;
140        } else {
141            return null;
142        }
143    }
144
145    static void initializeJavaUtilLogging() {
146        // Optionally remove existing handlers attached to j.u.l root logger
147        org.slf4j.bridge.SLF4JBridgeHandler.removeHandlersForRootLogger();  // (since SLF4J 1.6.5)
148
149        // add SLF4JBridgeHandler to j.u.l's root logger, should be done once during
150        // the initialization phase of your application
151        org.slf4j.bridge.SLF4JBridgeHandler.install();
152    }
153
154    @SuppressWarnings("unchecked")
155    @Nonnull
156    static public String startupInfo(@Nonnull String program) {
157        log.info(LOG_HEADER);
158        Logger logger = LogManager.getLogger();
159        Map<String, Appender  > appenderMap = ((org.apache.logging.log4j.core.Logger) logger).getAppenders();
160        appenderMap.forEach((key, a) -> {
161            if (a instanceof RollingFileAppender) {
162                RollingFileAppender rf = (RollingFileAppender)a;
163                String fileName = rf.getFileName();
164                if ( fileName.equals(rf.getFilePattern()) ) {
165                    log.info("This log is stored in file: {}", fileName);
166                } else {
167                    log.info("This log is appended to file: {}", fileName);
168                }
169            } else if (a instanceof FileAppender) {
170                log.info("This log is stored in file: {}", ((FileAppender) a).getFileName());
171            }
172        });
173        return (program + " version " + jmri.Version.name()
174                + " starts under Java " + System.getProperty("java.version", "<unknown>")
175                + " on " + System.getProperty("os.name", "<unknown>")
176                + " " + System.getProperty("os.arch", "<unknown>")
177                + " v" + System.getProperty("os.version", "<unknown>")
178                + " at " + (new java.util.Date()));
179    }
180
181    /**
182     * Configure Log4J using the specified properties file.
183     * <p>
184     * This method sets the system property <i>jmri.log.path</i> to the JMRI
185     * preferences directory if not specified.
186     *
187     * @see jmri.util.FileUtil#getPreferencesPath()
188     */
189    static private void configureLogging(@Nonnull String configFile) {
190        // System.out.println("Log4JUtil configureLogging " + configFile);
191
192        // set the log4j config file location programatically
193        // so that JUL adapter is enabled first
194        // and Jython / JavaScript use the same LoggerContext
195        System.setProperty("log4j2.configurationFile", configFile);
196
197        // ensure the logging directory exists
198        // if it's not writable, the console will get the error from log4j, so
199        // we don't need to explictly test for that here, just make sure the
200        // directory is created if need be.
201        if (System.getProperty(SYS_PROP_LOG_PATH) == null ) {
202            System.setProperty(SYS_PROP_LOG_PATH, FileUtil.getPreferencesPath() + "log" + File.separator);
203        }
204        File logDir = new File(System.getProperty(SYS_PROP_LOG_PATH));
205        String createLogErr = null;
206        if (!logDir.exists()) {
207            try {
208                Files.createDirectories(logDir.toPath());
209            } catch ( IOException ex ) {
210                createLogErr = "Could not create directory for log files, " + ex.getMessage();
211            }
212        }
213        try {
214            Configurator.initialize(null, configFile);
215            log.debug("Logging initialised with {}", configFile);
216        } catch ( Exception ex ) {
217            Configurator.reconfigure();
218            Configurator.setRootLevel(Level.INFO);
219            if (!GraphicsEnvironment.isHeadless()) {
220                JmriJOptionPane.showMessageDialog(null,
221                        "Could not Initialise Logging " + ex.getMessage(),
222                        configFile,
223                        JmriJOptionPane.ERROR_MESSAGE);
224            }
225        }
226        if (createLogErr!=null) { // wait until Logging init
227            log.error("Could not create directory for log files at {} {}",
228                System.getProperty(SYS_PROP_LOG_PATH), createLogErr);
229        }
230    }
231
232    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Log4JUtil.class);
233
234}