/*
 * FindBugs - Find bugs in Java programs
 * Copyright (C) 2004-2005 University of Maryland
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package edu.umd.cs.findbugs;

import static java.util.Arrays.asList;
import static java.util.Collections.unmodifiableSet;

import java.io.File;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.logging.Logger;
import java.util.regex.Pattern;

import javax.annotation.CheckForNull;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import edu.umd.cs.findbugs.ba.ClassHash;
import edu.umd.cs.findbugs.filter.AndMatcher;
import edu.umd.cs.findbugs.filter.BugMatcher;
import edu.umd.cs.findbugs.filter.ClassMatcher;
import edu.umd.cs.findbugs.filter.CompoundMatcher;
import edu.umd.cs.findbugs.filter.DesignationMatcher;
import edu.umd.cs.findbugs.filter.FieldMatcher;
import edu.umd.cs.findbugs.filter.Filter;
import edu.umd.cs.findbugs.filter.FirstVersionMatcher;
import edu.umd.cs.findbugs.filter.LastVersionMatcher;
import edu.umd.cs.findbugs.filter.LocalMatcher;
import edu.umd.cs.findbugs.filter.Matcher;
import edu.umd.cs.findbugs.filter.MethodMatcher;
import edu.umd.cs.findbugs.filter.NotMatcher;
import edu.umd.cs.findbugs.filter.OrMatcher;
import edu.umd.cs.findbugs.filter.PriorityMatcher;
import edu.umd.cs.findbugs.filter.RankMatcher;
import edu.umd.cs.findbugs.model.ClassFeatureSet;
import edu.umd.cs.findbugs.util.MapCache;
import edu.umd.cs.findbugs.util.Strings;

/**
 * Build a BugCollection based on SAX events. This is intended to replace the
 * old DOM-based parsing of XML bug result files, which was very slow.
 *
 * @author David Hovemeyer
 */
public class SAXBugCollectionHandler extends DefaultHandler {
    private static final String FIND_BUGS_FILTER = "FindBugsFilter";

    private static final String PROJECT = "Project";

    private static final String BUG_COLLECTION = "BugCollection";

    private static final Logger LOGGER = Logger.getLogger(SAXBugCollectionHandler.class.getName());

    /**
     * @param attributes
     * @param qName
     * @return
     */
    public String getOptionalAttribute(Attributes attributes, String qName) {
        return memoized(attributes.getValue(qName));
    }

    @CheckForNull
    private final BugCollection bugCollection;

    @CheckForNull
    private final Project project;

    private final Stack<CompoundMatcher> matcherStack = new Stack<CompoundMatcher>();

    private Filter filter;

    private final MapCache<String, String> cache = new MapCache<String, String>(2000);

    private final ArrayList<String> elementStack;

    private final StringBuilder textBuffer;

    private BugInstance bugInstance;

    private BugAnnotationWithSourceLines bugAnnotationWithSourceLines;

    private AnalysisError analysisError;

    // private ClassHash classHash;
    private ClassFeatureSet classFeatureSet;

    private final ArrayList<String> stackTrace;

    private int nestingOfIgnoredElements = 0;

    private final @CheckForNull File base;

    private final String topLevelName;

    private String cloudPropertyKey;

    private SAXBugCollectionHandler(String topLevelName, @CheckForNull BugCollection bugCollection,
            @CheckForNull Project project, @CheckForNull File base) {
        this.topLevelName = topLevelName;
        this.bugCollection = bugCollection;
        this.project = project;

        this.elementStack = new ArrayList<String>();
        this.textBuffer = new StringBuilder();
        this.stackTrace = new ArrayList<String>();
        this.base = base;

    }

    public SAXBugCollectionHandler(BugCollection bugCollection, @CheckForNull File base) {
        this(BUG_COLLECTION, bugCollection, bugCollection.getProject(), base);
    }

    public SAXBugCollectionHandler(BugCollection bugCollection) {
        this(BUG_COLLECTION, bugCollection, bugCollection.getProject(), null);
    }

    public SAXBugCollectionHandler(Project project, File base) {
        this(PROJECT, null, project, base);
    }

    public SAXBugCollectionHandler(Filter filter, File base) {
        this(FIND_BUGS_FILTER, null, null, base);
        this.filter = filter;
        pushCompoundMatcher(filter);
    }

