James Moger
2013-11-25 c6f3d01c29bb67156b8154bfe5780537b0ef43ac
Add support for per-repository bugtraq configuration

Imported the reference implementation contributed by syntevo which
is used in their SmartGit product. You may create a bugtraq config
section inf your .git/config file OR you may add a .gitbugtraq file
to the root of your repository.

Example:

[bugtraq "issues"]
url = http://code.google.com/p/gitblit/issues/detail?id=%BUGID%
logRegex = "[Ii]ssue[-#:\\s]{1}\\d+"
logRegex1 = "\\d+"
[bugtraq "[pullrequests"]
url = "https://github.com/gitblit/gitblit/pull/%BUGID%"
logRegex = "(?:pull request|pull|pr)\\s*[-#]?([0-9]+)"

Change-Id: Iaba305bf4280d08cc4d1abf533c2f1365470a43f
10 files added
10 files modified
1117 ■■■■■ changed files
.classpath 3 ●●●●● patch | view | raw | blame | history
.gitbugtraq 7 ●●●●● patch | view | raw | blame | history
build.moxie 3 ●●●●● patch | view | raw | blame | history
gitblit.iml 13 ●●●●● patch | view | raw | blame | history
releases.moxie 2 ●●●●● patch | view | raw | blame | history
src/main/bugtraq/LICENSE 43 ●●●●● patch | view | raw | blame | history
src/main/bugtraq/com/syntevo/bugtraq/BugtraqConfig.java 240 ●●●●● patch | view | raw | blame | history
src/main/bugtraq/com/syntevo/bugtraq/BugtraqEntry.java 61 ●●●●● patch | view | raw | blame | history
src/main/bugtraq/com/syntevo/bugtraq/BugtraqException.java 49 ●●●●● patch | view | raw | blame | history
src/main/bugtraq/com/syntevo/bugtraq/BugtraqFormatter.java 118 ●●●●● patch | view | raw | blame | history
src/main/bugtraq/com/syntevo/bugtraq/BugtraqParser.java 151 ●●●●● patch | view | raw | blame | history
src/main/bugtraq/com/syntevo/bugtraq/BugtraqParserIssueId.java 61 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/SyndicationServlet.java 2 ●●● patch | view | raw | blame | history
src/main/java/com/gitblit/utils/MessageProcessor.java 59 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java 2 ●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/CommitPage.java 6 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/RepositoryPage.java 2 ●●● patch | view | raw | blame | history
src/main/resources/gitblit.css 4 ●●●● patch | view | raw | blame | history
src/test/bugtraq/com/syntevo/bugtraq/BugtraqFormatterTest.java 170 ●●●●● patch | view | raw | blame | history
src/test/bugtraq/com/syntevo/bugtraq/BugtraqParserTest.java 121 ●●●●● patch | view | raw | blame | history
.classpath
@@ -1,12 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
    <classpathentry kind="src" path="src/main/java" />
    <classpathentry kind="src" path="src/main/bugtraq" />
    <classpathentry kind="src" path="src/test/java" output="bin/test-classes" />
    <classpathentry kind="src" path="src/test/bugtraq" output="bin/test-classes" />
    <classpathentry kind="src" path="src/main/resources" />
    <classpathentry kind="lib" path="ext/dagger-1.1.0.jar" sourcepath="ext/src/dagger-1.1.0.jar" />
    <classpathentry kind="lib" path="ext/javax.inject-1.jar" sourcepath="ext/src/javax.inject-1.jar" />
    <classpathentry kind="lib" path="ext/dagger-compiler-1.1.0.jar" sourcepath="ext/src/dagger-compiler-1.1.0.jar" />
    <classpathentry kind="lib" path="ext/javawriter-2.1.1.jar" sourcepath="ext/src/javawriter-2.1.1.jar" />
    <classpathentry kind="lib" path="ext/annotations-12.0.jar" sourcepath="ext/src/annotations-12.0.jar" />
    <classpathentry kind="lib" path="ext/jcommander-1.17.jar" sourcepath="ext/src/jcommander-1.17.jar" />
    <classpathentry kind="lib" path="ext/log4j-1.2.17.jar" sourcepath="ext/src/log4j-1.2.17.jar" />
    <classpathentry kind="lib" path="ext/slf4j-api-1.6.6.jar" sourcepath="ext/src/slf4j-api-1.6.6.jar" />
.gitbugtraq
New file
@@ -0,0 +1,7 @@
[bugtraq "issues"]
  url = http://code.google.com/p/gitblit/issues/detail?id=%BUGID%
  logRegex = "[Ii]ssue[-#:\\s]{1}\\d+"
  logRegex1 = "\\d+"
[bugtraq "[pullrequests"]
  url = "https://github.com/gitblit/gitblit/pull/%BUGID%"
  logRegex = "(?:pull request|pull|pr)\\s*[-#]?([0-9]+)"
