fr.dudie.acrachilisync.utils.IssueDescriptionReader.java Source code

Java tutorial

Introduction

Here is the source code for fr.dudie.acrachilisync.utils.IssueDescriptionReader.java

Source

/*
 * Copyright (C) 2011 Jeremie Huchet
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package fr.dudie.acrachilisync.utils;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Predicate;
import org.apache.commons.lang.StringUtils;
import org.redmine.ta.beans.CustomField;
import org.redmine.ta.beans.Issue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import fr.dudie.acrachilisync.exception.IssueParseException;
import fr.dudie.acrachilisync.model.ErrorOccurrence;

/**
 * Helper class to extract informations from an issue description generated with
 * {@link IssueDescriptionBuilder}.
 * 
 * @author Jrmie Huchet
 * @see IssueDescriptionBuilder
 */
public final class IssueDescriptionReader {

    /** The event logger. */
    private static final Logger LOGGER = LoggerFactory.getLogger(IssueDescriptionReader.class);

    /**
     * Regexp :
     * <code>|jsglq4354gjdslg4434|dd/MM/yyyy hh:mm:ss|1d 2h 21m 55s|2.3.3|15|0.3.1|Nexus One / google / passion|</code>
     * .
     */
    public static final String OCCURR_LINE_PATTERN;
    static {
        final StringBuilder buf = new StringBuilder();
        // |jsglq4354gjdslg4434
        buf.append("\\|").append("[\\w-]+");
        // |dd/MM/yyyy hh:mm:ss
        buf.append("\\|").append("\\d{2}/\\d{2}/\\d{4} \\d{2}:\\d{2}:\\d{2}");
        // |1d 2h 21m 55s OR |2h 21m 55s OR |21m 55s OR | 55s
        buf.append("\\|").append("(((\\d{1,2}d )?\\d{1,2}h )?\\d{1,2}m )?\\d{1,2}s");
        // |2.3.3
        buf.append("\\|").append("[^|]+");
        // |15
        buf.append("\\|").append("\\d+");
        // |0.3.1
        buf.append("\\|").append("[^|]+");
        // |Nexus One / google / passion
        buf.append("\\|").append("[^|]+");
        // |
        buf.append("\\|");
        OCCURR_LINE_PATTERN = buf.toString();
    }

    public static final Pattern DESCRIPTION_VERSION_PATTERN = Pattern

            .compile("\\s*%\\(acrachilisync-description-version\\)description_version_(\\d+)%\\s*",
                    Pattern.CASE_INSENSITIVE);

    /** The error stacktrace. */
    private final String stacktrace;

    /** The error stacktrace MD5 hash. */
    private final String stacktraceMD5;

    /** The occurrence time for each ACRA bug report. */
    private final List<ErrorOccurrence> occurrences = new ArrayList<ErrorOccurrence>();