    Pattern ignoredElement = Pattern.compile("Message|ShortMessage|LongMessage");

    public boolean discardedElement(String qName) {
        return ignoredElement.matcher(qName).matches();

    }

    public String getTextContents() {
        return memoized(Strings.unescapeXml(textBuffer.toString()));
    }

    private String memoized(String s) {
        if (s == null)
            return s;
        String result = cache.get(s);
        if (result != null)
            return result;
        cache.put(s, s);
        return s;
    }

    private static boolean DEBUG = false;

    @SuppressWarnings("hiding")
    @Override
    public void startElement(String uri, String name, String qName, Attributes attributes) throws SAXException {
        // URI should always be empty.
        // So, qName is the name of the element.

        if (discardedElement(qName)) {
            nestingOfIgnoredElements++;
        } else if (nestingOfIgnoredElements > 0) {
            // ignore it
        } else {
            // We should be parsing the outer BugCollection element.
            if (elementStack.isEmpty() && !qName.equals(topLevelName))
                throw new SAXException("Invalid top-level element (expected " + topLevelName + ", saw " + qName + ")");

            if (qName.equals(BUG_COLLECTION)) {
                BugCollection bugCollection = this.bugCollection;
                assert bugCollection != null;
                // Read and set the sequence number.
                String version = getOptionalAttribute(attributes, "version");
                if (bugCollection instanceof SortedBugCollection)
                    bugCollection.setAnalysisVersion(version);

                // Read and set the sequence number.
                String sequence = getOptionalAttribute(attributes, "sequence");
                long seqval = parseLong(sequence, 0L);
                bugCollection.setSequenceNumber(seqval);

                // Read and set timestamp.
                String timestamp = getOptionalAttribute(attributes, "timestamp");
                long tsval = parseLong(timestamp, -1L);
                bugCollection.setTimestamp(tsval);
                // Read and set timestamp.
                String analysisTimestamp = getOptionalAttribute(attributes, "analysisTimestamp");
                if (analysisTimestamp != null) {
                    bugCollection.setAnalysisTimestamp(parseLong(analysisTimestamp, -1L));
                }
                String analysisVersion = getOptionalAttribute(attributes, "version");
                if (analysisVersion != null) {
                    bugCollection.setAnalysisVersion(analysisVersion);
                }

                // Set release name, if present.
                String releaseName = getOptionalAttribute(attributes, "release");
                bugCollection.setReleaseName((releaseName != null) ? releaseName : "");
            } else if (isTopLevelFilter(qName)) {
                if (project != null) {
                    filter = new Filter();
                    project.setSuppressionFilter(filter);
                }
                matcherStack.clear();
                pushCompoundMatcher(filter);
            } else if (qName.equals(PROJECT)) {
                Project project = this.project;
                assert project != null;
                // Project element
                String projectName = getOptionalAttribute(attributes, Project.PROJECTNAME_ATTRIBUTE_NAME);
                if (projectName != null)
                    project.setProjectName(projectName);
            } else {
                String outerElement = elementStack.get(elementStack.size() - 1);
                if (outerElement.equals(BUG_COLLECTION)) {

                    // Parsing a top-level element of the BugCollection
                    if (qName.equals("BugInstance")) {
                        // BugInstance element - get required type and priority
                        // attributes
                        String type = getRequiredAttribute(attributes, "type", qName);
                        String priority = getRequiredAttribute(attributes, "priority", qName);

                        try {
                            int prio = Integer.parseInt(priority);
                            bugInstance = new BugInstance(type, prio);
                        } catch (NumberFormatException e) {
                            throw new SAXException("BugInstance with invalid priority value \"" + priority + "\"", e);
                        }

                        String firstVersion = getOptionalAttribute(attributes, "first");
                        if (firstVersion != null) {
                            bugInstance.setFirstVersion(Long.parseLong(firstVersion));
                        }
                        String lastVersion = getOptionalAttribute(attributes, "last");
                        if (lastVersion != null) {
                            bugInstance.setLastVersion(Long.parseLong(lastVersion));
                        }

                        if (bugInstance.isDead() && bugInstance.getFirstVersion() > bugInstance.getLastVersion())
                            throw new IllegalStateException("huh");

                        String introducedByChange = getOptionalAttribute(attributes, "introducedByChange");
                        if (introducedByChange != null) {
                            bugInstance.setIntroducedByChangeOfExistingClass(Boolean.parseBoolean(introducedByChange));
                        }
                        String removedByChange = getOptionalAttribute(attributes, "removedByChange");
                        if (removedByChange != null) {
                            bugInstance.setRemovedByChangeOfPersistingClass(Boolean.parseBoolean(removedByChange));
                        }
                        String oldInstanceHash = getOptionalAttribute(attributes, "instanceHash");
                        if (oldInstanceHash == null)
                            oldInstanceHash = getOptionalAttribute(attributes, "oldInstanceHash");
                        if (oldInstanceHash != null) {
                            bugInstance.setOldInstanceHash(oldInstanceHash);
                        }

                        String firstSeen = getOptionalAttribute(attributes, "firstSeen");
                        if (firstSeen != null) {
                            try {
                                bugInstance.getXmlProps().setFirstSeen(BugInstance.firstSeenXMLFormat().parse(firstSeen));
                            } catch (ParseException e) {
                                LOGGER.warning("Could not parse first seen entry: " + firstSeen);
                                // ignore
                            }
                        }

                        String isInCloud = getOptionalAttribute(attributes, "isInCloud");
                        if (isInCloud != null)
                            bugInstance.getXmlProps().setIsInCloud(Boolean.parseBoolean(isInCloud));

                        String reviewCount = getOptionalAttribute(attributes, "reviews");
                        if (reviewCount != null) {
                            bugInstance.getXmlProps().setReviewCount(Integer.parseInt(reviewCount));
                        }

                        String consensus = getOptionalAttribute(attributes, "consensus");
                        if (consensus != null) {
                            bugInstance.getXmlProps().setConsensus(consensus);
                        }

                    } else if (qName.equals("FindBugsSummary")) {
                        BugCollection bugCollection = this.bugCollection;
                        assert bugCollection != null;
                        String timestamp = getRequiredAttribute(attributes, "timestamp", qName);
                        String vmVersion = getOptionalAttribute(attributes, "vm_version");
                        String totalClasses = getOptionalAttribute(attributes, "total_classes");
                        if (totalClasses != null && totalClasses.length() > 0)
                            bugCollection.getProjectStats().setTotalClasses(Integer.parseInt(totalClasses));

                        String totalSize = getOptionalAttribute(attributes, "total_size");
                        if (totalSize != null && totalSize.length() > 0)
                            bugCollection.getProjectStats().setTotalSize(Integer.parseInt(totalSize));

                        String referencedClasses = getOptionalAttribute(attributes, "referenced_classes");
                        if (referencedClasses != null && referencedClasses.length() > 0)
                            bugCollection.getProjectStats().setReferencedClasses(Integer.parseInt(referencedClasses));
                        bugCollection.getProjectStats().setVMVersion(vmVersion);
                        try {
                            bugCollection.getProjectStats().setTimestamp(timestamp);
                        } catch (java.text.ParseException e) {
                            throw new SAXException("Unparseable sequence number: '" + timestamp + "'", e);
                        }
                    }
                } else if (outerElement.equals("BugInstance")) {
                    parseBugInstanceContents(qName, attributes);
                } else if (outerElement.equals("Method") || outerElement.equals("Field") || outerElement.equals("Class")
                        || outerElement.equals("Type")) {
                    if (qName.equals("SourceLine")) {
                        // package member elements can contain nested SourceLine
                        // elements.
                        bugAnnotationWithSourceLines.setSourceLines(createSourceLineAnnotation(qName, attributes));
                    }
                } else if (outerElement.equals(BugCollection.ERRORS_ELEMENT_NAME)) {
                    if (qName.equals(BugCollection.ANALYSIS_ERROR_ELEMENT_NAME) || qName.equals(BugCollection.ERROR_ELEMENT_NAME)) {
                        analysisError = new AnalysisError("Unknown error");
                        stackTrace.clear();
                    }
                } else if (outerElement.equals("FindBugsSummary") && qName.equals("PackageStats")) {
                    BugCollection bugCollection = this.bugCollection;
                    assert bugCollection != null;
                    String packageName = getRequiredAttribute(attributes, "package", qName);
                    int numClasses = Integer.valueOf(getRequiredAttribute(attributes, "total_types", qName));
                    int size = Integer.valueOf(getRequiredAttribute(attributes, "total_size", qName));
                    bugCollection.getProjectStats().putPackageStats(packageName, numClasses, size);

                } else if (outerElement.equals("PackageStats")) {
                    BugCollection bugCollection = this.bugCollection;
                    assert bugCollection != null;
                    if (qName.equals("ClassStats")) {
                        String className = getRequiredAttribute(attributes, "class", qName);
                        Boolean isInterface = Boolean.valueOf(getRequiredAttribute(attributes, "interface", qName));
                        int size = Integer.valueOf(getRequiredAttribute(attributes, "size", qName));
                        String sourceFile = getOptionalAttribute(attributes, "sourceFile");
                        bugCollection.getProjectStats().addClass(className, sourceFile, isInterface, size, false);
                    }

                } else if (isTopLevelFilter(outerElement) || isCompoundElementTag(outerElement)) {
                    parseMatcher(qName, attributes);
                } else if (outerElement.equals("ClassFeatures")) {
                    if (qName.equals(ClassFeatureSet.ELEMENT_NAME)) {
                        String className = getRequiredAttribute(attributes, "class", qName);
                        classFeatureSet = new ClassFeatureSet();
                        classFeatureSet.setClassName(className);
                    }
                } else if (outerElement.equals(ClassFeatureSet.ELEMENT_NAME)) {
                    if (qName.equals(ClassFeatureSet.FEATURE_ELEMENT_NAME)) {
                        String value = getRequiredAttribute(attributes, "value", qName);
                        classFeatureSet.addFeature(value);
                    }
                } else if (outerElement.equals(BugCollection.HISTORY_ELEMENT_NAME)) {
                    if (qName.equals(AppVersion.ELEMENT_NAME)) {
                        BugCollection bugCollection = this.bugCollection;
                        assert bugCollection != null;

                        try {
                            String sequence = getRequiredAttribute(attributes, "sequence", qName);
                            String timestamp = getOptionalAttribute(attributes, "timestamp");
                            String releaseName = getOptionalAttribute(attributes, "release");
                            String codeSize = getOptionalAttribute(attributes, "codeSize");
                            String numClasses = getOptionalAttribute(attributes, "numClasses");
                            AppVersion appVersion = new AppVersion(Long.valueOf(sequence));
                            if (timestamp != null)
                                appVersion.setTimestamp(Long.valueOf(timestamp));
                            if (releaseName != null)
                                appVersion.setReleaseName(releaseName);
                            if (codeSize != null)
                                appVersion.setCodeSize(Integer.parseInt(codeSize));
                            if (numClasses != null)
                                appVersion.setNumClasses(Integer.parseInt(numClasses));

                            bugCollection.addAppVersion(appVersion);
                        } catch (NumberFormatException e) {
                            throw new SAXException("Invalid AppVersion element", e);
                        }
                    }
                } else if (outerElement.equals(BugCollection.PROJECT_ELEMENT_NAME)) {
                    Project project = this.project;
                    assert project != null;
                    if (qName.equals(Project.CLOUD_ELEMENT_NAME)) {
                        String cloudId = getRequiredAttribute(attributes, Project.CLOUD_ID_ATTRIBUTE_NAME, qName);
                        project.setCloudId(cloudId);
                        if(bugCollection != null){
                            Map<String,String> map = new HashMap<String, String>();
                            for (int i = 0; i < attributes.getLength(); i++) {
                                map.put(attributes.getLocalName(i), attributes.getValue(i));
                            }
                            bugCollection.setXmlCloudDetails(Collections.unmodifiableMap(map));
                        }
                    } else if (qName.equals(Project.PLUGIN_ELEMENT_NAME)) {
                        String pluginId = getRequiredAttribute(attributes, Project.PLUGIN_ID_ATTRIBUTE_NAME, qName);
                        Boolean enabled = Boolean.valueOf(getRequiredAttribute(attributes, Project.PLUGIN_STATUS_ELEMENT_NAME, qName));
                        project.setPluginStatusTrinary(pluginId, enabled);
                    }

                } else if (outerElement.equals(Project.CLOUD_ELEMENT_NAME)) {
                    if (qName.equals(Project.CLOUD_PROPERTY_ELEMENT_NAME)) {
                        cloudPropertyKey = getRequiredAttribute(attributes, "key", qName);
                    }

                }
            }
        }

        textBuffer.delete(0, textBuffer.length());
        elementStack.add(qName);
    }

