Add server side deobfuscation of stack traces to RF Remote log handler

Review at http://gwt-code-reviews.appspot.com/867802

Review by: jat@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@8795 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/DynaTableRf.gwt.xml b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/DynaTableRf.gwt.xml
index d7f348c..932bfe6 100644
--- a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/DynaTableRf.gwt.xml
+++ b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/DynaTableRf.gwt.xml
@@ -32,6 +32,12 @@
   <set-property name="gwt.logging.systemHandler" value="ENABLED" />
   <set-property name="gwt.logging.simpleRemoteHandler" value="DISABLED" />
   
+  <!-- Uncomment if you are enabling server side deobfuscation of StackTraces
+  <set-property name="compiler.emulatedStack" value="true" />
+  <set-configuration-property name="compiler.emulatedStack.recordLineNumbers" value="true" />
+  <set-configuration-property name="compiler.emulatedStack.recordFileNames" value="true" />
+  -->
+  
   <entry-point class='com.google.gwt.sample.dynatablerf.client.DynaTableRf' />
   
   <set-configuration-property name="CssResource.obfuscationPrefix" value="empty" />
diff --git a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/DynaTableRf.java b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/DynaTableRf.java
index 9d3fc32..81f4c07 100644
--- a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/DynaTableRf.java
+++ b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/DynaTableRf.java
@@ -44,7 +44,7 @@
   }
 
   private static final Logger log = Logger.getLogger(DynaTableRf.class.getName());
-
+  
   @UiField(provided = true)
   SummaryWidget calendar;
 
@@ -73,11 +73,10 @@
         public LoggingRequest getLoggingRequest() {
           return requests.loggingRequest();
         }
-      };
+    };
     Logger.getLogger("").addHandler(
         new RequestFactoryLogHandler(provider, Level.WARNING,
-            "WireActivityLogger"));
-
+            "WireActivityLogger", GWT.getPermutationStrongName()));
     FavoritesManager manager = new FavoritesManager(requests);
     PersonEditorWorkflow.register(eventBus, requests, manager);
 
diff --git a/samples/dynatablerf/war/WEB-INF/web.xml b/samples/dynatablerf/war/WEB-INF/web.xml
index be13961..b3c0fdd 100644
--- a/samples/dynatablerf/war/WEB-INF/web.xml
+++ b/samples/dynatablerf/war/WEB-INF/web.xml
@@ -9,6 +9,12 @@
   <servlet>
     <servlet-name>requestFactoryServlet</servlet-name>
     <servlet-class>com.google.gwt.requestfactory.server.RequestFactoryServlet</servlet-class>
+    <init-param>
+      <param-name>symbolMapsDirectory</param-name>
+      <!-- You'll need to compile with -extras and move the symbolMaps directory
+           to this location if you want stack trace deobfuscation to work -->
+      <param-value>WEB-INF/classes/symbolMaps/</param-value>
+    </init-param>
   </servlet>
 
   <servlet-mapping>
diff --git a/user/src/com/google/gwt/logging/client/JsonLogRecordClientUtil.java b/user/src/com/google/gwt/logging/client/JsonLogRecordClientUtil.java
index b210533..fdaa43f 100644
--- a/user/src/com/google/gwt/logging/client/JsonLogRecordClientUtil.java
+++ b/user/src/com/google/gwt/logging/client/JsonLogRecordClientUtil.java
@@ -49,6 +49,7 @@
     obj.put("level", getJsonString(slr.getLevel()));
     obj.put("loggerName", getJsonString(slr.getLoggerName()));
     obj.put("msg", getJsonString(slr.getMsg()));
+    obj.put("strongName", getJsonString(slr.getStrongName()));
     if (slr.getTimestamp() != null) {
       obj.put("timestamp", new JSONString(slr.getTimestamp().toString()));
     }
diff --git a/user/src/com/google/gwt/logging/client/SimpleRemoteLogHandler.java b/user/src/com/google/gwt/logging/client/SimpleRemoteLogHandler.java
index 8053c2d..356a34f 100644
--- a/user/src/com/google/gwt/logging/client/SimpleRemoteLogHandler.java
+++ b/user/src/com/google/gwt/logging/client/SimpleRemoteLogHandler.java
@@ -74,6 +74,7 @@
       // would lead to an infinite loop.
       return;
     }
