From 5edf6d8126f0f27c127886a51c80078004d882e3 Mon Sep 17 00:00:00 2001 From: djarvis Date: Tue, 29 Nov 2016 22:34:55 -0800 Subject: [PATCH] Added missing override annotations. Added final modifier to Interval member variables. Updated documentation for ignoreCase (issue #33) and moved the ignore methods to the top of the builder to reflect their preferred calling order. --- .../org/ahocorasick/interval/Interval.java | 8 +- src/main/java/org/ahocorasick/trie/Trie.java | 296 +++++++++--------- 2 files changed, 158 insertions(+), 146 deletions(-) diff --git a/src/main/java/org/ahocorasick/interval/Interval.java b/src/main/java/org/ahocorasick/interval/Interval.java index 96d4c60..e3b0012 100644 --- a/src/main/java/org/ahocorasick/interval/Interval.java +++ b/src/main/java/org/ahocorasick/interval/Interval.java @@ -2,8 +2,8 @@ package org.ahocorasick.interval; public class Interval implements Intervalable { - private int start; - private int end; + private final int start; + private final int end; /** * Constructs an interval with a start and end position. @@ -21,6 +21,7 @@ public class Interval implements Intervalable { * * @return A number between 0 (start of text) and the text length. */ + @Override public int getStart() { return this.start; } @@ -30,6 +31,7 @@ public class Interval implements Intervalable { * * @return A number between getStart() + 1 and the text length. */ + @Override public int getEnd() { return this.end; } @@ -39,6 +41,7 @@ public class Interval implements Intervalable { * * @return The end position less the start position, plus one. */ + @Override public int size() { return end - start + 1; } @@ -47,6 +50,7 @@ public class Interval implements Intervalable { * Answers whether the given interval overlaps this interval * instance. * + * @param other * @return true The intervals overlap. */ public boolean overlapsWith(final Interval other) { diff --git a/src/main/java/org/ahocorasick/trie/Trie.java b/src/main/java/org/ahocorasick/trie/Trie.java index eee9216..6df993d 100644 --- a/src/main/java/org/ahocorasick/trie/Trie.java +++ b/src/main/java/org/ahocorasick/trie/Trie.java @@ -1,24 +1,20 @@ package org.ahocorasick.trie; -import org.ahocorasick.interval.IntervalTree; -import org.ahocorasick.interval.Intervalable; -import org.ahocorasick.trie.handler.DefaultEmitHandler; -import org.ahocorasick.trie.handler.EmitHandler; - +import static java.lang.Character.isWhitespace; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Queue; import java.util.concurrent.LinkedBlockingDeque; - -import static java.lang.Character.*; - -import java.lang.Character; +import org.ahocorasick.interval.IntervalTree; +import org.ahocorasick.interval.Intervalable; +import org.ahocorasick.trie.handler.DefaultEmitHandler; +import org.ahocorasick.trie.handler.EmitHandler; /** * Based on the Aho-Corasick white paper, Bell technologies: * http://cr.yp.to/bib/1975/aho.pdf - * + * * @author Robert Bor */ public class Trie { @@ -31,41 +27,66 @@ public class Trie { this.trieConfig = trieConfig; this.rootState = new State(); } - + /** * Used by the builder to add a text search keyword. - * + * * @param keyword The search term to add to the list of search terms. + * * @throws NullPointerException if the keyword is null. */ private void addKeyword(String keyword) { - if (keyword.length() > 0) { - if (isCaseInsensitive()) { - keyword = keyword.toLowerCase(); - } - - addState(keyword).addEmit(keyword); + if( keyword.isEmpty() ) { + return; } + + if( isCaseInsensitive() ) { + keyword = keyword.toLowerCase(); + } + + addState(keyword).addEmit(keyword); + } + + /** + * Delegates to addKeyword. + * + * @param keywords List of search term to add to the list of search terms. + */ + private void addKeywords( final String[] keywords ) { + for( final String keyword : keywords ) { + addKeyword( keyword ); + } + } + + /** + * Delegates to addKeyword. + * + * @param keywords List of search term to add to the list of search terms. + */ + private void addKeywords( final Collection keywords ) { + for( final String keyword : keywords ) { + addKeyword( keyword ); + } } private State addState(final String keyword) { return getRootState().addState(keyword); } - + public Collection tokenize(final String text) { final Collection tokens = new ArrayList<>(); final Collection collectedEmits = parseText(text); int lastCollectedPosition = -1; - + for (final Emit emit : collectedEmits) { if (emit.getStart() - lastCollectedPosition > 1) { tokens.add(createFragment(emit, text, lastCollectedPosition)); } - + tokens.add(createMatch(emit, text)); lastCollectedPosition = emit.getEnd(); } - + if (text.length() - lastCollectedPosition > 1) { tokens.add(createFragment(null, text, lastCollectedPosition)); } @@ -73,15 +94,12 @@ public class Trie { return tokens; } - private Token createFragment( - final Emit emit, - final String text, - final int lastCollectedPosition) { - return new FragmentToken(text.substring(lastCollectedPosition + 1, emit == null ? text.length() : emit.getStart())); + private Token createFragment(final Emit emit, final String text, final int lastCollectedPosition) { + return new FragmentToken(text.substring(lastCollectedPosition+1, emit == null ? text.length() : emit.getStart())); } - private Token createMatch(final Emit emit, final String text) { - return new MatchToken(text.substring(emit.getStart(), emit.getEnd() + 1), emit); + private Token createMatch(Emit emit, String text) { + return new MatchToken(text.substring(emit.getStart(), emit.getEnd()+1), emit); } @SuppressWarnings("unchecked") @@ -100,7 +118,7 @@ public class Trie { } if (!trieConfig.isAllowOverlaps()) { - IntervalTree intervalTree = new IntervalTree((List) (List) collectedEmits); + IntervalTree intervalTree = new IntervalTree((List)(List)collectedEmits); intervalTree.removeOverlaps((List) (List) collectedEmits); } @@ -113,15 +131,15 @@ public class Trie { public void parseText(final CharSequence text, final EmitHandler emitHandler) { State currentState = getRootState(); - + for (int position = 0; position < text.length(); position++) { Character character = text.charAt(position); - + // TODO: Maybe lowercase the entire string at once? if (trieConfig.isCaseInsensitive()) { - character = toLowerCase(character); + character = Character.toLowerCase(character); } - + currentState = getState(currentState, character); if (storeEmits(position, currentState, emitHandler) && trieConfig.isStopOnHit()) { return; @@ -129,28 +147,35 @@ public class Trie { } } + /** + * The first matching text sequence. + * + * @param text The text to search for keywords. + * @return null if no matches found. + */ public Emit firstMatch(final CharSequence text) { if (!trieConfig.isAllowOverlaps()) { // Slow path. Needs to find all the matches to detect overlaps. - Collection parseText = parseText(text); + final Collection parseText = parseText(text); + if (parseText != null && !parseText.isEmpty()) { return parseText.iterator().next(); } } else { // Fast path. Returns first match found. State currentState = getRootState(); - + for (int position = 0; position < text.length(); position++) { Character character = text.charAt(position); - + // TODO: Lowercase the entire string at once? if (trieConfig.isCaseInsensitive()) { - character = toLowerCase(character); + character = Character.toLowerCase(character); } - + currentState = getState(currentState, character); Collection emitStrs = currentState.emit(); - + if (emitStrs != null && !emitStrs.isEmpty()) { for (final String emitStr : emitStrs) { final Emit emit = new Emit(position - emitStr.length() + 1, position, emitStr); @@ -165,26 +190,26 @@ public class Trie { } } } - + return null; } private boolean isPartialMatch(final CharSequence searchText, final Emit emit) { return (emit.getStart() != 0 && - isAlphabetic(searchText.charAt(emit.getStart() - 1))) || - (emit.getEnd() + 1 != searchText.length() && - isAlphabetic(searchText.charAt(emit.getEnd() + 1))); + Character.isAlphabetic(searchText.charAt(emit.getStart() - 1))) || + (emit.getEnd() + 1 != searchText.length() && + Character.isAlphabetic(searchText.charAt(emit.getEnd() + 1))); } private void removePartialMatches(final CharSequence searchText, final List collectedEmits) { final List removeEmits = new ArrayList<>(); - + for (final Emit emit : collectedEmits) { if (isPartialMatch(searchText, emit)) { removeEmits.add(emit); } } - + for (final Emit removeEmit : removeEmits) { collectedEmits.remove(removeEmit); } @@ -193,30 +218,29 @@ public class Trie { private void removePartialMatchesWhiteSpaceSeparated(final CharSequence searchText, final List collectedEmits) { final long size = searchText.length(); final List removeEmits = new ArrayList<>(); - + for (final Emit emit : collectedEmits) { if ((emit.getStart() == 0 || isWhitespace(searchText.charAt(emit.getStart() - 1))) && - (emit.getEnd() + 1 == size || isWhitespace(searchText.charAt(emit.getEnd() + 1)))) { + (emit.getEnd() + 1 == size || isWhitespace(searchText.charAt(emit.getEnd() + 1)))) { continue; } removeEmits.add(emit); } - + for (final Emit removeEmit : removeEmits) { collectedEmits.remove(removeEmit); } } - private State getState(final State initialState, final Character character) { - State currentState = initialState; - State updatedState = currentState.nextState(character); - - while (updatedState == null) { + private State getState(State currentState, final Character character) { + State newCurrentState = currentState.nextState(character); + + while (newCurrentState == null) { currentState = currentState.failure(); - updatedState = currentState.nextState(character); + newCurrentState = currentState.nextState(character); } - - return updatedState; + + return newCurrentState; } private void constructFailureStates() { @@ -249,13 +273,10 @@ public class Trie { } } - private boolean storeEmits( - final int position, - final State currentState, - final EmitHandler emitHandler) { + private boolean storeEmits(final int position, final State currentState, final EmitHandler emitHandler) { boolean emitted = false; final Collection emits = currentState.emit(); - + // TODO: The check for empty might be superfluous. if (emits != null && !emits.isEmpty()) { for (final String emit : emits) { @@ -263,31 +284,27 @@ public class Trie { emitted = true; } } - + return emitted; } private boolean isCaseInsensitive() { - return trieConfig.isCaseInsensitive(); + return trieConfig.isCaseInsensitive(); } - + private State getRootState() { - return this.rootState; + return this.rootState; } /** - * Constructs a TrieBuilder instance for configuring the Trie using a fluent - * interface. - * + * Provides a fluent interface for constructing Trie instances. + * * @return The builder used to configure its Trie. */ public static TrieBuilder builder() { return new TrieBuilder(); } - /** - * Provides a fluent interface for constructing Trie instances. - */ public static class TrieBuilder { private final TrieConfig trieConfig = new TrieConfig(); @@ -297,73 +314,75 @@ public class Trie { /** * Default (empty) constructor. */ - private TrieBuilder() { - } + private TrieBuilder() {} /** - * Adds a keyword to the Trie's list of text search keywords. - * - * @param keyword The keyword to add to the list. - * @return This builder. - * @throws NullPointerException if the keyword is null. - */ - public TrieBuilder addKeyword(final CharSequence keyword) { - getTrie().addKeyword(keyword.toString()); - return this; - } - - /** - * Adds a list of keywords to the Trie's list of text search keywords. - * - * @param keywords The keywords to add to the list. - * @return This builder. - */ - public TrieBuilder addKeywords(final CharSequence... keywords) { - for (final CharSequence keyword : keywords) { - addKeyword(keyword); - } - - return this; - } - - /** - * Adds a list of keywords to the Trie's list of text search keywords. - * - * @param keywords The keywords to add to the list. - * @return This builder. - */ - public TrieBuilder addKeywords(final Collection keywords) { - return addKeywords(keywords.toArray(new CharSequence[keywords.size()])); - } - - /** - * Configure the Trie to ignore case when searching for keywords in the - * text. - * + * Configure the Trie to ignore case when searching for keywords in + * the text. This must be called before calling addKeyword because + * the algorithm converts keywords to lowercase as they are added, + * depending on this case sensitivity setting. + * * @return This builder. */ public TrieBuilder ignoreCase() { - getTrieConfig().setCaseInsensitive(true); + this.trieConfig.setCaseInsensitive(true); return this; } /** * Configure the Trie to ignore overlapping keywords. - * + * * @return This builder. */ public TrieBuilder ignoreOverlaps() { - getTrieConfig().setAllowOverlaps(false); + this.trieConfig.setAllowOverlaps(false); return this; } + /** + * Adds a keyword to the Trie's list of text search keywords. + * + * @param keyword The keyword to add to the list. + * + * @return This builder. + * @throws NullPointerException if the keyword is null. + */ + public TrieBuilder addKeyword(final String keyword) { + this.trie.addKeyword(keyword); + return this; + } + + /** + * Adds a list of keywords to the Trie's list of text search keywords. + * + * @param keywords The keywords to add to the list. + * + * @return This builder. + */ + public TrieBuilder addKeywords(final String... keywords) { + this.trie.addKeywords(keywords); + return this; + } + + /** + * Adds a list of keywords to the Trie's list of text search keywords. + * + * @param keywords The keywords to add to the list. + * + * @return This builder. + */ + public TrieBuilder addKeywords(final Collection keywords) { + this.trie.addKeywords(keywords); + return this; + } + /** * Configure the Trie to match whole keywords in the text. - * + * * @return This builder. */ public TrieBuilder onlyWholeWords() { - getTrieConfig().setOnlyWholeWords(true); + this.trieConfig.setOnlyWholeWords(true); return this; } @@ -371,52 +390,39 @@ public class Trie { * Configure the Trie to match whole keywords that are separated by * whitespace in the text. For example, "this keyword thatkeyword" * would only match the first occurrence of "keyword". - * + * * @return This builder. */ public TrieBuilder onlyWholeWordsWhiteSpaceSeparated() { - getTrieConfig().setOnlyWholeWordsWhiteSpaceSeparated(true); + this.trieConfig.setOnlyWholeWordsWhiteSpaceSeparated(true); return this; } /** - * Configure the Trie to stop searching for matches after the first - * keyword is found in the text. - * + * Configure the Trie to stop after the first keyword is found in the + * text. + * * @return This builder. */ - public TrieBuilder onlyFirstMatch() { - getTrieConfig().setStopOnHit(true); + public TrieBuilder stopOnHit() { + trie.trieConfig.setStopOnHit(true); return this; } /** - * Construct the Trie using the builder settings. - * + * Configure the Trie based on the builder settings. + * * @return The configured Trie. */ public Trie build() { - getTrie().constructFailureStates(); - return getTrie(); - } - - private Trie getTrie() { + this.trie.constructFailureStates(); return this.trie; } - - private TrieConfig getTrieConfig() { - return this.trieConfig; - } - - /** - * @deprecated Use onlyFirstMatch() - */ - public TrieBuilder stopOnHit() { - return onlyFirstMatch(); - } - + /** * @deprecated Use ignoreCase() + * + * @return This builder. */ public TrieBuilder caseInsensitive() { return ignoreCase(); @@ -424,6 +430,8 @@ public class Trie { /** * @deprecated Use ignoreOverlaps() + * + * @return This builder. */ public TrieBuilder removeOverlaps() { return ignoreOverlaps();