public class MalformedQueryException extends Exception {
    public MalformedQueryException(String msg) {
        super(msg);
    }

    public MalformedQueryException(String msg, Throwable cause) {
        super(msg, cause);
    }
} public enum MatchCondition {
    EQUALS,
    LESS_THAN,
    GREATER_THAN,
    LESS_THAN_EQUALS,
    GREATER_THAN_EQUALS,
    NONE,
    IN,
    RADIUS
} public class Token<T> {
    private final T token;
    private final MatchCondition condition;

    public Token(T token) {
        this(token, MatchCondition.NONE);
    }

    public Token(T token, MatchCondition condition) {
        this.token = token;
        this.condition = condition;
    }

    public T getToken() {
        return token;
    }

    public int hashCode() {
        return token.hashCode();
    }

    public boolean equals(Object compare) {
        if (compare instanceof Token) {
            return token.equals(((Token) compare).getToken());
        }
        return false;
    }

    public MatchCondition getCondition() {
        return condition;
    }
} public interface Analyzer {
    /**
     * Tokenizes raw input
     *
     * @param input raw String that should be turned into tokens
     * @return list of tokens
     * @throws TokenizerException
     */
    public List<Token> tokenize(String input) throws TokenizerException;

    /**
     * Tokenizes raw input. It is possible to turn off generating formerly unknown tokens. This makes sense when
     * tokenizing documents, as any token in a document has to have been already seen in a query to have any chance
     * of matching.
     *
     * @param input              raw String that should be turned into tokens
     * @param dontGenerateNewIds if set to true no new tokens will be generated.
     * @return list of tokens
     * @throws TokenizerException
     */
    public List<Token> tokenize(String input, boolean dontGenerateNewIds) throws TokenizerException;
} public class TokenizerException extends Throwable {
} public class GeoPerimeter {

    public static final double EQUATORIAL_RADIUS = 6378137;

    public static final double POLAR_RADIUS = 6356752.3;

    private final double latitude;
    private final double longitude;
    private final int distance;

    private final double north;
    private final double south;
    private final double east;
    private final double west;

    /**
     * @param latitude  latitude of center
     * @param longitude longitude of center
     * @param distance  in meters
     */
    public GeoPerimeter(double latitude, double longitude, int distance) {
        this.latitude = Math.toRadians(latitude);
        this.longitude = Math.toRadians(longitude);
        this.distance = distance;

        double r = getRadiusAtLatitude(this.latitude);
        double pr = r * Math.cos(this.latitude);
        south = Math.toDegrees(this.latitude - distance / r);
        north = Math.toDegrees(this.latitude + distance / r);
        west = Math.toDegrees(this.longitude - distance / pr);
        east = Math.toDegrees(this.longitude + distance / pr);
    }

    /**
     * Get the the northern limit based on center latitude and distance
     *
     * @return northern limit converted to integer
     */
    public float getNorth() {
        return (float) north;
    }

    /**
     * Get the the southern limit based on center latitude and distance
     *
     * @return southern limit converted to integer
     */
    public float getSouth() {
        return (float) south;
    }

    /**
     * Get the the eastern limit based on center longitude and distance
     *
     * @return eastern limit
     */
    public float getEast() {
        return (float) east;
    }

    /**
     * Get the the western limit based on center longitude and distance
     *
     * @return western limit
     */
    public float getWest() {
        return (float) west;
    }

    /**
     * tests whether the box spans the 180° longitude.
     *
     * @return true if eastern limit lies in western hemisphere and western limit in eastern hemisphere
     */
    public boolean spans180Longitude() {
        if (getEast() > 180) {
            return true;
        } else if (getWest() < -180) {
            return true;
        }
        return false;
    }

    /**
     * Mirrors the perimeter into fake space. Used to mirror perimeter boxes that span hemisphere at 180 longitude.
     *
     * @return additional perimeter for indexing
     */
    public GeoPerimeter mirrorInFakeSpace() {
        if (longitude > 0) {
            return new GeoPerimeter(Math.toDegrees(latitude), Math.toDegrees(longitude) - 360, distance);
        } else {
            return new GeoPerimeter(Math.toDegrees(latitude), Math.toDegrees(longitude) + 360, distance);
        }
    }

    private double getRadiusAtLatitude(double latitude) {
        double a = EQUATORIAL_RADIUS * EQUATORIAL_RADIUS * Math.cos(latitude);
        double b = POLAR_RADIUS * POLAR_RADIUS * Math.sin(latitude);
        double c = EQUATORIAL_RADIUS * Math.cos(latitude);
        double d = POLAR_RADIUS * Math.sin(latitude);

        return Math.sqrt((a * a + b * b) / (c * c + d * d));
    }

    public Rectangle getRectangle() {
        return new Rectangle(getWest(), getSouth(), getEast(), getNorth());
    }
} public class DateTimeIndex extends AbstractRangeFieldIndex<Long> {
    private final DateFormat dateFormat;

    public DateTimeIndex(String name, DateFormat df) {
        super(name);
        dateFormat = df;
    }

    public DateFormat getDateFormat() {
        return dateFormat;
    }

    @Override
    public FieldType getFieldType() {
        return FieldType.DATE_TIME;
    }

} public interface FieldIndex {
    public String getName();

    /**
     * Finds all QueryPosting that match the given Field and returns them as a List
     *
     * @param field     Field instance from Document to match against
     */
    public void match(Field field, Matcher matcher);

    /**
     * Add a single QueryPosting that will be matched if token is present in the field in match()
     *
     * @param token   Token to match on later on
     * @param posting query posting
     */
    public void addPosting(Token token, Long posting);

    /**
     * Delete a QueryPosting from index
     *
     * @param token   Token to delete
     * @param posting query posting
     */
    public void removePosting(Token token, Long posting);

    /**
     * Get FieldType of this FieldIndex
     *
     * @return type of this field
     */
    public FieldType getFieldType();

    public void trim();
} public class FullTextIndex extends AbstractFieldIndex {
    private static final Logger LOGGER = LoggerFactory.getLogger(FullTextIndex.class);

    protected final TIntObjectHashMap<TLongList> index = new TIntObjectHashMap<TLongList>();

    private final Analyzer analyzer;

    public FullTextIndex(String name, Analyzer analyzer) {
        super(name);
        this.analyzer = analyzer;
    }

    @Override
    public void addPosting(Token token, Long posting) {
        TLongList postingList;
        if (index.containsKey((Integer) token.getToken())) {
            postingList = index.get((Integer) token.getToken());
        } else {
            postingList = new TLongArrayList();
            index.put((Integer) token.getToken(), postingList);
        }
        postingList.add(posting);
    }

    @Override
    public void removePosting(Token token, Long posting) {
        TLongList postingList = index.get((Integer) token.getToken());
        if (postingList != null && postingList.contains(posting)) {
            LOGGER.debug("removing posting: " + String.valueOf(posting));
            postingList.remove(posting);
        }
    }

    @Override
    public FieldType getFieldType() {
        return FieldType.FULL_TEXT;
    }

    public TLongList getQueryPostingsForTermId(Token token) {
        Integer t = (Integer) token.getToken();
        if (index.containsKey(t)) {
            return index.get(t);
        }
        return null;
    }

    @Override
    public void match(Field field, Matcher matcher) {
        for (Token token : field.getTokens()) {
            Integer t = (Integer) token.getToken();
            TLongList additionalPostings = index.get(t);
            if (additionalPostings != null) {
                matcher.addHits(additionalPostings);
            }
        }
    }

    public Analyzer getAnalyzer() {
        return analyzer;
    }

    public void trim() {
        for (Object list : index.values()) {
            if (list instanceof ArrayList) {
                ((ArrayList) list).trimToSize();
            }
        }
    }
} public interface Schema {
    /**
     * Add a new FieldIndex to the schema
     *
     * @param fieldName name of the field
     * @param index     index instance used to back this field
     */
    public void addFieldIndex(String fieldName, FieldIndex index) throws SchemaConfigurationError;

    /**
     * Get matches for a specific field. Useful for debugging index implementations.
     *
     * @param fieldIndexName name of the field
     * @param field          field instance from a Document
     * @throws UndefinedIndexFieldException
     */
    public void matchField(String fieldIndexName, Field field, Matcher matcher) throws UndefinedIndexFieldException;

    /**
     * Add query to index. The postings will be added to the corresponding field indices.
     *
     * @param query the parsed query
     * @throws UndefinedIndexFieldException
     */
    public void addQuery(Query query) throws UndefinedIndexFieldException;

    /**
     * Add query to index. The supplied String will be parsed as JSON and the resulting Query object is passed to
     * addQuery(Query query)
     *
     * @param json raw JSON string
     * @throws UndefinedIndexFieldException
     */
    public void addQuery(String json) throws UndefinedIndexFieldException, MalformedQueryException;

    /**
     * Collect all matches for a document
     *
     * @param doc     document instance
     * @param matcher an unused matcher
     * @return matcher
     */
    public Matcher matchDocument(Document doc, Matcher matcher);

    /**
     * Collect all matches from a document. A new Matcher instance is automatically created
     *
     * @param doc document instance
     * @return matcher
     */
    public Matcher matchDocument(Document doc);

    public int getFieldCount();

    public FieldIndex getFieldIndex(String name);

    public AdvancedQueryBuilder getQueryBuilder();

    public DocumentBuilder getDocumentBuilder();

    public Matcher getMatcher();

    public QueryManager getQueryManager();

    public void setQueryStorage(QueryStorage queryStorage);

    /**
     * Tidy up before shutting down, close files and network connections
     */
    public void close();

    /**
     * execute any code that should run after the SchemaBuilder is finished. For example loading queries from disk
     */
    public void init();

    public void deleteQuery(Integer queryId);

    /**
     * optimize storage requirements
     */
    public void trim();
} public class Condition implements ClauseNode {
    private final String fieldName;
    private final Token token;

    /**
     * if "not" is set the condition will result in a posting in a "NOT-index"
     */
    private boolean not;

    public Condition(String fieldName, Token token) {
        this.fieldName = fieldName;
        this.token = token;
        this.not = false;
    }

    public Condition(String fieldName, Token token, boolean not) {
        this.fieldName = fieldName;
        this.token = token;
        this.not = not;
    }

    public String getFieldName() {
        return fieldName;
    }

    public Token getToken() {
        return token;
    }

    public boolean isNot() {
        return not;
    }

    public void setNot(boolean not) {
        this.not = not;
    }

    @Override
    public boolean isLeaf() {
        return true;
    }

    /**
     * AIMA symbols need a unique, java conform name
     *
     * @return symbol name for use in AIMA
     */
    public String getSymbolName() {
        return fieldName + token.getCondition().name() + token.getToken().toString();
    }
}
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.fasterxml.jackson.databind.node.ObjectNode;
import de.danielbasedow.prospecter.core.MalformedQueryException;
import de.danielbasedow.prospecter.core.MatchCondition;
import de.danielbasedow.prospecter.core.Token;
import de.danielbasedow.prospecter.core.analysis.Analyzer;
import de.danielbasedow.prospecter.core.analysis.TokenizerException;
import de.danielbasedow.prospecter.core.geo.GeoPerimeter;
import de.danielbasedow.prospecter.core.index.DateTimeIndex;
import de.danielbasedow.prospecter.core.index.FieldIndex;
import de.danielbasedow.prospecter.core.index.FullTextIndex;
import de.danielbasedow.prospecter.core.query.*;
import de.danielbasedow.prospecter.core.schema.Schema;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.danielbasedow.prospecter.core.query.Condition;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
package de.danielbasedow.prospecter.core.query.build;



