/* 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.io.StringReader; import java.util.HashMap; import java.util.Map; import java.util.regex.Pattern; import javax.swing.undo.AbstractUndoableEdit; import net.sf.jabref.*; import net.sf.jabref.search.SearchExpressionLexer; import net.sf.jabref.search.SearchExpressionParser; import net.sf.jabref.search.SearchExpressionTreeParser; import net.sf.jabref.search.SearchExpressionTreeParserTokenTypes; import net.sf.jabref.util.QuotedStringTokenizer; import antlr.RecognitionException; import antlr.collections.AST; /** * @author jzieren * * TODO To change the template for this generated type comment go to Window - * Preferences - Java - Code Style - Code Templates */ public class SearchGroup extends AbstractGroup implements SearchRule { public static final String ID = "SearchGroup:"; private final String m_searchExpression; private final boolean m_caseSensitive; private final boolean m_regExp; private final AST m_ast; private static final SearchExpressionTreeParser m_treeParser = new SearchExpressionTreeParser(); /** * If m_searchExpression is in valid syntax for advanced search, <b>this * </b> will do the search; otherwise, either <b>RegExpRule </b> or * <b>SimpleSearchRule </b> will be used. */ private final SearchRule m_searchRule; /** * Creates a SearchGroup with the specified properties. */ public SearchGroup(String name, String searchExpression, boolean caseSensitive, boolean regExp, int context) { super(name, context); m_searchExpression = searchExpression; m_caseSensitive = caseSensitive; m_regExp = regExp; // create AST AST ast = null; try { SearchExpressionParser parser = new SearchExpressionParser( new SearchExpressionLexer(new StringReader( m_searchExpression))); parser.caseSensitive = m_caseSensitive; parser.regex = m_regExp; parser.searchExpression(); ast = parser.getAST(); } catch (Exception e) { ast = null; // nothing to do; set m_ast to null -> regular plaintext search } m_ast = ast; if (m_ast != null) { // do advanced search m_searchRule = this; } else { // do plaintext search if (m_regExp) m_searchRule = new RegExpRule(m_caseSensitive); else m_searchRule = new SimpleSearchRule(m_caseSensitive); } } /** * Parses s and recreates the SearchGroup from it. * * @param s * The String representation obtained from * SearchGroup.toString(), or null if incompatible */ public static AbstractGroup fromString(String s, BibtexDatabase db, int version) throws Exception { if (!s.startsWith(ID)) throw new Exception( "Internal error: SearchGroup 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: case 1: case 2: { String name = tok.nextToken(); String expression = tok.nextToken(); boolean caseSensitive = Integer.parseInt(tok.nextToken()) == 1; boolean regExp = Integer.parseInt(tok.nextToken()) == 1; // version 0 contained 4 additional booleans to specify search // fields; these are ignored now, all fields are always searched return new SearchGroup(Util.unquote(name, QUOTE_CHAR), Util .unquote(expression, QUOTE_CHAR), caseSensitive, regExp, AbstractGroup.INDEPENDENT); } case 3: { String name = tok.nextToken(); int context = Integer.parseInt(tok.nextToken()); String expression = tok.nextToken(); boolean caseSensitive = Integer.parseInt(tok.nextToken()) == 1; boolean regExp = Integer.parseInt(tok.nextToken()) == 1; // version 0 contained 4 additional booleans to specify search // fields; these are ignored now, all fields are always searched return new SearchGroup(Util.unquote(name, QUOTE_CHAR), Util .unquote(expression, QUOTE_CHAR), caseSensitive, regExp, context); } default: throw new UnsupportedVersionException("SearchGroup", version); } } public String getTypeId() { return ID; } /** * @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_searchExpression, SEPARATOR, QUOTE_CHAR) + SEPARATOR + (m_caseSensitive ? "1" : "0") + SEPARATOR + (m_regExp ? "1" : "0") + SEPARATOR; } public String getSearchExpression() { return m_searchExpression; } public boolean supportsAdd() { return false; } public boolean supportsRemove() { return false; } public AbstractUndoableEdit add(BibtexEntry[] entries) { // nothing to do, add is not supported return null; } public AbstractUndoableEdit remove(BibtexEntry[] entries) { // nothing to do, remove is not supported return null; } public boolean equals(Object o) { if (!(o instanceof SearchGroup)) return false; SearchGroup other = (SearchGroup) o; return m_name.equals(other.m_name) && 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 applyRule(searchOptions, entry) == 0 ? false : true; } public boolean contains(BibtexEntry entry) { // use dummy map return contains(new HashMap<String, String>(), entry); } public int applyRule(Map<String, String> searchOptions, BibtexEntry entry) { if (m_ast == null) { // the searchOptions object is a dummy; we need to insert // the actual search expression. searchOptions.put("option", m_searchExpression); return m_searchRule.applyRule(searchOptions, entry); } try { return m_treeParser.apply(m_ast, entry); } catch (RecognitionException e) { return 0; // this should never occur } } public AbstractGroup deepCopy() { try { return new SearchGroup(m_name, 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 SearchGroup.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 boolean isDynamic() { return true; } public String getDescription() { return getDescriptionForPreview(m_searchExpression, m_ast, m_caseSensitive, m_regExp); } public static String getDescriptionForPreview(String expr, AST ast, boolean caseSensitive, boolean regExp) { StringBuffer sb = new StringBuffer(); if (ast == null) { sb.append(regExp ? Globals.lang( "This group contains entries in which any field contains the regular expression <b>%0</b>", Util.quoteForHTML(expr)) : Globals.lang( "This group contains entries in which any field contains the term <b>%0</b>", Util.quoteForHTML(expr))); sb.append(" (").append(caseSensitive ? Globals.lang("case sensitive") : Globals.lang("case insensitive")).append("). "); sb.append(Globals.lang( "Entries cannot be manually assigned to or removed from this group.")); sb.append("<p><br>").append(Globals.lang( "Hint%c To search specific fields only, enter for example%c<p><tt>author%esmith and title%eelectrical</tt>")); return sb.toString(); } // describe advanced search expression sb.append(Globals.lang("This group contains entries in which")).append(" "); sb.append(describeNode(ast, regExp, false, false, false)); sb.append(". "); sb.append(caseSensitive ? Globals.lang("The search is case sensitive.") : Globals.lang("The search is case insensitive.")); return sb.toString(); } protected static String describeNode(AST node, boolean regExp, boolean not, boolean and, boolean or) { StringBuffer sb = new StringBuffer(); switch (node.getType()) { case SearchExpressionTreeParserTokenTypes.And: if (not) sb.append(Globals.lang("not")).append(" "); // if there was an "or" in this subtree so far, braces may be needed if (or || not) sb.append("("); sb.append(describeNode(node.getFirstChild(), regExp, false, true, false)).append(" ").append(Globals.lang("and")).append(" ").append(describeNode(node.getFirstChild() .getNextSibling(), regExp, false, true, false)); if (or || not) sb.append(")"); return sb.toString(); case SearchExpressionTreeParserTokenTypes.Or: if (not) sb.append(Globals.lang("not")).append(" "); // if there was an "and" in this subtree so far, braces may be // needed if (and || not) sb.append("("); sb.append(describeNode(node.getFirstChild(), regExp, false, false, true)).append(" ").append(Globals.lang("or")).append(" ").append(describeNode(node.getFirstChild() .getNextSibling(), regExp, false, false, true)); if (and || not) sb.append(")"); return sb.toString(); case SearchExpressionTreeParserTokenTypes.Not: return describeNode(node.getFirstChild(), regExp, !not, and, or); default: node = node.getFirstChild(); final String field = node.getText(); final boolean regExpFieldSpec = !Pattern.matches("\\w+", field); node = node.getNextSibling(); final int type = node.getType(); node = node.getNextSibling(); final String termQuoted = Util.quoteForHTML(node.getText()); final String fieldSpecQuoted = regExpFieldSpec ? Globals.lang( "any field that matches the regular expression <b>%0</b>", Util.quoteForHTML(field)) : Globals.lang("the field <b>%0</b>", Util.quoteForHTML(field)); switch (type) { case SearchExpressionTreeParserTokenTypes.LITERAL_contains: case SearchExpressionTreeParserTokenTypes.EQUAL: if (regExp) return not ? Globals.lang( "%0 doesn't contain the Regular Expression <b>%1</b>", fieldSpecQuoted, termQuoted) : Globals.lang( "%0 contains the Regular Expression <b>%1</b>", fieldSpecQuoted, termQuoted); return not ? Globals.lang( "%0 doesn't contain the term <b>%1</b>", fieldSpecQuoted, termQuoted) : Globals.lang("%0 contains the term <b>%1</b>", fieldSpecQuoted, termQuoted); case SearchExpressionTreeParserTokenTypes.LITERAL_matches: case SearchExpressionTreeParserTokenTypes.EEQUAL: if (regExp) return not ? Globals.lang( "%0 doesn't match the Regular Expression <b>%1</b>", fieldSpecQuoted, termQuoted) : Globals.lang( "%0 matches the Regular Expression <b>%1</b>", fieldSpecQuoted, termQuoted); return not ? Globals.lang( "%0 doesn't match the term <b>%1</b>", fieldSpecQuoted, termQuoted) : Globals.lang("%0 matches the term <b>%1</b>", fieldSpecQuoted, termQuoted); case SearchExpressionTreeParserTokenTypes.NEQUAL: if (regExp) return not ? Globals.lang( "%0 contains the Regular Expression <b>%1</b>", fieldSpecQuoted, termQuoted) : Globals.lang( "%0 doesn't contain the Regular Expression <b>%1</b>", fieldSpecQuoted, termQuoted); return not ? Globals.lang("%0 contains the term <b>%1</b>", fieldSpecQuoted, termQuoted) : Globals.lang( "%0 doesn't contain the term <b>%1</b>", fieldSpecQuoted, termQuoted); default: return "Internal error: Unknown AST node type. " + "Please report this on www.sf.net/projects/jabref"; // this should never happen } } } 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 (search expression: <b>")).*/ sb.append(Globals.lang("</b> - dynamic group (")+ Globals.lang("search expression: <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 boolean validateSearchStrings(Map<String, String> searchStrings) { return true; } }