package com.kenai.redminenb.issue;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.netbeans.modules.bugtracking.spi.IssueFinder;
import org.openide.ErrorManager;
import org.openide.util.lookup.ServiceProvider;
/**
* Redmine {@link IssueFinder}
*
* @author Anchialas <anchialas@gmail.com>
*/
@ServiceProvider(service = IssueFinder.class)
public class RedmineIssueFinder implements IssueFinder {
private static final int[] EMPTY_INT_ARR = new int[0];
@Override
public int[] getIssueSpans(CharSequence text) {
int[] result = findBoundaries(text);
return (result != null) ? result : EMPTY_INT_ARR;
}
@Override
public String getIssueId(String issueHyperlinkText) {
int pos = issueHyperlinkText.length() - 1;
while ((pos >= 0) && Impl.isDigit(issueHyperlinkText.charAt(pos))) {
pos--;
}
return issueHyperlinkText.substring(pos + 1);
}
@SuppressFBWarnings(value = "PZLA_PREFER_ZERO_LENGTH_ARRAYS")
private static int[] findBoundaries(CharSequence str) {
try {
return getImpl().findBoundaries(str);
} catch (Exception ex) {
ErrorManager.getDefault().notify(ErrorManager.EXCEPTION, ex);
return null;
}
}
private static Impl getImpl() {
return new Impl();
}
static RedmineIssueFinder getTestInstance() {
return new RedmineIssueFinder();
}
//--------------------------------------------------------------------------
@SuppressFBWarnings(value="DM_CONVERT_CASE", justification = "Relevent case are only invoked with enabled asserts and words are expected to be only english")
private static final class Impl {
/*
* This implementation is quite simple because of two preconditions:
*
* #1 - all defined bug-words ("bug", "issue") and all words of the bug
* number prefix ("duplicate of") consist of lowercase characters from the
* basic latin alphabet (a-z), no spaces #2 - all words that make a
* defined bug number prefix ("duplicate of") are unique
*
* This implementation relies on these preconditions and will may not work
* correctly if one or more of the preconditions are not met.
*
*
* Note that although all the bug words and the bug number prefix must be
* defined as lowercase, the implementation ignores case of characters
* that are passed to input of the finder, as long as these are from the
* basic latin alphabet. All letters that do not belong to the basic latin
* alphabet are considered as garbage, no matter what is their case.
*/
private static final String[] BUGWORDS = new String[]{"bug", "issue"}; //NOI18N
private static final String BUG_NUMBER_PREFIX = "duplicate of"; //NOI18N
private static final String[] BUGNUM_PREFIX_PARTS;
private static final String PUNCT_CHARS = ".,:;()[]{}"; //NOI18N
private static final int LOWER_A = 'a'; //automatic conversion to int
private static final int LOWER_Z = 'z'; //automatic conversion to int
private static final int INIT = 0;
private static final int CHARS = 1;
private static final int HASH = 2;
private static final int HASH_SPC = 3;
private static final int NUM = 4;
private static final int BUGWORD = 5;
private static final int BUGWORD_NL = 6;
private static final int STAR = 7;
private static final int GARBAGE = 8;
private CharSequence str;
private int pos;
private int state;
static {
BUGNUM_PREFIX_PARTS = BUG_NUMBER_PREFIX.split(" "); //NOI18N
boolean asserts = false;
assert asserts = true;
if (asserts) {
/*
* Checks that precondition #1 is met - all bugwords the bug number
* prefix are lowercase:
*/
for (int i = 0; i < BUGWORDS.length; i++) {
assert BUGWORDS[i].equals(
BUGWORDS[i].toLowerCase());
}
for (int i = 0; i < BUGNUM_PREFIX_PARTS.length; i++) {
assert BUGNUM_PREFIX_PARTS[i].equals(
BUGNUM_PREFIX_PARTS[i].toLowerCase());
}
/*
* Checks that precondition #2 is met - all elements of
* BUGNUM_PREFIX_PARTS are unique:
*/
Set<String> bugnumPrefixPartsSet = new HashSet<>(7);
bugnumPrefixPartsSet.addAll(Arrays.asList(BUGNUM_PREFIX_PARTS));
assert bugnumPrefixPartsSet.size() == BUGNUM_PREFIX_PARTS.length;
}
}
/**
* how many parts of the bugnum prefix ({@code "duplicate of"}) have been
* already parsed
*/
private int bugnumPrefixPartsProcessed;
int startOfWord;
int start;
int[] result;
private Impl() {
}
private int[] findBoundaries(CharSequence str) {
reset();
this.str = str;
for (pos = 0; pos < str.length(); pos++) {
handleChar(str.charAt(pos));
}
if (state == NUM) {
storeResult(start, pos);
}
return result;
}
private void reset() {
str = null;
pos = 0;
state = INIT;
bugnumPrefixPartsProcessed = 0;
startOfWord = -1;
start = -1;
result = null;
}
private void handleChar(int c) {
int newState;
boolean keepCountingBugwords = false;
switch (state) {
case INIT:
if (c == '#') {
rememberIsStart();
newState = HASH;
} else if (isLetter(c)) {
rememberIsStart();
newState = CHARS;
} else {
newState = getInitialState(c);
}
break;
case CHARS:
if (isLetter(c)) {
newState = CHARS;
keepCountingBugwords = true;
} else if ((c == ' ') || (c == '\t') || (c == '\r') || (c == '\n')) {
if ((bugnumPrefixPartsProcessed == 0) && isBugword()
|| tryHandleBugnumPrefixPart()) {
newState = ((c == ' ') || (c == '\t')) ? BUGWORD
: BUGWORD_NL;
keepCountingBugwords = true;
} else {
newState = getInitialState(c);
}
} else {
newState = getInitialState(c);
}
break;
case HASH:
case HASH_SPC:
if ((c == ' ') || (c == '\t')) {
newState = HASH_SPC;
} else if (isDigit(c)) {
newState = NUM;
} else {
newState = getInitialState(c);
}
break;
case NUM:
if (isDigit(c)) {
newState = NUM;
} else {
newState = getInitialState(c);
}
break;
case BUGWORD:
case BUGWORD_NL:
if ((state == BUGWORD_NL) && (c == '*')) {
keepCountingBugwords = true;
newState = STAR;
} else if ((c == ' ') || (c == '\t')) {
keepCountingBugwords = true;
newState = state;
} else if ((c == '\r') || (c == '\n')) {
keepCountingBugwords = true;
newState = BUGWORD_NL;
} else if (c == '#') {
newState = HASH;
if (isBugnumPrefix()) {
start = pos; //exclude "duplicate of"
}
} else if (isDigit(c)) {
if (isPartialBugnumPrefix()) {
newState = getInitialState(c);
} else {
newState = NUM;
if (isFullBugnumPrefix()) {
start = pos; //exclude "duplicate of"
}
}
} else if (isLetter(c)) {
newState = CHARS;
if (isPartialBugnumPrefix()) {
keepCountingBugwords = true;
startOfWord = pos;
} else {
/*
* relies on precondition #2 (see top of the class)
*/
rememberIsStart();
}
} else {
newState = getInitialState(c);
}
break;
case STAR:
if ((c == ' ') || (c == '\t')) {
keepCountingBugwords = true;
newState = BUGWORD;
} else if ((c == '\r') || (c == '\n')) {
keepCountingBugwords = true;
newState = BUGWORD_NL;
} else {
newState = getInitialState(c);
}
break;
case GARBAGE:
newState = getInitialState(c);
break;
default:
assert false;
newState = getInitialState(c);
break;
}
if ((state == NUM) && (newState != NUM)) {
if (isSpaceOrPunct(c)) {
storeResult(start, pos);
}
}
if ((newState == INIT) || (newState == GARBAGE)) {
start = -1;
}
if (!keepCountingBugwords) {
bugnumPrefixPartsProcessed = 0;
}
state = newState;
}
private int getInitialState(int c) {
return isSpaceOrPunct(c) ? INIT : GARBAGE;
}
private void rememberIsStart() {
start = pos;
startOfWord = pos;
}
private void storeResult(int start, int end) {
assert (start != -1);
if (result == null) {
result = new int[]{start, end};
} else {
int[] newResult = new int[result.length + 2];
System.arraycopy(result, 0, newResult, 0, result.length);
newResult[result.length] = start;
newResult[result.length + 1] = end;
result = newResult;
}
}
private static boolean isLetter(int c) {
/*
* relies on precondition #1 (see the top of the class)
*/
c |= 0x20;
return ((c >= LOWER_A) && (c <= LOWER_Z));
}
private static boolean isDigit(int c) {
return ((c >= '0') && (c <= '9'));
}
private static boolean isSpaceOrPunct(int c) {
return (c == '\r') || (c == '\n')
|| Character.isSpaceChar(c) || isPunct(c);
}
private static boolean isPunct(int c) {
return PUNCT_CHARS.indexOf(c) != -1;
}
private boolean isBugword() {
/*
* relies on precondition #1 (see the top of the class)
*/
CharSequence word = str.subSequence(start, pos);
for (int i = 0; i < BUGWORDS.length; i++) {
if (equalsIgnoreCase(BUGWORDS[i], word)) {
return true;
}
}
return false;
}
private boolean tryHandleBugnumPrefixPart() {
CharSequence word = str.subSequence(startOfWord, pos);
if ((bugnumPrefixPartsProcessed < BUGNUM_PREFIX_PARTS.length)
&& equalsIgnoreCase(BUGNUM_PREFIX_PARTS[bugnumPrefixPartsProcessed], word)) {
bugnumPrefixPartsProcessed++;
return true;
} else if ((bugnumPrefixPartsProcessed != 0)
&& equalsIgnoreCase(BUGNUM_PREFIX_PARTS[0], word)) {
/*
* handles strings such as "duplicate duplicate of"
*/
bugnumPrefixPartsProcessed = 1;
start = startOfWord;
return true;
} else {
return false;
}
}
private boolean isBugnumPrefix() {
return (bugnumPrefixPartsProcessed != 0);
}
private boolean isPartialBugnumPrefix() {
return (bugnumPrefixPartsProcessed > 0)
&& (bugnumPrefixPartsProcessed < BUGNUM_PREFIX_PARTS.length);
}
private boolean isFullBugnumPrefix() {
return bugnumPrefixPartsProcessed == BUGNUM_PREFIX_PARTS.length;
}
}
private static boolean equalsIgnoreCase(CharSequence pattern, CharSequence str) {
final int patternLength = pattern.length();
if (str.length() != patternLength) {
return false;
}
/*
* relies on precondition #1 (see the top of the class)
*/
for (int i = 0; i < patternLength; i++) {
if ((str.charAt(i) | 0x20) != pattern.charAt(i)) {
return false;
}
}
return true;
}
}