public class AdvancedQueryBuilder {
    private static final Logger LOGGER = LoggerFactory.getLogger(AdvancedQueryBuilder.class);
    private static final ObjectMapper objectMapper = new ObjectMapper();

    private final Schema schema;

    public AdvancedQueryBuilder(Schema schema) {
        this.schema = schema;
    }

    /**
     * Build a Query instance from a JSON String
     *
     * @param json JSON string
     * @return query instance
     * @throws de.danielbasedow.prospecter.core.MalformedQueryException
     */
    public Query buildFromJSON(String json) throws MalformedQueryException {
        try {
            ObjectNode root = (ObjectNode) objectMapper.readTree(json);
            int queryId = root.get("id").asInt();

            ClauseNode clauseNode = parseNode(root.get("query"));

            return new Query(queryId, clauseNode);
        } catch (Exception e) {
            throw new MalformedQueryException("Error parsing query", e);
        }
    }

    protected ClauseNode parseNode(JsonNode node) throws MalformedQueryException {
        if (node.getNodeType() == JsonNodeType.OBJECT) {
            if (isCondition(node)) {
                return handleCondition((ObjectNode) node);
            } else {
                return parseClauseNode(node);
            }
        }
        return null;
    }

    protected ClauseNode parseClauseNode(JsonNode node) throws MalformedQueryException {
        Clause.ClauseType clauseType;
        JsonNode arrObj;
        if (node.get("or") != null) {
            clauseType = Clause.ClauseType.OR;
            arrObj = node.get("or");
        } else if (node.get("not") != null) {
            clauseType = Clause.ClauseType.NOT;
            arrObj = node.get("not");
        } else if (node.get("and") != null) {
            clauseType = Clause.ClauseType.AND;
            arrObj = node.get("and");
        } else {
            throw new InvalidQueryException("No known connector found.");
        }
        return new Clause(clauseType, parseClauseChildren((ArrayNode) arrObj));
    }

    protected List<ClauseNode> parseClauseChildren(ArrayNode node) throws MalformedQueryException {
        List<ClauseNode> subClauses = new ArrayList<ClauseNode>();
        for (JsonNode subNode : node) {
            subClauses.add(parseNode(subNode));
        }
        return subClauses;
    }

    protected boolean isCondition(JsonNode node) {
        return node.get("field") != null;
    }

    /**
     * Handle Condition encountered in JSON structure. One condition in JSON can result in several Condition objects,
     * which have to be combined in a Clause using AND
     *
     * @param node ObjectNode for the condition
     * @return Clause containing condition(s) ANDed together
     * @throws MalformedQueryException
     */
    protected ClauseNode handleCondition(ObjectNode node) throws MalformedQueryException {
        String fieldName = node.get("field").asText();
        FieldIndex fieldIndex = schema.getFieldIndex(fieldName);
        if (fieldIndex == null) {
            LOGGER.error("field '" + fieldName + "' not in schema");
            throw new MalformedQueryException("Field '" + fieldName + "' not in schema.");
        }
        GenericFieldHandler handler;
        switch (fieldIndex.getFieldType()) {
            case FULL_TEXT:
                try {
                    return wrap(handleFullText(fieldName, node));
                } catch (TokenizerException e) {