    private boolean isCompoundElementTag(String qName) {
        return outerElementTags .contains(qName);
    }

    private boolean isTopLevelFilter(String qName) {
        return qName.equals(FIND_BUGS_FILTER) || qName.equals("SuppressionFilter");
    }

    private void addMatcher(Matcher m) {
        if (m == null)
            throw new IllegalArgumentException("matcher must not be null");

        CompoundMatcher peek = matcherStack.peek();
        if (peek == null)
            throw new NullPointerException("Top of stack is null");
        peek.addChild(m);
        if (nextMatchedIsDisabled) {
            if (peek instanceof Filter)
                ((Filter) peek).disable(m);
            else
                assert false;
            nextMatchedIsDisabled = false;
        }
    }

    private void pushCompoundMatcherAsChild(CompoundMatcher m) {
        addMatcher(m);
        pushCompoundMatcher(m);
    }

    private void pushCompoundMatcher(CompoundMatcher m) {
        if (m == null)
            throw new IllegalArgumentException("matcher must not be null");
        matcherStack.push(m);
    }

    boolean nextMatchedIsDisabled;
    private final Set<String> outerElementTags = unmodifiableSet(new HashSet<String>(asList("And", "Match", "Or", "Not")));;

    private void parseMatcher(String qName, Attributes attributes) throws SAXException {
        if (DEBUG)
            System.out.println(elementStack + " " + qName + " " + matcherStack);
        String disabled = getOptionalAttribute(attributes, "disabled");
        nextMatchedIsDisabled = "true".equals(disabled);
        if (qName.equals("Bug")) {
            addMatcher(new BugMatcher(getOptionalAttribute(attributes, "code"), getOptionalAttribute(attributes, "pattern"),
                    getOptionalAttribute(attributes, "category")));
        } else if (qName.equals("Class")) {
            addMatcher(new ClassMatcher(getRequiredAttribute(attributes, "name", qName)));
        } else if (qName.equals("FirstVersion")) {
            addMatcher(new FirstVersionMatcher(getRequiredAttribute(attributes, "value", qName), getRequiredAttribute(attributes,
                    "relOp", qName)));
        } else if (qName.equals("LastVersion")) {
            addMatcher(new LastVersionMatcher(getRequiredAttribute(attributes, "value", qName), getRequiredAttribute(attributes,
                    "relOp", qName)));
        } else if (qName.equals("Designation")) {
            addMatcher(new DesignationMatcher(getRequiredAttribute(attributes, "designation", qName)));
        } else if (qName.equals("BugCode")) {
            addMatcher(new BugMatcher(getRequiredAttribute(attributes, "name", qName), "", ""));
        } else if (qName.equals("Local")) {
            addMatcher(new LocalMatcher(getRequiredAttribute(attributes, "name", qName)));
        } else if (qName.equals("BugPattern")) {
            addMatcher(new BugMatcher("", getRequiredAttribute(attributes, "name", qName), ""));
        } else if (qName.equals("Priority") || qName.equals("Confidence")) {
            addMatcher(new PriorityMatcher(getRequiredAttribute(attributes, "value", qName)));
        } else if (qName.equals("Rank")) {
            addMatcher(new RankMatcher(getRequiredAttribute(attributes, "value", qName)));
        } else if (qName.equals("Package")) {
            String pName = getRequiredAttribute(attributes, "name", qName);
            pName = pName.startsWith("~") ? pName : "~" + pName.replace(".", "\\.");
            addMatcher(new ClassMatcher(pName + "\\.[^.]+"));
        } else if (qName.equals("Method")) {
            String name = getOptionalAttribute(attributes, "name");
            String params = getOptionalAttribute(attributes, "params");
            String returns = getOptionalAttribute(attributes, "returns");
            String role = getOptionalAttribute(attributes, "role");
            addMatcher(new MethodMatcher(name, params, returns, role));
        } else if (qName.equals("Field")) {
            String name = getOptionalAttribute(attributes, "name");
            String type = getOptionalAttribute(attributes, "type");
            addMatcher(new FieldMatcher(name, type));
        } else if (qName.equals("Or")) {
            CompoundMatcher matcher = new OrMatcher();
            pushCompoundMatcherAsChild(matcher);
        } else if (qName.equals("And") || qName.equals("Match")) {
            AndMatcher matcher = new AndMatcher();
            pushCompoundMatcherAsChild(matcher);
            if (qName.equals("Match")) {
                String classregex = getOptionalAttribute(attributes, "classregex");
                String classMatch = getOptionalAttribute(attributes, "class");

                if (classregex != null)
                    addMatcher(new ClassMatcher("~" + classregex));
                else if (classMatch != null)
                    addMatcher(new ClassMatcher(classMatch));
            }
        } else if(qName.equals("Not")) {
            NotMatcher matcher = new NotMatcher();
            pushCompoundMatcherAsChild(matcher);
        }
        nextMatchedIsDisabled = false;
    }

