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    }