net.sf.jabref.search.rules.SearchExpression.java Source code

Java tutorial

Introduction

Here is the source code for net.sf.jabref.search.rules.SearchExpression.java

Source

/*  Copyright (C) 2003-2011 JabRef contributors.
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.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package net.sf.jabref.search.rules;

import net.sf.jabref.BibtexEntry;
import net.sf.jabref.search.SearchBaseVisitor;
import net.sf.jabref.search.SearchRule;
import net.sf.jabref.search.SearchLexer;
import net.sf.jabref.search.SearchParser;
import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.misc.ParseCancellationException;
import org.antlr.v4.runtime.tree.ParseTree;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class SearchExpression implements SearchRule {

    static public class ThrowingErrorListener extends BaseErrorListener {

        public static final ThrowingErrorListener INSTANCE = new ThrowingErrorListener();

        @Override
        public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line,
                int charPositionInLine, String msg, RecognitionException e) throws ParseCancellationException {
            throw new ParseCancellationException("line " + line + ":" + charPositionInLine + " " + msg);
        }
    }

    private final boolean caseSensitiveSearch;
    private final boolean regExpSearch;

    private ParseTree tree;
    private String query;

    public SearchExpression(boolean caseSensitiveSearch, boolean regExpSearch) throws RecognitionException {
        this.caseSensitiveSearch = caseSensitiveSearch;
        this.regExpSearch = regExpSearch;
    }

    public static boolean isValid(boolean caseSensitive, boolean regExp, String query) {
        return new SearchExpression(caseSensitive, regExp).validateSearchStrings(query);
    }

    public boolean isCaseSensitiveSearch() {
        return this.caseSensitiveSearch;
    }

    public boolean isRegExpSearch() {
        return this.regExpSearch;
    }

    public ParseTree getTree() {
        return this.tree;
    }

    public String getQuery() {
        return this.query;
    }

    private void init(String query) throws ParseCancellationException {
        if (this.query != null && this.query.equals(query)) {
            return;
        }

        SearchLexer lexer = new SearchLexer(new ANTLRInputStream(query));
        lexer.removeErrorListeners(); // no infos on file system
        lexer.addErrorListener(ThrowingErrorListener.INSTANCE);
        SearchParser parser = new SearchParser(new CommonTokenStream(lexer));
        parser.removeErrorListeners(); // no infos on file system
        parser.addErrorListener(ThrowingErrorListener.INSTANCE);
        parser.setErrorHandler(new BailErrorStrategy()); // ParseCancellationException on parse errors
        tree = parser.start();
        this.query = query;
    }

    @Override
    public int applyRule(String query, BibtexEntry bibtexEntry) {
        Boolean result = new BibtexSearchVisitor(caseSensitiveSearch, regExpSearch, bibtexEntry).visit(tree);

        // convert to int value
        return result ? 1 : 0;
    }

    @Override
    public boolean validateSearchStrings(String query) {
        try {
            init(query);
            return true;
        } catch (ParseCancellationException e) {
            return false;
        }
    }

    public enum ComparisonOperator {
        EXACT, CONTAINS, DOES_NOT_CONTAIN;

        public static ComparisonOperator build(String value) {
            if (value.equalsIgnoreCase("CONTAINS") || value.equals("=")) {
                return CONTAINS;
            } else if (value.equalsIgnoreCase("MATCHES") || value.equals("==")) {
                return EXACT;
            } else {
                return DOES_NOT_CONTAIN;
            }
        }
    }

    public static class Comparator {

        private final ComparisonOperator operator;
        private final Pattern fieldPattern;
        private final Pattern valuePattern;

        public Comparator(String field, String value, ComparisonOperator operator, boolean caseSensitive,
                boolean regex) {
            this.operator = operator;

            this.fieldPattern = Pattern.compile(regex ? field : "\\Q" + field + "\\E",
                    caseSensitive ? 0 : Pattern.CASE_INSENSITIVE);
            this.valuePattern = Pattern.compile(regex ? value : "\\Q" + value + "\\E",
                    caseSensitive ? 0 : Pattern.CASE_INSENSITIVE);
        }

        public boolean compare(BibtexEntry entry) {
            // specification of fields to search is done in the search expression itself
            String[] searchKeys = entry.getAllFields().toArray(new String[entry.getAllFields().size()]);

            boolean noSuchField = true;
            // this loop iterates over all regular keys, then over pseudo keys like "type"
            for (int i = 0; i < searchKeys.length + 1; i++) {
                String content;
                if (i - searchKeys.length == 0) {
                    // PSEUDOFIELD_TYPE
                    if (!fieldPattern.matcher("entrytype").matches())
                        continue;
                    content = entry.getType().getName();
                } else {
                    String searchKey = searchKeys[i];
                    if (!fieldPattern.matcher(searchKey).matches())
                        continue;
                    content = entry.getField(searchKey);
                }
                noSuchField = false;
                if (content == null)
                    continue; // paranoia

                if (matchInField(content)) {
                    return true;
                }
            }

            return noSuchField && operator == ComparisonOperator.DOES_NOT_CONTAIN;
        }

        public boolean matchInField(String content) {
            Matcher matcher = valuePattern.matcher(content);
            if (operator == ComparisonOperator.CONTAINS) {
                return matcher.find();
            } else if (operator == ComparisonOperator.EXACT) {
                return matcher.matches();
            } else if (operator == ComparisonOperator.DOES_NOT_CONTAIN) {
                return !matcher.find();
            } else {
                throw new IllegalStateException("MUST NOT HAPPEN");
            }
        }

    }

    /**
     * Search results in boolean. It may be later on converted to an int.
     */
    static class BibtexSearchVisitor extends SearchBaseVisitor<Boolean> {

        private final boolean caseSensitive;
        private final boolean regex;

        private final BibtexEntry entry;

        public BibtexSearchVisitor(boolean caseSensitive, boolean regex, BibtexEntry bibtexEntry) {
            this.caseSensitive = caseSensitive;
            this.regex = regex;
            this.entry = bibtexEntry;
        }

        public boolean comparison(String field, ComparisonOperator operator, String value) {
            return new Comparator(field, value, operator, caseSensitive, regex).compare(entry);
        }

        @Override
        public Boolean visitStart(SearchParser.StartContext ctx) {
            return visit(ctx.expression());
        }

        @Override
        public Boolean visitComparison(SearchParser.ComparisonContext ctx) {
            return comparison(ctx.left.getText(), ComparisonOperator.build(ctx.operator.getText()),
                    ctx.right.getText());
        }

        @Override
        public Boolean visitUnaryExpression(SearchParser.UnaryExpressionContext ctx) {
            return !visit(ctx.expression()); // negate
        }

        @Override
        public Boolean visitParenExpression(SearchParser.ParenExpressionContext ctx) {
            return visit(ctx.expression()); // ignore parenthesis
        }

        @Override
        public Boolean visitBinaryExpression(SearchParser.BinaryExpressionContext ctx) {
            if (ctx.operator.getText().equalsIgnoreCase("AND")) {
                return visit(ctx.left) && visit(ctx.right); // and
            } else {
                return visit(ctx.left) || visit(ctx.right); // or
            }
        }

    }

}