-    service.logOnServer(new SerializableLogRecord(record), callback);
+    service.logOnServer(new SerializableLogRecord(
+        record, GWT.getPermutationStrongName()), callback);
   }
 }
diff --git a/user/src/com/google/gwt/logging/server/JsonLogRecordServerUtil.java b/user/src/com/google/gwt/logging/server/JsonLogRecordServerUtil.java
index 2173ddf..8f1d1c9 100644
--- a/user/src/com/google/gwt/logging/server/JsonLogRecordServerUtil.java
+++ b/user/src/com/google/gwt/logging/server/JsonLogRecordServerUtil.java
@@ -40,10 +40,12 @@
       String level = slr.getString("level");
       String loggerName = slr.getString("loggerName");
       String msg = slr.getString("msg");
+      String strongName = slr.getString("strongName");
       long timestamp = Long.parseLong(slr.getString("timestamp"));
       SerializableThrowable thrown =
         serializableThrowableFromJson(slr.getString("thrown"));
-      return new SerializableLogRecord(level, loggerName, msg, thrown, timestamp);
+      return new SerializableLogRecord(level, loggerName, msg, thrown,
+          timestamp, strongName);
     } catch (JSONException e) {
     }
     return null;
diff --git a/user/src/com/google/gwt/logging/server/StackTraceDeobfuscator.java b/user/src/com/google/gwt/logging/server/StackTraceDeobfuscator.java
new file mode 100644
index 0000000..753525c
--- /dev/null
+++ b/user/src/com/google/gwt/logging/server/StackTraceDeobfuscator.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2010 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.gwt.logging.server;
+
+import com.google.gwt.logging.shared.SerializableLogRecord;
+import com.google.gwt.logging.shared.SerializableThrowable;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Deobfuscates stack traces on the server side. This class requires that you
+ * have turned on emulated stack traces and moved your symbolMap files to a
+ * place accessible by your server. More concretely, you must compile with the
+ * -extra command line option, copy the symbolMaps directory to somewhere your
+ * server side code has access to it, and then set the symbolMapsDirectory in
+ * this class through the constructor, or the setter method.
+ * For example, this variable could be set to "WEB-INF/classes/symbolMaps/"
+ * if you copied the symbolMaps directory to there.
+ *
+ * TODO(unnurg): Combine this code with similar code in JUnitHostImpl
+ */
+public class StackTraceDeobfuscator {
+  
+  private static class SymbolMap extends HashMap<String, String> { }
+  
+  // From JsniRef class, which is in gwt-dev and so can't be accessed here
+  // TODO(unnurg) once there is a place for shared code, move this to there.
+  private static Pattern JsniRefPattern =
+    Pattern.compile("@?([^:]+)::([^(]+)(\\((.*)\\))?");
+  
+  private String symbolMapsDirectory = "";
+  
+  private Map<String, SymbolMap> symbolMaps =
+    new HashMap<String, SymbolMap>();
+  
+  public StackTraceDeobfuscator(String symbolMapsDirectory) {
+    this.symbolMapsDirectory = symbolMapsDirectory;
+  }
+  
+  public SerializableLogRecord deobfuscateLogRecord(
+      SerializableLogRecord slr) {
+    if (slr.getThrown() != null) {
+      slr.setThrown(deobfuscateThrowable(slr.getThrown(), slr.getStrongName()));
+    }
+    return slr;
+  }
+  
+  public void setSymbolMapsDirectory(String dir) {
+    symbolMapsDirectory = dir;
+  }
+  
+  private StackTraceElement[] deobfuscateStackTrace(
+      StackTraceElement[] st, String strongName) {
+    StackTraceElement[] newSt = new StackTraceElement[st.length];
+    for (int i = 0; i < st.length; i++) {
+      newSt[i] = resymbolize(st[i], strongName);
+    }
+    return newSt;
+  }
+  
+  private SerializableThrowable deobfuscateThrowable(
+      SerializableThrowable t, String strongName) {
+    if (t.getStackTrace() != null) {
+      t.setStackTrace(deobfuscateStackTrace(t.getStackTrace(), strongName));
+    }
+    if (t.getCause() != null) {
+      t.setCause(deobfuscateThrowable(t.getCause(), strongName));
+    }
+    return t;
+  }
+  
+  private SymbolMap loadSymbolMap(
+      String strongName) {
+    SymbolMap toReturn = symbolMaps.get(strongName);
+    if (toReturn != null) {
+      return toReturn;
+    }
+    toReturn = new SymbolMap();
+    String line;
+    String filename = symbolMapsDirectory + strongName + ".symbolMap";
+    try {
+      BufferedReader bin = new BufferedReader(new FileReader(filename));
+      while ((line = bin.readLine()) != null) {
+        if (line.charAt(0) == '#') {
+          continue;
+        }
+        int idx = line.indexOf(',');
+        toReturn.put(new String(line.substring(0, idx)),
+            line.substring(idx + 1));
+      }
+    } catch (IOException e) {
+      toReturn = null;
+    }
+
+    symbolMaps.put(strongName, toReturn);
+    return toReturn;
+  }
+  
+  private String[] parse(String refString) {
+    Matcher matcher = JsniRefPattern.matcher(refString);
+    if (!matcher.matches()) {
+      return null;
+    }
+    String className = matcher.group(1);
+    String memberName = matcher.group(2);
+    String[] toReturn = new String[] {className, memberName};
+    return toReturn;
+  }
+  
+  private StackTraceElement resymbolize(StackTraceElement ste,
+      String strongName) {
+    SymbolMap map = loadSymbolMap(strongName);
+    String symbolData = map == null ? null : map.get(ste.getMethodName());
+
+    if (symbolData != null) {
+      // jsniIdent, className, memberName, sourceUri, sourceLine
+      String[] parts = symbolData.split(",");
+      if (parts.length == 5) {
+        String[] ref = parse(
+            parts[0].substring(0, parts[0].lastIndexOf(')') + 1));
+        return new StackTraceElement(
+            ref[0], ref[1], ste.getFileName(), ste.getLineNumber());
+      }
+    }
+    // If anything goes wrong, just return the unobfuscated element
+    return ste;
+  }
+}
diff --git a/user/src/com/google/gwt/logging/shared/SerializableLogRecord.java b/user/src/com/google/gwt/logging/shared/SerializableLogRecord.java
index 92816f0..529af2d 100644
--- a/user/src/com/google/gwt/logging/shared/SerializableLogRecord.java
+++ b/user/src/com/google/gwt/logging/shared/SerializableLogRecord.java
@@ -28,16 +28,23 @@
  * the wire directly.
  */
 public class SerializableLogRecord implements IsSerializable {
+  // If you add/remove a field here, be sure to update JsonLogRecordServerUtil
+  // and JsonLogRecordClientUtil as well.
   private String level;
   private String loggerName = "";
   private String msg;
+  // TODO(unnurg): if/when we ever start passing the strong name in all headers
+  // and we're able to access request headers in the Vega logging handler
+  // remove this strongName variable from here.
+  private String strongName = "";
   private SerializableThrowable thrown = null;
   private long timestamp;
 
   /**
    * Create a new SerializableLogRecord from a LogRecord.
    */
-  public SerializableLogRecord(LogRecord lr) {
+  public SerializableLogRecord(LogRecord lr, String strongName) {
+    this.strongName = strongName;
     level = lr.getLevel().toString();
     loggerName = lr.getLoggerName();
     msg = lr.getMessage();
@@ -48,12 +55,13 @@
   }
   
   public SerializableLogRecord(String level, String loggerName, String msg,
-      SerializableThrowable thrown, long timestamp) {
+      SerializableThrowable thrown, long timestamp, String strongName) {
     this.level = level;
     this.loggerName = loggerName;
     this.msg = msg;
     this.timestamp = timestamp;
     this.thrown = thrown;
+    this.strongName = strongName;
   }
   
   protected SerializableLogRecord() {
@@ -85,6 +93,10 @@
     return msg;
   }
   
+  public String getStrongName() {
+    return strongName;
+  }
+  
   public SerializableThrowable getThrown() {
     return thrown;
   }
@@ -92,4 +104,8 @@
   public Long getTimestamp() {
     return timestamp;
   }
+  
+  public void setThrown(SerializableThrowable t) {
+    thrown = t;
+  }
 }
diff --git a/user/src/com/google/gwt/logging/shared/SerializableThrowable.java b/user/src/com/google/gwt/logging/shared/SerializableThrowable.java
index ebbe381..7c06259 100644
--- a/user/src/com/google/gwt/logging/shared/SerializableThrowable.java
+++ b/user/src/com/google/gwt/logging/shared/SerializableThrowable.java
@@ -29,17 +29,6 @@
   private String message;
   private StackTraceElement[] stackTrace;
   
-  /**
-   * Create a new SerializableThrowable from a Throwable.
-   */
-  public SerializableThrowable(Throwable t) {
-    message = t.getMessage();
-    if (t.getCause() != null) {
-      cause = new SerializableThrowable(t.getCause());
-    }
-    stackTrace = t.getStackTrace();
-  }
-  
   public SerializableThrowable(String message, SerializableThrowable cause,
       StackTraceElement[] stackTrace) {
     this.message = message;
@@ -47,6 +36,17 @@
     this.stackTrace = stackTrace;
   }
   
+  /**
+   * Create a new SerializableThrowable from a Throwable.
+   */
+  public SerializableThrowable(Throwable t) {
+    message = t.getMessage();
+    if (t.getCause() != null && t.getCause() != t) {
+      cause = new SerializableThrowable(t.getCause());
+    }
+    stackTrace = t.getStackTrace();
+  }
+  
   protected SerializableThrowable() {
     // for serialization
   }
@@ -54,7 +54,7 @@
   public SerializableThrowable getCause() {
     return cause;
   }
-
+  
   public String getMessage() {
     return message;
   }
@@ -62,7 +62,7 @@
   public StackTraceElement[] getStackTrace() {
     return stackTrace;
   }
-  
+
   /**
    * Create a new Throwable from this SerializableThrowable.
    */
@@ -76,4 +76,12 @@
     t.setStackTrace(stackTrace);
     return t;
   }