build.moxie
@@ -57,8 +57,10 @@
# a scope to each directory.
sourceDirectories:
- compile 'src/main/java'
- compile 'src/main/bugtraq'
- compile 'src/main/dagger' apt
- test 'src/test/java'
- test 'src/test/bugtraq'
# Moxie supports one site-scoped directory for mx:doc
- site 'src/site'
@@ -125,6 +127,7 @@
- compile 'com.squareup.dagger:dagger:1.1.0' :war apt
- compile 'com.squareup.dagger:dagger-compiler:1.1.0' :war optional apt
# Standard dependencies
- compile 'com.intellij:annotations:12.0' :war
- compile 'com.beust:jcommander:1.17' :fedclient :authority
- compile 'log4j:log4j:1.2.17' :war :fedclient :authority
- compile 'org.slf4j:slf4j-api:1.6.6' :war :fedclient :authority
gitblit.iml
@@ -6,8 +6,10 @@
    <exclude-output />
    <content url="file://$MODULE_DIR$">
      <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
      <sourceFolder url="file://$MODULE_DIR$/src/main/bugtraq" isTestSource="false" />
      <sourceFolder url="file://$MODULE_DIR$/src/main/dagger" isTestSource="false" />
      <sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
      <sourceFolder url="file://$MODULE_DIR$/src/test/bugtraq" isTestSource="true" />
      <sourceFolder url="file://$MODULE_DIR$/src/main/resources" isTestSource="false" />
    </content>
    <orderEntry type="sourceFolder" forTests="false" />
@@ -56,6 +58,17 @@
      </library>
    </orderEntry>
    <orderEntry type="module-library">
      <library name="annotations-12.0.jar">
        <CLASSES>
          <root url="jar://$MODULE_DIR$/ext/annotations-12.0.jar!/" />
        </CLASSES>
        <JAVADOC />
        <SOURCES>
          <root url="jar://$MODULE_DIR$/ext/src/annotations-12.0.jar!/" />
        </SOURCES>
      </library>
    </orderEntry>
    <orderEntry type="module-library">
      <library name="jcommander-1.17.jar">
        <CLASSES>
          <root url="jar://$MODULE_DIR$/ext/jcommander-1.17.jar!/" />
releases.moxie
@@ -53,6 +53,7 @@
    - Added a normalized diffstat display to the commit, commitdiff, and compare pages
    - Added GO setting to automatically redirect all http requests to the secure https connector
    - Automatically display common repository root documents as tabs on the docs page
    - Support bugtraq configuration in collaboration with syntevo
    dependencyChanges:
    - updated to Jetty 8.1.13
    - updated to JGit 3.1.0
@@ -85,6 +86,7 @@
    - fpeters.fae
    - David Ostrovsky
    - Alex Lewis
    - Marc Strapetz
}
#
src/main/bugtraq/LICENSE
New file
@@ -0,0 +1,43 @@
Copyright (c) 2013 by syntevo GmbH. All Rights Reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
 o Redistributions of source code must retain the above copyright notice,
   this list of conditions and the following disclaimer.
 o Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.
 o Neither the name of syntevo GmbH nor the names of
   its contributors may be used to endorse or promote products derived
   from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Third Parties
