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    }