+  
+  public void setCause(SerializableThrowable c) {
+    cause = c;
+  }
+  
+  public void setStackTrace(StackTraceElement[] st) {
+    stackTrace = st;
+  }
 }
diff --git a/user/src/com/google/gwt/requestfactory/client/RequestFactoryLogHandler.java b/user/src/com/google/gwt/requestfactory/client/RequestFactoryLogHandler.java
index 7ae3819..2f964e5 100644
--- a/user/src/com/google/gwt/requestfactory/client/RequestFactoryLogHandler.java
+++ b/user/src/com/google/gwt/requestfactory/client/RequestFactoryLogHandler.java
@@ -53,9 +53,14 @@
   private static Logger logger = 
     Logger.getLogger(RequestFactoryLogHandler.class.getName());
   
+  // A separate logger for wire activity, which does not get logged
+  // by the remote log handler, so we avoid infinite loops.
+  private static Logger wireLogger = Logger.getLogger("WireActivityLogger");
+  
   private boolean closed;
   private LoggingRequestProvider requestProvider;
   private String ignoredLoggerSubstring;
+  private String strongName;
   
   /**
    * Since records from this handler go accross the wire, it should only be
@@ -67,9 +72,10 @@
    * infinite loop would occur.
    */
   public RequestFactoryLogHandler(LoggingRequestProvider requestProvider,
-      Level level, String ignoredLoggerSubstring) {
+      Level level, String ignoredLoggerSubstring, String strongName) {
     this.requestProvider = requestProvider;
     this.ignoredLoggerSubstring = ignoredLoggerSubstring;
+    this.strongName = strongName;
     closed = false;
     setLevel(level);
   }
