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;
    }
  }
}