org.codice.ddf.opensearch.endpoint.query.OpenSearchQuery.java Source code

Java tutorial

Introduction

Here is the source code for org.codice.ddf.opensearch.endpoint.query.OpenSearchQuery.java

Source

/**
 * Copyright (c) Codice Foundation
 *
 * <p>This is free software: you can redistribute it and/or modify it under the terms of the GNU
 * Lesser General Public License as published by the Free Software Foundation, either version 3 of
 * the License, or any later version.
 *
 * <p>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 Lesser General Public License for more details. A copy of the GNU Lesser General Public
 * License is distributed along with this program and can be found at
 * <http://www.gnu.org/licenses/lgpl.html>.
 */
package org.codice.ddf.opensearch.endpoint.query;

import ddf.catalog.data.Metacard;
import ddf.catalog.data.Result;
import ddf.catalog.filter.FilterBuilder;
import ddf.catalog.impl.filter.SpatialDistanceFilter;
import ddf.catalog.impl.filter.SpatialFilter;
import ddf.catalog.impl.filter.TemporalFilter;
import ddf.catalog.operation.Query;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.codice.ddf.opensearch.OpenSearchConstants;
import org.codice.ddf.opensearch.endpoint.ASTNode;
import org.codice.ddf.opensearch.endpoint.KeywordFilterGenerator;
import org.codice.ddf.opensearch.endpoint.KeywordTextParser;
import org.codice.ddf.opensearch.endpoint.query.filter.BBoxSpatialFilter;
import org.codice.ddf.opensearch.endpoint.query.filter.PolygonSpatialFilter;
import org.geotools.filter.FilterFactoryImpl;
import org.geotools.styling.UomOgcMapping;
import org.geotools.temporal.object.DefaultInstant;
import org.geotools.temporal.object.DefaultPeriod;
import org.geotools.temporal.object.DefaultPosition;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory;
import org.opengis.filter.FilterVisitor;
import org.opengis.filter.sort.SortBy;
import org.opengis.filter.sort.SortOrder;
import org.opengis.geometry.Geometry;
import org.opengis.temporal.Instant;
import org.opengis.temporal.Period;
import org.parboiled.Parboiled;
import org.parboiled.buffers.InputBuffer;
import org.parboiled.errors.InvalidInputError;
import org.parboiled.errors.ParseError;
import org.parboiled.errors.ParsingException;
import org.parboiled.parserunners.RecoveringParseRunner;
import org.parboiled.support.ParsingResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class OpenSearchQuery implements Query {
    private static final Logger LOGGER = LoggerFactory.getLogger(OpenSearchQuery.class);

    public static final String CARET = "^";

    // TODO remove this and only use filterbuilder
    private static final FilterFactory FILTER_FACTORY = new FilterFactoryImpl();

    private static final Pattern SELECTOR_PATTERN = Pattern.compile(OpenSearchConstants.SELECTORS_DELIMITER);

    private static final Pattern VERSION_PATTERN = Pattern.compile(OpenSearchConstants.VERSIONS_DELIMITER);

    private final FilterBuilder filterBuilder;

    private final Integer startIndex;

    private final Integer count;

    private long maxTimeout;

    private boolean isEnterprise;

    private Set<String> siteIds;

    private SortBy sortBy;

    private List<Filter> filters;

    private List<Filter> spatialFilters;

    /**
     * Creates an Implementation of a DDF Query interface. This object is passed from the endpoint to
     * DDF and will be used by sites to perform queries on their respective systems.
     *
     * @param startIndex Offset of the returned results.
     * @param count Number of results to return.
     * @param sortField Area that the results should be sorted by. Possible values: 'date' and
     *     'relevance'
     * @param sortOrderIn Order of the results. Possible values: 'asc', 'desc'
     * @param maxTimeout Maximum amount of time for a query to respond.
     * @param filterBuilder FilterBuilder object to use for filter creation.
     */
    public OpenSearchQuery(Integer startIndex, Integer count, String sortField, String sortOrderIn, long maxTimeout,
            FilterBuilder filterBuilder) {

        this.startIndex = startIndex;
        this.count = count;
        this.filterBuilder = filterBuilder;
        SortOrder sortOrder;

        // Query must specify a valid sort order if a sort field was specified, i.e., query
        // cannot specify just "date:", must specify "date:asc"
        if (OpenSearchConstants.ORDER_ASCENDING.equalsIgnoreCase(sortOrderIn)) {
            sortOrder = SortOrder.ASCENDING;
        } else if (OpenSearchConstants.ORDER_DESCENDING.equalsIgnoreCase(sortOrderIn)) {
            sortOrder = SortOrder.DESCENDING;
        } else {
            throw new IllegalArgumentException("Incorrect sort order received, must be "
                    + OpenSearchConstants.ORDER_ASCENDING + " or " + OpenSearchConstants.ORDER_DESCENDING);
        }

        if (sortField.equalsIgnoreCase(OpenSearchConstants.SORT_RELEVANCE)) {
            // this.sortPolicy = new SortPolicyImpl( true, Constants.DDF_SORT_QUALIFIER,
            // Constants.SORT_POLICY_VALUE_FULLTEXT, this.sortOrder );
            this.sortBy = FILTER_FACTORY.sort(sortField.toUpperCase(), sortOrder);
        } else if (sortField.equalsIgnoreCase(OpenSearchConstants.SORT_TEMPORAL)) {
            // this.sortPolicy = new SortPolicyImpl( true, Constants.DDF_SORT_QUALIFIER,
            // Constants.SORT_POLICY_VALUE_TEMPORAL, this.sortOrder );
            this.sortBy = FILTER_FACTORY.sort(Result.TEMPORAL, sortOrder);
        } else {
            throw new IllegalArgumentException("Incorrect sort field received, must be "
                    + OpenSearchConstants.SORT_RELEVANCE + " or " + OpenSearchConstants.SORT_TEMPORAL);
        }

        this.maxTimeout = maxTimeout;
        this.filters = new ArrayList<>();
        this.spatialFilters = new ArrayList<>();
        this.siteIds = new HashSet<>();
    }

    public void addContextualFilter(String searchTerms, String selectors) throws ParsingException {
        Filter filter = null;
        KeywordFilterGenerator keywordFilterGenerator = new KeywordFilterGenerator(filterBuilder);

        KeywordTextParser parser = Parboiled.createParser(KeywordTextParser.class);

        // translate the search terms into an abstract syntax tree
        ParsingResult<ASTNode> result = new RecoveringParseRunner(parser.inputPhrase()).run(searchTerms);

        // make sure it's a good result before using it
        if (result.matched && !result.hasErrors()) {
            filter = generateContextualFilter(selectors, keywordFilterGenerator, result);
        } else if (result.hasErrors()) {
            throw new ParsingException("Unable to parse keyword search phrase. " + generateParsingError(result));
        }

        if (filter != null) {
            filters.add(filter);
        }
    }

    private String generateParsingError(ParsingResult<ASTNode> result) {
        StringBuilder parsingErrorBuilder = new StringBuilder(
                "Parsing error" + ((result.parseErrors.size() > 1) ? "s" : "") + ": \n");
        InputBuffer inputBuffer = result.inputBuffer;
        String parsedLine = inputBuffer.extract(0, Integer.MAX_VALUE);

        StringBuilder invalidInputLineBuilder = null;
        for (ParseError parseError : result.parseErrors) {
            StringBuilder otherErrorLineBuilder = getCaratLineStringBuilder(parsedLine);

            // NOTE for some reason, these indexes start at 1, not 0
            int originalEndIndex = inputBuffer.getOriginalIndex(parseError.getEndIndex()) - 1;
            int originalStartIndex = inputBuffer.getOriginalIndex(parseError.getStartIndex()) - 1;

            if (parseError.getClass().isAssignableFrom(InvalidInputError.class)) {
                // Combine all InvalidInputError's
                if (invalidInputLineBuilder == null) {
                    invalidInputLineBuilder = getCaratLineStringBuilder(parsedLine);
                }

                addCaretsToStringBuilder(invalidInputLineBuilder, originalEndIndex, originalStartIndex);
            } else {
                // output other types of errors separately
                parsingErrorBuilder.append("\nError found in: \n");

                addCaretsToStringBuilder(otherErrorLineBuilder, originalEndIndex, originalStartIndex);

                parsingErrorBuilder.append("\n\t");
                parsingErrorBuilder.append(parsedLine);
                parsingErrorBuilder.append("\n\t");
                parsingErrorBuilder.append(otherErrorLineBuilder);
            }
        }

        if (invalidInputLineBuilder != null) {
            // if the first and last occurrence of CARET aren't the same, there are more than one in
            // the string
            parsingErrorBuilder.append("\nInvalid character")
                    .append((invalidInputLineBuilder.indexOf(CARET) != invalidInputLineBuilder.lastIndexOf(CARET))
                            ? "s"
                            : "")
                    .append(" found in: \n");
            parsingErrorBuilder.append("\n\t");
            parsingErrorBuilder.append(parsedLine);
            parsingErrorBuilder.append("\n\t");
            parsingErrorBuilder.append(invalidInputLineBuilder);
        }
        return parsingErrorBuilder.toString();
    }

    private Filter generateContextualFilter(String selectors, KeywordFilterGenerator keywordFilterGenerator,
            ParsingResult<ASTNode> result) throws ParsingException {
        Filter filter = null;

        try {
            if (selectors != null) {
                // generate a filter for each selector
                for (String selector : SELECTOR_PATTERN.split(selectors)) {
                    if (filter == null) {
                        filter = keywordFilterGenerator.getFilterFromASTNode(result.resultValue, selector);
                    } else {
                        filter = filterBuilder.anyOf(filter,
                                keywordFilterGenerator.getFilterFromASTNode(result.resultValue, selector));
                    }
                }
            } else {
                filter = keywordFilterGenerator.getFilterFromASTNode(result.resultValue);
            }

        } catch (IllegalStateException e) {
            throw new ParsingException("Unable to parse keyword search phrase. ", e);
        }

        return filter;
    }

    private void addCaretsToStringBuilder(StringBuilder stringBuilder, int endIndex, int startIndex) {
        for (int insertCaretIndex = startIndex + 1; insertCaretIndex <= endIndex; insertCaretIndex++) {
            stringBuilder.replace(insertCaretIndex, insertCaretIndex + 1, CARET);
        }
    }

    private StringBuilder getCaratLineStringBuilder(String parsedLine) {
        StringBuilder caratLineBuilder = new StringBuilder();
        for (int index = 0; index < parsedLine.length(); index++) {
            caratLineBuilder.append(" ");
        }
        return caratLineBuilder;
    }

    public void addStartEndTemporalFilter(String dateStart, String dateEnd) {
        addTemporalFilter(new TemporalFilter(StringUtils.trimToNull(dateStart), StringUtils.trimToNull(dateEnd)));
    }

    public void addOffsetTemporalFilter(String dateOffset) {
        addTemporalFilter(new TemporalFilter(Long.parseLong(StringUtils.trimToNull(dateOffset))));
    }

    private void addTemporalFilter(TemporalFilter temporalFilter) {
        if (temporalFilter != null) {
            // t1.start < timeType instance < t1.end
            Instant startInstant = new DefaultInstant(new DefaultPosition(temporalFilter.getStartDate()));
            Instant endInstant = new DefaultInstant(new DefaultPosition(temporalFilter.getEndDate()));
            Period period = new DefaultPeriod(startInstant, endInstant);

            Filter filter = FILTER_FACTORY.during(
                    FILTER_FACTORY.property(OpenSearchConstants.SUPPORTED_TEMPORAL_SEARCH_TERM),
                    FILTER_FACTORY.literal(period));
            LOGGER.trace("Adding temporal filter");
            filters.add(filter);
        }
    }

    public void addGeometrySpatialFilter(String geometryWkt) {
        SpatialFilter spatialFilter = new SpatialFilter(geometryWkt);
        addSpatialFilter(spatialFilter);
    }

    public void addBBoxSpatialFilter(String bbox) {
        BBoxSpatialFilter bboxFilter = new BBoxSpatialFilter(bbox);
        addSpatialFilter(bboxFilter);
    }

    public void addPolygonSpatialFilter(String polygon) {
        PolygonSpatialFilter polygonFilter = new PolygonSpatialFilter(polygon);
        addSpatialFilter(polygonFilter);
    }

    public void addPointRadiusSpatialFilter(String lon, String lat, String radius) {
        SpatialDistanceFilter distanceFilter = new SpatialDistanceFilter(lon, lat, radius);

        Geometry geometry = distanceFilter.getGeometry();

        if (geometry != null) {
            Filter filter = FILTER_FACTORY.dwithin(OpenSearchConstants.SUPPORTED_SPATIAL_SEARCH_TERM, geometry,
                    Double.parseDouble(radius), UomOgcMapping.METRE.name());
            LOGGER.trace("Adding spatial filter");
            spatialFilters.add(filter);
        }
    }

    private void addSpatialFilter(SpatialFilter spatialFilter) {
        Geometry geometry = spatialFilter.getGeometry();

        if (geometry != null) {
            Filter filter = FILTER_FACTORY.intersects(OpenSearchConstants.SUPPORTED_SPATIAL_SEARCH_TERM, geometry);
            LOGGER.trace("Adding spatial filter");
            spatialFilters.add(filter);
        }
    }

    public void addTypeFilter(String type, String versions) {
        Filter filter;

        Filter typeFilter;
        if (type.contains("*")) {
            typeFilter = FILTER_FACTORY.like(FILTER_FACTORY.property(Metacard.CONTENT_TYPE), type);
        } else {
            typeFilter = FILTER_FACTORY.equals(FILTER_FACTORY.property(Metacard.CONTENT_TYPE),
                    FILTER_FACTORY.literal(type));
        }

        if (StringUtils.isNotEmpty(versions)) {
            LOGGER.trace("Received versions from client.");
            List<Filter> typeVersionPairsFilters = new ArrayList<>();

            for (String version : VERSION_PATTERN.split(versions)) {
                Filter versionFilter;
                if (version.contains("*")) {
                    versionFilter = FILTER_FACTORY.like(FILTER_FACTORY.property(Metacard.CONTENT_TYPE_VERSION),
                            version);
                } else {
                    versionFilter = FILTER_FACTORY.equals(FILTER_FACTORY.property(Metacard.CONTENT_TYPE_VERSION),
                            FILTER_FACTORY.literal(version));
                }
                typeVersionPairsFilters.add(FILTER_FACTORY.and(typeFilter, versionFilter));
            }

            if (CollectionUtils.isNotEmpty(typeVersionPairsFilters)) {
                filter = FILTER_FACTORY.or(typeVersionPairsFilters);
            } else {
                filter = typeFilter;
            }
        } else {
            filter = typeFilter;
        }

        if (filter != null) {
            LOGGER.trace("Adding type filter");
            filters.add(filter);
        }
    }

    @Override
    public Object accept(FilterVisitor visitor, Object obj) {
        Filter filter = getFilter();

        if (LOGGER.isDebugEnabled()) {
            LOGGER.trace("filter being visited: {}", filter);
        }

        if (filter != null) {
            return filter.accept(visitor, obj);
        }

        return null;
    }

    @Override
    public boolean evaluate(Object object) {
        Filter filter = getFilter();
        if (LOGGER.isDebugEnabled()) {
            LOGGER.trace("filter being evaluated: {}", filter);
        }

        return filter != null && filter.evaluate(object);
    }

    @Override
    public int getStartIndex() {
        return startIndex;
    }

    @Override
    public int getPageSize() {
        return count;
    }

    @Override
    public boolean requestsTotalResultsCount() {
        // always send back total count
        return true;
    }

    @Override
    public long getTimeoutMillis() {
        return maxTimeout;
    }

    public Set<String> getSiteIds() {
        return this.siteIds;
    }

    public void setSiteIds(Set<String> siteIds) {
        this.siteIds = siteIds;
    }

    public boolean isEnterprise() {
        return this.isEnterprise;
    }

    public void setIsEnterprise(boolean isEnterprise) {
        this.isEnterprise = isEnterprise;
    }

    @Override
    public SortBy getSortBy() {
        return sortBy;
    }

    /** @return a combined spatial, contextual and temporal filters together with a logical AND */
    public Filter getFilter() {
        return getSpatialFilterOption().map(filterOption -> getFilterWithSpatial(filterOption))
                .orElseGet(this::getFilterWithoutSpatial);
    }

    private Optional<Filter> getSpatialFilterOption() {
        if (CollectionUtils.isEmpty(spatialFilters)) {
            return Optional.empty();
        } else if (spatialFilters.size() == 1) {
            return Optional.ofNullable(spatialFilters.get(0));
        } else {
            return Optional.ofNullable(FILTER_FACTORY.or(spatialFilters));
        }
    }

    private Filter getFilterWithSpatial(Filter spatialFilter) {
        if (filters.isEmpty()) {
            return spatialFilter;
        } else {
            List<Filter> combinedFilters = new ArrayList<>();
            combinedFilters.addAll(filters);
            combinedFilters.add(spatialFilter);
            return FILTER_FACTORY.and(combinedFilters);
        }
    }

    private Filter getFilterWithoutSpatial() {
        if (filters.size() > 1) {
            return FILTER_FACTORY.and(filters);
        } else if (filters.size() == 1) {
            // If only one filter, then just return it
            // (AND'ing it would create an erroneous </ogc:and> closing tag)
            return filters.get(0);
        } else {
            return null;
        }
    }

    @Override
    public String toString() {
        Filter queryFilter = getFilter();
        if (queryFilter == null) {
            return "OpenSearchQuery: FILTERS:{ NULL }";
        } else {
            return "OpenSearchQuery: " + "FILTERS:{" + queryFilter.toString() + "}";
        }
    }
}