@@ -92,16 +98,14 @@
     if (record.getLoggerName().contains(ignoredLoggerSubstring)) {
       return;
     }
-    SerializableLogRecord slr = new SerializableLogRecord(record);
+    SerializableLogRecord slr =
+      new SerializableLogRecord(record, strongName);
     String json = JsonLogRecordClientUtil.serializableLogRecordAsJson(slr);
     requestProvider.getLoggingRequest().logMessage(json).fire(
         new Receiver<Boolean>() {
           @Override
           public void onSuccess(Boolean response, Set<SyncResult> syncResults) {
             if (!response) {
-              // A separate logger for wire activity, which does not get logged
-              // by the remote log handler, so we avoid infinite loops.
-              Logger wireLogger = Logger.getLogger("WireActivityLogger");
               wireLogger.severe("Remote Logging failed to parse JSON");
             }
           }
diff --git a/user/src/com/google/gwt/requestfactory/server/Logging.java b/user/src/com/google/gwt/requestfactory/server/Logging.java
index fff5ebd..59d012d 100644
--- a/user/src/com/google/gwt/requestfactory/server/Logging.java
+++ b/user/src/com/google/gwt/requestfactory/server/Logging.java
@@ -17,6 +17,7 @@
 package com.google.gwt.requestfactory.server;
 
 import com.google.gwt.logging.server.JsonLogRecordServerUtil;
+import com.google.gwt.logging.server.StackTraceDeobfuscator;
 import com.google.gwt.logging.shared.SerializableLogRecord;
 
 import java.util.logging.LogRecord;
@@ -25,24 +26,40 @@
 /**
  * Server side object that handles log messages sent by
  * {@link RequestFactoryLogHandler}.
+ * 
+ * TODO(unnurg): Before the end of Sept 2010, combine this class intelligently
+ * with SimpleRemoteLogHandler so they share functionality and patterns.
  */
 public class Logging {
-  private static Logger logger = Logger.getLogger(Logging.class.getName());
 
+  private static StackTraceDeobfuscator deobfuscator =
+    new StackTraceDeobfuscator("");
+  
   public static Boolean logMessage(String serializedLogRecordString) {
     SerializableLogRecord slr =
       JsonLogRecordServerUtil.serializableLogRecordFromJson(
           serializedLogRecordString);
+    slr = deobfuscator.deobfuscateLogRecord(slr);
     LogRecord lr = slr.getLogRecord();
     if (lr == null) {
       return false;
     }
+    Logger logger = Logger.getLogger(lr.getLoggerName());
     logger.log(lr);
     return true;
   }
   
+  /**
+   * This function is only for server side use which is why it's not in the
+   * LoggingRequest interface.
+   */
+  public static void setSymbolMapsDirectory(String dir) {
+    deobfuscator.setSymbolMapsDirectory(dir);
+  }
+  
   private Long id = 0L;
-  private Integer version = 0;
+  
+  private Integer version = 0;  
   
   public Long getId() {
     return this.id;
@@ -51,11 +68,11 @@
   public Integer getVersion() {
     return this.version;
   }
-    
+
   public void setId(Long id) {
     this.id = id;
   }
-  
+    
   public void setVersion(Integer version) {
     this.version = version;
   }
diff --git a/user/src/com/google/gwt/requestfactory/server/RequestFactoryServlet.java b/user/src/com/google/gwt/requestfactory/server/RequestFactoryServlet.java
index 185c36e..f3592f6 100644
--- a/user/src/com/google/gwt/requestfactory/server/RequestFactoryServlet.java
+++ b/user/src/com/google/gwt/requestfactory/server/RequestFactoryServlet.java
@@ -127,5 +127,11 @@
     if (userInfoClass != null) {
       UserInformation.setUserInformationImplClass(userInfoClass);
     }
+    
+    String symbolMapsDirectory =
+      getServletConfig().getInitParameter("symbolMapsDirectory");
+    if (symbolMapsDirectory != null) {
+      Logging.setSymbolMapsDirectory(symbolMapsDirectory);
+    }
   }
 }
diff --git a/user/src/com/google/gwt/requestfactory/shared/LoggingRequest.java b/user/src/com/google/gwt/requestfactory/shared/LoggingRequest.java
index cf07757..6ddd7c3 100644
--- a/user/src/com/google/gwt/requestfactory/shared/LoggingRequest.java
+++ b/user/src/com/google/gwt/requestfactory/shared/LoggingRequest.java
@@ -28,5 +28,5 @@
   // TODO(unnurg): Pass a SerializableLogRecord here rather than it's
   // serialized string.
   RequestObject<Boolean> logMessage(String serializedLogRecordString);
- 
+
 }
diff --git a/user/super/com/google/gwt/emul/java/util/logging/Level.java b/user/super/com/google/gwt/emul/java/util/logging/Level.java
index 5129d33..305d8e6 100644
--- a/user/super/com/google/gwt/emul/java/util/logging/Level.java
+++ b/user/super/com/google/gwt/emul/java/util/logging/Level.java
@@ -50,7 +50,7 @@
     impl.setName(name);
     impl.setValue(value);
   }
-  
+
   public String getName() {
     return impl.getName();
   }