    private void parseBugInstanceContents(String qName, Attributes attributes) throws SAXException {
        // Parsing an attribute or property of a BugInstance
        BugAnnotation bugAnnotation = null;
        if (qName.equals("Class")) {
            String className = getRequiredAttribute(attributes, "classname", qName);
            bugAnnotation = bugAnnotationWithSourceLines = new ClassAnnotation(className);
        } else if (qName.equals("Type")) {
            String typeDescriptor = getRequiredAttribute(attributes, "descriptor", qName);
            TypeAnnotation typeAnnotation;
            bugAnnotation = bugAnnotationWithSourceLines = typeAnnotation = new TypeAnnotation(typeDescriptor);
            String typeParameters = getOptionalAttribute(attributes, "typeParameters");
            if (typeParameters != null)
                typeAnnotation.setTypeParameters(Strings.unescapeXml(typeParameters));

        } else if (qName.equals("Method") || qName.equals("Field")) {
            String classname = getRequiredAttribute(attributes, "classname", qName);
            String fieldOrMethodName = getRequiredAttribute(attributes, "name", qName);
            String signature = getRequiredAttribute(attributes, "signature", qName);
            if (qName.equals("Method")) {
                String isStatic = getOptionalAttribute(attributes, "isStatic");
                if (isStatic == null) {
                    isStatic = "false"; // Hack for old data
                }

                bugAnnotation = bugAnnotationWithSourceLines = new MethodAnnotation(classname, fieldOrMethodName, signature,
                        Boolean.valueOf(isStatic));

            } else {
                String isStatic = getRequiredAttribute(attributes, "isStatic", qName);
                bugAnnotation = bugAnnotationWithSourceLines = new FieldAnnotation(classname, fieldOrMethodName, signature,
                        Boolean.valueOf(isStatic));
            }

        } else if (qName.equals("SourceLine")) {
            SourceLineAnnotation sourceAnnotation = createSourceLineAnnotation(qName, attributes);
            if (!sourceAnnotation.isSynthetic())
                bugAnnotation = sourceAnnotation;
        } else if (qName.equals("Int")) {
            try {
                String value = getRequiredAttribute(attributes, "value", qName);
                bugAnnotation = new IntAnnotation(Integer.parseInt(value));
            } catch (NumberFormatException e) {
                throw new SAXException("Bad integer value in Int");
            }
        } else if (qName.equals("String")) {
            String value = getRequiredAttribute(attributes, "value", qName);
            bugAnnotation = StringAnnotation.fromXMLEscapedString(value);
        } else if (qName.equals("LocalVariable")) {
            try {
                String varName = getRequiredAttribute(attributes, "name", qName);
                int register = Integer.parseInt(getRequiredAttribute(attributes, "register", qName));
                int pc = Integer.parseInt(getRequiredAttribute(attributes, "pc", qName));
                bugAnnotation = new LocalVariableAnnotation(varName, register, pc);
            } catch (NumberFormatException e) {
                throw new SAXException("Invalid integer value in attribute of LocalVariable element");
            }
        } else if (qName.equals("Property")) {
            // A BugProperty.
            String propName = getRequiredAttribute(attributes, "name", qName);
            String propValue = getRequiredAttribute(attributes, "value", qName);
            bugInstance.setProperty(propName, propValue);
        } else if (qName.equals("UserAnnotation")) {
            // ignore AnnotationText for now; will handle in endElement
            String s = getOptionalAttribute(attributes, "designation"); // optional
            if (s != null) {
                bugInstance.setUserDesignationKey(s, null);
            }
            s = getOptionalAttribute(attributes, "user"); // optional
            if (s != null)
                bugInstance.setUser(s);
            s = getOptionalAttribute(attributes, "timestamp"); // optional
            if (s != null)
                try {
                    long timestamp = Long.valueOf(s);
                    bugInstance.setUserAnnotationTimestamp(timestamp);
                } catch (NumberFormatException nfe) {
                    // ok to contine -- just won't set a timestamp for the user
                    // designation.
                    // but is there anyplace to report this?
                }
            s = getOptionalAttribute(attributes, "needsSync"); // optional
            if (s == null || s.equals("false"))
                bugInstance.setUserAnnotationDirty(false);

        } else
            throw new SAXException("Unknown bug annotation named " + qName);

        if (bugAnnotation != null) {
            String role = getOptionalAttribute(attributes, "role");
            if (role != null)
                bugAnnotation.setDescription(role);
            setAnnotationRole(attributes, bugAnnotation);
            bugInstance.add(bugAnnotation);
        }
    }

