net.di2e.ecdr.querylanguage.basic.CDRKeywordQueryLanguage.java Source code

Java tutorial

Introduction

Here is the source code for net.di2e.ecdr.querylanguage.basic.CDRKeywordQueryLanguage.java

Source

/**
 * Copyright (C) 2014 Cohesive Integrations, LLC (info@cohesiveintegrations.com)
 * Copyright (C) 2016 Pink Summit, LLC (info@pinksummit.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.di2e.ecdr.querylanguage.basic;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.ws.rs.core.MultivaluedMap;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.opengis.filter.Filter;
import org.opengis.filter.sort.SortBy;
import org.opengis.filter.sort.SortOrder;
import org.parboiled.Parboiled;
import org.parboiled.parserunners.ParseRunner;
import org.parboiled.parserunners.RecoveringParseRunner;
import org.parboiled.support.ParsingResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.io.ParseException;
import com.vividsolutions.jts.io.WKTReader;
import com.vividsolutions.jts.io.WKTWriter;

import ddf.catalog.data.Metacard;
import ddf.catalog.data.Result;
import ddf.catalog.filter.FilterBuilder;
import ddf.catalog.filter.impl.SortByImpl;
import ddf.catalog.source.UnsupportedQueryException;
import net.di2e.ecdr.api.config.SortTypeConfiguration;
import net.di2e.ecdr.api.query.QueryConfiguration;
import net.di2e.ecdr.api.query.QueryCriteria;
import net.di2e.ecdr.api.query.QueryLanguage;
import net.di2e.ecdr.commons.CDRMetacard;
import net.di2e.ecdr.commons.constants.SearchConstants;
import net.di2e.ecdr.commons.query.CDRQueryCriteriaImpl;
import net.di2e.ecdr.commons.util.DateTypeMap;
import net.di2e.ecdr.commons.util.GeospatialUtils;
import net.di2e.ecdr.commons.util.SearchUtils;
import net.di2e.ecdr.querylanguage.basic.GeospatialCriteria.SpatialOperator;
import net.di2e.ecdr.querylanguage.basic.PropertyCriteria.Operator;
import net.di2e.ecdr.querylanguage.basic.keywordparser.ASTNode;
import net.di2e.ecdr.querylanguage.basic.keywordparser.KeywordTextParser;

public class CDRKeywordQueryLanguage implements QueryLanguage {

    private static final Logger LOGGER = LoggerFactory.getLogger(CDRKeywordQueryLanguage.class);

    private static Map<String, String> queryParametersMap = null;

    private FilterBuilder filterBuilder = null;
    private List<SortTypeConfiguration> sortTypeConfigurationList = null;
    private DateTypeMap dateTypeMap = null;

    public CDRKeywordQueryLanguage(FilterBuilder builder, List<SortTypeConfiguration> sortTypeConfigurations,
            DateTypeMap dateMap) {
        filterBuilder = builder;
        sortTypeConfigurationList = sortTypeConfigurations;
        dateTypeMap = dateMap;

        queryParametersMap = new HashMap<String, String>();
        queryParametersMap.put(SearchConstants.UID_PARAMETER, "geo:uid");
        queryParametersMap.put(SearchConstants.RESOURCE_URI_PARAMETER, "ddf:resource-uri");
        queryParametersMap.put(SearchConstants.CASESENSITIVE_PARAMETER, "cdrsx:caseSensitive");
        queryParametersMap.put(SearchConstants.CONTENT_COLLECTIONS_PARAMETER, "ecdr:collections");
        queryParametersMap.put(SearchConstants.FUZZY_PARAMETER, "ecdr:fuzzy");
        queryParametersMap.put(SearchConstants.BOX_PARAMETER, "geo:box");
        queryParametersMap.put(SearchConstants.LATITUDE_PARAMETER, "geo:lat");
        queryParametersMap.put(SearchConstants.LONGITUDE_PARAMETER, "geo:lon");
        queryParametersMap.put(SearchConstants.RADIUS_PARAMETER, "geo:radius");
        queryParametersMap.put(SearchConstants.GEOMETRY_PARAMETER, "geo:geometry");
        queryParametersMap.put(SearchConstants.POLYGON_PARAMETER, "polygon");
        queryParametersMap.put(SearchConstants.GEO_RELATION_PARAMETER, "geo:relation");
        queryParametersMap.put(SearchConstants.GEO_NAME_PARAMETER, "geo:name");
        queryParametersMap.put(SearchConstants.STARTDATE_PARAMETER, "time:start");
        queryParametersMap.put(SearchConstants.ENDDATE_PARAMETER, "time:end");
        queryParametersMap.put(SearchConstants.DATETYPE_PARAMETER, "cdrsx:dateType");
        queryParametersMap.put(SearchConstants.DATE_RELATION_PARAMETER, "time:relation");
        queryParametersMap.put(SearchConstants.GEORSS_RESULT_FORMAT_PARAMETER, "ecdr:georssFormat");
        queryParametersMap.put(SearchConstants.CONTENT_TYPE_PARAMETER, "ddf:metadata-content-type");
        queryParametersMap.put(SearchConstants.TEXTPATH_PARAMETER, "ecdr:textPath");
        queryParametersMap.put(SearchConstants.SORTKEYS_PARAMETER, "sru:sortKeys");
    }

    @Override
    public String getName() {
        return SearchConstants.CDR_KEYWORD_QUERY_LANGUAGE;
    }

    @Override
    public String getUrlTemplateParameters() {
        StringBuilder sb = new StringBuilder();
        for (Entry<String, String> entry : queryParametersMap.entrySet()) {
            sb.append("&" + entry.getKey() + "={" + entry.getValue() + "?}");
        }
        return sb.toString();
    }

    @Override
    public String getLanguageDescription(QueryConfiguration queryConfig) {
        // @formatter:off
        String description = "CDR Keyword Basic Query Language" + System.lineSeparator()
                + "****************************" + System.lineSeparator()
                + "Usage: To use the CQL query language specify the '" + getName()
                + "' in the {cdrs:queryLanguage} parameter placeholder." + System.lineSeparator()
                + "       The CDR Keyword Basic query language supports booleans (AND, OR, NOT) and parenthesis in the {os:searchTerms} parameter value"
                + System.lineSeparator()
                + "       Additionally the parameters below can be used for temporal, geospatial, property, or enhanced keyword searches"
                + System.lineSeparator() + System.lineSeparator()
                + "The examples below are only for the keywords that can be used in the {os:searchTerms}.  They can be combined with any of the "
                + "additional parameters defined in the sections that follow.       " + System.lineSeparator()
                + "Examples:  ballpark" + System.lineSeparator() + "           ballpark AND goodyear"
                + System.lineSeparator() + "           ballpark AND (goodyear or peoria)" + System.lineSeparator()
                + "           " + System.lineSeparator() + "           " + System.lineSeparator()
                + "**** ID/URI Search Parameters ****" + System.lineSeparator() + System.lineSeparator()
                + "geo:uid - unique identifier of the record, matches the Metacard.ID field"
                + System.lineSeparator() + System.lineSeparator()
                + "ddf:resource-uri - URL encoded resource URI value that will be directly matched on, matches the Metacard.RESOURCE_URI field"
                + System.lineSeparator() + System.lineSeparator() + System.lineSeparator()
                + "**** Contextual Search Parameters ****" + System.lineSeparator() + System.lineSeparator()
                + "cdrsx:caseSensitive - boolean (1 or 0) specifying whether or not the keyword search should be case sensitive"
                + System.lineSeparator() + "            default: 0 (false - case insensitive) "
                + System.lineSeparator() + System.lineSeparator()
                + "ecdr:fuzzy - boolean (1 or 0) specifying whether or not the keyword search should be fuzzy (fuzzy allows for slight misspellings or derivations to be found)"
                + System.lineSeparator() + "            default: ${defaultFuzzyCustom} (${defaultFuzzy}) "
                + System.lineSeparator() + System.lineSeparator() + System.lineSeparator()
                + "**** Geospatial Search Parameters ****" + System.lineSeparator() + System.lineSeparator()
                + "geo:box - comma delimited list of lat/lon (deg) bounding box coordinates (geo format: geo:bbox ~ west,south,east,north). "
                + "This is also commonly referred to by minX, minY, maxX, maxY (where longitude is the X-axis, and latitude is the Y-axis)."
                + System.lineSeparator() + System.lineSeparator()
                + "geo:lat/lon - latitude and longitude, respectively, in decimal degrees (typical GPS receiver WGS84 coordinates). Should include a 'radius' parameter "
                + "that specifies the search radius in meters." + System.lineSeparator() + System.lineSeparator()
                + "geo:radius - the radius (in meters) parameter, used with the lat and lon parameters, specifies the search distance from this point."
                + System.lineSeparator() + "            default: ${defaultRadius}" + System.lineSeparator()
                + System.lineSeparator()
                + "geo:geometry - The geometry is defined using the Well Known Text and supports the following 2D geographic shapes: POINT, LINESTRING, POLYGON, MULTIPOINT, "
                + "MULTILINESTRING, MULTIPOLYGON (the Geometry shall be expressed using the EPSG:4326e)"
                + System.lineSeparator() + "            examples: POINT(1 5)" + System.lineSeparator()
                + "                      POLYGON((1 1,5 1,5 5,1 5,1 1),(2 2,2 3,3 3,3 2,2 2))"
                + System.lineSeparator() + System.lineSeparator()
                + "geo:polygon - (deprecated) polygon defined as comma separated latitude, longitude pairs, in clockwise order, with the last point being the same as the first "
                + "in order to close the polygon." + System.lineSeparator()
                + "            example: 45.256,-110.45,46.46,-109.48,43.84,-109.86,45.256,-110.45"
                + System.lineSeparator() + System.lineSeparator()
                + "geo:relation - spatial operator for the relation to the result set " + System.lineSeparator()
                + "            default: intersects" + System.lineSeparator()
                + "            allowedValues: 'intersects', 'contains', 'disjoint'" + System.lineSeparator()
                + System.lineSeparator()
                + "geo:name - A string describing the location (place name) to perform the search "
                + System.lineSeparator() + "            examples: Washington DC" + System.lineSeparator()
                + "                      Baltimore, MD" + System.lineSeparator() + System.lineSeparator()
                + "**** Temporal Search Parameters ****" + System.lineSeparator() + System.lineSeparator()
                + "time:start - replaced with a string of the beginning of the time slice of the search (RFC-3339 - Date and Time format, i.e. YYYY-MM-DDTHH:mm:ssZ). "
                + "Default value of \"1970-01-01T00:00:00Z\" is used when {time:end} is indicated but {time:start} is not specified."
                + System.lineSeparator() + System.lineSeparator()
                + "time:end - replaced with a string of the ending of the time slice of the search (RFC-3339 - Date and Time format, i.e. YYYY-MM-DDTHH:mm:ssZ). "
                + "Current GMT date/time is used when {time:start} is specified but not {time:end}."
                + System.lineSeparator() + System.lineSeparator()
                + "time:relation - temporal operation for the relation to the result set" + System.lineSeparator()
                + "            default: intersects" + System.lineSeparator()
                + "            allowedValues: 'intersects', 'contains', 'during', 'disjoint', 'equals'"
                + System.lineSeparator() + System.lineSeparator()
                + "cdrsx:dateType - the date type to compare against" + System.lineSeparator()
                + "            default: ${defaultDateType}" + System.lineSeparator()
                + "            allowedValues: ${dateTypeValues}" + System.lineSeparator() + System.lineSeparator()
                + System.lineSeparator() + "**** Content Collections Search Parameters ****"
                + System.lineSeparator() + System.lineSeparator()
                + "ecdr:collections - a comma separated list of content collections to search over.  list of content collections can be retrieved by using the Describe spec"
                + System.lineSeparator() + System.lineSeparator() + System.lineSeparator()
                + "**** Other Parameters ****" + System.lineSeparator() + System.lineSeparator()
                + "ecdr:georssFormat - specifies how to return the results that include geospatial data, can be as GML or as Simple GeoRSS"
                + System.lineSeparator() + "            allowedValues: 'simple', 'gml'" + System.lineSeparator()
                + System.lineSeparator()
                + "ddf:metadata-content-type - comma separate list that maps to the Metacard.CONTENT_TYPE attribute"
                + System.lineSeparator() + System.lineSeparator()
                + "ecdr:textPath - comma separated list of text paths (XPath-like) values to be searched over"
                + System.lineSeparator()
                + "            example: /ddms:Resource/subtitle  (this would return all records that contain an element of subtitle under the ddms:Resource root element"
                + System.lineSeparator() + System.lineSeparator() + System.lineSeparator() + "**** Sort Order ****"
                + System.lineSeparator() + System.lineSeparator()
                + "sru:sortKeys - space-separated list of sort keys, with individual sort keys comprised of a comma-separated sequence of "
                + "sub-parameters in the order listed below." + System.lineSeparator()
                + "    path - Mandatory. An XPath expression for a tagpath to be used in the sort  (wildcards '*' may be supported, see allowed values)"
                + System.lineSeparator()
                + "    sortSchema - Optional. A short name for a URI identifying an XML schema to which the XPath expression applies"
                + System.lineSeparator() + "    ascending - Optional. Boolean, default 'true'."
                + System.lineSeparator() + "    caseSensitive - Optional. Boolean, default 'false'."
                + System.lineSeparator() + "    missingValue - Optional. Default is 'highValue'."
                + System.lineSeparator() + "            examples: Sort by relevance - score,relevance"
                + System.lineSeparator()
                + "                      Sort by updated time descending - entry/date,,false "
                + System.lineSeparator() + "                      Sort by distance - distance,cdrsx"
                + System.lineSeparator() + "            'path' allowedValues: "
                + SearchUtils.getAllowedSortValues(sortTypeConfigurationList) + System.lineSeparator();

        // @formatter:on
        boolean fuzzy = queryConfig.isDefaultFuzzySearch();
        description = StringUtils.replace(description, "${defaultFuzzy}", String.valueOf(fuzzy), 1);
        description = StringUtils.replace(description, "${defaultFuzzyCustom}",
                fuzzy ? SearchConstants.TRUE_STRING : SearchConstants.FALSE_STRING, 1);
        description = StringUtils.replace(description, "${defaultRadius}",
                String.valueOf(queryConfig.getDefaultRadius()), 1);
        description = StringUtils.replace(description, "${defaultDateType}", queryConfig.getDefaultDateType(), 1);
        description = StringUtils.replace(description, "${dateTypeValues}", dateTypeMap.keySet().toString(), 1);
        return description;
    }

    @Override
    public boolean isValidQuery(MultivaluedMap<String, String> queryParameters, boolean strict) {
        boolean isValid = true;
        //if ( strict ) {
        // Todo fill this out
        // queryParameters.get
        //}
        return isValid;
    }

    @Override
    public QueryCriteria getQueryCriteria(MultivaluedMap<String, String> queryParameters,
            QueryConfiguration queryConfig) throws UnsupportedQueryException {
        try {
            LOGGER.debug("Parsing query using the CDRKeywordQueryLanguage parser");
            List<Filter> filters = new ArrayList<Filter>();

            SortBy sortBy = SearchUtils.getSortBy(queryParameters.getFirst(SearchConstants.SORTKEYS_PARAMETER),
                    sortTypeConfigurationList, true);
            StringBuilder humanReadableQuery = new StringBuilder();

            boolean defaultFuzzySearch = queryConfig.isDefaultFuzzySearch();
            double defaultRadius = queryConfig.getDefaultRadius();
            String defaultDateType = queryConfig.getDefaultDateType();

            // keyword parameters
            TextualCriteria textualCriteria = getTextualCriteria(queryParameters, defaultFuzzySearch);
            if (textualCriteria != null) {
                boolean fuzzy = textualCriteria.isFuzzy();
                LOGGER.debug(
                        "Attempting to create a Contextual filter with params keywords=[{}], isCaseSensitive=[{}], fuzzy=[{}]",
                        textualCriteria.getKeywords(), textualCriteria.isCaseSensitive(), fuzzy);
                Filter filter = getContextualFilter(textualCriteria.getKeywords(),
                        textualCriteria.isCaseSensitive(), fuzzy, humanReadableQuery);
                SearchUtils.addFilter(filters, filter);
                if (sortBy == null) {
                    sortBy = new SortByImpl(Result.RELEVANCE, SortOrder.DESCENDING);
                }
            }

            // Geospatial query parameters
            GeospatialCriteria geoCriteria = createGeospatialCriteria(
                    queryParameters.getFirst(SearchConstants.RADIUS_PARAMETER),
                    queryParameters.getFirst(SearchConstants.LATITUDE_PARAMETER),
                    queryParameters.getFirst(SearchConstants.LONGITUDE_PARAMETER),
                    queryParameters.getFirst(SearchConstants.BOX_PARAMETER),
                    queryParameters.getFirst(SearchConstants.GEOMETRY_PARAMETER),
                    queryParameters.getFirst(SearchConstants.POLYGON_PARAMETER),
                    queryParameters.getFirst(SearchConstants.GEO_RELATION_PARAMETER), defaultRadius);
            if (geoCriteria != null) {
                LOGGER.debug(
                        "Attempting to create a Geospatial filter with params radius=[{}], latitude=[{}], longitude=[{}], geometry=[{}]",
                        geoCriteria.getRadius(), geoCriteria.getLatitude(), geoCriteria.getLongitude(),
                        geoCriteria.getGeometryWKT());
                Filter filter = getGeoFilter(geoCriteria.getRadius(), geoCriteria.getLatitude(),
                        geoCriteria.getLongitude(), geoCriteria.isBBox(), geoCriteria.getGeometryWKT(),
                        geoCriteria.getSpatialOperator(), humanReadableQuery);
                SearchUtils.addFilter(filters, filter);
                if (sortBy == null) {
                    sortBy = new SortByImpl(Result.DISTANCE, SortOrder.ASCENDING);
                }
            }

            // Temporal Criteria
            TemporalCriteria temporalCriteria = createTemporalCriteria(
                    queryParameters.getFirst(SearchConstants.STARTDATE_PARAMETER),
                    queryParameters.getFirst(SearchConstants.ENDDATE_PARAMETER),
                    queryParameters.getFirst(SearchConstants.DATETYPE_PARAMETER), humanReadableQuery,
                    defaultDateType);
            if (temporalCriteria != null) {
                LOGGER.debug(
                        "Attempting to create a Temporal filter with params startDate=[{}], endDate=[{}], dateType=[{}]",
                        temporalCriteria.getStartDate(), temporalCriteria.getEndDate(),
                        temporalCriteria.getDateType());
                Filter filter = getTemporalFilter(temporalCriteria.getStartDate(), temporalCriteria.getEndDate(),
                        temporalCriteria.getDateType(), humanReadableQuery);
                SearchUtils.addFilter(filters, filter);
            }

            // Property Criteria
            List<PropertyCriteria> propertyCriteriaList = getPropertyCriteria(queryParameters,
                    queryConfig.getParameterExtensionMap());
            if (propertyCriteriaList != null && !propertyCriteriaList.isEmpty()) {
                for (PropertyCriteria propCriteria : propertyCriteriaList) {
                    LOGGER.debug(
                            "Attempting to create a Property filter with params property=[{}], value=[{}], operator=[{}]",
                            propCriteria.getProperty(), propCriteria.getValue(), propCriteria.getOperator());
                    Filter filter = getPropertyFilter(propCriteria.getProperty(), propCriteria.getValue(),
                            propCriteria.getOperator(), humanReadableQuery);
                    SearchUtils.addFilter(filters, filter);
                }
            }

            if (filters.isEmpty()) {
                throw new UnsupportedQueryException(
                        "There was no valid search criteria presented from the user, cannot complete search");
            }

            SearchUtils.logSort(sortBy);
            return new CDRQueryCriteriaImpl(filterBuilder.allOf(filters), sortBy, humanReadableQuery.toString(),
                    true, queryParameters, new HashMap<String, String>(), queryConfig);
        } catch (Exception e) {
            LOGGER.warn(e.getMessage(), e);
            if (e instanceof UnsupportedQueryException) {
                throw (UnsupportedQueryException) e;
            }
            throw new UnsupportedQueryException("Could not create query criteria from provided query parmaeters",
                    e);
        }
    }

    protected TextualCriteria getTextualCriteria(MultivaluedMap<String, String> queryParameters,
            boolean defaultFuzzySearch) throws UnsupportedQueryException {
        String words = queryParameters.getFirst(SearchConstants.KEYWORD_PARAMETER);

        TextualCriteria textualCriteria = null;
        if (StringUtils.isNotBlank(words)) {
            String stringFuzzy = queryParameters.getFirst(SearchConstants.FUZZY_PARAMETER);
            LOGGER.debug("Attempting to set 'fuzzy' value from request [{}]", stringFuzzy);
            Boolean fuzzy = SearchUtils.getBoolean(stringFuzzy, null);
            if (fuzzy == null) {
                LOGGER.debug("The 'fuzzy' parameter was not specified, defaulting value to [{}]",
                        defaultFuzzySearch);
                fuzzy = defaultFuzzySearch;
            }

            String caseSensitiveString = queryParameters.getFirst(SearchConstants.CASESENSITIVE_PARAMETER);
            LOGGER.debug("Attempting to set '{}' value from request [{}], will default to false if not boolean",
                    SearchConstants.CASESENSITIVE_PARAMETER, caseSensitiveString);

            textualCriteria = new TextualCriteria(words, SearchUtils.getBoolean(caseSensitiveString, Boolean.FALSE),
                    fuzzy);
        }
        return textualCriteria;
    }

    protected Filter getContextualFilter(String keywords, boolean caseSensitive, boolean fuzzy,
            StringBuilder humanReadableQuery) throws UnsupportedQueryException {
        Filter filter = null;
        if (keywords != null) {
            KeywordTextParser keywordParser = Parboiled.createParser(KeywordTextParser.class);
            ParseRunner<ASTNode> runner = new RecoveringParseRunner<ASTNode>(keywordParser.inputPhrase());
            ParsingResult<ASTNode> parsingResult = runner.run(keywords);

            if (!parsingResult.hasErrors()) {
                try {
                    filter = getFilterFromASTNode(parsingResult.resultValue, caseSensitive, fuzzy);
                } catch (IllegalStateException e) {
                    throw new UnsupportedQueryException("searchTerms parameter [" + keywords
                            + "] was invalid and resulted in the error: " + e.getMessage());
                }
            } else {
                throw new UnsupportedQueryException(
                        "searchTerms parameter [" + keywords + "] was invalid and resulted in the error: "
                                + parsingResult.parseErrors.get(0).getErrorMessage());
            }
            humanReadableQuery.append(" " + SearchConstants.KEYWORD_PARAMETER + "=[" + keywords + "] "
                    + SearchConstants.CASESENSITIVE_PARAMETER + "=[" + caseSensitive + "] "
                    + SearchConstants.FUZZY_PARAMETER + "=[" + fuzzy + "]");
        }
        return filter;
    }

    protected Filter getFilterFromASTNode(ASTNode astNode, boolean caseSensitive, boolean fuzzy) {

        if (astNode.isKeyword()) {
            String keyword = astNode.getKeyword();
            // this means it is an Text Path
            if (keyword.startsWith("{") && keyword.contains("}:")) {
                int endXpath = keyword.lastIndexOf("}:");
                String xpath = keyword.substring(1, endXpath);
                String literal = keyword.substring(endXpath + 2);
                if (literal.trim().isEmpty()) {
                    return filterBuilder.xpath(xpath).exists();
                } else {
                    if (fuzzy) {
                        return filterBuilder.xpath(xpath).like().fuzzyText(literal);
                    } else if (caseSensitive) {
                        return filterBuilder.xpath(xpath).like().caseSensitiveText(literal);
                    } else {
                        return filterBuilder.xpath(xpath).like().text(literal);
                    }
                }
            } else {
                if (fuzzy) {
                    return filterBuilder.attribute(Metacard.ANY_TEXT).like().fuzzyText(astNode.getKeyword());
                } else if (caseSensitive) {
                    return filterBuilder.attribute(Metacard.ANY_TEXT).like()
                            .caseSensitiveText(astNode.getKeyword());
                } else {
                    return filterBuilder.attribute(Metacard.ANY_TEXT).like().text(astNode.getKeyword());
                }
            }
        } else if (astNode.isOperator()) {
            switch (astNode.getOperator()) {
            case AND:
                return filterBuilder.allOf(getFilterFromASTNode(astNode.left(), caseSensitive, fuzzy),
                        getFilterFromASTNode(astNode.right(), caseSensitive, fuzzy));
            case OR:

                return filterBuilder.anyOf(getFilterFromASTNode(astNode.left(), caseSensitive, fuzzy),
                        getFilterFromASTNode(astNode.right(), caseSensitive, fuzzy));
            case NOT: // since NOT really means AND NOT
                return filterBuilder.allOf(getFilterFromASTNode(astNode.left(), caseSensitive, fuzzy),
                        filterBuilder.not(getFilterFromASTNode(astNode.right(), caseSensitive, fuzzy)));
            default:
                throw new IllegalStateException("Unable to generate Filter from invalid OperatorASTNode.");
            }
        }

        throw new IllegalStateException(
                "Unable to generate Filter from ASTNode. Found invalid ASTNode in the tree");
    }

    protected GeospatialCriteria createGeospatialCriteria(String rad, String lat, String lon, String box,
            String geom, String polygon, String geoRelation, double defaultRadius)
            throws UnsupportedQueryException {
        GeospatialCriteria geoCriteria = null;
        if (StringUtils.isNotBlank(box)) {
            try {
                String[] bboxArray = box.split(" |,\\p{Space}?");

                if (bboxArray.length != 3) {
                    double minX = NumberUtils.createDouble(bboxArray[0]);
                    double minY = NumberUtils.createDouble(bboxArray[1]);
                    double maxX = NumberUtils.createDouble(bboxArray[2]);
                    double maxY = NumberUtils.createDouble(bboxArray[3]);
                    geoCriteria = new GeospatialCriteria(minX, minY, maxX, maxY);
                } else {
                    throw new UnsupportedQueryException("Invalid values found for bbox [" + box + "]");
                }

            } catch (NumberFormatException e) {
                LOGGER.warn("Invalid values found for bbox [{}].  Resulted in exception: {}", box, e.getMessage());
                throw new UnsupportedQueryException(
                        "Invalid values found for bbox [" + box + "], values must be numeric.");
            }
            // Only check lat and lon. If Radius is blank is should be defaulted
        } else if (StringUtils.isNotBlank(lat) && StringUtils.isNotBlank(lon)) {
            try {
                double longitude = NumberUtils.createDouble(lon);
                double latitude = NumberUtils.createDouble(lat);
                double radius = StringUtils.isNotBlank(rad) ? NumberUtils.createDouble(rad) : defaultRadius;
                geoCriteria = new GeospatialCriteria(latitude, longitude, radius);

            } catch (NumberFormatException e) {
                LOGGER.warn(
                        "Invalid Number found for lat [{}], lon [{}], and/or radius [{}].  Resulted in exception: {}",
                        lat, lon, rad, e.getMessage());
                throw new UnsupportedQueryException("Invalid Number found for lat [" + lat + "], lon [" + lon
                        + "], and/or radius [" + rad + "].");
            }

        } else if (StringUtils.isNotBlank(geom)) {
            try {
                WKTReader reader = new WKTReader();
                reader.read(geom);
            } catch (ParseException e) {
                LOGGER.warn("The following is not a valid WKT String: {}", geom);
                throw new UnsupportedQueryException("Invalid WKT, cannot create geospatial query.");
            }
            geoCriteria = new GeospatialCriteria(geom);
        } else if (StringUtils.isNotBlank(polygon)) {
            String wkt = GeospatialUtils.polygonToWKT(polygon);
            try {
                WKTReader reader = new WKTReader();
                reader.read(wkt);
            } catch (ParseException e) {
                LOGGER.warn("The following is not a valid WKT String: {}", wkt);
                throw new UnsupportedQueryException("Invalid WKT, cannot create geospatial query.");
            }
            geoCriteria = new GeospatialCriteria(wkt);
        }
        if (geoCriteria != null && geoRelation != null) {
            SpatialOperator spatialOp = SearchUtils.enumEqualsIgnoreCase(SpatialOperator.class, geoRelation);
            if (spatialOp != null) {
                geoCriteria.setSpatialOperator(spatialOp);
            }
        }

        return geoCriteria;
    }

    protected Filter getGeoFilter(Double radius, Double latitude, Double longitude, boolean isBbox, String geometry,
            SpatialOperator operator, StringBuilder humanReadableQuery) throws UnsupportedQueryException {
        Filter filter = null;
        if (latitude != null && longitude != null && radius != null) {
            String wkt = WKTWriter.toPoint(new Coordinate(longitude, latitude));
            filter = filterBuilder.attribute(Metacard.ANY_GEO).withinBuffer().wkt(wkt, radius);
            humanReadableQuery.append(" " + SearchConstants.LATITUDE_PARAMETER + "=[" + latitude + "] "
                    + SearchConstants.LONGITUDE_PARAMETER + "=[" + longitude + "] "
                    + SearchConstants.RADIUS_PARAMETER + "=[" + radius + "]");
        } else {
            filter = getGeoFilter(operator, geometry,
                    isBbox ? SearchConstants.BOX_PARAMETER : SearchConstants.GEOMETRY_PARAMETER,
                    humanReadableQuery);
        }
        return filter;
    }

    protected Filter getGeoFilter(SpatialOperator operator, String wkt, String geoParameter,
            StringBuilder humanReadableQuery) throws UnsupportedQueryException {
        Filter filter = null;
        if (wkt != null) {
            if (operator != null) {
                switch (operator) {
                case Contains:
                    filter = filterBuilder.attribute(Metacard.ANY_GEO).within().wkt(wkt);
                    break;
                case Disjoint:
                    throw new UnsupportedQueryException("Geospatial disjoint query is not currently supported");
                case Within:
                    filter = filterBuilder.attribute(Metacard.ANY_GEO).containing().wkt(wkt);
                    break;
                case Overlaps:
                default:
                    filter = filterBuilder.attribute(Metacard.ANY_GEO).intersecting().wkt(wkt);
                    break;
                }
                humanReadableQuery.append(" " + geoParameter + "=[" + wkt + "] "
                        + SearchConstants.GEO_RELATION_PARAMETER + "=[" + operator.toString().toLowerCase() + "]");
            } else {
                filter = filterBuilder.attribute(Metacard.ANY_GEO).intersecting().wkt(wkt);
                humanReadableQuery.append(" " + geoParameter + "=[" + wkt + "]");
            }
        }
        return filter;

    }

    protected TemporalCriteria createTemporalCriteria(String start, String end, String type,
            StringBuilder humanReadableQuery, String defaultDateType) throws UnsupportedQueryException {
        TemporalCriteria temporalCriteria = null;

        if (StringUtils.isNotBlank(start) || StringUtils.isNotBlank(end)) {
            Date startDate = SearchUtils.parseDate(start);
            Date endDate = SearchUtils.parseDate(end);
            if (startDate != null && endDate != null) {
                if (startDate.after(endDate)) {
                    throw new UnsupportedQueryException(
                            "Start date value [" + startDate + "] cannot be after endDate [" + endDate + "]");
                }
            }
            String dateType = null;
            LOGGER.debug("Getting date type name for type [{}]", type);
            if (StringUtils.isNotBlank(type)) {
                if (dateTypeMap.containsKey(type)) {
                    dateType = dateTypeMap.getMappedValue(type);

                    LOGGER.debug(
                            "Date type value received in map for request value [{}], setting internal query value to [{}]",
                            type, dateType);
                } else {
                    String message = "Date type value not found in map for type [" + type
                            + "], defaulting internal query value to [" + dateType + "]";
                    LOGGER.warn(message);
                    throw new UnsupportedQueryException(message);
                }
            } else {
                dateType = dateTypeMap.getMappedValue(defaultDateType);
                LOGGER.debug(
                        "Date type value was not specified in request, defaulting internal query value to [{}]",
                        dateType);
            }
            temporalCriteria = new TemporalCriteria(startDate, endDate, dateType);
        }
        return temporalCriteria;

    }

    protected Filter getTemporalFilter(Date startDate, Date endDate, String type,
            StringBuilder humanReadableQueryBuilder) throws UnsupportedQueryException {
        Filter filter = null;
        if (startDate != null || endDate != null) {
            if (startDate != null && endDate != null) {
                if (startDate.after(endDate)) {
                    throw new UnsupportedQueryException(
                            "Start date value [" + startDate + "] cannot be after endDate [" + endDate + "]");
                }
                filter = filterBuilder.attribute(type).during().dates(startDate, endDate);
                humanReadableQueryBuilder.append(" " + SearchConstants.STARTDATE_PARAMETER + "=[" + startDate + "] "
                        + SearchConstants.ENDDATE_PARAMETER + "=[" + endDate + "] "
                        + SearchConstants.DATETYPE_PARAMETER + "=[" + type + "]");
            } else if (startDate != null) {
                filter = filterBuilder.attribute(type).after().date(startDate);
                humanReadableQueryBuilder.append(" " + SearchConstants.STARTDATE_PARAMETER + "=[" + startDate + "] "
                        + SearchConstants.DATETYPE_PARAMETER + "=[" + type + "]");
            } else if (endDate != null) {
                filter = filterBuilder.attribute(type).before().date(endDate);
                humanReadableQueryBuilder.append(" " + SearchConstants.ENDDATE_PARAMETER + "=[" + endDate + "] "
                        + SearchConstants.DATETYPE_PARAMETER + "=[" + type + "]");
            }
        }

        return filter;
    }

    protected List<PropertyCriteria> getPropertyCriteria(MultivaluedMap<String, String> queryParameters,
            Map<String, String> parameterExtensionMap) {
        List<PropertyCriteria> criteriaList = new ArrayList<PropertyCriteria>();
        for (Entry<String, List<String>> entry : queryParameters.entrySet()) {
            String key = entry.getKey();
            List<String> valueList = entry.getValue();
            if (CollectionUtils.isNotEmpty(valueList)) {
                String value = valueList.get(0);
                if (StringUtils.isNotBlank(value) && parameterExtensionMap.containsKey(key)) {
                    criteriaList.add(new PropertyCriteria(parameterExtensionMap.get(key), value, Operator.LIKE));
                }
            }
        }
        if (queryParameters.containsKey(SearchConstants.CONTENT_COLLECTIONS_PARAMETER)) {
            String contentCollections = queryParameters.getFirst(SearchConstants.CONTENT_COLLECTIONS_PARAMETER);
            if (StringUtils.isNotEmpty(contentCollections)) {
                criteriaList.add(new PropertyCriteria(CDRMetacard.METACARD_CONTENT_COLLECTION_ATTRIBUTE,
                        contentCollections, Operator.LIKE));
            }
        }

        if (queryParameters.containsKey(SearchConstants.RESOURCE_URI_PARAMETER)) {
            String uriString = queryParameters.getFirst(SearchConstants.RESOURCE_URI_PARAMETER);

            if (StringUtils.isNotEmpty(uriString)) {
                if (uriString.startsWith(SearchConstants.DAD_SCHEME)) {
                    try {
                        String uriSubstring = uriString;
                        StringBuilder sb = new StringBuilder(SearchConstants.DAD_SCHEME);
                        uriSubstring = uriSubstring.substring(SearchConstants.DAD_SCHEME.length());
                        int index = uriSubstring.indexOf('?');
                        sb.append(URLEncoder.encode(uriSubstring.substring(0, index), "UTF-8"));
                        sb.append("?");
                        uriSubstring = uriSubstring.substring(index + 1);
                        index = uriSubstring.indexOf('#');
                        sb.append(URLEncoder.encode(uriSubstring.substring(0, index), "UTF-8"));
                        sb.append("#");
                        uriSubstring = uriSubstring.substring(index + 1);
                        sb.append(URLEncoder.encode(uriSubstring, "UTF-8"));

                        uriString = sb.toString();
                    } catch (UnsupportedEncodingException | RuntimeException e) {
                        LOGGER.warn(
                                "Could parse the 'resource-uri' due to exception so falling back to not parsing: "
                                        + e.getMessage());
                    }
                }
                criteriaList.add(new PropertyCriteria(Metacard.RESOURCE_URI, uriString, Operator.EQUALS));
            }
        }
        if (queryParameters.containsKey(SearchConstants.CONTENT_TYPE_PARAMETER)) {
            String contentTypesString = queryParameters.getFirst(SearchConstants.CONTENT_TYPE_PARAMETER);
            if (StringUtils.isNotEmpty(contentTypesString)) {
                criteriaList.add(new PropertyCriteria(Metacard.CONTENT_TYPE, contentTypesString, Operator.EQUALS));
            }
        }
        return criteriaList;
    }

    protected Filter getPropertyFilter(String property, String value, Operator operator,
            StringBuilder humanReadableQueryBuilder) {
        Filter filter = null;
        if (property != null && operator != null) {

            if (property.equals(Metacard.CONTENT_TYPE)) {
                filter = getContentTypeFilter(value);
                humanReadableQueryBuilder.append(" " + property + "=like[" + value + "] ");
            } else if (property.equals(CDRMetacard.METACARD_CONTENT_COLLECTION_ATTRIBUTE)) {
                filter = getContentCollectionsFilter(property, value);
                humanReadableQueryBuilder.append(" " + property + "=like[" + value + "] ");
            } else {
                if (Operator.EQUALS.equals(operator)) {
                    filter = filterBuilder.attribute(property).equalTo().text(value);
                    humanReadableQueryBuilder.append(" " + property + "=[" + value + "] ");
                } else if (Operator.LIKE.equals(operator)) {
                    filter = filterBuilder.attribute(property).like().text(value);
                    humanReadableQueryBuilder.append(" " + property + "=like[" + value + "] ");
                }
            }
        }
        return filter;
    }

    protected Filter getContentCollectionsFilter(String property, String value) {
        List<Filter> filterList = new ArrayList<Filter>();
        String[] collections = value.split(",");
        for (String collection : collections) {
            filterList.add(filterBuilder.attribute(property).like().text(collection));
        }

        return filterBuilder.anyOf(filterList);
    }

    protected Filter getContentTypeFilter(String value) {
        List<Filter> filterList = new ArrayList<Filter>();
        String[] contentTypes = value.split(",");
        for (String contentType : contentTypes) {
            String[] typeAndVersion = contentType.split(":");
            String type = typeAndVersion[0];
            if (typeAndVersion.length == 1) {
                filterList.add(filterBuilder.attribute(Metacard.CONTENT_TYPE).like().text(type));
            } else {
                List<Filter> typeVersionPairs = new ArrayList<Filter>();
                String[] versions = typeAndVersion[1].split("\\|");
                for (String version : versions) {
                    Filter typeFilter = filterBuilder.attribute(Metacard.CONTENT_TYPE).like().text(type);
                    Filter versionFilter = filterBuilder.attribute(Metacard.CONTENT_TYPE_VERSION).like()
                            .text(version);
                    typeVersionPairs.add(filterBuilder.allOf(typeFilter, versionFilter));
                }

                // Check if we had any type/version pairs and 'OR' them together.
                if (!typeVersionPairs.isEmpty()) {
                    filterList.add(filterBuilder.anyOf(typeVersionPairs));
                }
            }
        }

        return filterBuilder.anyOf(filterList);
    }

}