1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements.  See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License.  You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */

17 package org.apache.juli;
18
19 import java.io.File;
20 import java.io.FileInputStream;
21 import java.io.FilePermission;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.net.URL;
25 import java.net.URLClassLoader;
26 import java.security.AccessControlException;
27 import java.security.AccessController;
28 import java.security.Permission;
29 import java.security.PrivilegedAction;
30 import java.util.Collections;
31 import java.util.Enumeration;
32 import java.util.HashMap;
33 import java.util.Iterator;
34 import java.util.Map;
35 import java.util.Properties;
36 import java.util.StringTokenizer;
37 import java.util.WeakHashMap;
38 import java.util.concurrent.ConcurrentHashMap;
39 import java.util.logging.Handler;
40 import java.util.logging.Level;
41 import java.util.logging.LogManager;
42 import java.util.logging.Logger;
43
44
45 /**
46  * Per classloader LogManager implementation.
47  *
48  * For light debugging, set the system property
49  * <code>org.apache.juli.ClassLoaderLogManager.debug=true</code>.
50  * Short configuration information will be sent to <code>System.err</code>.
51  */

52 public class ClassLoaderLogManager extends LogManager {
53
54     private static final boolean isJava9;
55
56     private static ThreadLocal<Boolean> addingLocalRootLogger = new ThreadLocal<Boolean>() {
57         @Override
58         protected Boolean initialValue() {
59             return Boolean.FALSE;
60         }
61     };
62
63     public static final String DEBUG_PROPERTY =
64             ClassLoaderLogManager.class.getName() + ".debug";
65
66     static {
67         Class<?> c = null;
68         try {
69             c = Class.forName("java.lang.Runtime$Version");
70         } catch (ClassNotFoundException e) {
71             // Must be Java 8
72         }
73         isJava9 = c != null;
74     }
75
76     private final class Cleaner extends Thread {
77
78         @Override
79         public void run() {
80             if (useShutdownHook) {
81                 shutdown();
82             }
83         }
84
85     }
86
87
88     // ------------------------------------------------------------Constructors
89
90     public ClassLoaderLogManager() {
91         super();
92         try {
93             Runtime.getRuntime().addShutdownHook(new Cleaner());
94         } catch (IllegalStateException ise) {
95             // We are probably already being shutdown. Ignore this error.
96         }
97     }
98
99
100     // -------------------------------------------------------------- Variables
101
102
103     /**
104      * Map containing the classloader information, keyed per classloader. A
105      * weak hashmap is used to ensure no classloader reference is leaked from
106      * application redeployment.
107      */

108     protected final Map<ClassLoader, ClassLoaderLogInfo> classLoaderLoggers =
109             new WeakHashMap<>(); // Guarded by this
110
111
112     /**
113      * This prefix is used to allow using prefixes for the properties names
114      * of handlers and their subcomponents.
115      */

116     protected final ThreadLocal<String> prefix = new ThreadLocal<>();
117
118
119     /**
120      * Determines if the shutdown hook is used to perform any necessary
121      * clean-up such as flushing buffered handlers on JVM shutdown. Defaults to
122      * <code>true</code> but may be set to false if another component ensures
123      * that {@link #shutdown()} is called.
124      */

125     protected volatile boolean useShutdownHook = true;
126
127
128     // ------------------------------------------------------------- Properties
129
130
131     public boolean isUseShutdownHook() {
132         return useShutdownHook;
133     }
134
135
136     public void setUseShutdownHook(boolean useShutdownHook) {
137         this.useShutdownHook = useShutdownHook;
138     }
139
140
141     // --------------------------------------------------------- Public Methods
142
143
144     /**
145      * Add the specified logger to the classloader local configuration.
146      *
147      * @param logger The logger to be added
148      */

149     @Override
150     public synchronized boolean addLogger(final Logger logger) {
151
152         final String loggerName = logger.getName();
153
154         ClassLoader classLoader =
155             Thread.currentThread().getContextClassLoader();
156         ClassLoaderLogInfo info = getClassLoaderInfo(classLoader);
157         if (info.loggers.containsKey(loggerName)) {
158             return false;
159         }
160         info.loggers.put(loggerName, logger);
161
162         // Apply initial level for new logger
163         final String levelString = getProperty(loggerName + ".level");
164         if (levelString != null) {
165             try {
166                 AccessController.doPrivileged(new PrivilegedAction<Void>() {
167                     @Override
168                     public Void run() {
169                         logger.setLevel(Level.parse(levelString.trim()));
170                         return null;
171                     }
172                 });
173             } catch (IllegalArgumentException e) {
174                 // Leave level set to null
175             }
176         }
177
178         // Always instantiate parent loggers so that
179         // we can control log categories even during runtime
180         int dotIndex = loggerName.lastIndexOf('.');
181         if (dotIndex >= 0) {
182             final String parentName = loggerName.substring(0, dotIndex);
183             Logger.getLogger(parentName);
184         }
185
186         // Find associated node
187         LogNode node = info.rootNode.findNode(loggerName);
188         node.logger = logger;
189
190         // Set parent logger
191         Logger parentLogger = node.findParentLogger();
192         if (parentLogger != null) {
193             doSetParentLogger(logger, parentLogger);
194         }
195
196         // Tell children we are their new parent
197         node.setParentLogger(logger);
198
199         // Add associated handlers, if any are defined using the .handlers property.
200         // In this case, handlers of the parent logger(s) will not be used
201         String handlers = getProperty(loggerName + ".handlers");
202         if (handlers != null) {
203             logger.setUseParentHandlers(false);
204             StringTokenizer tok = new StringTokenizer(handlers, ",");
205             while (tok.hasMoreTokens()) {
206                 String handlerName = (tok.nextToken().trim());
207                 Handler handler = null;
208                 ClassLoader current = classLoader;
209                 while (current != null) {
210                     info = classLoaderLoggers.get(current);
211                     if (info != null) {
212                         handler = info.handlers.get(handlerName);
213                         if (handler != null) {
214                             break;
215                         }
216                     }
217                     current = current.getParent();
218                 }
219                 if (handler != null) {
220                     logger.addHandler(handler);
221                 }
222             }
223         }
224
225         // Parse useParentHandlers to set if the logger should delegate to its parent.
226         // Unlike java.util.logging, the default is to not delegate if a list of handlers
227         // has been specified for the logger.
228         String useParentHandlersString = getProperty(loggerName + ".useParentHandlers");
229         if (Boolean.parseBoolean(useParentHandlersString)) {
230             logger.setUseParentHandlers(true);
231         }
232
233         return true;
234     }
235
236
237     /**
238      * Get the logger associated with the specified name inside
239      * the classloader local configuration. If this returns null,
240      * and the call originated for Logger.getLogger, a new
241      * logger with the specified name will be instantiated and
242      * added using addLogger.
243      *
244      * @param name The name of the logger to retrieve
245      */

246     @Override
247     public synchronized Logger getLogger(final String name) {
248         ClassLoader classLoader = Thread.currentThread()
249                 .getContextClassLoader();
250         return getClassLoaderInfo(classLoader).loggers.get(name);
251     }
252
253
254     /**
255      * Get an enumeration of the logger names currently defined in the
256      * classloader local configuration.
257      */

258     @Override
259     public synchronized Enumeration<String> getLoggerNames() {
260         ClassLoader classLoader = Thread.currentThread()
261                 .getContextClassLoader();
262         return Collections.enumeration(getClassLoaderInfo(classLoader).loggers.keySet());
263     }
264
265
266     /**
267      * Get the value of the specified property in the classloader local
268      * configuration.
269      *
270      * @param name The property name
271      */

272     @Override
273     public String getProperty(String name) {
274
275         // Use a ThreadLocal to work around
276         // https://bugs.openjdk.java.net/browse/JDK-8195096
277         if (".handlers".equals(name) && !addingLocalRootLogger.get().booleanValue()) {
278             return null;
279         }
280
281         String prefix = this.prefix.get();
282         String result = null;
283
284         // If a prefix is defined look for a prefixed property first
285         if (prefix != null) {
286             result = findProperty(prefix + name);
287         }
288
289         // If there is no prefix or no property match with the prefix try just
290         // the name
291         if (result == null) {
292             result = findProperty(name);
293         }
294
295         // Simple property replacement (mostly for folder names)
296         if (result != null) {
297             result = replace(result);
298         }
299         return result;
300     }
301
302
303     private synchronized String findProperty(String name) {
304         ClassLoader classLoader = Thread.currentThread()
305                 .getContextClassLoader();
306         ClassLoaderLogInfo info = getClassLoaderInfo(classLoader);
307         String result = info.props.getProperty(name);
308         // If the property was not found, and the current classloader had no
309         // configuration (property list is empty), look for the parent classloader
310         // properties.
311         if ((result == null) && (info.props.isEmpty())) {
312             ClassLoader current = classLoader.getParent();
313             while (current != null) {
314                 info = classLoaderLoggers.get(current);
315                 if (info != null) {
316                     result = info.props.getProperty(name);
317                     if ((result != null) || (!info.props.isEmpty())) {
318                         break;
319                     }
320                 }
321                 current = current.getParent();
322             }
323             if (result == null) {
324                 result = super.getProperty(name);
325             }
326         }
327         return result;
328     }
329
330     @Override
331     public void readConfiguration()
332         throws IOException, SecurityException {
333
334         checkAccess();
335
336         readConfiguration(Thread.currentThread().getContextClassLoader());
337
338     }
339
340     @Override
341     public void readConfiguration(InputStream is)
342         throws IOException, SecurityException {
343
344         checkAccess();
345         reset();
346
347         readConfiguration(is, Thread.currentThread().getContextClassLoader());
348
349     }
350
351     @Override
352     public void reset() throws SecurityException {
353         Thread thread = Thread.currentThread();
354         if (thread.getClass().getName().startsWith(
355                 "java.util.logging.LogManager$")) {
356             // Ignore the call from java.util.logging.LogManager.Cleaner,
357             // because we have our own shutdown hook
358             return;
359         }
360         ClassLoader classLoader = thread.getContextClassLoader();
361         ClassLoaderLogInfo clLogInfo = getClassLoaderInfo(classLoader);
362         resetLoggers(clLogInfo);
363         // Do not call super.reset(). It should be a NO-OP as all loggers should
364         // have been registered via this manager. Very rarely a
365         // ConcurrentModificationException has been seen in the unit tests when
366         // calling super.reset() and that exception could cause the stop of a
367         // web application to fail.
368     }
369
370     /**
371      * Shuts down the logging system.
372      */

373     public synchronized void shutdown() {
374         // The JVM is being shutdown. Make sure all loggers for all class
375         // loaders are shutdown
376         for (ClassLoaderLogInfo clLogInfo : classLoaderLoggers.values()) {
377             resetLoggers(clLogInfo);
378         }
379     }
380
381     // -------------------------------------------------------- Private Methods
382     private void resetLoggers(ClassLoaderLogInfo clLogInfo) {
383         // This differs from LogManager#resetLogger() in that we close not all
384         // handlers of all loggers, but only those that are present in our
385         // ClassLoaderLogInfo#handlers list. That is because our #addLogger(..)
386         // method can use handlers from the parent class loaders, and closing
387         // handlers that the current class loader does not own would be not
388         // good.
389         synchronized (clLogInfo) {
390             for (Logger logger : clLogInfo.loggers.values()) {
391                 Handler[] handlers = logger.getHandlers();
392                 for (Handler handler : handlers) {
393                     logger.removeHandler(handler);
394                 }
395             }
396             for (Handler handler : clLogInfo.handlers.values()) {
397                 try {
398                     handler.close();
399                 } catch (Exception e) {
400                     // Ignore
401                 }
402             }
403             clLogInfo.handlers.clear();
404         }
405     }
406
407     // ------------------------------------------------------ Protected Methods
408
409
410     /**
411      * Retrieve the configuration associated with the specified classloader. If
412      * it does not exist, it will be created.
413      *
414      * @param classLoader The classloader for which we will retrieve or build the
415      *                    configuration
416      * @return the log configuration
417      */

418     protected synchronized ClassLoaderLogInfo getClassLoaderInfo(ClassLoader classLoader) {
419
420         if (classLoader == null) {
421             classLoader = ClassLoader.getSystemClassLoader();
422         }
423         ClassLoaderLogInfo info = classLoaderLoggers.get(classLoader);
424         if (info == null) {
425             final ClassLoader classLoaderParam = classLoader;
426             AccessController.doPrivileged(new PrivilegedAction<Void>() {
427                 @Override
428                 public Void run() {
429                     try {
430                         readConfiguration(classLoaderParam);
431                     } catch (IOException e) {
432                         // Ignore
433                     }
434                     return null;
435                 }
436             });
437             info = classLoaderLoggers.get(classLoader);
438         }
439         return info;
440     }
441
442
443     /**
444      * Read configuration for the specified classloader.
445      *
446      * @param classLoader The classloader
447      * @throws IOException Error reading configuration
448      */

449     protected synchronized void readConfiguration(ClassLoader classLoader)
450         throws IOException {
451
452         InputStream is = null;
453         // Special case for URL classloaders which are used in containers:
454         // only look in the local repositories to avoid redefining loggers 20 times
455         try {
456             if (classLoader instanceof WebappProperties) {
457                 if (((WebappProperties) classLoader).hasLoggingConfig()) {
458                     is = classLoader.getResourceAsStream("logging.properties");
459                 }
460             } else if (classLoader instanceof URLClassLoader) {
461                 URL logConfig = ((URLClassLoader)classLoader).findResource("logging.properties");
462
463                 if(null != logConfig) {
464                     if(Boolean.getBoolean(DEBUG_PROPERTY))
465                         System.err.println(getClass().getName()
466                                            + ".readConfiguration(): "
467                                            + "Found logging.properties at "
468                                            + logConfig);
469
470                     is = classLoader.getResourceAsStream("logging.properties");
471                 } else {
472                     if(Boolean.getBoolean(DEBUG_PROPERTY))
473                         System.err.println(getClass().getName()
474                                            + ".readConfiguration(): "
475                                            + "Found no logging.properties");
476                 }
477             }
478         } catch (AccessControlException ace) {
479             // No permission to configure logging in context
480             // Log and carry on
481             ClassLoaderLogInfo info = classLoaderLoggers.get(ClassLoader.getSystemClassLoader());
482             if (info != null) {
483                 Logger log = info.loggers.get("");
484                 if (log != null) {
485                     Permission perm = ace.getPermission();
486                     if (perm instanceof FilePermission && perm.getActions().equals("read")) {
487                         log.warning("Reading " + perm.getName() + " is not permitted. See \"per context logging\" in the default catalina.policy file.");
488                     }
489                     else {
490                         log.warning("Reading logging.properties is not permitted in some context. See \"per context logging\" in the default catalina.policy file.");
491                         log.warning("Original error was: " + ace.getMessage());
492                     }
493                 }
494             }
495         }
496         if ((is == null) && (classLoader == ClassLoader.getSystemClassLoader())) {
497             String configFileStr = System.getProperty("java.util.logging.config.file");
498             if (configFileStr != null) {
499                 try {
500                     is = new FileInputStream(replace(configFileStr));
501                 } catch (IOException e) {
502                     System.err.println("Configuration error");
503                     e.printStackTrace();
504                 }
505             }
506             // Try the default JVM configuration
507             if (is == null) {
508                 File defaultFile = new File(new File(System.getProperty("java.home"),
509                                                      isJava9 ? "conf" : "lib"),
510                     "logging.properties");
511                 try {
512                     is = new FileInputStream(defaultFile);
513                 } catch (IOException e) {
514                     System.err.println("Configuration error");
515                     e.printStackTrace();
516                 }
517             }
518         }
519
520         Logger localRootLogger = new RootLogger();
521         if (is == null) {
522             // Retrieve the root logger of the parent classloader instead
523             ClassLoader current = classLoader.getParent();
524             ClassLoaderLogInfo info = null;
525             while (current != null && info == null) {
526                 info = getClassLoaderInfo(current);
527                 current = current.getParent();
528             }
529             if (info != null) {
530                 localRootLogger.setParent(info.rootNode.logger);
531             }
532         }
533         ClassLoaderLogInfo info =
534             new ClassLoaderLogInfo(new LogNode(null, localRootLogger));
535         classLoaderLoggers.put(classLoader, info);
536
537         if (is != null) {
538             readConfiguration(is, classLoader);
539         }
540         try {
541             // Use a ThreadLocal to work around
542             // https://bugs.openjdk.java.net/browse/JDK-8195096
543             addingLocalRootLogger.set(Boolean.TRUE);
544             addLogger(localRootLogger);
545         } finally {
546             addingLocalRootLogger.set(Boolean.FALSE);
547         }
548     }
549
550
551     /**
552      * Load specified configuration.
553      *
554      * @param is InputStream to the properties file
555      * @param classLoader for which the configuration will be loaded
556      * @throws IOException If something wrong happens during loading
557      */

558     protected synchronized void readConfiguration(InputStream is, ClassLoader classLoader)
559         throws IOException {
560
561         ClassLoaderLogInfo info = classLoaderLoggers.get(classLoader);
562
563         try {
564             info.props.load(is);
565         } catch (IOException e) {
566             // Report error
567             System.err.println("Configuration error");
568             e.printStackTrace();
569         } finally {
570             try {
571                 is.close();
572             } catch (IOException ioe) {
573                 // Ignore
574             }
575         }
576
577         // Create handlers for the root logger of this classloader
578         String rootHandlers = info.props.getProperty(".handlers");
579         String handlers = info.props.getProperty("handlers");
580         Logger localRootLogger = info.rootNode.logger;
581         if (handlers != null) {
582             StringTokenizer tok = new StringTokenizer(handlers, ",");
583             while (tok.hasMoreTokens()) {
584                 String handlerName = (tok.nextToken().trim());
585                 String handlerClassName = handlerName;
586                 String prefix = "";
587                 if (handlerClassName.length() <= 0) {
588                     continue;
589                 }
590                 // Parse and remove a prefix (prefix start with a digit, such as
591                 // "10WebappFooHandler.")
592                 if (Character.isDigit(handlerClassName.charAt(0))) {
593                     int pos = handlerClassName.indexOf('.');
594                     if (pos >= 0) {
595                         prefix = handlerClassName.substring(0, pos + 1);
596                         handlerClassName = handlerClassName.substring(pos + 1);
597                     }
598                 }
599                 try {
600                     this.prefix.set(prefix);
601                     Handler handler = (Handler) classLoader.loadClass(
602                             handlerClassName).getConstructor().newInstance();
603                     // The specification strongly implies all configuration should be done
604                     // during the creation of the handler object.
605                     // This includes setting level, filter, formatter and encoding.
606                     this.prefix.set(null);
607                     info.handlers.put(handlerName, handler);
608                     if (rootHandlers == null) {
609                         localRootLogger.addHandler(handler);
610                     }
611                 } catch (Exception e) {
612                     // Report error
613                     System.err.println("Handler error");
614                     e.printStackTrace();
615                 }
616             }
617
618         }
619
620     }
621
622
623     /**
624      * Set parent child relationship between the two specified loggers.
625      *
626      * @param logger The logger
627      * @param parent The parent logger
628      */

629     protected static void doSetParentLogger(final Logger logger,
630             final Logger parent) {
631         AccessController.doPrivileged(new PrivilegedAction<Void>() {
632             @Override
633             public Void run() {
634                 logger.setParent(parent);
635                 return null;
636             }
637         });
638     }
639
640
641     /**
642      * System property replacement in the given string.
643      *
644      * @param str The original string
645      * @return the modified string
646      */

647     protected String replace(String str) {
648         String result = str;
649         int pos_start = str.indexOf("${");
650         if (pos_start >= 0) {
651             StringBuilder builder = new StringBuilder();
652             int pos_end = -1;
653             while (pos_start >= 0) {
654                 builder.append(str, pos_end + 1, pos_start);
655                 pos_end = str.indexOf('}', pos_start + 2);
656                 if (pos_end < 0) {
657                     pos_end = pos_start - 1;
658                     break;
659                 }
660                 String propName = str.substring(pos_start + 2, pos_end);
661
662                 String replacement = replaceWebApplicationProperties(propName);
663                 if (replacement == null) {
664                     replacement = propName.length() > 0 ? System.getProperty(propName) : null;
665                 }
666                 if (replacement != null) {
667                     builder.append(replacement);
668                 } else {
669                     builder.append(str, pos_start, pos_end + 1);
670                 }
671                 pos_start = str.indexOf("${", pos_end + 1);
672             }
673             builder.append(str, pos_end + 1, str.length());
674             result = builder.toString();
675         }
676         return result;
677     }
678
679
680     private String replaceWebApplicationProperties(String propName) {
681         ClassLoader cl = Thread.currentThread().getContextClassLoader();
682         if (cl instanceof WebappProperties) {
683             WebappProperties wProps = (WebappProperties) cl;
684             if ("classloader.webappName".equals(propName)) {
685                 return wProps.getWebappName();
686             } else if ("classloader.hostName".equals(propName)) {
687                 return wProps.getHostName();
688             } else if ("classloader.serviceName".equals(propName)) {
689                 return wProps.getServiceName();
690             } else {
691                 return null;
692             }
693         } else {
694             return null;
695         }
696     }
697
698
699     // ---------------------------------------------------- LogNode Inner Class
700
701
702     protected static final class LogNode {
703         Logger logger;
704
705         final Map<String, LogNode> children = new HashMap<>();
706
707         final LogNode parent;
708
709         LogNode(final LogNode parent, final Logger logger) {
710             this.parent = parent;
711             this.logger = logger;
712         }
713
714         LogNode(final LogNode parent) {
715             this(parent, null);
716         }
717
718         LogNode findNode(String name) {
719             LogNode currentNode = this;
720             if (logger.getName().equals(name)) {
721                 return this;
722             }
723             while (name != null) {
724                 final int dotIndex = name.indexOf('.');
725                 final String nextName;
726                 if (dotIndex < 0) {
727                     nextName = name;
728                     name = null;
729                 } else {
730                     nextName = name.substring(0, dotIndex);
731                     name = name.substring(dotIndex + 1);
732                 }
733                 LogNode childNode = currentNode.children.get(nextName);
734                 if (childNode == null) {
735                     childNode = new LogNode(currentNode);
736                     currentNode.children.put(nextName, childNode);
737                 }
738                 currentNode = childNode;
739             }
740             return currentNode;
741         }
742
743         Logger findParentLogger() {
744             Logger logger = null;
745             LogNode node = parent;
746             while (node != null && logger == null) {
747                 logger = node.logger;
748                 node = node.parent;
749             }
750             return logger;
751         }
752
753         void setParentLogger(final Logger parent) {
754             for (final Iterator<LogNode> iter =
755                 children.values().iterator(); iter.hasNext();) {
756                 final LogNode childNode = iter.next();
757                 if (childNode.logger == null) {
758                     childNode.setParentLogger(parent);
759                 } else {
760                     doSetParentLogger(childNode.logger, parent);
761                 }
762             }
763         }
764
765     }
766
767
768     // -------------------------------------------- ClassLoaderInfo Inner Class
769
770
771     protected static final class ClassLoaderLogInfo {
772         final LogNode rootNode;
773         final Map<String, Logger> loggers = new ConcurrentHashMap<>();
774         final Map<String, Handler> handlers = new HashMap<>();
775         final Properties props = new Properties();
776
777         ClassLoaderLogInfo(final LogNode rootNode) {
778             this.rootNode = rootNode;
779         }
780
781     }
782
783
784     // ------------------------------------------------- RootLogger Inner Class
785
786
787     /**
788      * This class is needed to instantiate the root of each per classloader
789      * hierarchy.
790      */

791     protected static class RootLogger extends Logger {
792         public RootLogger() {
793             super(""null);
794         }
795     }
796
797
798 }
799