001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.modules.tomcat5.util;
043:
044: import java.io.BufferedReader;
045: import java.io.File;
046: import java.io.FileReader;
047: import java.io.IOException;
048: import java.text.DateFormat;
049: import java.text.SimpleDateFormat;
050: import java.util.Collections;
051: import java.util.Date;
052: import java.util.EventListener;
053: import java.util.Iterator;
054: import java.util.LinkedList;
055: import java.util.List;
056: import java.util.logging.Level;
057: import java.util.logging.Logger;
058: import org.netbeans.api.java.classpath.GlobalPathRegistry;
059: import org.netbeans.modules.tomcat5.TomcatManager;
060: import org.netbeans.modules.tomcat5.util.LogSupport.LineInfo;
061: import org.openide.modules.InstalledFileLocator;
062: import org.openide.util.Exceptions;
063: import org.openide.util.NbBundle;
064: import org.openide.windows.IOProvider;
065: import org.openide.windows.InputOutput;
066: import org.openide.windows.OutputWriter;
067:
068: /**
069: * Thread which displays Tomcat log files in the output window. The output
070: * window name equals prefix minus trailing dot, if present.
071: *
072: * Currently only <code>org.apache.catalina.logger.FileLogger</code> logger
073: * is supported.
074: *
075: * @author Stepan Herold
076: */
077: public class LogViewer extends Thread {
078: private volatile boolean stop = false;
079: private final TomcatManager tomcatManager;
080: private InputOutput inOut;
081: private OutputWriter writer;
082: private OutputWriter errorWriter;
083: private File directory;
084: private String prefix;
085: private String suffix;
086: private boolean isTimestamped;
087: private boolean takeFocus;
088:
089: private ContextLogSupport logSupport;
090: private String catalinaWorkDir;
091: private String webAppContext;
092: private boolean isStarted;
093:
094: /**
095: * List of listeners which are notified when the log viewer is stoped.
096: */
097: private List/*<LogViewerStopListener>*/stopListeners = Collections
098: .synchronizedList(new LinkedList());
099:
100: private String displayName;
101:
102: /**
103: * Create a new LogViewer thread.
104: *
105: * @param tomcatManager Tomcat deployment manager.
106: * @param webAppContext web application's context this logger is declared for,
107: * may be <code>null</code> for shared context log. It is used to look
108: * up sources of servlets generated from JSPs.
109: * @param className class name of logger implementation
110: * @param directory absolute or relative pathname of a directory in which log
111: * files reside, if null catalina default is used.
112: * @param prefix log file prefix, if null catalina default is used.
113: * @param suffix log file suffix, if null catalina default is used.
114: * @param isTimestamped whether logged messages are timestamped.
115: * @param takeFocus whether output window should get focus after each change.
116: *
117: * @throws UnsupportedLoggerException logger specified by the className parameter
118: * is not supported.
119: */
120: public LogViewer(TomcatManager tomcatManager, String webAppContext,
121: String className, String directory, String prefix,
122: String suffix, boolean isTimestamped, boolean takeFocus)
123: throws UnsupportedLoggerException {
124: super ("LogViewer - Thread"); // NOI18N
125: this .tomcatManager = tomcatManager;
126: this .catalinaWorkDir = tomcatManager.getCatalinaWork();
127: if (className != null
128: && !"org.apache.catalina.logger.FileLogger"
129: .equals(className)) { // NOI18N
130: throw new UnsupportedLoggerException(className);
131: }
132: if (directory != null) {
133: this .directory = new File(directory);
134: if (!this .directory.isAbsolute()) {
135: this .directory = new File(tomcatManager
136: .getTomcatProperties().getCatalinaDir(),
137: directory);
138: }
139: } else {
140: this .directory = new File(tomcatManager
141: .getTomcatProperties().getCatalinaDir(), "logs"); // NOI18N
142: }
143: if (prefix != null) {
144: this .prefix = prefix;
145: } else {
146: this .prefix = "catalina."; // NOI18N
147: }
148: if (suffix != null) {
149: this .suffix = suffix;
150: } else {
151: this .suffix = ".log"; // NOI18N
152: }
153: this .isTimestamped = isTimestamped;
154: this .takeFocus = takeFocus;
155: this .webAppContext = webAppContext;
156: logSupport = new ContextLogSupport(catalinaWorkDir,
157: webAppContext);
158: setDaemon(true);
159: }
160:
161: public void setDisplayName(String displayName) {
162: this .displayName = displayName;
163: }
164:
165: /**
166: * Stop the LogViewer thread.
167: */
168: public void close() {
169: synchronized (this ) {
170: stop = true;
171: notify();
172: }
173: }
174:
175: public boolean equals(Object obj) {
176: if (this == obj) {
177: return true;
178: }
179: if (obj instanceof LogViewer) {
180: LogViewer anotherLogViewer = (LogViewer) obj;
181: if (catalinaWorkDir
182: .equals(anotherLogViewer.catalinaWorkDir)
183: && (((webAppContext != null) && webAppContext
184: .equals(anotherLogViewer.webAppContext)) || (webAppContext == anotherLogViewer.webAppContext))
185: && directory.equals(anotherLogViewer.directory)
186: && prefix.equals(anotherLogViewer.prefix)
187: && suffix.equals(anotherLogViewer.suffix)
188: && isTimestamped) {
189: return true;
190: }
191: }
192: return false;
193: }
194:
195: /**
196: * Tests whether LogViewer thread is still running.
197: * @return <code>false</code> if thread was stopped or its output window
198: * was closed, <code>true</code> otherwise.
199: */
200: public boolean isOpen() {
201: InputOutput io = inOut;
202: return !(io == null || stop || (isStarted && io.isClosed()));
203: }
204:
205: /**
206: * Make the log tab visible
207: */
208: public void takeFocus() {
209: InputOutput io = inOut;
210: if (io != null) {
211: io.select();
212: }
213: }
214:
215: private File getLogFile(String timestamp) throws IOException {
216: File f = new File(directory, prefix + timestamp + suffix);
217: f.createNewFile(); // create, if does not exist
218: return f;
219: }
220:
221: private String getTimestamp() {
222: DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); //NOI18N
223: return df.format(new Date());
224: }
225:
226: private void processLine(String line) {
227: ContextLogSupport.LineInfo lineInfo = logSupport
228: .analyzeLine(line);
229: if (lineInfo.isError()) {
230: if (lineInfo.isAccessible()) {
231: try {
232: errorWriter.println(line, logSupport.getLink(
233: lineInfo.message(), lineInfo.path(),
234: lineInfo.line()));
235: } catch (IOException ex) {
236: Exceptions.printStackTrace(ex);
237: }
238: } else {
239: errorWriter.println(line);
240: }
241: } else {
242: if (line.contains("java.lang.LinkageError: JAXB 2.0 API")) { // NOI18N
243: File file = InstalledFileLocator.getDefault().locate(
244: "modules/ext/jaxws21/api/jaxws-api.jar", null,
245: false); // NOI18N
246: File endoresedDir = tomcatManager.getTomcatProperties()
247: .getJavaEndorsedDir();
248: if (file != null) {
249: writer.println(NbBundle.getMessage(LogViewer.class,
250: "MSG_WSSERVLET11", file.getParent(),
251: endoresedDir));
252: } else {
253: writer.println(NbBundle.getMessage(LogViewer.class,
254: "MSG_WSSERVLET11_NOJAR", endoresedDir));
255: }
256: }
257: writer.println(line);
258: }
259: }
260:
261: public void run() {
262: if (displayName == null) {
263: // cut off trailing dot
264: displayName = this .prefix;
265: int trailingDot = displayName.lastIndexOf('.');
266: if (trailingDot > -1) {
267: displayName = displayName.substring(0, trailingDot);
268: }
269: }
270: inOut = IOProvider.getDefault().getIO(displayName, false);
271: try {
272: inOut.getOut().reset();
273: } catch (IOException e) {
274: // not a critical error, continue
275: Logger.getLogger(LogViewer.class.getName()).log(Level.INFO,
276: null, e);
277: }
278: inOut.select();
279: writer = inOut.getOut();
280: errorWriter = inOut.getErr();
281: isStarted = true;
282:
283: String timestamp = getTimestamp();
284: String oldTimestamp = timestamp;
285: try {
286: File logFile = getLogFile(timestamp);
287: BufferedReader reader = new BufferedReader(new FileReader(
288: logFile));
289: try {
290: while (!stop && !inOut.isClosed()) {
291: // check whether a log file has rotated
292: timestamp = getTimestamp();
293: if (!timestamp.equals(oldTimestamp)) {
294: oldTimestamp = timestamp;
295: reader.close();
296: logFile = getLogFile(timestamp);
297: reader = new BufferedReader(new FileReader(
298: logFile));
299: }
300: int count = 0;
301: // take a nap after 1024 read cycles, this should ensure responsiveness
302: // even if log file is growing fast
303: boolean updated = false;
304: while (reader.ready() && count++ < 1024) {
305: processLine(reader.readLine());
306: updated = true;
307: }
308: if (updated) {
309: writer.flush();
310: errorWriter.flush();
311: if (takeFocus) {
312: inOut.select();
313: }
314: }
315: // wait for the next attempt
316: try {
317: synchronized (this ) {
318: if (!stop && !inOut.isClosed()) {
319: wait(100);
320: }
321: }
322: } catch (InterruptedException ex) {
323: // no op - the thread was interrupted
324: }
325: }
326: } finally {
327: reader.close();
328: }
329: } catch (IOException ex) {
330: Exceptions.printStackTrace(ex);
331: } finally {
332: writer.close();
333: }
334: fireLogViewerStopListener();
335: logSupport.detachAnnotation();
336: }
337:
338: /**
339: * Add a <code>LogViewerStopListener</code>.
340: *
341: * @param listener <code>LogViewerStopListener</code> which will be notified
342: * when the <code>LogViewer</code> stops running.
343: */
344: public void addLogViewerStopListener(LogViewerStopListener listener) {
345: stopListeners.add(listener);
346: }
347:
348: /**
349: * Remove all registered <code>LogViewerStopListener</code> listeners.
350: *
351: * @param listener <code>LogViewerStopListener</code> which will be notified
352: * when the <code>LogViewer</code> stops running.
353: */
354: public void removeAllLogViewerStopListener() {
355: stopListeners.removeAll(stopListeners);
356: }
357:
358: private void fireLogViewerStopListener() {
359: for (Iterator i = stopListeners.iterator(); i.hasNext();) {
360: ((LogViewerStopListener) i.next()).callOnStop();
361: }
362: }
363:
364: /**
365: * <code>LogViewerStopListener</code> is notified when the <code>LogViewer</code>
366: * stops running.
367: */
368: public static interface LogViewerStopListener extends EventListener {
369: public void callOnStop();
370: }
371:
372: /**
373: * Support class for context log line analyzation and for creating links in
374: * the output window.
375: */
376: static class ContextLogSupport extends LogSupport {
377: private final String CATALINA_WORK_DIR;
378: private String context = null;
379: private String prevMessage = null;
380: private static final String STANDARD_CONTEXT = "StandardContext["; // NOI18N
381: private static final int STANDARD_CONTEXT_LENGTH = STANDARD_CONTEXT
382: .length();
383: private GlobalPathRegistry globalPathReg = GlobalPathRegistry
384: .getDefault();
385:
386: public ContextLogSupport(String catalinaWork,
387: String webAppContext) {
388: CATALINA_WORK_DIR = catalinaWork;
389: context = webAppContext;
390: }
391:
392: public LineInfo analyzeLine(String logLine) {
393: String path = null;
394: int line = -1;
395: String message = null;
396: boolean error = false;
397: boolean accessible = false;
398:
399: logLine = logLine.trim();
400: int lineLenght = logLine.length();
401:
402: // look for unix file links (e.g. /foo/bar.java:51: 'error msg')
403: if (logLine.startsWith("/")) {
404: error = true;
405: int colonIdx = logLine.indexOf(':');
406: if (colonIdx > -1) {
407: path = logLine.substring(0, colonIdx);
408: accessible = true;
409: if (lineLenght > colonIdx) {
410: int nextColonIdx = logLine.indexOf(':',
411: colonIdx + 1);
412: if (nextColonIdx > -1) {
413: String lineNum = logLine.substring(
414: colonIdx + 1, nextColonIdx);
415: try {
416: line = Integer.valueOf(lineNum)
417: .intValue();
418: } catch (NumberFormatException nfe) {
419: // ignore it
420: Logger.getLogger(
421: LogViewer.class.getName()).log(
422: Level.INFO, null, nfe);
423: }
424: if (lineLenght > nextColonIdx) {
425: message = logLine.substring(
426: nextColonIdx + 1, lineLenght);
427: }
428: }
429: }
430: }
431: }
432: // look for windows file links (e.g. c:\foo\bar.java:51: 'error msg')
433: else if (lineLenght > 3
434: && Character.isLetter(logLine.charAt(0))
435: && (logLine.charAt(1) == ':')
436: && (logLine.charAt(2) == '\\')) {
437: error = true;
438: int secondColonIdx = logLine.indexOf(':', 2);
439: if (secondColonIdx > -1) {
440: path = logLine.substring(0, secondColonIdx);
441: accessible = true;
442: if (lineLenght > secondColonIdx) {
443: int thirdColonIdx = logLine.indexOf(':',
444: secondColonIdx + 1);
445: if (thirdColonIdx > -1) {
446: String lineNum = logLine.substring(
447: secondColonIdx + 1, thirdColonIdx);
448: try {
449: line = Integer.valueOf(lineNum)
450: .intValue();
451: } catch (NumberFormatException nfe) { // ignore it
452: Logger.getLogger(
453: LogViewer.class.getName()).log(
454: Level.INFO, null, nfe);
455: }
456: if (lineLenght > thirdColonIdx) {
457: message = logLine.substring(
458: thirdColonIdx + 1, lineLenght);
459: }
460: }
461: }
462: }
463: }
464: // look for stacktrace links (e.g. at java.lang.Thread.run(Thread.java:595)
465: // at t.HyperlinkTest$1.run(HyperlinkTest.java:24))
466: else if (logLine.startsWith("at ") && lineLenght > 3) {
467: error = true;
468: int parenthIdx = logLine.indexOf('(');
469: if (parenthIdx > -1) {
470: String classWithMethod = logLine.substring(3,
471: parenthIdx);
472: int lastDotIdx = classWithMethod.lastIndexOf('.');
473: if (lastDotIdx > -1) {
474: int lastParenthIdx = logLine.lastIndexOf(')');
475: int lastColonIdx = logLine.lastIndexOf(':');
476: if (lastParenthIdx > -1 && lastColonIdx > -1) {
477: String lineNum = logLine.substring(
478: lastColonIdx + 1, lastParenthIdx);
479: try {
480: line = Integer.valueOf(lineNum)
481: .intValue();
482: } catch (NumberFormatException nfe) { // ignore it
483: Logger.getLogger(
484: LogViewer.class.getName()).log(
485: Level.INFO, null, nfe);
486: }
487: message = prevMessage;
488: }
489: int firstDolarIdx = classWithMethod
490: .indexOf('$'); // > -1 for inner classes
491: String className = classWithMethod.substring(0,
492: firstDolarIdx > -1 ? firstDolarIdx
493: : lastDotIdx);
494: path = className.replace('.', '/') + ".java"; // NOI18N
495: accessible = globalPathReg.findResource(path) != null;
496: if (className.startsWith("org.apache.jsp.")
497: && context != null) { // NOI18N
498: if (context != null) {
499: String contextPath = context
500: .equals("/") ? "/_" // hande ROOT context
501: : context;
502: path = CATALINA_WORK_DIR + contextPath
503: + "/" + path;
504: accessible = new File(path).exists();
505: }
506: }
507: }
508: }
509: }
510: // every other message treat as normal info message
511: else {
512: prevMessage = logLine;
513: // try to get context, if stored
514: int stdContextIdx = logLine.indexOf(STANDARD_CONTEXT);
515: int lBracketIdx = -1;
516: if (stdContextIdx > -1) {
517: lBracketIdx = stdContextIdx
518: + STANDARD_CONTEXT_LENGTH;
519: }
520: int rBracketIdx = logLine.indexOf(']');
521: if (lBracketIdx > -1 && rBracketIdx > -1
522: && rBracketIdx > lBracketIdx) {
523: context = logLine.substring(lBracketIdx,
524: rBracketIdx);
525: }
526: }
527: return new LineInfo(path, line, message, error, accessible);
528: }
529: }
530: }
|