package com.yworks.yguard; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.InputSource; import org.xml.sax.Locator; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import javax.swing.Icon; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreeNode; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import java.awt.Color; import java.awt.Component; import java.awt.EventQueue; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.geom.Ellipse2D; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.net.URL; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.StringTokenizer; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.GZIPInputStream; public class YGuardLogParser { private DefaultTreeModel tree; private final MyContentHandler contentHandler = new MyContentHandler(); interface Mapped { String getName(); String getMappedName(); Icon getIcon(); } private static class AbstractMappedStruct implements Mapped { private String name; private String mappedName; private Icon icon; public AbstractMappedStruct(String namePart, String mappedName, Icon icon) { this.name = namePart; this.mappedName = mappedName; this.icon = icon; } public String getMappedName() { return mappedName; } public Icon getIcon() { return icon; } public String getName() { return name; } public void setMappedName(String n) { this.mappedName = n; } public void setName(String n) { this.name = n; } public String toString() { return getName()+" -> "+getMappedName(); } } static final class PackageStruct extends AbstractMappedStruct { PackageStruct(String name, String map) { super(name, map, Icons.PACKAGE_ICON); } public String toString() { return getName()+" -> "+getMappedName(); } } static final class ClassStruct extends AbstractMappedStruct { ClassStruct(String name, String map) { super(name, map, Icons.CLASS_ICON); } public String toString() { return getName()+" -> "+getMappedName(); } } static final class MethodStruct extends AbstractMappedStruct { MethodStruct(String name, String map) { super(name, map, Icons.METHOD_ICON); } public String toString() { return getName()+" -> "+getMappedName(); } } private static final class FieldStruct extends AbstractMappedStruct { FieldStruct(String name, String map) { super(name, map, Icons.FIELD_ICON); } public String toString() { return getName()+" -> "+getMappedName(); } } public YGuardLogParser() { DefaultMutableTreeNode root = new DefaultMutableTreeNode(null, true); this.tree = new DefaultTreeModel(root, true); } protected DefaultMutableTreeNode findChild(TreeNode node, String name, Class ofType) { return findChild(node, name, ofType, false); } protected DefaultMutableTreeNode findChild(TreeNode node, String name, Class ofType, boolean useMap) { for (Enumeration enumeration = node.children(); enumeration.hasMoreElements();) { DefaultMutableTreeNode child = (DefaultMutableTreeNode) enumeration.nextElement(); Mapped m = (Mapped) child.getUserObject(); if (ofType == null || ofType.isAssignableFrom(m.getClass())) { if (useMap) { if (m.getMappedName().equals(name)) { return child; } } else { if (m.getName().equals(name)) { return child; } } } } return null; } // protected DefaultMutableTreeNode find(String name, StringBuffer buffer, boolean useMap) { // DefaultMutableTreeNode node = (DefaultMutableTreeNode) tree.getRoot(); // for (StringTokenizer st = new StringTokenizer(name,".$",false); st.hasMoreElements();) { // String token = st.nextToken(); // DefaultMutableTreeNode child = findChild(node, token, null, useMap); // if (child == null) { // return null; // } // node = child; // } // return node; // } protected DefaultMutableTreeNode getPackageNode(String packageName) { return getPackageNode(packageName, false); } protected DefaultMutableTreeNode getPackageNode(String packageName, boolean useMap) { DefaultMutableTreeNode node = getRoot(); if (packageName != null) { StringTokenizer st = new StringTokenizer(packageName, ".", false); while (st.hasMoreTokens()) { String token = st.nextToken(); DefaultMutableTreeNode child = findChild(node, token, PackageStruct.class, useMap); if (child == null) { PackageStruct ps = new PackageStruct(token, token); child = new DefaultMutableTreeNode(ps, true); node.insert(child, calcChildIndex(node, child)); } node = child; } } return node; } protected ClassStruct getClass(String fqn) { return (ClassStruct)getClassNode(fqn).getUserObject(); } protected PackageStruct getPackage(String fqn) { return (PackageStruct)getPackageNode(fqn).getUserObject(); } protected MethodStruct getMethod(String fqn, String signature) { return (MethodStruct)getMethodNode(fqn, signature).getUserObject(); } protected FieldStruct getField(String fqn, String signature) { return (FieldStruct)getFieldNode(fqn, signature).getUserObject(); } protected DefaultMutableTreeNode getClassNode(String fqn) { return getClassNode(fqn, false); } protected DefaultMutableTreeNode getClassNode(String fqn, boolean useMap) { String packageName; String className; if (fqn.indexOf('.')<0) { packageName = null; className = fqn; } else { packageName = fqn.substring(0, fqn.lastIndexOf('.')); className = fqn.substring(fqn.lastIndexOf('.')+1); } DefaultMutableTreeNode pn = getPackageNode(packageName); if (className.indexOf('$') > 0) { for (StringTokenizer st = new StringTokenizer(className, "$", false); st.hasMoreTokens();) { String token = st.nextToken(); DefaultMutableTreeNode child = findChild(pn, token, ClassStruct.class, useMap); if (child == null) { child = new DefaultMutableTreeNode(new ClassStruct(token, token), true); pn.insert(child, calcChildIndex(pn, child)); } pn = child; } return pn; } else { DefaultMutableTreeNode child = findChild(pn, className, ClassStruct.class, useMap); if (child == null) { child = new DefaultMutableTreeNode(new ClassStruct(className, className), true); pn.insert(child, calcChildIndex(pn, child)); } return child; } } protected DefaultMutableTreeNode getMethodNode(String cname, String fqn) { return getMethodNode(cname, fqn, false); } protected DefaultMutableTreeNode getMethodNode(String cname, String fqn, boolean useMap) { DefaultMutableTreeNode cn = getClassNode(cname); DefaultMutableTreeNode child = findChild(cn, fqn, MethodStruct.class, useMap); if (child == null) { MethodStruct ms = new MethodStruct(fqn, fqn); child = new DefaultMutableTreeNode(ms, false); cn.insert(child, calcChildIndex(cn, child)); } return child; } private int calcChildIndex(DefaultMutableTreeNode cn, DefaultMutableTreeNode child) { int left = 0; int right = cn.getChildCount() - 1; Object userObject = child.getUserObject(); while (right >= left) { int test = (left + right) /2; Object testObject = ((DefaultMutableTreeNode)cn.getChildAt(test)).getUserObject(); int cmp = compare(userObject, testObject); if (cmp == 0) { return test; } else { if (cmp < 0) { right = test - 1; } else { left = test + 1; } } } return left; } private int compare(Object o1, Object o2) { Mapped m1 = (Mapped) o1; Mapped m2 = (Mapped) o2; if (m1.getClass() != m2.getClass()) { if (m1.getClass() == PackageStruct.class) { return -1; } else if (m2.getClass() == PackageStruct.class) { return 1; } if (m1.getClass() == ClassStruct.class) { return -1; } else if (m2.getClass() == ClassStruct.class) { return 1; } if (m1.getClass() == MethodStruct.class) { return -1; } else if (m2.getClass() == MethodStruct.class) { return 1; } } return m1.getName().compareTo(m2.getName()); } protected DefaultMutableTreeNode getFieldNode(String cname, String fqn) { return getFieldNode(cname, fqn, false); } protected DefaultMutableTreeNode getFieldNode(String cname, String fqn, boolean useMap) { DefaultMutableTreeNode cn = getClassNode(cname); DefaultMutableTreeNode child = findChild(cn, fqn, FieldStruct.class, useMap); if (child == null) { FieldStruct ms = new FieldStruct(fqn, fqn); child = new DefaultMutableTreeNode(ms, false); cn.insert(child, calcChildIndex(cn, child)); } return child; } void parse( final File file ) throws ParserConfigurationException, SAXException, IOException { if (file.getName().toLowerCase().endsWith(".gz")) { parse(new InputSource(new GZIPInputStream(new FileInputStream(file)))); } else { URL url = file.toURI().toURL(); if (url != null) { parse(url); } } } public void parse(URL url) throws ParserConfigurationException, SAXException, IOException { parse(new InputSource(url.openStream())); } public void parse(InputSource is) throws ParserConfigurationException, SAXException, IOException { SAXParserFactory f = SAXParserFactory.newInstance(); f.setValidating(false); SAXParser parser = f.newSAXParser(); XMLReader r = parser.getXMLReader(); r.setContentHandler(contentHandler); r.parse(is); } public String translate(String fqn) { DefaultMutableTreeNode node = getRoot(); final StringBuffer ocnSb = new StringBuffer(); final StringBuffer sb = new StringBuffer(); boolean buildPrefix = true; for (StringTokenizer st = new StringTokenizer(fqn,"$.",true); st.hasMoreTokens();) { String token = st.nextToken(); sb.append(token); if ("$".equals(token) || ".".equals(token)) { continue; } final boolean hasNext = st.hasMoreTokens(); final Class type = hasNext ? null : ClassStruct.class; DefaultMutableTreeNode child = findChild(node, sb.toString(), type, true); if (child == null) { if (buildPrefix && hasNext) { // next token is a dot ... st.nextToken(); // ... however obfuscation prefixes are prepended with a slash delimiter sb.append('/'); continue; } else { if (hasNext) { ocnSb.append(sb.toString().replace('/', '.')); append(ocnSb, st); } else if (buildPrefix) { ocnSb.append(fqn); } else if (node.getUserObject().getClass() == ClassStruct.class) { ocnSb.append(translateMethodName(node, sb.toString())); } else { ocnSb.append(sb.toString().replace('/', '.')); } node = null; break; } } buildPrefix = false; sb.setLength(0); node = child; ocnSb.append(getOriginalName(child)); if (st.hasMoreTokens()) { ocnSb.append(st.nextToken()); } } return ocnSb.toString(); } public MyStackTraceElement translate(MyStackTraceElement ste) { try { DefaultMutableTreeNode classNode = getRoot(); int dollarPos = ste.getClassName().indexOf('$'); if (dollarPos < 0) { dollarPos = ste.getClassName().length(); } int lastDot = ste.getClassName().substring(0, dollarPos).lastIndexOf('.'); String packageName = ste.getClassName().substring(0, lastDot + 1); String classAndInnerClassName = ste.getClassName().substring(lastDot + 1); final StringBuffer ocnSb = new StringBuffer(); final StringBuffer sb = new StringBuffer(); boolean buildPrefix = true; for (StringTokenizer st = new StringTokenizer(packageName,".",true); st.hasMoreTokens();) { String token = st.nextToken(); sb.append(token); DefaultMutableTreeNode child = findChild(classNode, sb.toString(), PackageStruct.class, true); if (child == null) { if (buildPrefix && st.hasMoreTokens()) { // next token is a dot ... st.nextToken(); // ... however obfuscation prefixes are prepended with a slash delimiter sb.append('/'); continue; } else { classNode = null; break; } } buildPrefix = false; sb.setLength(0); classNode = child; ocnSb.append(getOriginalName(classNode)); if (st.hasMoreTokens()) { ocnSb.append(st.nextToken()); } } if (buildPrefix) { classNode = null; } sb.setLength(0); for (StringTokenizer st = new StringTokenizer(classAndInnerClassName,"$.",true); st.hasMoreTokens();) { String token = st.nextToken(); sb.append(token); if (!"$".equals(token) && !".".equals(token)) { token = sb.toString(); sb.setLength(0); DefaultMutableTreeNode child = findChild(classNode, token, ClassStruct.class, true); if (child == null) { ocnSb.append(token); append(ocnSb, st); classNode = null; break; } classNode = child; ocnSb.append(getOriginalName(classNode)); if (st.hasMoreTokens()) { ocnSb.append(st.nextToken()); } } } final String newMethodName = translateMethodName(classNode, ste.getMethodName()); int lineNumber = 0; final String originalClassName = ocnSb.toString(); try { lineNumber = ste.getLineNumber(); if (lineNumber > 0) { Map property = (Map) this.contentHandler.ownerProperties.get(originalClassName); long salt = -1; if (property != null) { String saltString = (String) property.get("scrambling-salt"); if (saltString != null) { try { salt = Long.parseLong(saltString); final long seed = salt ^ originalClassName.replace('$','.').hashCode(); final ObfuscatorTask.LineNumberScrambler scrambler = new ObfuscatorTask.LineNumberScrambler(3584, seed); lineNumber = scrambler.unscramble(lineNumber); } catch (NumberFormatException nfe) { // too bad } } } } } catch (Exception ex) { ex.printStackTrace(); } String fileName = classNode == null ? "" : buildFilename(originalClassName); return new MyStackTraceElement(originalClassName, newMethodName, fileName, lineNumber); } catch (Exception e) { return ste; } } private static String translateMethodName(DefaultMutableTreeNode node, String mappedName) { final StringBuffer originalName = new StringBuffer(); if (node != null) { String del = ""; for (Enumeration en = node.children(); en.hasMoreElements();) { DefaultMutableTreeNode child = (DefaultMutableTreeNode) en.nextElement(); Mapped mapped = (Mapped) child.getUserObject(); if (mapped.getClass() == MethodStruct.class) { if (mapped.getMappedName().equals(mappedName)) { String name = mapped.getName(); // strip empty signature int braceIndex = name.indexOf('('); if (0 < braceIndex && braceIndex + 1 == name.indexOf(')')) { name = name.substring(0, braceIndex); } // strip return value int spaceIndex = name.lastIndexOf(' ', braceIndex < 0 ? name.length() : braceIndex); if (0 < spaceIndex) { name = name.substring(spaceIndex + 1); } originalName.append(del).append(name); del = "|"; } } } } return originalName.length() < 1 ? mappedName : originalName.toString(); } public static void main(String[] args) throws Exception { if (args.length < 1) { System.out.println("Usage java -jar yguard.jar logfile.xml[.gz] [-pipe] [name]"); System.out.println(" where 'logfile.xml' is the logfile that has been generated "); System.out.println(" during the obfuscation process"); System.out.println(" and which may be gzipped (with .gz extension)"); System.out.println(" and where 'name' is an optional string, which will be translated"); System.out.println(" according to the logfile automatically."); System.out.println(" If no 'name' is given, a tiny GUI will popup that will help in translating"); System.out.println(" stacktraces, fully qualified classnames etc."); System.out.println(" If '-pipe' is specified as the last argument after the logfile the tool"); System.out.println(" will translate the input from standard in and output the translation to"); System.out.println(" standard out until the input is closed."); System.exit(-1); } final File file = new File(args[0]); if (!file.isFile() || !file.canRead()) { System.err.println("Could not open file "+args[0]); System.exit(-1); } if (args.length < 2) { EventQueue.invokeLater(new Runnable() { @Override public void run() { (new LogParserView()).show(file); } }); } else { final YGuardLogParser parser = new YGuardLogParser(); parser.parse(file); if (args[1].equals("-pipe")) { InputStreamReader er = new InputStreamReader(System.in); BufferedReader br = new BufferedReader(er); String s; while ( (s = br.readLine()) != null) { System.out.println(parser.translate(new String[] {s})[0]); } } else { String[] strings = new String[args.length - 1]; System.arraycopy(args, 1, strings, 0, args.length - 1); String [] s = parser.translate(strings); for (int i = 0; i < s.length; i++) { System.out.println(s[i]); } } } } // at A.A.A.A.E.main([Ljava.lang.String;)V(y:1866) // at A.A.A.A.E.main([Ljava.lang.String;)V(Unknown Source) // at java.lang.Thread.run()V(Unknown Source) // at java.lang.Thread.startThreadFromVM(Ljava.lang.Thread;)V(Unknown //java.lang.Exception: Stack trace // at java.lang.Thread.dumpStack(Thread.java:1158) // at A.A.A.A.E.main(y:1866) // at A.A.A.A.E.main(Unknown Source) String[] translate(String[] args) { String[] resultArr = new String[args.length]; final Pattern jrockitPattern = Pattern.compile("(.*\\s+)?([^;()\\s]+)\\.([^;()\\s]+)\\(([^)]*)\\)(.+)\\(([^:)]+)(?::(\\d*))?\\)(.*)"); final Pattern stePattern = Pattern.compile("(.*\\s+)?([^(\\s]+)\\.([^(\\s]+)\\(([^:)]*)(?::(\\d*))?\\)(.*)"); final Pattern fqnPattern = Pattern.compile("([^:;()\\s]+\\.)+([^:;()\\s]+)"); for (int i = 0; i < args.length; i++) { args[i] = CharConverter.convert( args[i]); Matcher m2 = jrockitPattern.matcher(args[i]); if (m2.matches()) { final String[] moduleAndType = split(m2.group(2)); MyStackTraceElement ste; if (m2.group(7) == null) { ste = new MyStackTraceElement(moduleAndType[1], m2.group(3), "", 0); } else { ste = new MyStackTraceElement(moduleAndType[1], m2.group(3), m2.group(6), Integer.parseInt(m2.group(7))); } String params = m2.group(4); try { params = Conversion.toJavaArguments(params); } catch (RuntimeException rte) { // ignore } resultArr[i] = (m2.group(1) != null ? m2.group(1) : "") + moduleAndType[0] + format(ste, m2.group(7) == null ? m2.group(6) : null) + " [" + params + "]" + m2.group(8); } else { Matcher m = stePattern.matcher(args[i]); if (m.matches()) { final String[] moduleAndType = split(m.group(2)); MyStackTraceElement ste; if (m.group(5) == null) { ste = new MyStackTraceElement(moduleAndType[1], m.group(3), "", 0); } else { ste = new MyStackTraceElement(moduleAndType[1], m.group(3), m.group(4), Integer.parseInt(m.group(5))); } resultArr[i] = (m.group(1) != null ? m.group(1) : "") + moduleAndType[0] + format(translate(ste), m.group(5) == null ? m.group(4) : null) + m.group(6); } else { StringBuffer replacement = new StringBuffer(); final Matcher fqnMatcher = fqnPattern.matcher(args[i]); while (fqnMatcher.find()) { final String[] moduleAndType = split(fqnMatcher.group()); String result; try { result = translate(moduleAndType[1]); } catch (Exception ex) { result = moduleAndType[1]; } fqnMatcher.appendReplacement(replacement, moduleAndType[0] + escapeReplacement(result)); } fqnMatcher.appendTail(replacement); resultArr[i] = replacement.toString(); } } } return resultArr; } private static String[] split( final String moduleAndType ) { if (moduleAndType == null) { return new String[] {"", ""}; } else { final int idx1 = moduleAndType.indexOf('$'); final int idx2 = idx1 > -1 ? moduleAndType.lastIndexOf('/', idx1) : moduleAndType.lastIndexOf('/'); if (idx2 > -1) { return new String[] { moduleAndType.substring(0, idx2 + 1), moduleAndType.substring(idx2 + 1) }; } else { return new String[] {"", moduleAndType}; } } } private static String format( final MyStackTraceElement ste, final String s ) { final String fn = ste.getFileName(); if ((fn == null || fn.length() == 0) && s != null) { return ste.getClassName() + '.' + ste.getMethodName() + '(' + s + ')'; } else { return ste.toString(); } } DefaultTreeModel getTreeModel() { return tree; } private DefaultMutableTreeNode getRoot() { return (DefaultMutableTreeNode)tree.getRoot(); } private static String getOriginalName(DefaultMutableTreeNode node) { return ((Mapped) node.getUserObject()).getName(); } private static String escapeReplacement(String replacementString) { if ((replacementString.indexOf('\\') == -1) && (replacementString.indexOf('$') == -1)) { return replacementString; } StringBuffer result = new StringBuffer(); for (int i = 0; i < replacementString.length(); i++) { char c = replacementString.charAt(i); if (c == '\\') { result.append('\\').append('\\'); } else if (c == '$') { result.append('\\').append('$'); } else { result.append(c); } } return result.toString(); } private static StringBuffer append(StringBuffer sb, StringTokenizer st) { while (st.hasMoreTokens()) { sb.append(st.nextToken()); } return sb; } private static String buildFilename(String qualifiedName) { String fileName = ""; int idxDot = qualifiedName.lastIndexOf('.'); if (idxDot > 0) { fileName = qualifiedName.substring(idxDot + 1); } else { fileName = qualifiedName; } int idxDollar = fileName.indexOf('$'); if (idxDollar > 0) { fileName = fileName.substring(0, idxDollar); } return fileName + ".java"; } private class MyContentHandler implements ContentHandler { private boolean inMapSection; private boolean inLogSection; private boolean inExposeSection; final Map ownerProperties = new HashMap(); public void characters(char[] ch, int start, int length) throws SAXException { } public void endDocument() throws SAXException { } public void endElement(String uri, String localName, String qName) throws SAXException { if ("expose".equals(qName)) { inExposeSection = false; } if ("map".equals(qName)) { inMapSection = false; } if ("yguard".equals(qName)) { inLogSection = false; } } public void endPrefixMapping(String prefix) throws SAXException { } public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException { } public void processingInstruction(String target, String data) throws SAXException { } public void setDocumentLocator(Locator locator) { } public void skippedEntity(String name) throws SAXException { } public void startDocument() throws SAXException { ownerProperties.clear(); } public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { if ("expose".equals(qName)) { inExposeSection = true; } if ("map".equals(qName)) { inMapSection = true; } if ("yguard".equals(qName)) { inLogSection = true; String version = attributes.getValue("version"); if ("1.5".compareTo(version) < 0) { throw new IllegalStateException("Version should not be greater than 1.5 but was " + version); } } if (inLogSection && !inMapSection) { if ("property".equals(qName)) { String key = attributes.getValue("name"); String value = attributes.getValue("value"); String owner = attributes.getValue("owner"); Map map = (Map) ownerProperties.get(owner); if (map == null) { map = new HashMap(); ownerProperties.put(owner, map); } map.put(key, value); } } if (inExposeSection) { if ("method".equals(qName)) { String className = attributes.getValue("class"); String name = attributes.getValue("name"); MethodStruct fs = getMethod(className, name); } if ("field".equals(qName)) { String className = attributes.getValue("class"); String name = attributes.getValue("name"); FieldStruct fs = getField(className, name); } if ("package".equals(qName)) { String name = attributes.getValue("name"); PackageStruct ps = getPackage(name); } if ("class".equals(qName)) { String name = attributes.getValue("name"); ClassStruct cs = YGuardLogParser.this.getClass(name); } } if (inMapSection) { if ("method".equals(qName)) { String className = attributes.getValue("class"); String name = attributes.getValue("name"); String map = attributes.getValue("map"); MethodStruct fs = getMethod(className, name); fs.setMappedName(map); } if ("field".equals(qName)) { String className = attributes.getValue("class"); String name = attributes.getValue("name"); String map = attributes.getValue("map"); FieldStruct fs = getField(className, name); fs.setMappedName(map); } if ("package".equals(qName)) { String name = attributes.getValue("name"); String map = attributes.getValue("map"); PackageStruct ps = getPackage(name); ps.setMappedName(map); } if ("class".equals(qName)) { String name = attributes.getValue("name"); String map = attributes.getValue("map"); ClassStruct cs = YGuardLogParser.this.getClass(name); cs.setMappedName(map); } } } public void startPrefixMapping(String prefix, String uri) throws SAXException { } } public static final class CharConverter { private static final Pattern unicodeEscape = Pattern.compile( "&#(\\d{1,5});" ); public static String convert( String s ) { StringBuilder r = new StringBuilder( ); Matcher matcher = unicodeEscape.matcher( s ); int lastMatchEnd = 0; while( matcher.find()) { String match = matcher.group( 1 ); r.append( s.substring( lastMatchEnd, matcher.start() ) ); r.append( (char)(Integer.parseInt( match )) ); lastMatchEnd = matcher.end(); } r.append( s.substring( lastMatchEnd, s.length() ) ); return r.toString(); } } public static final class MyStackTraceElement { private String className; private String methodName; private String fileName; private int lineNumber; public MyStackTraceElement(String className, String methodName, String fileName, int lineNumber) { this.className = className; this.methodName = methodName; this.fileName = fileName; this.lineNumber = lineNumber; } public String getClassName() { return className; } public void setMethodName(String methodName) { this.methodName = methodName; } public String getFileName() { return fileName; } public void setFileName(String fileName) { this.fileName = fileName; } public int getLineNumber() { return lineNumber; } public void setLineNumber(int lineNumber) { this.lineNumber = lineNumber; } public String getMethodName() { return methodName; } public String toString() { return getClassName() + "." + getMethodName() + "(" + (fileName != null && lineNumber >= 0 ? fileName + ":" + lineNumber : "unknown source") + ")"; } } public static final class Icons implements Icon { public static final Icon CLASS_ICON = new Icons(Color.blue, "C"); public static final Icon METHOD_ICON = new Icons(Color.red, "M"); public static final Icon PACKAGE_ICON = new Icons(Color.yellow, "P"); public static final Icon FIELD_ICON = new Icons(Color.green, "F"); private static final Ellipse2D circle = new Ellipse2D.Double(1,1, 14, 14); protected Color color; protected String label; public Icons(Color color, String label) { this.color = color; this.label = label; } public void paintIcon(Component c, Graphics g, int x, int y) { g.translate(x, y); g.setColor(color); Graphics2D g2d = (Graphics2D) g; Object a = g2d.getRenderingHint(RenderingHints.KEY_ANTIALIASING); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.fill(circle); g2d.setColor(color.darker()); g2d.draw(circle); float width = (float) g2d.getFontMetrics().getStringBounds(label, g2d).getWidth(); g2d.setColor(Color.black); g2d.drawString(label, 9 - width * 0.5f, 14); g2d.setColor(Color.white); g2d.drawString(label, 8 - width * 0.5f, 13); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, a); g.translate(-x, -y); } public int getIconWidth() { return 16; } public int getIconHeight() { return 16; } } }