001 package hirondelle.web4j.webmaster; 002 003 import static hirondelle.web4j.util.Consts.FILE_SEPARATOR; 004 import static hirondelle.web4j.util.Consts.NOT_FOUND; 005 import hirondelle.web4j.BuildImpl; 006 import hirondelle.web4j.model.AppException; 007 import hirondelle.web4j.model.DateTime; 008 import hirondelle.web4j.readconfig.Config; 009 import hirondelle.web4j.util.TimeSource; 010 import hirondelle.web4j.util.Util; 011 012 import java.io.File; 013 import java.io.IOException; 014 import java.util.ArrayList; 015 import java.util.List; 016 import java.util.Map; 017 import java.util.TimeZone; 018 import java.util.logging.FileHandler; 019 import java.util.logging.Handler; 020 import java.util.logging.Level; 021 import java.util.logging.LogRecord; 022 import java.util.logging.Logger; 023 import java.util.logging.SimpleFormatter; 024 025 /** 026 Default implementation of {@link LoggingConfig}, to set up simple logging. 027 028 <P>This implementation uses JDK logging, and appends logging output to a single file, 029 with no size limit on the file. It uses two settings in <tt>web.xml</tt>: 030 <ul> 031 <li><tt>LoggingDirectory</tt> - the absolute directory which will hold the logging 032 output file. This class will always use a file name using the system date/time, as 033 returned by {@link DateTime#now(TimeZone)} using the <tt>DefaultUserTimeZone</tt> setting in 034 <tt>web.xml</tt>, in the form <tt>2007_12_31_59_59.txt</tt>. If the directory does not exist, WEB4J will 035 attempt to create it upon startup. If set to the special value of <tt>'NONE'</tt>, then 036 this class will not configure JDK logging in any way. 037 <li><tt>LoggingLevels</tt> - a comma-separated list of logger names and their corresponding 038 levels. To verify operation, this class will emit test logging entries for each of these loggers, 039 at the stated logging levels. 040 </ul> 041 */ 042 public final class LoggingConfigImpl implements LoggingConfig { 043 044 /** See class comment. */ 045 public void setup(Map<String, String> aConfig) throws AppException { 046 /* This impl uses the unpublished Config class, not the given map. Custom impls will need the given map. */ 047 logStdOut("Logging directory from web.xml : " + Util.quote(fConfig.getLoggingDirectory())); 048 logStdOut("Logging levels from web.xml : " + Util.quote(fConfig.getLoggingLevels())); 049 if( isTurnedOff() ) { 050 logStdOut("Default logging config is turned off, since directory is set to " + Util.quote(NONE)); 051 } 052 else { 053 logStdOut("Setting up logging config..."); 054 validateDirectorySetting(); 055 parseLoggers(); 056 createFileHandler(); 057 attachLoggersToFileHandler(); 058 tryTestMessages(); 059 fLogger.config("Logging to directory : " + Util.quote(fConfig.getLoggingDirectory())); 060 DateTime now = DateTime.now(fConfig.getDefaultUserTimeZone()); 061 fLogger.config("Current date-time: " + now.format("YYYY-MM-DD hh:mm:ss.fffffffff") + " (uses your TimeSource implementation and the DefaultUserTimeZone setting in web.xml)"); 062 fLogger.config("Raw value of System.currentTimeMillis(): " + System.currentTimeMillis()); 063 showLoggerLevels(); 064 } 065 } 066 067 // PRIVATE 068 private Config fConfig = new Config(); 069 private static final int NO_SIZE_LIMIT = 0; 070 private static final int MAX_BYTES = NO_SIZE_LIMIT; 071 private static final int NUM_FILES = 1; 072 private static final boolean APPEND_TO_EXISTING = true; 073 private static final String NONE = "NONE"; 074 private static final String SEPARATOR = "="; 075 076 /** List of loggers. Each Logger stores its own Level as part of its state. */ 077 private final List<Logger> fLoggers = new ArrayList<Logger>(); 078 private FileHandler fHandler; 079 private static final Logger fLogger = Util.getLogger(LoggingConfigImpl.class); 080 081 private boolean isTurnedOff(){ 082 return NONE.equalsIgnoreCase(fConfig.getLoggingDirectory()); 083 } 084 085 private void validateDirectorySetting() { 086 if( ! fConfig.getLoggingDirectory().endsWith(FILE_SEPARATOR) ){ 087 String message = "*** PROBLEM *** LoggingDirectory setting in web.xml does not end in with a directory separator : " + Util.quote(fConfig.getLoggingDirectory()); 088 logStdOut(message); 089 throw new IllegalArgumentException(message); 090 } 091 if( ! targetDirectoryExists() ){ 092 String message = "LoggingDirectory setting in web.xml does not refer to an existing, writable directory. Will attempt to create directory : " + Util.quote(fConfig.getLoggingDirectory()); 093 logStdOut(message); 094 File directory = new File(fConfig.getLoggingDirectory()); 095 boolean success = directory.mkdirs(); 096 if (success) { 097 logStdOut("Directory created successfully"); 098 } 099 else { 100 logStdOut("*** PROBLEM *** : Unable to create LoggingDirectory specified in web.xml! Permissions problem? Directory already exists, but not writable?"); 101 } 102 } 103 } 104 105 private void parseLoggers(){ 106 for(String logLevel : fConfig.getLoggingLevels()){ 107 int separator = logLevel.indexOf(SEPARATOR); 108 String logger = logLevel.substring(0, separator).trim(); 109 String level = logLevel.substring(separator + 1).trim(); 110 addLogger(removeSuffix(logger), level); 111 } 112 } 113 114 private String removeSuffix(String aLogger){ 115 int suffix = aLogger.indexOf(".level"); 116 if ( suffix == NOT_FOUND ) { 117 throw new IllegalArgumentException("*** PROBLEM *** LoggingLevels setting in web.xml does not end with '.level'"); 118 } 119 return aLogger.substring(0, suffix); 120 } 121 122 private void addLogger(String aLogger, String aLevel){ 123 if( ! Util.textHasContent(aLogger) ){ 124 throw new IllegalArgumentException("Logger name specified in web.xml has no content."); 125 } 126 Logger logger = Logger.getLogger(aLogger); //creates Logger if does not yet exist 127 logger.setLevel(Level.parse(aLevel)); 128 fLogger.config("Adding Logger " + Util.quote(logger.getName() ) + " with level " + Util.quote(logger.getLevel()) ); 129 fLoggers.add(logger); 130 } 131 132 private void createFileHandler() throws AppException { 133 try { 134 fHandler = new FileHandler(getFileName(), MAX_BYTES, NUM_FILES, APPEND_TO_EXISTING); 135 fHandler.setLevel(Level.FINEST); 136 fHandler.setFormatter(new TimeSensitiveFormatter()); 137 } 138 catch (IOException ex){ 139 throw new AppException("Cannot create FileHandler: " + ex.toString() , ex); 140 } 141 } 142 143 private void attachLoggersToFileHandler(){ 144 for (Logger logger: fLoggers){ 145 if( hasNoFileHandler(logger) ){ 146 logger.addHandler(fHandler); 147 } 148 } 149 } 150 151 private boolean hasNoFileHandler(Logger aLogger){ 152 boolean result = true; 153 Handler[] handlers = aLogger.getHandlers(); 154 fLogger.config("Logger " + aLogger.getName() + " has this many existing handlers: " + handlers.length); 155 for (int idx = 0; idx < handlers.length; ++idx){ 156 if ( FileHandler.class.isAssignableFrom(handlers[idx].getClass()) ){ 157 fLogger.config("FileHandler already exists for Logger " + Util.quote(aLogger.getName()) + ". Will not add a new one."); 158 result = false; 159 break; 160 } 161 } 162 return result; 163 } 164 165 /** Log a test message at each logger's configured level. */ 166 private void tryTestMessages(){ 167 logStdOut("Sending test messages to configured loggers. Please confirm output to above log file."); 168 for(Logger logger: fLoggers){ 169 logger.log(logger.getLevel(), "This is a test message for Logger " + Util.quote(logger.getName())); 170 } 171 } 172 173 /** 174 Return the complete name of the logging file. 175 Example file name : <tt>C:\log\fish_and_chips\2007_12_31_23_59.txt</tt> 176 */ 177 private String getFileName(){ 178 String result = null; 179 DateTime now = DateTime.now(fConfig.getDefaultUserTimeZone()); 180 result = fConfig.getLoggingDirectory() + now.format("YYYY|_|MM|_|DD|_|hh|_|mm"); 181 result = result + ".txt"; 182 logStdOut("Logging file name : " + Util.quote(result)); 183 return result; 184 } 185 186 private boolean targetDirectoryExists(){ 187 File directory = new File(fConfig.getLoggingDirectory()); 188 return directory.exists() && directory.isDirectory() && directory.canWrite(); 189 } 190 191 private void logStdOut(Object aObject){ 192 String message = String.valueOf(aObject); 193 System.out.println(message); 194 } 195 196 private void showLoggerLevels() { 197 for(Logger logger : fLoggers){ 198 fLogger.config("Logger " + logger.getName() + " has level " + logger.getLevel()); 199 } 200 } 201 202 private static final class TimeSensitiveFormatter extends SimpleFormatter { 203 public TimeSensitiveFormatter() { } 204 @Override public String format(LogRecord aLogRecord) { 205 aLogRecord.setMillis(fTimeSource.currentTimeMillis()); 206 return super.format(aLogRecord); 207 } 208 private TimeSource fTimeSource = BuildImpl.forTimeSource(); 209 } 210 }