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