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
018// Contributors: Mathias Rupprecht <mmathias.rupprecht@fja.com>
019
020package org.apache.log4j.spi;
021
022import org.apache.log4j.Layout;
023import org.apache.log4j.helpers.LogLog;
024
025import java.io.PrintWriter;
026import java.io.StringWriter;
027import java.io.InterruptedIOException;
028import java.lang.reflect.Method;
029import java.lang.reflect.InvocationTargetException;
030
031/**
032   The internal representation of caller location information.
033
034   @since 0.8.3
035*/
036public class LocationInfo implements java.io.Serializable {
037
038  /**
039     Caller's line number.
040  */
041  transient String lineNumber;
042  /**
043     Caller's file name.
044  */
045  transient String fileName;
046  /**
047     Caller's fully qualified class name.
048  */
049  transient String className;
050  /**
051     Caller's method name.
052  */
053  transient String methodName;
054  /**
055     All available caller information, in the format
056     <code>fully.qualified.classname.of.caller.methodName(Filename.java:line)</code>
057    */
058  public String fullInfo;
059
060  private static StringWriter sw = new StringWriter();
061  private static PrintWriter pw = new PrintWriter(sw);
062
063  private static Method getStackTraceMethod;
064  private static Method getClassNameMethod;
065  private static Method getMethodNameMethod;
066  private static Method getFileNameMethod;
067  private static Method getLineNumberMethod;
068
069
070  /**
071     When location information is not available the constant
072     <code>NA</code> is returned. Current value of this string
073     constant is <b>?</b>.  */
074  public final static String NA = "?";
075
076  static final long serialVersionUID = -1325822038990805636L;
077
078    /**
079     * NA_LOCATION_INFO is provided for compatibility with log4j 1.3.
080     * @since 1.2.15
081     */
082    public static final LocationInfo NA_LOCATION_INFO =
083            new LocationInfo(NA, NA, NA, NA);
084
085
086
087  // Check if we are running in IBM's visual age.
088  static boolean inVisualAge = false;
089  static {
090    try {
091      inVisualAge = Class.forName("com.ibm.uvm.tools.DebugSupport") != null;
092      LogLog.debug("Detected IBM VisualAge environment.");
093    } catch(Throwable e) {
094      // nothing to do
095    }
096      try {
097          Class[] noArgs = null;
098          getStackTraceMethod = Throwable.class.getMethod("getStackTrace", noArgs);
099          Class stackTraceElementClass = Class.forName("java.lang.StackTraceElement");
100          getClassNameMethod = stackTraceElementClass.getMethod("getClassName", noArgs);
101          getMethodNameMethod = stackTraceElementClass.getMethod("getMethodName", noArgs);
102          getFileNameMethod = stackTraceElementClass.getMethod("getFileName", noArgs);
103          getLineNumberMethod = stackTraceElementClass.getMethod("getLineNumber", noArgs);
104      } catch(ClassNotFoundException ex) {
105          LogLog.debug("LocationInfo will use pre-JDK 1.4 methods to determine location.");
106      } catch(NoSuchMethodException ex) {
107          LogLog.debug("LocationInfo will use pre-JDK 1.4 methods to determine location.");
108      }
109  }
110
111  /**
112     Instantiate location information based on a Throwable. We
113     expect the Throwable <code>t</code>, to be in the format
114
115       <pre>
116        java.lang.Throwable
117        ...
118          at org.apache.log4j.PatternLayout.format(PatternLayout.java:413)
119          at org.apache.log4j.FileAppender.doAppend(FileAppender.java:183)
120        at org.apache.log4j.Category.callAppenders(Category.java:131)
121        at org.apache.log4j.Category.log(Category.java:512)
122        at callers.fully.qualified.className.methodName(FileName.java:74)
123        ...
124       </pre>
125
126       <p>However, we can also deal with JIT compilers that "lose" the
127       location information, especially between the parentheses.
128        @param t throwable used to determine location, may be null.
129        @param fqnOfCallingClass class name of first class considered part of
130           the logging framework.  Location will be site that calls a method on this class.
131
132    */
133    public LocationInfo(Throwable t, String fqnOfCallingClass) {
134      if(t == null || fqnOfCallingClass == null)
135        return;
136      if (getLineNumberMethod != null) {
137          try {
138              Object[] noArgs = null;
139              Object[] elements =  (Object[]) getStackTraceMethod.invoke(t, noArgs);
140              String prevClass = NA;
141              for(int i = elements.length - 1; i >= 0; i--) {
142                  String thisClass = (String) getClassNameMethod.invoke(elements[i], noArgs);
143                  if(fqnOfCallingClass.equals(thisClass)) {
144                      int caller = i + 1;
145                      if (caller < elements.length) {
146                          className = prevClass;
147                          methodName = (String) getMethodNameMethod.invoke(elements[caller], noArgs);
148                          fileName = (String) getFileNameMethod.invoke(elements[caller], noArgs);
149                          if (fileName == null) {
150                              fileName = NA;
151                          }
152                          int line = ((Integer) getLineNumberMethod.invoke(elements[caller], noArgs)).intValue();
153                          if (line < 0) {
154                              lineNumber = NA;
155                          } else {
156                              lineNumber = String.valueOf(line);
157                          }
158                          StringBuffer buf = new StringBuffer();
159                          buf.append(className);
160                          buf.append(".");
161                          buf.append(methodName);
162                          buf.append("(");
163                          buf.append(fileName);
164                          buf.append(":");
165                          buf.append(lineNumber);
166                          buf.append(")");
167                          this.fullInfo = buf.toString();
168                      }
169                      return;
170                  }
171                  prevClass = thisClass;
172              }
173              return;
174          } catch(IllegalAccessException ex) {
175              LogLog.debug("LocationInfo failed using JDK 1.4 methods", ex);
176          } catch(InvocationTargetException ex) {
177              if (ex.getTargetException() instanceof InterruptedException
178                      || ex.getTargetException() instanceof InterruptedIOException) {
179                  Thread.currentThread().interrupt();
180              }
181              LogLog.debug("LocationInfo failed using JDK 1.4 methods", ex);
182          } catch(RuntimeException ex) {
183              LogLog.debug("LocationInfo failed using JDK 1.4 methods", ex);
184          }
185      }
186
187      String s;
188      // Protect against multiple access to sw.
189      synchronized(sw) {
190        t.printStackTrace(pw);
191        s = sw.toString();
192        sw.getBuffer().setLength(0);
193      }
194      //System.out.println("s is ["+s+"].");
195      int ibegin, iend;
196
197      // Given the current structure of the package, the line
198      // containing "org.apache.log4j.Category." should be printed just
199      // before the caller.
200
201      // This method of searching may not be fastest but it's safer
202      // than counting the stack depth which is not guaranteed to be
203      // constant across JVM implementations.
204      ibegin = s.lastIndexOf(fqnOfCallingClass);
205      if(ibegin == -1)
206        return;
207
208      //
209      //   if the next character after the class name exists
210      //       but is not a period, see if the classname is
211      //       followed by a period earlier in the trace.
212      //       Minimizes mistakeningly matching on a class whose
213      //       name is a substring of the desired class.
214      //       See bug 44888.
215      if (ibegin + fqnOfCallingClass.length() < s.length() &&
216              s.charAt(ibegin + fqnOfCallingClass.length()) != '.') {
217          int i = s.lastIndexOf(fqnOfCallingClass + ".");
218          if (i != -1) {
219              ibegin = i;
220          }
221      }
222
223
224      ibegin = s.indexOf(Layout.LINE_SEP, ibegin);
225      if(ibegin == -1)
226        return;
227      ibegin+= Layout.LINE_SEP_LEN;
228
229      // determine end of line
230      iend = s.indexOf(Layout.LINE_SEP, ibegin);
231      if(iend == -1)
232        return;
233
234      // VA has a different stack trace format which doesn't
235      // need to skip the inital 'at'
236      if(!inVisualAge) {
237        // back up to first blank character
238        ibegin = s.lastIndexOf("at ", iend);
239        if(ibegin == -1)
240          return;
241        // Add 3 to skip "at ";
242        ibegin += 3;
243      }
244      // everything between is the requested stack item
245      this.fullInfo = s.substring(ibegin, iend);
246    }
247
248    /**
249     *   Appends a location fragment to a buffer to build the 
250     *     full location info.
251     *    @param buf StringBuffer to receive content.
252     *    @param fragment fragment of location (class, method, file, line),
253     *        if null the value of NA will be appended.
254     *    @since 1.2.15
255     */
256    private static final void appendFragment(final StringBuffer buf,
257                                             final String fragment) {
258          if (fragment == null) {
259             buf.append(NA);
260          } else {
261             buf.append(fragment);
262          }
263    }
264
265    /**
266     * Create new instance.
267     * @param file source file name
268     * @param classname class name
269     * @param method method
270     * @param line source line number
271     *
272     * @since 1.2.15
273     */
274    public LocationInfo(
275      final String file,
276      final String classname,
277      final String method,
278      final String line) {
279      this.fileName = file;
280      this.className = classname;
281      this.methodName = method;
282      this.lineNumber = line;
283      StringBuffer buf = new StringBuffer();
284          appendFragment(buf, classname);
285          buf.append(".");
286          appendFragment(buf, method);
287          buf.append("(");
288          appendFragment(buf, file);
289          buf.append(":");
290          appendFragment(buf, line);
291          buf.append(")");
292          this.fullInfo = buf.toString();
293    }
294
295    /**
296       Return the fully qualified class name of the caller making the
297       logging request.
298    */
299    public
300    String getClassName() {
301      if(fullInfo == null) return NA;
302      if(className == null) {
303        // Starting the search from '(' is safer because there is
304        // potentially a dot between the parentheses.
305        int iend = fullInfo.lastIndexOf('(');
306        if(iend == -1)
307          className = NA;
308        else {
309          iend =fullInfo.lastIndexOf('.', iend);
310
311          // This is because a stack trace in VisualAge looks like:
312
313          //java.lang.RuntimeException
314          //  java.lang.Throwable()
315          //  java.lang.Exception()
316          //  java.lang.RuntimeException()
317          //  void test.test.B.print()
318          //  void test.test.A.printIndirect()
319          //  void test.test.Run.main(java.lang.String [])
320          int ibegin = 0;
321          if (inVisualAge) {
322            ibegin = fullInfo.lastIndexOf(' ', iend)+1;
323          }
324
325          if(iend == -1)
326            className = NA;
327          else
328            className = this.fullInfo.substring(ibegin, iend);
329        }
330      }
331      return className;
332    }
333
334    /**
335       Return the file name of the caller.
336
337       <p>This information is not always available.
338    */
339    public
340    String getFileName() {
341      if(fullInfo == null) return NA;
342
343      if(fileName == null) {
344        int iend = fullInfo.lastIndexOf(':');
345        if(iend == -1)
346          fileName = NA;
347        else {
348          int ibegin = fullInfo.lastIndexOf('(', iend - 1);
349          fileName = this.fullInfo.substring(ibegin + 1, iend);
350        }
351      }
352      return fileName;
353    }
354
355    /**
356       Returns the line number of the caller.
357
358       <p>This information is not always available.
359    */
360    public
361    String getLineNumber() {
362      if(fullInfo == null) return NA;
363
364      if(lineNumber == null) {
365        int iend = fullInfo.lastIndexOf(')');
366        int ibegin = fullInfo.lastIndexOf(':', iend -1);
367        if(ibegin == -1)
368          lineNumber = NA;
369        else
370          lineNumber = this.fullInfo.substring(ibegin + 1, iend);
371      }
372      return lineNumber;
373    }
374
375    /**
376       Returns the method name of the caller.
377    */
378    public
379    String getMethodName() {
380      if(fullInfo == null) return NA;
381      if(methodName == null) {
382        int iend = fullInfo.lastIndexOf('(');
383        int ibegin = fullInfo.lastIndexOf('.', iend);
384        if(ibegin == -1)
385          methodName = NA;
386        else
387          methodName = this.fullInfo.substring(ibegin + 1, iend);
388      }
389      return methodName;
390    }
391}