/*
All programs in this directory and subdirectories are published under the
GNU General Public License as described below.
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 2 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, write to the Free Software Foundation, Inc., 59
Temple Place, Suite 330, Boston, MA 02111-1307 USA
Further information about the GNU GPL is available at:
http://www.gnu.org/copyleft/gpl.ja.html
*/
package net.sf.jabref.groups;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.swing.undo.AbstractUndoableEdit;
import net.sf.jabref.*;
import net.sf.jabref.undo.NamedCompound;
import net.sf.jabref.undo.UndoableFieldChange;
import net.sf.jabref.util.QuotedStringTokenizer;
/**
* @author jzieren
*/
public class KeywordGroup extends AbstractGroup implements SearchRule {
public static final String ID = "KeywordGroup:";
private final String m_searchField;
private final String m_searchExpression;
private final boolean m_caseSensitive;
private final boolean m_regExp;
private Pattern m_pattern = null;
/**
* Creates a KeywordGroup with the specified properties.
*/
public KeywordGroup(String name, String searchField,
String searchExpression, boolean caseSensitive, boolean regExp,
int context) throws IllegalArgumentException,
PatternSyntaxException {
super(name, context);
m_searchField = searchField;
m_searchExpression = searchExpression;
m_caseSensitive = caseSensitive;
m_regExp = regExp;
if (m_regExp)
compilePattern();
}
protected void compilePattern() throws IllegalArgumentException,
PatternSyntaxException {
m_pattern = m_caseSensitive ? Pattern.compile("\\b"+m_searchExpression+"\\b")
: Pattern.compile("\\b"+m_searchExpression+"\\b", Pattern.CASE_INSENSITIVE);
}
/**
* Parses s and recreates the KeywordGroup from it.
*
* @param s
* The String representation obtained from
* KeywordGroup.toString()
*/
public static AbstractGroup fromString(String s, BibtexDatabase db,
int version) throws Exception {
if (!s.startsWith(ID))
throw new Exception(
"Internal error: KeywordGroup cannot be created from \""
+ s
+ "\". "
+ "Please report this on www.sf.net/projects/jabref");
QuotedStringTokenizer tok = new QuotedStringTokenizer(s.substring(ID
.length()), SEPARATOR, QUOTE_CHAR);
switch (version) {
case 0: {
String name = tok.nextToken();
String field = tok.nextToken();
String expression = tok.nextToken();
// assume caseSensitive=false and regExp=true for old groups
return new KeywordGroup(Util.unquote(name, QUOTE_CHAR), Util
.unquote(field, QUOTE_CHAR), Util.unquote(expression,
QUOTE_CHAR), false, true, AbstractGroup.INDEPENDENT);
}
case 1:
case 2: {
String name = tok.nextToken();
String field = tok.nextToken();
String expression = tok.nextToken();
boolean caseSensitive = Integer.parseInt(tok.nextToken()) == 1;
boolean regExp = Integer.parseInt(tok.nextToken()) == 1;
return new KeywordGroup(Util.unquote(name, QUOTE_CHAR), Util
.unquote(field, QUOTE_CHAR), Util.unquote(expression,
QUOTE_CHAR), caseSensitive, regExp,
AbstractGroup.INDEPENDENT);
}
case 3: {
String name = tok.nextToken();
int context = Integer.parseInt(tok.nextToken());
String field = tok.nextToken();
String expression = tok.nextToken();
boolean caseSensitive = Integer.parseInt(tok.nextToken()) == 1;
boolean regExp = Integer.parseInt(tok.nextToken()) == 1;
return new KeywordGroup(Util.unquote(name, QUOTE_CHAR), Util
.unquote(field, QUOTE_CHAR), Util.unquote(expression,
QUOTE_CHAR), caseSensitive, regExp, context);
}
default:
throw new UnsupportedVersionException("KeywordGroup", version);
}
}
/**
* @see net.sf.jabref.groups.AbstractGroup#getSearchRule()
*/
public SearchRule getSearchRule() {
return this;
}
/**
* Returns a String representation of this object that can be used to
* reconstruct it.
*/
public String toString() {
return ID + Util.quote(m_name, SEPARATOR, QUOTE_CHAR) + SEPARATOR
+ m_context + SEPARATOR
+ Util.quote(m_searchField, SEPARATOR, QUOTE_CHAR) + SEPARATOR
+ Util.quote(m_searchExpression, SEPARATOR, QUOTE_CHAR)
+ SEPARATOR + (m_caseSensitive ? "1" : "0") + SEPARATOR
+ (m_regExp ? "1" : "0") + SEPARATOR;
}
public boolean supportsAdd() {
return !m_regExp;
}
public boolean supportsRemove() {
return !m_regExp;
}
public AbstractUndoableEdit add(BibtexEntry[] entries) {
if (!supportsAdd())
return null;
if ((entries != null) && (entries.length > 0)) {
NamedCompound ce = new NamedCompound(Globals
.lang("add entries to group"));
boolean modified = false;
for (int i = 0; i < entries.length; i++) {
if (applyRule(null, entries[i]) == 0) {
String oldContent = entries[i]
.getField(m_searchField),
pre = Globals.prefs.get("groupKeywordSeparator");
String newContent = (oldContent == null ? "" : oldContent
+ pre)
+ m_searchExpression;
entries[i].setField(m_searchField, newContent);
// Store undo information.
ce.addEdit(new UndoableFieldChange(entries[i],
m_searchField, oldContent, newContent));
modified = true;
}
}
if (modified)
ce.end();
return modified ? ce : null;
}
return null;
}
public AbstractUndoableEdit remove(BibtexEntry[] entries) {
if (!supportsRemove())
return null;
if ((entries != null) && (entries.length > 0)) {
NamedCompound ce = new NamedCompound(Globals
.lang("remove from group"));
boolean modified = false;
for (int i = 0; i < entries.length; ++i) {
if (applyRule(null, entries[i]) > 0) {
String oldContent = entries[i]
.getField(m_searchField);
removeMatches(entries[i]);
// Store undo information.
ce.addEdit(new UndoableFieldChange(entries[i],
m_searchField, oldContent, entries[i]
.getField(m_searchField)));
modified = true;
}
}
if (modified)
ce.end();
return modified ? ce : null;
}
return null;
}
public boolean equals(Object o) {
if (!(o instanceof KeywordGroup))
return false;
KeywordGroup other = (KeywordGroup) o;
return m_name.equals(other.m_name)
&& m_searchField.equals(other.m_searchField)
&& m_searchExpression.equals(other.m_searchExpression)
&& m_caseSensitive == other.m_caseSensitive
&& m_regExp == other.m_regExp
&& getHierarchicalContext() == other.getHierarchicalContext();
}
/*
* (non-Javadoc)
*
* @see net.sf.jabref.groups.AbstractGroup#contains(java.util.Map,
* net.sf.jabref.BibtexEntry)
*/
public boolean contains(Map<String, String> searchOptions, BibtexEntry entry) {
return contains(entry);
}
public boolean contains(BibtexEntry entry) {
String content = entry.getField(m_searchField);
if (content == null)
return false;
if (m_regExp)
return m_pattern.matcher(content).find();
if (m_caseSensitive)
return containsWord(m_searchExpression, content);
return containsWord(m_searchExpression.toLowerCase(), content.toLowerCase());
}
/**
* Look for the given non-regexp string in another string, but check whether a
* match concerns a complete word, not part of a word.
* @param word The word to look for.
* @param text The string to look in.
* @return true if the word was found, false otherwise.
*/
private static boolean containsWord(String word, String text) {
int piv = 0;
while (piv < text.length()) {
int ind = text.indexOf(word, piv);
if (ind < 0)
return false;
// Found a match. See if it is a complete word:
if (((ind == 0) || !Character.isLetterOrDigit(text.charAt(ind-1))) &&
((ind+word.length() == text.length()) || !Character.isLetterOrDigit(text.charAt(ind+word.length())))) {
return true;
}
else piv = ind+1;
}
return false;
}
/**
* Removes matches of searchString in the entry's field. This is only
* possible if the search expression is not a regExp.
*/
private void removeMatches(BibtexEntry entry) {
String content = entry.getField(m_searchField);
if (content == null)
return; // nothing to modify
StringBuffer sbOrig = new StringBuffer(content);
StringBuffer sbLower = new StringBuffer(content.toLowerCase());
StringBuffer haystack = m_caseSensitive ? sbOrig : sbLower;
String needle = m_caseSensitive ? m_searchExpression
: m_searchExpression.toLowerCase();
int i, j, k;
final String separator = Globals.prefs.get("groupKeywordSeparator");
while ((i = haystack.indexOf(needle)) >= 0) {
sbOrig.replace(i, i + needle.length(), "");
sbLower.replace(i, i + needle.length(), "");
// reduce spaces at i to 1
j = i;
k = i;
while (j - 1 >= 0 && separator.indexOf(haystack.charAt(j - 1)) >= 0)
--j;
while (k < haystack.length() && separator.indexOf(haystack.charAt(k)) >= 0)
++k;
sbOrig.replace(j, k, j >= 0 && k < sbOrig.length() ? separator : "");
sbLower.replace(j, k, j >= 0 && k < sbOrig.length() ? separator : "");
}
String result = sbOrig.toString().trim();
entry.setField(m_searchField, (result.length() > 0 ? result : null));
}
public int applyRule(Map<String, String> searchOptions, BibtexEntry entry) {
return contains(searchOptions, entry) ? 1 : 0;
}
public AbstractGroup deepCopy() {
try {
return new KeywordGroup(m_name, m_searchField, m_searchExpression,
m_caseSensitive, m_regExp, m_context);
} catch (Throwable t) {
// this should never happen, because the constructor obviously
// succeeded in creating _this_ instance!
System.err.println("Internal error: Exception " + t
+ " in KeywordGroup.deepCopy(). "
+ "Please report this on www.sf.net/projects/jabref");
return null;
}
}
public boolean isCaseSensitive() {
return m_caseSensitive;
}
public boolean isRegExp() {
return m_regExp;
}
public String getSearchExpression() {
return m_searchExpression;
}
public String getSearchField() {
return m_searchField;
}
public boolean isDynamic() {
return true;
}
public String getDescription() {
return getDescriptionForPreview(m_searchField, m_searchExpression, m_caseSensitive,
m_regExp);
}
public static String getDescriptionForPreview(String field, String expr,
boolean caseSensitive, boolean regExp) {
StringBuffer sb = new StringBuffer();
sb.append(regExp ? Globals.lang(
"This group contains entries whose <b>%0</b> field contains the regular expression <b>%1</b>",
field, Util.quoteForHTML(expr))
: Globals.lang(
"This group contains entries whose <b>%0</b> field contains the keyword <b>%1</b>",
field, Util.quoteForHTML(expr)));
sb.append(" (").append(caseSensitive ? Globals.lang("case sensitive")
: Globals.lang("case insensitive")).append("). ");
sb.append(regExp ? Globals.lang(
"Entries cannot be manually assigned to or removed from this group.")
: Globals.lang(
"Additionally, entries whose <b>%0</b> field does not contain "
+ "<b>%1</b> can be assigned manually to this group by selecting them "
+ "then using either drag and drop or the context menu. "
+ "This process adds the term <b>%1</b> to "
+ "each entry's <b>%0</b> field. "
+ "Entries can be removed manually from this group by selecting them "
+ "then using the context menu. "
+ "This process removes the term <b>%1</b> from "
+ "each entry's <b>%0</b> field.",
field, Util.quoteForHTML(expr)));
return sb.toString();
}
public String getShortDescription() {
StringBuffer sb = new StringBuffer();
sb.append("<b>");
if (Globals.prefs.getBoolean("groupShowDynamic"))
sb.append("<i>").append(Util.quoteForHTML(getName())).append("</i>");
else
sb.append(Util.quoteForHTML(getName()));
sb.append(Globals.lang("</b> - dynamic group (<b>")).append(m_searchField).
append(Globals.lang("</b> contains <b>")).
append(Util.quoteForHTML(m_searchExpression)).append("</b>)");
switch (getHierarchicalContext()) {
case AbstractGroup.INCLUDING:
sb.append(Globals.lang(", includes subgroups"));
break;
case AbstractGroup.REFINING:
sb.append(Globals.lang(", refines supergroup"));
break;
default:
break;
}
return sb.toString();
}
public String getTypeId() {
return ID;
}
}