    private long parseLong(String s, long defaultValue) {
        long value;
        try {
            value = (s != null) ? Long.parseLong(s) : defaultValue;
        } catch (NumberFormatException e) {
            value = defaultValue;
        }
        return value;
    }

    /**
     * Extract a hash value from an element.
     *
     * @param qName
     *            name of element containing hash value
     * @param attributes
     *            element attributes
     * @return the decoded hash value
     * @throws SAXException
     */
    private byte[] extractHash(String qName, Attributes attributes) throws SAXException {
        String encodedHash = getRequiredAttribute(attributes, "value", qName);
        byte[] hash;
        try {
            // System.out.println("Extract hash " + encodedHash);
            hash = ClassHash.stringToHash(encodedHash);
        } catch (IllegalArgumentException e) {
            throw new SAXException("Invalid class hash", e);
        }
        return hash;
    }

    private void setAnnotationRole(Attributes attributes, BugAnnotation bugAnnotation) {
        String role = getOptionalAttribute(attributes, "role");
        if (role != null)
            bugAnnotation.setDescription(role);
    }

    private SourceLineAnnotation createSourceLineAnnotation(String qName, Attributes attributes) throws SAXException {
        String classname = getRequiredAttribute(attributes, "classname", qName);
        String sourceFile = getOptionalAttribute(attributes, "sourcefile");
        if (sourceFile == null)
            sourceFile = SourceLineAnnotation.UNKNOWN_SOURCE_FILE;
        String startLine = getOptionalAttribute(attributes, "start"); // "start"/"end"
                                                                      // are now
                                                                      // optional
        String endLine = getOptionalAttribute(attributes, "end"); // (were too
                                                                  // many "-1"s
                                                                  // in the xml)
        String startBytecode = getOptionalAttribute(attributes, "startBytecode");
        String endBytecode = getOptionalAttribute(attributes, "endBytecode");
        String synthetic = getOptionalAttribute(attributes, "synthetic");

        try {
            int sl = startLine != null ? Integer.parseInt(startLine) : -1;
            int el = endLine != null ? Integer.parseInt(endLine) : -1;
            int sb = startBytecode != null ? Integer.parseInt(startBytecode) : -1;
            int eb = endBytecode != null ? Integer.parseInt(endBytecode) : -1;

            SourceLineAnnotation s = new SourceLineAnnotation(classname, sourceFile, sl, el, sb, eb);
            if ("true".equals(synthetic))
                s.setSynthetic(true);
            return s;
        } catch (NumberFormatException e) {
            throw new SAXException("Bad integer value in SourceLine element", e);
        }
    }