-------------
The following third parties have rights on parts of the SOFTWARE:
- IDEA annotations.jar, copyright by JetBrains, Inc.
- JGit, copyright by various authors (http://repo.or.cz/w/jgit.git and
  http://jgit.org).
  The corresponding license agreement can be found at
  http://repo.or.cz/w/jgit.git/blob/HEAD:/LICENSE.
- JUnit, copyright by www.hamcrest.org.
  The corresponding license agreement can be found at
  in subdirectory lib/JUNIT-LICENSE.
src/main/bugtraq/com/syntevo/bugtraq/BugtraqConfig.java
New file
@@ -0,0 +1,240 @@
/*
 * Copyright (c) 2013 by syntevo GmbH. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  o Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  o Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 *  o Neither the name of syntevo GmbH nor the names of
 *    its contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.syntevo.bugtraq;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public final class BugtraqConfig {
    // Constants ==============================================================
    private static final String DOT_GIT_BUGTRAQ = ".gitbugtraq";
    private static final String BUGTRAQ = "bugtraq";
    private static final String URL = "url";
    private static final String ENABLED = "enabled";
    private static final String LOG_REGEX = "logRegex";
    // Static =================================================================
    @Nullable
    public static BugtraqConfig read(@NotNull Repository repository) throws IOException, ConfigInvalidException {
        final Config baseConfig = getBaseConfig(repository);
        final Set<String> allNames = new HashSet<String>();
        final Config config = repository.getConfig();
        allNames.addAll(config.getSubsections(BUGTRAQ));
        if (baseConfig != null) {
            allNames.addAll(baseConfig.getSubsections(BUGTRAQ));
        }
        final List<BugtraqEntry> entries = new ArrayList<BugtraqEntry>();
        for (String name : allNames) {
            final String url = getString(name, URL, config, baseConfig);
            final String enabled = getString(name, ENABLED, config, baseConfig);
            if (enabled != null && !"true".equals(enabled)) {
                continue;
            }
            final String logIdRegex = getString(name, LOG_REGEX, config, baseConfig);
            if (url == null || logIdRegex == null) {
                return null;
            }
            final List<String> logIdRegexs = new ArrayList<String>();
            logIdRegexs.add(logIdRegex);
            for (int index = 1; index < Integer.MAX_VALUE; index++) {
                final String logIdRegexN = getString(name, LOG_REGEX + index, config, baseConfig);
                if (logIdRegexN == null) {
                    break;
                }
                logIdRegexs.add(logIdRegexN);
            }
            entries.add(new BugtraqEntry(url, logIdRegexs));
        }
        if (entries.isEmpty()) {
            return null;
        }
        return new BugtraqConfig(entries);
    }
    // Fields =================================================================
    @NotNull
    private final List<BugtraqEntry> entries;
    // Setup ==================================================================
    BugtraqConfig(@NotNull List<BugtraqEntry> entries) {
        this.entries = entries;
    }
    // Accessing ==============================================================
    @NotNull
    public List<BugtraqEntry> getEntries() {
        return Collections.unmodifiableList(entries);
    }
    // Utils ==================================================================
    @Nullable
    private static Config getBaseConfig(Repository repository) throws IOException, ConfigInvalidException {
        final Config baseConfig;
        if (repository.isBare()) {
            // read bugtraq config directly from the repository
            String content = null;
            String head = repository.getFullBranch();
            RevWalk rw = new RevWalk(repository);
            TreeWalk tw = new TreeWalk(repository);
            tw.setFilter(PathFilterGroup.createFromStrings(DOT_GIT_BUGTRAQ));
            try {
                ObjectId headId = repository.resolve(head);
                RevCommit commit = rw.parseCommit(headId);
                RevTree tree = commit.getTree();
                tw.reset(tree);
                while (tw.next()) {
                    if (tw.isSubtree()) {
                        tw.enterSubtree();
                        continue;
                    }
                    ObjectId entid = tw.getObjectId(0);
                    FileMode entmode = tw.getFileMode(0);
                    if (entmode == FileMode.REGULAR_FILE) {
                        RevObject ro = rw.lookupAny(entid, entmode.getObjectType());
                        rw.parseBody(ro);
                        ByteArrayOutputStream os = new ByteArrayOutputStream();
                        ObjectLoader ldr = repository.open(ro.getId(), Constants.OBJ_BLOB);
                        byte[] tmp = new byte[4096];
                        InputStream in = ldr.openStream();
                        int n;
                        while ((n = in.read(tmp)) > 0) {
                            os.write(tmp, 0, n);
                        }
                        in.close();
                        content = new String(os.toByteArray(), commit.getEncoding());
                    }
                }
            } finally {
                rw.dispose();
                tw.release();
            }
            if (content == null) {
                // config not found
                baseConfig = null;
            } else {
                // parse the config
                Config cfg = new Config();
                cfg.fromText(content);
                baseConfig = new StoredConfig(cfg) {
                    @Override
                    public void save() throws IOException {
                    }
                    @Override
                    public void load() throws IOException, ConfigInvalidException {
                    }
                };
            }
        } else {
            // read bugtraq config from work tree
            final File baseFile = new File(repository.getWorkTree(), DOT_GIT_BUGTRAQ);
            if (baseFile.isFile()) {
                FileBasedConfig fileConfig = new FileBasedConfig(baseFile, repository.getFS());
                fileConfig.load();
                baseConfig = fileConfig;
            }
            else {
                baseConfig = null;
            }
        }
        return baseConfig;
    }
    @Nullable
    private static String getString(@NotNull String subsection, @NotNull String key, @NotNull Config config, @Nullable Config baseConfig) {
        final String value = config.getString(BUGTRAQ, subsection, key);
        if (value != null) {
            return trimMaybeNull(value);
        }
        if (baseConfig != null) {
            return trimMaybeNull(baseConfig.getString(BUGTRAQ, subsection, key));
        }
        return value;
    }
    @Nullable
    private static String trimMaybeNull(@Nullable String string) {
        if (string == null) {
            return null;
        }
        string = string.trim();
        if (string.length() == 0) {
            return null;
        }
        return string;
    }
}
src/main/bugtraq/com/syntevo/bugtraq/BugtraqEntry.java
New file
@@ -0,0 +1,61 @@
/*
 * Copyright (c) 2013 by syntevo GmbH. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  o Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  o Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 *  o Neither the name of syntevo GmbH nor the names of
 *    its contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.syntevo.bugtraq;
import java.util.*;
import org.jetbrains.annotations.*;
final class BugtraqEntry {
    // Fields =================================================================
    private final String url;
    private final BugtraqParser parser;
    // Setup ==================================================================
    public BugtraqEntry(@NotNull String url, @NotNull List<String> logIdRegexs) throws BugtraqException {
        this.url = url;
        this.parser = BugtraqParser.createInstance(logIdRegexs);
    }
    // Accessing ==============================================================
    @NotNull
    public String getUrl() {
        return url;
    }
    @NotNull
    public BugtraqParser getParser() {
        return parser;
    }
}
src/main/bugtraq/com/syntevo/bugtraq/BugtraqException.java
New file
@@ -0,0 +1,49 @@
/*
 * Copyright (c) 2013 by syntevo GmbH. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  o Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  o Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 *  o Neither the name of syntevo GmbH nor the names of
 *    its contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.syntevo.bugtraq;
import java.io.*;
public class BugtraqException extends IOException {
    // Setup ==================================================================
    public BugtraqException(String message) {
        super(message);
    }
    public BugtraqException(String message, Throwable cause) {
        super(message, cause);
    }
    public BugtraqException(Throwable cause) {
        super(cause);
    }
}
src/main/bugtraq/com/syntevo/bugtraq/BugtraqFormatter.java
New file
@@ -0,0 +1,118 @@
/*
 * Copyright (c) 2013 by syntevo GmbH. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  o Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  o Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 *  o Neither the name of syntevo GmbH nor the names of
 *    its contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.syntevo.bugtraq;
import java.util.*;
import org.jetbrains.annotations.*;
public final class BugtraqFormatter {
    // Fields =================================================================
    private final BugtraqConfig config;
    // Setup ==================================================================
    public BugtraqFormatter(@NotNull BugtraqConfig config) {
        this.config = config;
    }
    // Accessing ==============================================================
    public void formatLogMessage(@NotNull String message, @NotNull OutputHandler outputHandler) {
        final SortedSet<IssueId> allIds = new TreeSet<IssueId>(new Comparator<IssueId>() {
            @Override
            public int compare(IssueId o1, IssueId o2) {
                final int from1 = o1.id.getFrom();
                final int from2 = o2.id.getFrom();
                return from1 > from2 ? +1 : from1 < from2 ? -1 : 0;
            }
        });
        for (BugtraqEntry entry : config.getEntries()) {
            final List<BugtraqParserIssueId> ids = entry.getParser().parse(message);
            if (ids == null) {
                continue;
            }
            for (BugtraqParserIssueId id : ids) {
                allIds.add(new IssueId(entry, id));
            }
        }
        int lastIdEnd = -1;
        for (IssueId issueId : allIds) {
            final BugtraqParserIssueId id = issueId.id;
            if (id.getFrom() <= lastIdEnd) {
                continue;
            }
            appendText(message.substring(lastIdEnd + 1, id.getFrom()), outputHandler);
            final String linkText = message.substring(id.getFrom(), id.getTo() + 1);
            final String target = issueId.entry.getUrl().replace("%BUGID%", id.getId());
            outputHandler.appendLink(linkText, target);
            lastIdEnd = id.getTo();
        }
        if (lastIdEnd - 1 < message.length()) {
            appendText(message.substring(lastIdEnd + 1, message.length()), outputHandler);
        }
    }
    // Utils ==================================================================
    private static void appendText(@NotNull String message, @NotNull OutputHandler outputHandler) {
        if (message.length() == 0) {
            return;
        }
        outputHandler.appendText(message);
    }
    // Inner Classes ==========================================================
    public interface OutputHandler {
        void appendText(@NotNull String text);
        void appendLink(@NotNull String name, @NotNull String target);
    }
    private static class IssueId {
        private final BugtraqEntry entry;
        private final BugtraqParserIssueId id;
        private IssueId(BugtraqEntry entry, BugtraqParserIssueId id) {
            this.entry = entry;
            this.id = id;
        }
    }
}
src/main/bugtraq/com/syntevo/bugtraq/BugtraqParser.java
New file
@@ -0,0 +1,151 @@
/*
 * Copyright (c) 2013 by syntevo GmbH. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  o Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  o Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 *  o Neither the name of syntevo GmbH nor the names of
 *    its contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.syntevo.bugtraq;
import java.util.*;
import java.util.regex.*;
import org.jetbrains.annotations.*;
final class BugtraqParser {
    // Static =================================================================
    @NotNull
    public static BugtraqParser createInstance(@NotNull List<String> regexs) throws BugtraqException {
        try {
            return new BugtraqParser(regexs);
        }
        catch (PatternSyntaxException ex) {
            throw new BugtraqException(ex);
        }
    }
    // Fields =================================================================
    private final List<Pattern> patterns;
    // Setup ==================================================================
    private BugtraqParser(List<String> regexs) {
        this.patterns = new ArrayList<Pattern>();
        for (String regex : regexs) {
            patterns.add(compilePatternSafe(regex));
        }
    }
    // Accessing ==============================================================
    @Nullable
    public List<BugtraqParserIssueId> parse(@NotNull String message) {
        List<Part> parts = new ArrayList<Part>();
        parts.add(new Part(message, 0, message.length() - 1));
        boolean firstMatch = false;
        for (Pattern pattern : patterns) {
            final List<Part> newParts = new ArrayList<Part>();
            for (Part part : parts) {
                final Matcher matcher = pattern.matcher(part.text);
                while (matcher.find()) {
                    firstMatch = true;
                    if (matcher.groupCount() == 0) {
                        addNewPart(part, matcher, 0, newParts);
                    }
                    else {
                        addNewPart(part, matcher, 1, newParts);
                    }
                }
            }
            parts = newParts;
            if (parts.isEmpty()) {
                parts = null;
                break;
            }
        }
        if (!firstMatch) {
            return null;
        }
        if (parts == null) {
            parts = new ArrayList<Part>();
        }
        final List<BugtraqParserIssueId> ids = new ArrayList<BugtraqParserIssueId>();
        for (Part part : parts) {
            final BugtraqParserIssueId id = new BugtraqParserIssueId(part.from, part.to, part.text);
            if (ids.size() > 0) {
                final BugtraqParserIssueId lastId = ids.get(ids.size() - 1);
                if (id.getFrom() <= lastId.getTo()) {
                    continue;
                }
            }
            ids.add(id);
        }
        return ids;
    }
    // Utils ==================================================================
    private static void addNewPart(Part part, Matcher matcher, int group, List<Part> newParts) {
        final int textStart = matcher.start(group) + part.from;
        final int textEnd = matcher.end(group) - 1 + part.from;
        if (textEnd < 0) {
            return;
        }
        final Part newPart = new Part(matcher.group(group), textStart, textEnd);
        newParts.add(newPart);
    }
    private static Pattern compilePatternSafe(String pattern) throws PatternSyntaxException {
        return Pattern.compile(pattern);
    }
    // Inner Classes ==========================================================
    private static class Part {
        private final int from;
        private final int to;
        private final String text;
        public Part(String text, int from, int to) {
            this.text = text;
            this.from = from;
            this.to = to;
        }
    }
}
src/main/bugtraq/com/syntevo/bugtraq/BugtraqParserIssueId.java
New file
@@ -0,0 +1,61 @@
/*
 * Copyright (c) 2013 by syntevo GmbH. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  o Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  o Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 *  o Neither the name of syntevo GmbH nor the names of
 *    its contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.syntevo.bugtraq;
final class BugtraqParserIssueId {
    // Fields =================================================================
    private final int from;
    private final int to;
    private final String id;
    // Setup ==================================================================
    public BugtraqParserIssueId(int from, int to, String id) {
        this.id = id;
        this.from = from;
        this.to = to;
    }
    // Accessing ==============================================================
    public int getFrom() {
        return from;
    }
    public int getTo() {
        return to;
    }
    public String getId() {
        return id;
    }
}
src/main/java/com/gitblit/servlet/SyndicationServlet.java
@@ -273,7 +273,7 @@
                        StringUtils.encodeURL(model.name.replace('/', fsc)), commit.getName());
                entry.published = commit.getCommitterIdent().getWhen();
                entry.contentType = "text/html";
                String message = processor.processCommitMessage(model, commit.getFullMessage());
                String message = processor.processCommitMessage(repository, model, commit.getFullMessage());
                entry.content = message;
                entry.repository = model.name;
                entry.branch = objectId;
src/main/java/com/gitblit/utils/MessageProcessor.java
@@ -15,17 +15,24 @@
 */
package com.gitblit.utils;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gitblit.IStoredSettings;
import com.gitblit.Keys;
import com.gitblit.models.RepositoryModel;
import com.syntevo.bugtraq.BugtraqConfig;
import com.syntevo.bugtraq.BugtraqFormatter;
import com.syntevo.bugtraq.BugtraqFormatter.OutputHandler;
public class MessageProcessor {
@@ -44,14 +51,15 @@
     * This method uses the preferred renderer to transform the commit message.
     *
     * @param repository
     * @param model
     * @param text
     * @return html version of the commit message
     */
    public String processCommitMessage(RepositoryModel repository, String text) {
        switch (repository.commitMessageRenderer) {
    public String processCommitMessage(Repository repository, RepositoryModel model, String text) {
        switch (model.commitMessageRenderer) {
        case MARKDOWN:
            try {
                String prepared = processCommitMessageRegex(repository.name, text);
                String prepared = processCommitMessageRegex(repository, model.name, text);
                return MarkdownUtils.transformMarkdown(prepared);
            } catch (Exception e) {
                logger.error("Failed to render commit message as markdown", e);
@@ -62,7 +70,7 @@
            break;
        }
        return processPlainCommitMessage(repository.name, text);
        return processPlainCommitMessage(repository, model.name, text);
    }
    /**
@@ -71,13 +79,14 @@
     *
     * This method assumes the commit message is plain text.
     *
     * @param repository
     * @param repositoryName
     * @param text
     * @return html version of the commit message
     */
    public String processPlainCommitMessage(String repositoryName, String text) {
    public String processPlainCommitMessage(Repository repository, String repositoryName, String text) {
        String html = StringUtils.escapeForHtml(text, false);
        html = processCommitMessageRegex(repositoryName, html);
        html = processCommitMessageRegex(repository, repositoryName, html);
        return StringUtils.breakLinesForHtml(html);
    }
@@ -86,11 +95,12 @@
     * Apply globally or per-repository specified regex substitutions to the
     * commit message.
     *
     * @param repository
     * @param repositoryName
     * @param text
     * @return the processed commit message
     */
    protected String processCommitMessageRegex(String repositoryName, String text) {
    protected String processCommitMessageRegex(Repository repository, String repositoryName, String text) {
        Map<String, String> map = new HashMap<String, String>();
        // global regex keys
        if (settings.getBoolean(Keys.regex.global, false)) {
@@ -121,6 +131,41 @@
                        + definition);
            }
        }
        try {
            // parse bugtraq repo config
            BugtraqConfig config = BugtraqConfig.read(repository);
            if (config != null) {
                BugtraqFormatter formatter = new BugtraqFormatter(config);
                StringBuilder sb = new StringBuilder();
                formatter.formatLogMessage(text, new BugtraqOutputHandler(sb));
                text = sb.toString();
            }
        } catch (IOException e) {
            logger.error(MessageFormat.format("Bugtraq config for {0} is invalid!", repositoryName), e);
        } catch (ConfigInvalidException e) {
            logger.error(MessageFormat.format("Bugtraq config for {0} is invalid!", repositoryName), e);
        }
        return text;
    }
    private class BugtraqOutputHandler implements OutputHandler {
        final StringBuilder sb;
        BugtraqOutputHandler(StringBuilder sb) {
            this.sb = sb;
        }
        @Override
        public void appendText(String text) {
            sb.append(text);
        }
        @Override
        public void appendLink(String name, String target) {
            sb.append(MessageFormat.format("<a class=\"bugtraq\" href=\"{1}\">{0}</a>", name, target));
        }
    }
}
src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java
@@ -107,7 +107,7 @@
                item.add(new GravatarImage("noteAuthorAvatar", entry.notesRef.getAuthorIdent()));
                item.add(WicketUtils.createTimestampLabel("authorDate", entry.notesRef
                        .getAuthorIdent().getWhen(), getTimeZone(), getTimeUtils()));
                item.add(new Label("noteContent", messageProcessor().processPlainCommitMessage(repositoryName,
                item.add(new Label("noteContent", messageProcessor().processPlainCommitMessage(getRepository(), repositoryName,
                        entry.content)).setEscapeModelStrings(false));
            }
        };
src/main/java/com/gitblit/wicket/pages/CommitPage.java
@@ -135,7 +135,7 @@
                item.add(new GravatarImage("noteAuthorAvatar", entry.notesRef.getAuthorIdent()));
                item.add(WicketUtils.createTimestampLabel("authorDate", entry.notesRef
                        .getAuthorIdent().getWhen(), getTimeZone(), getTimeUtils()));
                item.add(new Label("noteContent", messageProcessor().processPlainCommitMessage(repositoryName,
                item.add(new Label("noteContent", messageProcessor().processPlainCommitMessage(getRepository(), repositoryName,
                        entry.content)).setEscapeModelStrings(false));
            }
        };
@@ -198,11 +198,11 @@
                                    .newPathParameter(repositoryName, entry.commitId, path)));
                }
                // quick links
                if (entry.isSubmodule()) {
                    item.add(new ExternalLink("raw", "").setEnabled(false));
                    // submodule
                    item.add(new BookmarkablePageLink<Void>("diff", BlobDiffPage.class, WicketUtils
                            .newPathParameter(repositoryName, entry.commitId, entry.path))
src/main/java/com/gitblit/wicket/pages/RepositoryPage.java
@@ -516,7 +516,7 @@
    protected void addFullText(String wicketId, String text) {
        RepositoryModel model = getRepositoryModel();
        String content = messageProcessor().processCommitMessage(model, text);
        String content = messageProcessor().processCommitMessage(r, model, text);
        String html;
        switch (model.commitMessageRenderer) {
        case MARKDOWN:
src/main/resources/gitblit.css
@@ -34,6 +34,10 @@
    vertical-align: text-bottom;
}
a.bugtraq {
    font-weight: bold;
}
[class^="icon-"], [class*=" icon-"] i {
    /* override for a links that look like bootstrap buttons */
    vertical-align: text-bottom;
src/test/bugtraq/com/syntevo/bugtraq/BugtraqFormatterTest.java
New file
@@ -0,0 +1,170 @@
/*
 * Copyright (c) 2013 by syntevo GmbH. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  o Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  o Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 *  o Neither the name of syntevo GmbH nor the names of
 *    its contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.syntevo.bugtraq;
import junit.framework.*;
import java.util.*;
import org.jetbrains.annotations.*;
public class BugtraqFormatterTest extends TestCase {
    // Accessing ==============================================================
    public void testSimple() throws BugtraqException {
        final BugtraqFormatter formatter = createFormatter(createEntry("https://jira.atlassian.com/browse/%BUGID%", "JRA-\\d+"));
        doTest(formatter, "JRA-7399: Email subject formatting", l("JRA-7399", "https://jira.atlassian.com/browse/JRA-7399"), t(": Email subject formatting"));
        doTest(formatter, " JRA-7399, JRA-7398: Email subject formatting", t(" "), l("JRA-7399", "https://jira.atlassian.com/browse/JRA-7399"), t(", "), l("JRA-7398", "https://jira.atlassian.com/browse/JRA-7398"), t(": Email subject formatting"));
        doTest(formatter, "Fixed JRA-7399", t("Fixed "), l("JRA-7399", "https://jira.atlassian.com/browse/JRA-7399"));
    }
    public void testTwoNonIntersectingConfigurations() throws BugtraqException {
        final BugtraqFormatter formatter = createFormatter(createEntry("https://jira.atlassian.com/browse/%BUGID%", "JRA-\\d+"),
                                                           createEntry("https://issues.apache.org/jira/browse/%BUGID%", "VELOCITY-\\d+"));
        doTest(formatter, "JRA-7399, VELOCITY-847: fix", l("JRA-7399", "https://jira.atlassian.com/browse/JRA-7399"), t(", "), l("VELOCITY-847", "https://issues.apache.org/jira/browse/VELOCITY-847"), t(": fix"));
        doTest(formatter, " JRA-7399: fix/VELOCITY-847", t(" "), l("JRA-7399", "https://jira.atlassian.com/browse/JRA-7399"), t(": fix/"), l("VELOCITY-847", "https://issues.apache.org/jira/browse/VELOCITY-847"));
        doTest(formatter, "JRA-7399VELOCITY-847", l("JRA-7399", "https://jira.atlassian.com/browse/JRA-7399"), l("VELOCITY-847", "https://issues.apache.org/jira/browse/VELOCITY-847"));
    }
    public void testTwoIntersectingConfigurations() throws BugtraqException {
        final BugtraqFormatter formatter = createFormatter(createEntry("https://host1/%BUGID%", "A[AB]"),
                                                           createEntry("https://host2/%BUGID%", "BA[A]?"));
        doTest(formatter, "AA: fix", l("AA", "https://host1/AA"), t(": fix"));
        doTest(formatter, "AB: fix", l("AB", "https://host1/AB"), t(": fix"));
        doTest(formatter, "BA: fix", l("BA", "https://host2/BA"), t(": fix"));
        doTest(formatter, "BAA: fix", l("BAA", "https://host2/BAA"), t(": fix"));
        doTest(formatter, "BAAA: fix", l("BAA", "https://host2/BAA"), t("A: fix"));
        doTest(formatter, "BAAAA: fix", l("BAA", "https://host2/BAA"), l("AA", "https://host1/AA"), t(": fix"));
        doTest(formatter, "BAAAAA: fix", l("BAA", "https://host2/BAA"), l("AA", "https://host1/AA"), t("A: fix"));
        doTest(formatter, "BAAABA: fix", l("BAA", "https://host2/BAA"), l("AB", "https://host1/AB"), t("A: fix"));
        doTest(formatter, "BAAABAA: fix", l("BAA", "https://host2/BAA"), l("AB", "https://host1/AB"), l("AA", "https://host1/AA"), t(": fix"));
        doTest(formatter, "BAAB: fix", l("BAA", "https://host2/BAA"), t("B: fix"));
        doTest(formatter, "BAAAB: fix", l("BAA", "https://host2/BAA"), l("AB", "https://host1/AB"), t(": fix"));
        doTest(formatter, "BAABBA: fix", l("BAA", "https://host2/BAA"), t("B"), l("BA", "https://host2/BA"), t(": fix"));
    }
    // Utils ==================================================================
    private BugtraqFormatter createFormatter(BugtraqEntry ... entries) {
        return new BugtraqFormatter(new BugtraqConfig(Arrays.asList(entries)));
    }
    private BugtraqEntry createEntry(String url, String ... logRegexs) throws BugtraqException {
        return new BugtraqEntry(url, Arrays.asList(logRegexs));
    }
    private Text t(String text) {
        return new Text(text);
    }
    private Link l(String text, String url) {
        return new Link(text, url);
    }
    private void doTest(BugtraqFormatter formatter, String message, Atom ... expectedAtoms) {
        final List<Atom> actualAtoms = new ArrayList<Atom>();
        formatter.formatLogMessage(message, new BugtraqFormatter.OutputHandler() {
            @Override
            public void appendText(@NotNull String text) {
                actualAtoms.add(t(text));
            }
            @Override
            public void appendLink(@NotNull String name, @NotNull String target) {
                actualAtoms.add(l(name, target));
            }
        });
        assertEquals(Arrays.asList(expectedAtoms), actualAtoms);
    }
    // Inner Classes ==========================================================
    private static interface Atom {
    }
    private static class Text implements Atom {
        private final String text;
        private Text(String text) {
            this.text = text;
        }
        @Override
        public String toString() {
            return text;
        }
        @Override
        public int hashCode() {
            return text.hashCode();
        }
        @Override
        public boolean equals(Object obj) {
            if (obj == null || obj.getClass() != getClass()) {
                return false;
            }
            return text.equals(((Text)obj).text);
        }
    }
    private static class Link implements Atom {
        private final String text;
        private final String url;
        private Link(String text, String url) {
            this.text = text;
            this.url = url;
        }
        @Override
        public String toString() {
            return "(" + text + "," + url + ")";
        }
        @Override
        public int hashCode() {
            return text.hashCode() ^ url.hashCode();
        }
        @Override
        public boolean equals(Object obj) {
            if (obj == null || obj.getClass() != getClass()) {
                return false;
            }
            return text.equals(((Link)obj).text)
                    && url.equals(((Link)obj).url);
        }
    }
}
src/test/bugtraq/com/syntevo/bugtraq/BugtraqParserTest.java
New file
@@ -0,0 +1,121 @@
/*
 * Copyright (c) 2013 by syntevo GmbH. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  o Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  o Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 *  o Neither the name of syntevo GmbH nor the names of
 *    its contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.syntevo.bugtraq;
import junit.framework.*;
import java.util.*;
public class BugtraqParserTest extends TestCase {
    // Accessing ==============================================================
    public void testSimple1() throws BugtraqException {
        final BugtraqParser parser = createParser("\\d");
        assertNull(parser.parse(""));
        doTest("1", parser, id(0, 0, "1"));
        doTest("1 2 3", parser, id(0, 0, "1"), id(2, 2, "2"), id(4, 4, "3"));
    }
    public void testSimple2() throws BugtraqException {
        final BugtraqParser parser = createParser("(\\d)");
        assertNull(parser.parse(""));
        doTest("1", parser, id(0, 0, "1"));
        doTest("1 2 3", parser, id(0, 0, "1"), id(2, 2, "2"), id(4, 4, "3"));
    }
    public void testSimple3() throws BugtraqException {
        final BugtraqParser parser = createParser("(SG-\\d)");
        assertNull(parser.parse(""));
        doTest("SG-1", parser, id(0, 3, "SG-1"));
        doTest("SG-1 SG-2 SG-3", parser, id(0, 3, "SG-1"), id(5, 8, "SG-2"), id(10, 13, "SG-3"));
    }
    public void testSimple4() throws BugtraqException {
        final BugtraqParser parser = createParser("SG-(\\d)");
        assertNull(parser.parse(""));
        doTest("SG-1", parser, id(3, 3, "1"));
        doTest("SG-1 SG-2 SG-3", parser, id(3, 3, "1"), id(8, 8, "2"), id(13, 13, "3"));
    }
    public void testTwoLevel1() throws BugtraqException {
        final BugtraqParser parser = createParser("(SG-\\d)", "\\d");
        doTest("SG-1", parser, id(3, 3, "1"));
        doTest("SG-1 SG-2 SG-3", parser, id(3, 3, "1"), id(8, 8, "2"), id(13, 13, "3"));
    }
    public void testTwoLevel2() throws BugtraqException {
        final BugtraqParser parser = createParser("xSG-\\dx", "\\d");
        doTest("SG-1 xSG-2x SG-3", parser, id(9, 9, "2"));
    }
    public void testTwoLevel3() throws BugtraqException {
        final BugtraqParser parser = createParser("[Ii]ssues?:?((\\s*(,|and)?\\s*#\\d+)+)", "\\d");
        doTest("Issues #3, #4 and #5: Git Bugtraq Configuration options (see #12)", parser, id(8, 8, "3"), id(12, 12, "4"), id(19, 19, "5"));
    }
    public void testThreeLevel() throws BugtraqException {
        final BugtraqParser parser = createParser("[ab]\\d[cd]", "a\\dc|b\\dd", "\\d");
        doTest("a1c a2d b3c b4d", parser, id(1, 1, "1"), id(13, 13, "4"));
    }
    public void testFogBugz() throws BugtraqException {
        final BugtraqParser parser = createParser("(?:Bug[zs]?\\s*IDs?\\s*|Cases?)[#:; ]+((\\d+[ ,:;#]*)+)", "\\d");
        doTest("Bug IDs: 3, 4, 5", parser, id(9, 9, "3"), id(12, 12, "4"), id(15, 15, "5"));
    }
    public void testFogBugzInvalid() throws BugtraqException {
        final BugtraqParser parser = createParser("Bug[zs]?\\s*IDs?\\s*|Cases?[#:; ]+((\\d+[ ,:;#]*)+)", "\\d");
        doTest("Bug IDs: 3, 4, 5", parser);
    }
    // Utils ==================================================================
    private BugtraqParser createParser(String ... regexs) throws BugtraqException {
        return BugtraqParser.createInstance(Arrays.asList(regexs));
    }
    private BugtraqParserIssueId id(int from, int to, String id) {
        return new BugtraqParserIssueId(from, to, id);
    }
    private void doTest(String message, BugtraqParser parser, BugtraqParserIssueId... expectedIds) {
        final List<BugtraqParserIssueId> actualIds = parser.parse(message);
        assertEquals(expectedIds.length, actualIds.size());
        for (int index = 0; index < expectedIds.length; index++) {
            final BugtraqParserIssueId expectedId = expectedIds[index];
            final BugtraqParserIssueId actualId = actualIds.get(index);
            assertEquals(expectedId.getFrom(), actualId.getFrom());
            assertEquals(expectedId.getTo(), actualId.getTo());
            assertEquals(expectedId.getId(), actualId.getId());
        }
    }
}