| /* | |
| * 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 java.io.BufferedReader; | |
| import java.io.File; | |
| import java.io.FileInputStream; | |
| import java.io.IOException; | |
| import java.io.InputStream; | |
| import java.io.InputStreamReader; | |
| import java.util.HashMap; | |
| import java.util.Map; | |
| import java.util.logging.LogRecord; | |
| 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, via | |
| * <code><set-property name="compiler.stackMode" value="emulated" /></code> | |
| * in your <code>.gwt.xml</code> module file, and moved your symbol map files to | |
| * a location accessible by your server sever side code. You can use the GWT | |
| * compiler <code>-deploy</code> command line argument to specify the location | |
| * of the folder into which the generated <code>symbolMaps</code> directory is | |
| * written. By default, the final <code>symbolMaps</code> directory is | |
| * <code>war/WEB-INF/deploy/<i>yourmodulename</i>/symbolMaps/</code>. Pass the | |
| * resulting directory location into this class' | |
| * {@link StackTraceDeobfuscator#symbolMapsDirectory} constructor or | |
| * {@link #setSymbolMapsDirectory(String)} setter method. | |
| * | |
| * 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("@?([^:]+)::([^(]+)(\\((.*)\\))?"); | |
| // The javadoc for StackTraceElement.getLineNumber() says it returns -1 when | |
| // the line number is unavailable | |
| private static final int LINE_NUMBER_UNKNOWN = -1; | |
| private File symbolMapsDirectory; | |
| private Map<String, SymbolMap> symbolMaps = | |
| new HashMap<String, SymbolMap>(); | |
| /** | |
| * Constructor, which takes a <code>symbolMaps</code> directory as its | |
| * argument. Symbol maps are generated into the location specified by the | |
| * GWT compiler <code>-deploy</code> command line argument. | |
| * | |
| * @param symbolMapsDirectory the <code>symbolMaps</code> directory with, or | |
| * without trailing directory separator character | |
| */ | |
| public StackTraceDeobfuscator(String symbolMapsDirectory) { | |
| setSymbolMapsDirectory(symbolMapsDirectory); | |
| } | |
| /** | |
| * Best effort resymbolization of a log record's stack trace. | |
| * | |
| * @param lr the log record to resymbolize | |
| * @param strongName the GWT permutation strong name | |
| * @return the best effort resymbolized log record | |
| */ | |
| public LogRecord deobfuscateLogRecord(LogRecord lr, String strongName) { | |
| if (lr.getThrown() != null && strongName != null) { | |
| lr.setThrown(deobfuscateThrowable(lr.getThrown(), strongName)); | |
| } | |
| return lr; | |
| } | |
| /** | |
| * Convenience method which resymbolizes an entire stack trace to extent | |
| * possible. | |
| * | |
| * @param st the stack trace to resymbolize | |
| * @param strongName the GWT permutation strong name | |
| * @return a best effort resymbolized stack trace | |
| */ | |
| public 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; | |
| } | |
| /** | |
| * Best effort resymbolization of a a single stack trace element. | |
| * | |
| * @param ste the stack trace element to resymbolize | |
| * @param strongName the GWT permutation strong name | |
| * @return the best effort resymbolized stack trace element | |
| */ | |
| public 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)); | |
| String declaringClass; | |
| String methodName; | |
| if (ref != null) { | |
| declaringClass = ref[0]; | |
| methodName = ref[1]; | |
| } else { | |
| declaringClass = ste.getClassName(); | |
| methodName = ste.getMethodName(); | |
| } | |
| // parts[3] contains the source file URI or "Unknown" | |
| String filename = "Unknown".equals(parts[3]) ? null | |
| : parts[3].substring(parts[3].lastIndexOf('/') + 1); | |
| int lineNumber = ste.getLineNumber(); | |
| /* | |
| * When lineNumber is LINE_NUMBER_UNKNOWN, either because | |
| * compiler.stackMode is not emulated or | |
| * compiler.emulatedStack.recordLineNumbers is false, use the method | |
| * declaration line number from the symbol map. | |
| */ | |
| if (lineNumber == LINE_NUMBER_UNKNOWN) { | |
| lineNumber = Integer.parseInt(parts[4]); | |
| } | |
| return new StackTraceElement( | |
| declaringClass, methodName, filename, lineNumber); | |
| } | |
| } | |
| // If anything goes wrong, just return the unobfuscated element | |
| return ste; | |
| } | |
| public void setSymbolMapsDirectory(String symbolMapsDirectory) { | |
| // permutations are unique, no need to clear the symbolMaps hash map | |
| this.symbolMapsDirectory = new File(symbolMapsDirectory); | |
| } | |
| /** | |
| * Retrieves a new {@link InputStream} for the given permutation strong name. | |
| * This implementation, which subclasses may override, returns a | |
| * {@link InputStream} for the <code> | |
| * <i>permutation-strong-name</i>.symbolMap</code> file in the | |
| * <code>symbolMaps</code> directory. | |
| * | |
| * @param permutationStrongName the GWT permutation strong name | |
| * @return a new {@link InputStream} | |
| * @throws IOException | |
| */ | |
| protected InputStream getSymbolMapInputStream(String permutationStrongName) | |
| throws IOException { | |
| String filename = symbolMapsDirectory.getCanonicalPath() | |
| + File.separatorChar + permutationStrongName + ".symbolMap"; | |
| return new FileInputStream(filename); | |
| } | |
| private Throwable deobfuscateThrowable(Throwable old, String strongName) { | |
| Throwable t = new Throwable(old.getMessage()); | |
| if (old.getStackTrace() != null) { | |
| t.setStackTrace(deobfuscateStackTrace(old.getStackTrace(), strongName)); | |
| } else { | |
| t.setStackTrace(new StackTraceElement[0]); | |
| } | |
| if (old.getCause() != null) { | |
| t.initCause(deobfuscateThrowable(old.getCause(), strongName)); | |
| } | |
| return t; | |
| } | |
| private SymbolMap loadSymbolMap( | |
| String strongName) { | |
| SymbolMap toReturn = symbolMaps.get(strongName); | |
| if (toReturn != null) { | |
| return toReturn; | |
| } | |
| toReturn = new SymbolMap(); | |
| String line; | |
| try { | |
| BufferedReader bin = new BufferedReader( | |
| new InputStreamReader(getSymbolMapInputStream(strongName))); | |
| try { | |
| 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)); | |
| } | |
| } finally { | |
| bin.close(); | |
| } | |
| } catch (IOException e) { | |
| // use empty symbol map to avoid repeated lookups | |
| toReturn = new SymbolMap(); | |
| } | |
| symbolMaps.put(strongName, toReturn); | |
| return toReturn; | |
| } | |
| /** | |
| * Extracts the declaring class and method name from a JSNI ref, or null if | |
| * the information cannot be extracted. | |
| * | |
| * @see com.google.gwt.dev.util.JsniRef | |
| * @param refString symbol map reference string | |
| * @return a string array contains the declaring class and method name, or | |
| * null when the regex match fails | |
| */ | |
| 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; | |
| } | |
| } |