    @SuppressWarnings("hiding")
    @Override
    public void endElement(String uri, String name, String qName) throws SAXException {
        // URI should always be empty.
        // So, qName is the name of the element.

        if (discardedElement(qName)) {
            nestingOfIgnoredElements--;
        } else if (nestingOfIgnoredElements > 0) {
            // ignore it
        } else if (qName.equals("Project")) {
            // noop
        } else if (elementStack.size() > 1) {
            String outerElement = elementStack.get(elementStack.size() - 2);

            if (isTopLevelFilter(qName) || isCompoundElementTag(qName)) {
                if (DEBUG)
                    System.out.println("  ending " + elementStack + " " + qName + " " + matcherStack);

                matcherStack.pop();
            } else if (outerElement.equals(BUG_COLLECTION)) {
                BugCollection bugCollection = this.bugCollection;
                assert bugCollection != null;
                if (qName.equals("BugInstance")) {
                    bugCollection.add(bugInstance, false);
                }
            } else if (outerElement.equals(PROJECT)) {
                Project project = this.project;
                assert project != null;
                if (qName.equals("Jar"))
                    project.addFile(makeAbsolute(getTextContents()));
                else if (qName.equals("SrcDir"))
                    project.addSourceDir(makeAbsolute(getTextContents()));
                else if (qName.equals("AuxClasspathEntry"))
                    project.addAuxClasspathEntry(makeAbsolute(getTextContents()));



            } else if (outerElement.equals(Project.CLOUD_ELEMENT_NAME) && qName.equals(Project.CLOUD_PROPERTY_ELEMENT_NAME)) {
                Project project = this.project;
                assert project != null;
                assert cloudPropertyKey != null;
                project.getCloudProperties().setProperty(cloudPropertyKey, getTextContents());
                cloudPropertyKey = null;
            } else if (outerElement.equals("BugInstance")) {
                if (qName.equals("UserAnnotation")) {
                    bugInstance.setAnnotationText(getTextContents(), null);
                }
            } else if (outerElement.equals(BugCollection.ERRORS_ELEMENT_NAME)) {
                BugCollection bugCollection = this.bugCollection;
                assert bugCollection != null;
                if (qName.equals(BugCollection.ANALYSIS_ERROR_ELEMENT_NAME)) {
                    analysisError.setMessage(getTextContents());
                    bugCollection.addError(analysisError);
                } else if (qName.equals(BugCollection.ERROR_ELEMENT_NAME)) {
                    if (stackTrace.size() > 0) {
                        analysisError.setStackTrace(stackTrace.toArray(new String[stackTrace.size()]));
                    }
                    bugCollection.addError(analysisError);
                } else if (qName.equals(BugCollection.MISSING_CLASS_ELEMENT_NAME)) {
                    bugCollection.addMissingClass(getTextContents());
                }

            } else if (outerElement.equals(BugCollection.ERROR_ELEMENT_NAME)) {
                if (qName.equals(BugCollection.ERROR_MESSAGE_ELEMENT_NAME)) {
                    analysisError.setMessage(getTextContents());
                } else if (qName.equals(BugCollection.ERROR_EXCEPTION_ELEMENT_NAME)) {
                    analysisError.setExceptionMessage(getTextContents());
                } else if (qName.equals(BugCollection.ERROR_STACK_TRACE_ELEMENT_NAME)) {
                    stackTrace.add(getTextContents());
                }
            } else if (outerElement.equals("ClassFeatures")) {
                if (qName.equals(ClassFeatureSet.ELEMENT_NAME)) {
                    BugCollection bugCollection = this.bugCollection;
                    assert bugCollection != null;

                    bugCollection.setClassFeatureSet(classFeatureSet);
                    classFeatureSet = null;
                }
            }
        }

        elementStack.remove(elementStack.size() - 1);
    }

    private String makeAbsolute(String possiblyRelativePath) {
        if (possiblyRelativePath.contains("://") || possiblyRelativePath.startsWith("http:")
                || possiblyRelativePath.startsWith("https:") || possiblyRelativePath.startsWith("file:"))
            return possiblyRelativePath;
        if (base == null)
            return possiblyRelativePath;
        if (new File(possiblyRelativePath).isAbsolute())
            return possiblyRelativePath;

        return new File(base.getParentFile(), possiblyRelativePath).getAbsolutePath();
    }

    @Override
    public void characters(char[] ch, int start, int length) {
        textBuffer.append(ch, start, length);
    }

    private String getRequiredAttribute(Attributes attributes, String attrName, String elementName) throws SAXException {
        String value = attributes.getValue(attrName);
        if (value == null)
            throw new SAXException(elementName + " element missing " + attrName + " attribute");
        return memoized(Strings.unescapeXml(value));
    }

}

// vim:ts=4
