001    /**
002     *  Licensed to the Apache Software Foundation (ASF) under one or more
003     *  contributor license agreements.  See the NOTICE file distributed with
004     *  this work for additional information regarding copyright ownership.
005     *  The ASF licenses this file to You under the Apache License, Version 2.0
006     *  (the "License"); you may not use this file except in compliance with
007     *  the License.  You may obtain a copy of the License at
008     *
009     *     http://www.apache.org/licenses/LICENSE-2.0
010     *
011     *  Unless required by applicable law or agreed to in writing, software
012     *  distributed under the License is distributed on an "AS IS" BASIS,
013     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     *  See the License for the specific language governing permissions and
015     *  limitations under the License.
016     */
017    package org.apache.geronimo.tomcat;
018    
019    import org.apache.geronimo.gbean.GBeanInfo;
020    import org.apache.geronimo.gbean.GBeanInfoBuilder;
021    import org.apache.geronimo.system.serverinfo.ServerInfo;
022    import org.apache.commons.logging.Log;
023    import org.apache.commons.logging.LogFactory;
024    import org.apache.catalina.valves.AccessLogValve;
025    
026    import java.util.*;
027    import java.util.regex.Pattern;
028    import java.util.regex.Matcher;
029    import java.io.File;
030    import java.io.FilenameFilter;
031    import java.io.RandomAccessFile;
032    import java.nio.channels.FileChannel;
033    import java.nio.MappedByteBuffer;
034    import java.nio.CharBuffer;
035    import java.nio.charset.Charset;
036    import java.text.SimpleDateFormat;
037    import java.text.ParseException;
038    
039    /**
040     * Tomcat implementation of the WebAccessLog management interface.
041     *
042     * @version $Rev: 476049 $ $Date: 2006-11-16 23:35:17 -0500 (Thu, 16 Nov 2006) $
043     */
044    public class TomcatLogManagerImpl implements TomcatLogManager {
045        private final static Log log = LogFactory.getLog(TomcatLogManagerImpl.class);
046    
047        // Pattern that matches the date in the logfile name
048        private final static Pattern FILENAME_DATE_PATTERN = Pattern.compile("[-_ /.](((19|20)\\d\\d)[-_ /.](0[1-9]|1[012])[-_ /.](0[1-9]|[12][0-9]|3[01]))");
049        private final static int GROUP_FILENAME_FULL_DATE = 1;
050        private final static int GROUP_FILENAME_YEAR  = 2;
051        private final static int GROUP_FILENAME_MONTH = 4;
052        private final static int GROUP_FILENAME_DAY   = 5;
053        // NOTE:  The file separators are specified here rather than using something like File.separator because
054        //        they are hard coded in config plans and sometimes in java code itself rather than being dependent
055        //        upon the OS.  This should be fixed someday, but for now we will manually check for either format.
056        private final static String FILE_SEPARATOR_UNIX_STYLE = "/";
057        private final static String FILE_SEPARATOR_WIN_STYLE = "\\";
058    
059        // Pattern that matches a single line  (used to calculate line numbers)
060        private final static Pattern FULL_LINE_PATTERN = Pattern.compile("^.*", Pattern.MULTILINE);
061        private final static Pattern ACCESS_LOG_PATTERN = Pattern.compile("(\\S*) (\\S*) (\\S*) \\[(.*)\\] \\\"(\\S*) (\\S*).*?\\\" (\\S*) (\\S*).*");
062        private final static int GROUP_HOST = 1;
063        private final static int GROUP_USER = 3;
064        private final static int GROUP_DATE = 4;
065        private final static int GROUP_METHOD = 5;
066        private final static int GROUP_URI = 6;
067        private final static int GROUP_RESPONSE_CODE = 7;
068        private final static int GROUP_RESPONSE_LENGTH = 8;
069        private final static String ACCESS_LOG_DATE_FORMAT = "dd/MMM/yyyy:HH:mm:ss ZZZZ";
070        private final static String LOG_FILE_NAME_FORMAT = "yyyy-MM-dd";
071        private final Collection logGbeans;
072        private final ServerInfo serverInfo;
073    
074        public TomcatLogManagerImpl(ServerInfo serverInfo, Collection logGbeans) {
075            this.serverInfo = serverInfo;
076            this.logGbeans = logGbeans;
077        }
078    
079        /**
080         * Gets the name of all logs used by this system.  Typically there
081         * is only one, but specialized cases may use more.
082         *
083         * @return An array of all log names
084         *
085         */
086        public String[] getLogNames() {
087            List logNames = new ArrayList();
088            for (Iterator it = logGbeans.iterator(); it.hasNext();) {
089                ValveGBean logGBean = (ValveGBean) it.next();
090                AccessLogValve logFile = (AccessLogValve) logGBean.getInternalObject();
091                if(logFile != null) {
092                    logNames.add( "var/catalina/logs/"+logFile.getPrefix()+LOG_FILE_NAME_FORMAT+logFile.getSuffix());    
093                }
094            }
095            return (String[]) logNames.toArray(new String[logNames.size()]);
096        }
097    
098        /**
099         * Gets the names of all log files for this log name.  
100         *
101         * @param logName The name of the log for which to return the specific file names.
102         *
103         * @return An array of log file names
104         *
105         */
106        public String[] getLogFileNames(String logName) {
107            List names = new ArrayList();
108    
109            // Find all the files for this logName
110            File[] logFiles = getLogFiles(logName);
111    
112            if (logFiles !=null) {
113                for (int i = 0; i < logFiles.length; i++) {
114                    names.add(logFiles[i].getName());
115                }
116            }
117            return (String[]) names.toArray(new String[names.size()]);
118        }
119    
120        /**
121         * Gets the name of all log files used by this log.  Typically there
122         * is only one, but specialized cases may use more.
123         *
124         * @param logName The name of the log for which to return the specific files.
125         *
126         * @return An array of all log file names
127         *
128         */
129        private File[] getLogFiles(String logName) {
130            File[] logFiles = null;
131    
132            try {
133                String fileNamePattern = logName;
134                if (fileNamePattern.indexOf(FILE_SEPARATOR_UNIX_STYLE) > -1) {
135                    fileNamePattern = fileNamePattern.substring(fileNamePattern.lastIndexOf(FILE_SEPARATOR_UNIX_STYLE) + 1);
136                } else if (fileNamePattern.indexOf(FILE_SEPARATOR_WIN_STYLE) > -1) {
137                    fileNamePattern = fileNamePattern.substring(fileNamePattern.lastIndexOf(FILE_SEPARATOR_WIN_STYLE) + 1);
138                }
139    
140                String logFile = serverInfo.resolvePath(logName);
141    
142                File parent = new File(logFile).getParentFile();
143    
144                if (parent != null) {
145                    logFiles = parent.listFiles(new PatternFilenameFilter(fileNamePattern));
146                }
147            } catch (Exception e) {
148                log.error("Exception attempting to locate Tomcat log files", e);
149                logFiles = new File[0];
150            }
151            return logFiles;
152        }
153    
154        /**
155         * Searches the log for records matching the specified parameters.  The
156         * maximum results returned will be the lesser of 1000 and the
157         * provided maxResults argument.
158         *
159         * @see #MAX_SEARCH_RESULTS
160         */
161        public SearchResults getMatchingItems(String logName, String host, String user, String method, String uri,
162                                              Date startDate, Date endDate, Integer skipResults, Integer maxResults) {
163    
164            // Clean up the arguments so we know what we've really got
165            if(host != null && host.equals("")) host = null;
166            if(user != null && user.equals("")) user = null;
167            if(method != null && method.equals("")) method = null;
168            if(uri != null && uri.equals("")) uri = null;
169    
170            long start = startDate == null ? 0 : startDate.getTime();
171            long end = endDate == null ? 0 : endDate.getTime();
172    
173            List list = new LinkedList();
174            boolean capped = false;
175            int lineCount = 0, fileLineCount = 0;
176    
177            // Find all the files for this logName
178            File logFiles[] = getLogFiles(logName);
179    
180            if (logFiles !=null) {
181                for (int i = 0; i < logFiles.length; i++) {
182                    fileLineCount = 0;
183                    try {
184                        // Obtain the date for the current log file
185                        String fileName = logFiles[i].getName();
186                        Matcher fileDate = FILENAME_DATE_PATTERN.matcher(fileName);
187                        fileDate.find();
188                        SimpleDateFormat simpleFileDate = new SimpleDateFormat(LOG_FILE_NAME_FORMAT);
189                        long logFileTime = simpleFileDate.parse(fileDate.group(GROUP_FILENAME_FULL_DATE)).getTime();
190                        Date logFileDate = new Date(logFileTime);
191    
192                        // Check if the dates are null (ignore) or fall within the search range
193                        if (  (start==0 && end==0)
194                           || (start>0 && start<=logFileTime && end>0 && end>=logFileTime)) {
195    
196                            // It's in the range, so process the file
197                            RandomAccessFile raf = new RandomAccessFile(logFiles[i], "r");
198                            FileChannel fc = raf.getChannel();
199                            MappedByteBuffer bb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());
200                            CharBuffer cb = Charset.forName("US-ASCII").decode(bb); //todo: does Tomcat use a different charset on a foreign PC?
201                            Matcher lines = FULL_LINE_PATTERN.matcher(cb);
202                            Matcher target = ACCESS_LOG_PATTERN.matcher("");
203                            SimpleDateFormat format = (start == 0 && end == 0) ? null : new SimpleDateFormat(ACCESS_LOG_DATE_FORMAT);
204                            int max = maxResults == null ? MAX_SEARCH_RESULTS : Math.min(maxResults.intValue(), MAX_SEARCH_RESULTS);
205    
206                            while(lines.find()) {
207                                ++lineCount;
208                                ++fileLineCount;
209                                if(capped) {
210                                    continue;
211                                }
212                                CharSequence line = cb.subSequence(lines.start(), lines.end());
213                                target.reset(line);
214                                if(target.find()) {
215                                    if(host != null && !host.equals(target.group(GROUP_HOST))) {
216                                        continue;
217                                    }
218                                    if(user != null && !user.equals(target.group(GROUP_USER))) {
219                                        continue;
220                                    }
221                                    if(method != null && !method.equals(target.group(GROUP_METHOD))) {
222                                        continue;
223                                    }
224                                    if(uri != null && !target.group(GROUP_URI).startsWith(uri)) {
225                                        continue;
226                                    }
227                                    if(format != null) {
228                                        try {
229                                            long entry = format.parse(target.group(GROUP_DATE)).getTime();
230                                            if(start > entry) {
231                                                continue;
232                                            }
233                                            if(end > 0 && end < entry) {
234                                                continue;
235                                            }
236                                        } catch (ParseException e) {
237                                            // can't read the date, guess this record counts.
238                                        }
239                                    }
240                                    if(skipResults != null && skipResults.intValue() > lineCount) {
241                                        continue;
242                                    }
243                                    if(list.size() > max) {
244                                        capped = true;
245                                        continue;
246                                    }
247                                    list.add(new LogMessage(fileLineCount,line.toString()));
248                                }
249                            }
250                            fc.close();
251                            raf.close();
252                        }
253                    } catch (Exception e) {
254                        log.error("Unexpected error processing logs", e);
255                    }
256                }
257            }
258            return new SearchResults(lineCount, (LogMessage[]) list.toArray(new LogMessage[list.size()]), capped);
259        }
260    
261    
262        public static final GBeanInfo GBEAN_INFO;
263    
264        static {
265            GBeanInfoBuilder infoFactory = GBeanInfoBuilder.createStatic("Tomcat Log Manager", TomcatLogManagerImpl.class);
266            infoFactory.addReference("LogGBeans", ValveGBean.class);
267            infoFactory.addReference("ServerInfo", ServerInfo.class, "GBean");
268            infoFactory.addInterface(TomcatLogManager.class);
269    
270            infoFactory.setConstructor(new String[]{"ServerInfo","LogGBeans"});
271            GBEAN_INFO = infoFactory.getBeanInfo();
272        }
273    
274        public static GBeanInfo getGBeanInfo() {
275            return GBEAN_INFO;
276        }
277    
278        /*
279         * Static inner class implementation of java.io.Filename. This will help us
280         * filter for only the files that we are interested in.
281         */
282        static class PatternFilenameFilter implements FilenameFilter {
283            Pattern pattern;
284    
285            PatternFilenameFilter(String fileNamePattern) {
286                fileNamePattern = fileNamePattern.replaceAll("yyyy", "\\\\d{4}");
287                fileNamePattern = fileNamePattern.replaceAll("yy", "\\\\d{2}");
288                fileNamePattern = fileNamePattern.replaceAll("mm", "\\\\d{2}");
289                fileNamePattern = fileNamePattern.replaceAll("MM", "\\\\d{2}");
290                fileNamePattern = fileNamePattern.replaceAll("dd", "\\\\d{2}")
291                        + ".*";
292                this.pattern = Pattern.compile(fileNamePattern);
293            }
294    
295            public boolean accept(File file, String fileName) {
296                return pattern.matcher(fileName).matches();
297            }
298        }
299    
300    /*
301        public static void main(String[] args) {
302            String jetty = "127.0.0.1 - - [07/Sep/2005:19:54:41 +0000] \"GET /console/ HTTP/1.1\" 302 0 \"-\" \"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.10) Gecko/20050715 Firefox/1.0.6 SUSE/1.0.6-4.1\" -";
303            String tomcat = "127.0.0.1 - - [07/Sep/2005:15:51:18 -0500] \"GET /console/portal/server/server_info HTTP/1.1\" 200 11708";
304    
305            SimpleDateFormat format = new SimpleDateFormat("dd/MMM/yyyy:HH:mm:ss ZZZZ");
306            try {
307                Pattern p = Pattern.compile("(\\S*) (\\S*) (\\S*) \\[(.*)\\] \\\"(\\S*) (\\S*).*?\\\" (\\S*) (\\S*).*");
308                Matcher m = p.matcher(jetty);
309                if(m.matches()) {
310                    System.out.println("Group 1: "+m.group(1)); // client
311                    System.out.println("Group 2: "+m.group(2)); // ?? server host?
312                    System.out.println("Group 3: "+m.group(3)); // username
313                    System.out.println("Group 4: "+format.parse(m.group(4))); // date
314                    System.out.println("Group 5: "+m.group(5)); // method
315                    System.out.println("Group 5: "+m.group(6)); // URI
316                    System.out.println("Group 6: "+m.group(7)); // response code
317                    System.out.println("Group 7: "+m.group(8)); // response length
318                } else {
319                    System.out.println("No match");
320                }
321                m = p.matcher(tomcat);
322                if(m.matches()) {
323                    System.out.println("Group 1: "+m.group(1));
324                    System.out.println("Group 2: "+m.group(2));
325                    System.out.println("Group 3: "+m.group(3));
326                    System.out.println("Group 4: "+format.parse(m.group(4)));
327                    System.out.println("Group 5: "+m.group(5)); // method
328                    System.out.println("Group 5: "+m.group(6)); // URI
329                    System.out.println("Group 6: "+m.group(7)); // response code
330                    System.out.println("Group 7: "+m.group(8)); // response length
331                } else {
332                    System.out.println("No match");
333                }
334            } catch (ParseException e) {
335                e.printStackTrace();
336            }
337        }
338    */
339    }