001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH & Co. KG, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.main;
029
030import java.io.File;
031import java.io.IOException;
032import java.io.InterruptedIOException;
033import java.io.LineNumberReader;
034import java.io.PrintWriter;
035import java.io.StringReader;
036import java.io.StringWriter;
037import java.net.URL;
038import java.net.URLDecoder;
039import java.nio.charset.Charset;
040import java.util.ArrayList;
041import java.util.concurrent.Callable;
042import java.util.concurrent.CopyOnWriteArraySet;
043
044import org.apache.commons.logging.Log;
045import org.apache.commons.logging.LogFactory;
046import org.apache.logging.log4j.core.config.ConfigurationSource;
047import org.apache.logging.log4j.core.config.Configurator;
048import org.apache.logging.log4j.core.util.Loader;
049
050/**
051 * Provides the OpenCms logging mechanism.<p>
052 *
053 * The OpenCms logging mechanism is based on Apache Commons Logging.
054 * However, log4j is shipped with OpenCms and assumed to be used as default logging mechanism.
055 * Since apparently Commons Logging may cause issues in more complex classloader scenarios,
056 * we may switch the logging interface to log4j <code>UGLI</code> once the final release is available.<p>
057 *
058 * The log4j configuration file shipped with OpenCms is located in <code>WEB-INF/classes/log4j2.xml</code>.
059 * By default, OpenCms will configure log4j using the first {@code log4j2.xml} available found by
060 * {@link Loader#getResource(String, ClassLoader)}.<p>
061 *
062 * The following system properties are available for fine-tunning the logging configuration:
063 * <dl>
064 *     <dt>{@code opencms.set.logfile} (default: {@code true})</dt>
065 *     <dd>Set to {@code false} to bypass the OpenCms standard logging configuration mechanism, delegating this to the
066 *     logging framework of chose. Note that in this case,
067 *         <ul>
068 *             <li>the Workplace logging visualization tools will not be available, and</li>
069 *             <li>you may need to enable automatic configuration of log4j2 (see <a
070 *             href="http://logging.apache.org/log4j/2.x/manual/webapp.html">Using Log4j 2 in Web Applications</a>.
071 *             This configuration takes place before OpenCms is correctly initialized, and has been disabled in the
072 *             standard OpenCms distribution because at that point, it has not yet set default values for the
073 *             relevant system properties</li>
074 *         </ul></dd>
075 *     <dt>{@code opencms.logfolder} (default: "{@code WEB-INF/logs/}")</dt>
076 *     <dd>Path to the folder where OpenCms will look for logging files to show in the Workplace GUI.</dd>
077 *     <dt>{@code opencms.logfile} (default: "{@code WEB-INF/logs/opencms.log}")</dt>
078 *     <dd>Absolute path to the OpenCms log file.</dd>
079 * </dl>
080 *
081 * If the logging configuration is not bypassed by setting {@code opencms.set.logfile=false}, OpenCms will first assing
082 * them default values if needed and then will make these system properties available for the configuration of log4j.
083 * In the {@code log4j2.xml} configuration file, system properties can be referenced using the prefix "{@code sys:}" as
084 * in this example:
085 * <pre>
086 *     fileName="${sys:opencms.logfolder}opencms-search.log"
087 * </pre>
088 *
089 * @since 6.0.0
090 */
091public final class CmsLog {
092
093    /** The name of the opencms.log file. */
094    public static final String FILE_LOG = "opencms.log";
095
096    /** Path to the "logs" folder relative to the "WEB-INF" directory of the application. */
097    public static final String FOLDER_LOGS = "logs" + File.separatorChar;
098
099    /** Name of the system property that configures the path where OpenCms looks for logs to show in the Workplace App. */
100    public static final String PROPERTY_LOGFOLDER = "opencms.logfolder";
101
102    /** Name of the system property that configures the absolute path to the file where OpenCms will write its log. */
103    public static final String PROPERTY_LOGFILE = "opencms.logfile";
104
105    /** Log for initialization messages. */
106    public static Log INIT;
107
108    /** The absolute path to the folder of the main OpenCms log file (in the "real" file system). */
109    private static String m_logFileRfsFolder;
110
111    /** The absolute path to the OpenCms log file (in the "real" file system). */
112    private static String m_logFileRfsPath;
113
114    /** Set of names of channels that should not be managed via the GUI. */
115    private static CopyOnWriteArraySet<String> NON_MANAGEABLE_CHANNELS = new CopyOnWriteArraySet<>();
116
117    /**
118     * Initializes the OpenCms logger configuration.<p>
119     */
120    static {
121        //
122        // DO NOT USE ANY OPENCMS CLASSES THAT USE STATIC LOGGER INSTANCES IN THIS STATIC BLOCK
123        // OTHERWISE THEIR LOGGER WOULD NOT BE INITIALIZED PROPERLY
124        //
125        try {
126            // look for the log4j2.xml that shipped with OpenCms
127            URL log4j2Url = Loader.getResource("log4j2.xml", null);
128            if (log4j2Url != null) {
129                // found log4j properties
130                File log4jProps = new File(URLDecoder.decode(log4j2Url.getPath(), Charset.defaultCharset().name()));
131                String path = log4jProps.getAbsolutePath();
132
133                // check if OpenCms should set the log file environment variable
134                boolean setLogFile = Boolean.parseBoolean(System.getProperty("opencms.set.logfile", "true"));
135                if (setLogFile) {
136                    final String propertyLogfolder = System.getProperty(PROPERTY_LOGFOLDER);
137                    final String propertyLogfile = System.getProperty(PROPERTY_LOGFILE);
138
139                    // in a default OpenCms configuration, the following path would point to the OpenCms "WEB-INF" folder
140                    String webInfPath = log4jProps.getParent();
141                    webInfPath = webInfPath.substring(0, webInfPath.lastIndexOf(File.separatorChar) + 1);
142
143                    String logFilePath = ((propertyLogfile != null)
144                    ? propertyLogfile
145                    : webInfPath + FOLDER_LOGS + FILE_LOG);
146                    String logFolderPath = ((propertyLogfolder != null) ? propertyLogfolder : webInfPath + FOLDER_LOGS);
147                    m_logFileRfsPath = new File(logFilePath).getAbsolutePath();
148                    m_logFileRfsFolder = new File(logFolderPath).getAbsolutePath() + File.separatorChar;
149
150                    // Set "opencms.log*" variables if not set. These should be used in the log42.xml config file
151                    // like this: 'fileName="${sys:opencms.logfolder}opencms-search.log"'
152                    if (propertyLogfile == null) {
153                        System.setProperty(PROPERTY_LOGFILE, m_logFileRfsPath);
154                    }
155                    if (propertyLogfolder == null) {
156                        System.setProperty(PROPERTY_LOGFOLDER, m_logFileRfsFolder);
157                    }
158
159                    // re-read the configuration with the new environment variable available
160                    ConfigurationSource source = ConfigurationSource.fromUri(log4j2Url.toURI());
161                    Configurator.initialize(null, source);
162
163                    // In case we have multiple OpenCms instances running in the servlet container, we don't want them to use
164                    // the same log file path, so clear the system properties. Users will have to modify the log4j configuration
165                    // if they want to customize the log file locations for multiple instances.
166                    for (String prop : new String[] {PROPERTY_LOGFILE, PROPERTY_LOGFOLDER}) {
167                        System.clearProperty(prop);
168                    }
169                }
170                // can't localize this message since this would end in an endless logger init loop
171                INIT = LogFactory.getLog("org.opencms.init");
172                INIT.info(". Log4j config file    : " + path);
173                INIT.debug(". m_logFileRfsPath    : " + m_logFileRfsPath);
174                INIT.debug(". m_logFileRfsFolder  : " + m_logFileRfsFolder);
175            } else {
176                System.err.println("'log4j2.xml' not found. (Default location: WEB-INF/classes/log4j2.xml)");
177            }
178        } catch (SecurityException e) {
179            // may be caused if environment can't be written
180            e.printStackTrace(System.err);
181        } catch (Exception e) {
182            // unexpected but nothing we can do about it, print stack trace and continue
183            e.printStackTrace(System.err);
184        }
185    }
186
187    /**
188     * Hides the public constructor.<p>
189     */
190    private CmsLog() {
191
192        // hides the public constructor
193    }
194
195    /**
196     * Helper for safely evaluating lambda functions to produce log output and catch exceptions they might throw.
197     *
198     * @param log the logger to use for logging errors
199     * @param stringProvider the string provider (normally just given as a lambda function)
200     *
201     * @return the result of the function (or &lt;ERROR&gt; if an exception was thrown)
202     */
203    public static String eval(Log log, Callable<String> stringProvider) {
204
205        try {
206            return stringProvider.call();
207        } catch (Exception e) {
208            log.error(e.getLocalizedMessage(), e);
209            return "<ERROR>";
210        }
211    }
212
213    /**
214     * Returns the log for the selected object.<p>
215     *
216     * If the provided object is a String, this String will
217     * be used as channel name. Otherwise the objects
218     * class name will be used as channel name.<p>
219     *
220     * @param obj the object channel to use
221     * @return the log for the selected object channel
222     */
223    public static Log getLog(Object obj) {
224
225        if (obj instanceof String) {
226            return LogFactory.getLog((String)obj);
227        } else if (obj instanceof Class<?>) {
228            return LogFactory.getLog((Class<?>)obj);
229        } else {
230            return LogFactory.getLog(obj.getClass());
231        }
232    }
233
234    /**
235     * Checks whether a log channel should be manageable through the GUI.
236     *
237     * @param channel the name of the channel
238     *
239     * @return true if the channel should be manageable through the GUI
240     */
241    public static boolean isManageable(String channel) {
242
243        return !NON_MANAGEABLE_CHANNELS.contains(channel);
244    }
245
246    /**
247    * Adds a log channel that should not be manageable via the GUI.
248    *
249    * @param channel the channel to add
250    */
251    public static void makeChannelNonManageable(String channel) {
252
253        NON_MANAGEABLE_CHANNELS.add(channel);
254    }
255
256    /**
257     * Render throwable using Throwable.printStackTrace.
258     * <p>This code copy from "org.apache.log4j.DefaultThrowableRenderer.render(Throwable throwable)"</p>
259     * @param throwable throwable, may not be null.
260     * @return string representation.
261     */
262    public static String[] render(final Throwable throwable) {
263
264        StringWriter sw = new StringWriter();
265        PrintWriter pw = new PrintWriter(sw);
266        try {
267            throwable.printStackTrace(pw);
268        } catch (RuntimeException ex) {
269            // nothing to do
270        }
271        pw.flush();
272        LineNumberReader reader = new LineNumberReader(new StringReader(sw.toString()));
273        ArrayList<String> lines = new ArrayList<>();
274        try {
275            String line = reader.readLine();
276            while (line != null) {
277                lines.add(line);
278                line = reader.readLine();
279            }
280        } catch (IOException ex) {
281            if (ex instanceof InterruptedIOException) {
282                Thread.currentThread().interrupt();
283            }
284            lines.add(ex.toString());
285        }
286        //String[] tempRep = new String[lines.size()];
287        return lines.toArray(new String[0]);
288    }
289
290    /**
291     * Returns the absolute path to the folder of the main OpenCms log file
292     * (in the "real" file system).<p>
293     *
294     * If the method returns <code>null</code>, this means that the log
295     * file is not managed by OpenCms.<p>
296     *
297     * @return the absolute path to the folder of the main OpenCms log file (in
298     * the "real" file system)
299     */
300    protected static String getLogFileRfsFolder() {
301
302        return m_logFileRfsFolder;
303    }
304
305    /**
306     * Returns the filename of the log file (in the "real" file system).<p>
307     *
308     * If the method returns <code>null</code>, this means that the log
309     * file is not managed by OpenCms.<p>
310     *
311     * @return the filename of the log file (in the "real" file system)
312     */
313    protected static String getLogFileRfsPath() {
314
315        return m_logFileRfsPath;
316    }
317}