    /**
     * Constructor.
     * <p>
     * The description must have been formatted with {@link IssueDescriptionBuilder} in order to
     * make this reader working.
     * 
     * @param pIssue
     *            the issue
     * @throws IssueParseException
     *             unable to parse the issue
     */
    public IssueDescriptionReader(final Issue pIssue) throws IssueParseException {

        checkDescriptionVersion(pIssue);

        stacktraceMD5 = getStacktraceMD5(pIssue);
        if (!StringUtils.isBlank(pIssue.getDescription())) {
            occurrences.addAll(parseAcraOccurrencesTable(pIssue.getDescription(), stacktraceMD5));
            stacktrace = parseStacktrace(pIssue.getDescription(), stacktraceMD5);
        } else {
            stacktrace = "";
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Issue {} have an empty description", pIssue.getId());
            }
        }
    }

    /**
     * Checks the description version tag.
     * 
     * @param pIssue
     *            the issue
     * @throws IssueParseException
     *             the description version tag doesn't describe the expected version
     *             {@link IssueDescriptionUtils#DESCRIPTION_VERSION}
     */
    private void checkDescriptionVersion(final Issue pIssue) throws IssueParseException {

        final Matcher m = DESCRIPTION_VERSION_PATTERN.matcher(pIssue.getDescription());
        final int version;
        if (m.find()) {
            version = Integer.parseInt(m.group(1));
        } else {
            // default version is 1
            version = 1;
        }
        if (IssueDescriptionUtils.DESCRIPTION_VERSION != version) {
            throw new IssueParseException(
                    String.format("Issue #%s has unsupported description: %d. Expected version %d ", pIssue.getId(),
                            version, IssueDescriptionUtils.DESCRIPTION_VERSION));
        }
    }

    /**
     * Extracts the stacktrace MD5 custom field value from the issue.
     * 
     * @param pIssue
     *            the issue
     * @return the stacktrace MD5
     * @throws IssueParseException
     *             issue doesn't contains a stack trace md5 custom field
     */
    private String getStacktraceMD5(final Issue pIssue) throws IssueParseException {

        final int stackCfId = ConfigurationManager.getInstance().CHILIPROJECT_STACKTRACE_MD5_CF_ID;
        final Predicate findStacktraceMD5 = new CustomFieldIdPredicate(stackCfId);
        final CustomField field = (CustomField) CollectionUtils.find(pIssue.getCustomFields(), findStacktraceMD5);
        if (null == field) {
            throw new IssueParseException(
                    String.format("Issue %d doesn't contains a custom_field id=%d", pIssue.getId(), stackCfId));
        }
        return field.getValue();
    }

    /**
     * Extracts the list of bug occurrences from the description.
     * 
     * @param pDescription
     *            the issue description
     * @param pStacktraceMD5
     *            the stacktrace MD5 hash the issue is related to
     * @return the ACRA bug occurrences listed in the description
     * @throws IssueParseException
     *             malformed issue description
     */
    private List<ErrorOccurrence> parseAcraOccurrencesTable(final String pDescription, final String pStacktraceMD5)
            throws IssueParseException {

        final List<ErrorOccurrence> occur = new ArrayList<ErrorOccurrence>();

        // escape braces { and } to use strings in regexp
        final String header = IssueDescriptionUtils.getOccurrencesTableHeader();
        final String escHeader = Pattern.quote(header);

        // regexp to find occurrences tables
        final Pattern p = Pattern.compile(escHeader + IssueDescriptionUtils.EOL + "(?:" + OCCURR_LINE_PATTERN
                + IssueDescriptionUtils.EOL + "+)+", Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
        final Matcher m = p.matcher(pDescription);

        if (m.find()) {
            // regexp to find occurrences lines
            final Pattern pLine = Pattern.compile(OCCURR_LINE_PATTERN);
            final Matcher mLine = pLine.matcher(m.group());
            while (mLine.find()) {
                try {
                    final StringTokenizer line = new StringTokenizer(mLine.group(), "|");
                    final String acraReportId = line.nextToken();
                    final String acraUserCrashDate = line.nextToken();
                    final String acraRunFor = line.nextToken();
                    final String acraAndroidVersion = line.nextToken();
                    final String acraVersionCode = line.nextToken();
                    final String acraVersionName = line.nextToken();
                    final String acraDevice = line.nextToken();
                    final ErrorOccurrence error = new ErrorOccurrence();
                    error.setReportId(acraReportId);
                    try {
                        error.setCrashDate(IssueDescriptionUtils.parseDate(acraUserCrashDate));
                        error.setRunFor(RunningTimeUtils.parseRunningTime(acraRunFor));
                    } catch (final ParseException e) {
                        throw new IssueParseException(
                                "Unable to parse user crash date of ACRA report " + acraReportId, e);
                    }
                    error.setAndroidVersion(acraAndroidVersion);
                    error.setVersionCode(acraVersionCode);
                    error.setVersionName(acraVersionName);
                    error.setDevice(acraDevice);
                    occur.add(error);
                } catch (final NoSuchElementException e) {
                    throw new IssueParseException("Unable to parse ACRA report line: " + mLine.group(), e);
                }
            }
        } else {
            throw new IssueParseException("No crash occurrence table found in the description");
        }

        if (m.find()) {
            throw new IssueParseException("More than 1 occurrence table found in the description");
        }

        if (CollectionUtils.isEmpty(occur)) {
            throw new IssueParseException("0 user crash occurrence found in the description");
        }

        return occur;
    }

    /**
     * Extracts the bug stacktrace from the description.
     * 
     * @param pDescription
     *            the issue description
     * @param pStacktraceMD5
     *            the stacktrace MD5 hash the issue is related to
     * @return the stacktrace
     * @throws IssueParseException
     *             malformed issue description
     */
    private String parseStacktrace(final String pDescription, final String pStacktraceMD5)
            throws IssueParseException {

        String stacktrace = null;

        // escape braces { and } to use strings in regexp
        final String start = "<pre class=\"javastacktrace\">";
        final String qStart = Pattern.quote(start);
        final String end = "</pre>";
        final String qEnd = Pattern.quote(end);

        final Pattern p = Pattern.compile(qStart + "(.*)" + qEnd, Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
        final Matcher m = p.matcher(pDescription);

        if (m.find()) {
            stacktrace = m.group(1);
            // if a start tag or an end tag is found in the stacktrace, then there is a problem
            if (StringUtils.contains(stacktrace, start) || StringUtils.contains(stacktrace, end)) {
                throw new IssueParseException("Invalid stacktrace block");
            }
        } else {
            throw new IssueParseException("0 stacktrace block found in the description");
        }

        return stacktrace;
    }

    /**
     * Gets the stacktrace.
     * 
     * @return the stacktrace
     */
    public String getStacktrace() {

        return stacktrace;
    }

    /**
     * Gets the stacktraceMD5.
     * 
     * @return the stacktraceMD5
     */
    public String getStacktraceMD5() {

        return stacktraceMD5;
    }

    /**
     * Gets the occurrences.
     * 
     * @return the occurrences
     */
    public List<ErrorOccurrence> getOccurrences() {

        return occurrences;
    }

}