001    package hirondelle.web4jtools.logview.simpleview;
002    
003    import static hirondelle.web4j.util.Consts.NEW_LINE;
004    import hirondelle.web4j.database.DAOException;
005    import hirondelle.web4j.util.Util;
006    import hirondelle.web4jtools.logview.directories.LogInfo;
007    import hirondelle.web4jtools.logview.directories.LogInfoDAO;
008    import hirondelle.web4jtools.logview.parsedview.LoggerRecord;
009    import hirondelle.web4jtools.logview.parsedview.ParsedCriteria;
010    import hirondelle.web4jtools.util.Ensure;
011    import hirondelle.web4jtools.logview.parser.LogParser;
012    import hirondelle.web4jtools.logview.parser.LogParserInstance;
013    
014    import java.io.File;
015    import java.io.FileFilter;
016    import java.io.FileNotFoundException;
017    import java.util.*;
018    import java.util.logging.Logger;
019    
020    import javax.servlet.ServletConfig;
021    import javax.servlet.http.HttpServletRequest;
022    
023    /**
024    * Data Access Object for log files.
025    * 
026    *<P>This class is used both for viewing log files as plain text, and for 
027    * parsing log files into {@link hirondelle.web4jtools.logview.parsedview.LoggerRecord}s.
028    */
029    public final class LogFileDAO {
030    
031      /** Read  in config from <tt>web.xml</tt>. */
032      public static void readConfig(ServletConfig aConfig){
033        fAPP_ENCODING = aConfig.getInitParameter(APP_ENCODING);
034        fSERVER_ENCODING = aConfig.getInitParameter(SERVER_ENCODING);
035        Ensure.isPresentInWebXml(APP_ENCODING, fAPP_ENCODING);
036        Ensure.isPresentInWebXml(SERVER_ENCODING, fSERVER_ENCODING);
037      }
038      
039      /** Full constructor. */
040      public LogFileDAO(HttpServletRequest aRequest){
041        fDirInfoDAO = new LogInfoDAO(aRequest);  
042      }
043      
044      /**
045      * Return the most recent log file in a given directory.
046      *   
047      * Returns <tt>null</tt> if no file found in the logging directory.  
048      */
049      public File getMostRecentLogFile(LogFor aLogFor) throws DAOException {
050        return getMostRecentFile(aLogFor);
051      }
052      
053      /**
054      * Return the contents of a log file as a <tt>String</tt>.
055      * 
056      * <P>If the given criteria desire only a section of the file, then only the first or last 
057      * section is returned; otherwise, the contents of the entire file is returned.
058      */
059      public String getLogFileContents(File aFile, SimpleCriteria aCriteria) throws DAOException {
060        String result = null;
061        if( aCriteria.getSection() == null || aCriteria.getSection() == Section.First) {
062          result = getAllOrFirst(aFile, aCriteria.getNumLines());
063        }
064        else {
065          result = getEnd(aFile, aCriteria.getNumLines());
066        }
067        return result;
068      }
069      
070      /**
071      * Return the entire content of a log file as a {@code List<LoggerRecord>}.
072      */
073      public List<LoggerRecord> getParsedLogFile(File aFile, ParsedCriteria aCriteria) throws DAOException {
074        LogParser parser = LogParserInstance.getFor(aCriteria.getLogFor());
075        List<LoggerRecord> result = parser.parse(getAllOrFirst(aFile, null), aCriteria);
076        if( aCriteria.getReverseOrder() ) {
077          Collections.reverse(result);
078        }
079        return result;
080      }
081      
082      /**
083      * Return a <tt>List</tt> of all <em>application</em> log files.
084      * 
085      * Any files having zero size (such as lock files) are ignored. The files are returned in order of increasing 
086      * 'last-modified' date, such that the oldest log is first. This method is used when calculating down-times 
087      * from logs. 
088      */
089      public List<File> listAllAppLogFiles(){
090        File appLogDir = getDirectory(LogFor.Application);
091        File[] logFiles = appLogDir.listFiles(getFilter(LogFor.Application));
092        List<File> result = Arrays.asList(logFiles); //assumes all are logs, with no directories underneath
093        Collections.sort(result, byModifiedDateDesc());
094        Collections.reverse(result);
095        fLogger.fine("Number of app log files in app log directory : " + result.size());
096        return result;
097      }
098    
099      /**
100      * Return a <tt>List</tt> containing the <em>first</em> {@link LoggerRecord} in each 
101      * of the given log files. This method is used when calculating down times from logs.
102      */
103      public List<LoggerRecord> listFirstRecordsFor(List<File> aAllLogFiles) throws DAOException {
104        List<LoggerRecord> result = new ArrayList<LoggerRecord>();
105        for(File file : aAllLogFiles){
106          result.add(firstRecordFor(file));
107        }
108        return result;
109      }
110      
111      // PRIVATE //
112      
113      private final LogInfoDAO fDirInfoDAO;
114      
115      private static String SERVER_ENCODING = "ServerLogFileEncoding";
116      private static String fSERVER_ENCODING;
117      
118      private static String APP_ENCODING = "ApplicationLogFileEncoding";
119      private static String fAPP_ENCODING;
120      
121      private static final Logger fLogger = Util.getLogger(LogFileDAO.class);
122    
123      /** Returns null if no file in the directory at all.  */
124      private File getMostRecentFile(LogFor aLogFor) {
125        File result = null;
126        List<File> allFiles = Arrays.asList(getDirectory(aLogFor).listFiles(getFilter(aLogFor)));
127        Collections.sort( allFiles, byModifiedDateDesc() );
128        if ( ! allFiles.isEmpty() ) {
129          result =  allFiles.get(0);
130          fLogger.fine("Most recent " + aLogFor + " log file : " + Util.quote(result.getAbsolutePath()));
131        }
132        else {
133          fLogger.fine("No log files detected in directory : " + getDirectory(aLogFor));
134        }
135        return result;
136      }
137      
138      private File getDirectory(LogFor aLogFor){
139        File result = null;
140        if( LogFor.Server == aLogFor ) {
141          result = new File(fDirInfoDAO.fetch().getServerLoggingDirectory().getRawString());
142        }
143        else if ( LogFor.Application == aLogFor ) {
144          result = new File(fDirInfoDAO.fetch().getAppLoggingDirectory().getRawString());
145        }
146        else {
147          throw new AssertionError("Unknown value of LogFor enumeration.");
148        }
149        return result;
150      }
151    
152      /** Sort files by date.  */
153      private Comparator<File> byModifiedDateDesc(){
154        return new Comparator<File>() {
155          public int compare(File aThis, File aThat) {
156            Long thisDate = aThis.lastModified();
157            Long thatDate = aThat.lastModified();
158            return thisDate.compareTo(thatDate) * (-1);
159          }
160        };
161      }
162      
163      /** 
164      * Return either all or a portion of a file as a String.
165      * 
166      * @param aNumLines num lines to return; if <tt>null</tt>, then all lines are returned.  
167      */ 
168      private String getAllOrFirst(File aFile, Integer aNumLines) throws DAOException {
169        fLogger.fine("Getting either all of log file, or first section.");
170        StringBuilder result = new StringBuilder();
171        Scanner scanner = null;
172        try {
173          scanner = new Scanner(aFile);
174        }
175        catch(FileNotFoundException ex){
176          throw new DAOException("Cannot find file " + Util.quote(aFile.getAbsolutePath()), ex);
177        }
178        int lineCount = 0;
179        while ( scanner.hasNextLine()) {
180          result.append(scanner.nextLine() + NEW_LINE);
181          lineCount++;
182          if ( aNumLines != null && lineCount == aNumLines) break;
183        }
184        scanner.close();
185        fLogger.fine("Number of log lines returned : " + lineCount);
186        return result.toString();
187      }
188    
189      /**
190      * Return the end section of a file as a String.
191      * 
192      * @param aNumLinesForDisplay number of lines to return.
193      */
194      private String getEnd(File aFile, Integer aNumLinesForDisplay) throws DAOException {
195        fLogger.fine("Getting up to " + aNumLinesForDisplay + " lines from the end of the log file.");
196        String result = null;
197        Integer numLinesInFile = getNumLines(aFile);
198        if ( numLinesInFile <=  aNumLinesForDisplay ) {
199          result = getAllOrFirst(aFile, null);
200        }
201        else {
202          result = getOnlyFinalLines(aFile, numLinesInFile, aNumLinesForDisplay);
203        }
204        return result;
205      }
206      
207      /** Return the number of lines in the given file.  */
208      private int getNumLines(File aFile) throws DAOException {
209        int result = 0;
210        Scanner scanner = null;
211        try {
212          scanner = new Scanner(aFile);
213        }
214        catch(FileNotFoundException ex){
215          throw new DAOException("Cannot find file " + Util.quote(aFile.getAbsolutePath()), ex);
216        }
217        while ( scanner.hasNextLine()) {
218          scanner.nextLine();
219          result++;
220        }
221        scanner.close();
222        fLogger.fine("Number of lines in the file : " + result);
223        return result;
224      }
225      
226      /** Truncate file content to a gven number of lines at the end of the file.   */
227      private String getOnlyFinalLines(File aFile, Integer aTotalLinesInFile, Integer aNumLinesForDisplay) throws DAOException {
228        fLogger.fine("Truncating. Total lines in file : " + aTotalLinesInFile + " Num Lines desired for display :  " + aNumLinesForDisplay);
229        StringBuilder result = new StringBuilder();
230        assert(aTotalLinesInFile >= aNumLinesForDisplay);
231        int start = aTotalLinesInFile - aNumLinesForDisplay;
232        Scanner scanner = null;
233        try {
234          scanner = new Scanner(aFile);
235        }
236        catch(FileNotFoundException ex){
237          throw new DAOException("Cannot find file " + Util.quote(aFile.getAbsolutePath()), ex);
238        }
239        int lineCount = 0;
240        while ( scanner.hasNextLine()) {
241          String line = scanner.nextLine();
242          lineCount++;
243          if ( lineCount >= start ) {
244            result.append(line + NEW_LINE);
245          }
246        }
247        scanner.close();
248        return result.toString();
249      }
250      
251      /** Return a FileFilter appropriate for the type of log.  */
252      private FileFilter getFilter(LogFor aLogFor){
253        FileFilter result = null; 
254        final LogInfo dirInfo = fDirInfoDAO.fetch();
255        if( LogFor.Server == aLogFor ) {
256          result = acceptIfNameStartsWithSpecificText(dirInfo);
257        }
258        else if ( LogFor.Application == aLogFor ) {
259          result = rejectEmptyFiles();
260        }
261        return result;  
262      }
263    
264      /** Accept if non-zero size and name starts with specific text.  */
265      private FileFilter acceptIfNameStartsWithSpecificText(LogInfo aDirInfo){
266        final LogInfo dirInfo = aDirInfo;
267        return new FileFilter() {
268          public boolean accept(File aFile) {
269            boolean nonZeroSize = (aFile.length() > 0);
270            boolean fileNameStartsWith = aFile.getAbsolutePath().startsWith(dirInfo.getServerLoggingDirectory().getRawString() + dirInfo.getServerLogFileStartsWith());
271            return nonZeroSize && fileNameStartsWith;
272          }
273        };
274      }
275    
276      /** Accept only if non-zero size.  */
277      private FileFilter rejectEmptyFiles(){
278        return new FileFilter() {
279          public boolean accept(File aFile) {
280            return (aFile.length() > 0);
281          }
282        };
283      }
284      
285      /**
286      * Return the first {@link LoggerRecord} appearing in a file. 
287      */
288      private LoggerRecord firstRecordFor(File aLogFile) throws DAOException {
289        //first N lines - will often cut off record at the end
290        fLogger.fine("Getting first record for " + aLogFile.getName());
291        String firstLines = getAllOrFirst(aLogFile, 100);
292        LogParser parser = LogParserInstance.getFor(LogFor.Application);
293        List<LoggerRecord> records = parser.parse(firstLines, ParsedCriteria.forDowntimeListing());
294        fLogger.fine("Num logger records at start of file " + records.size());
295        return records.get(0);
296      }
297    }