org.n52.svalbard.odata.ODataFesParser.java Source code

Java tutorial

Introduction

Here is the source code for org.n52.svalbard.odata.ODataFesParser.java

Source

/*
 * Copyright 2015-2018 52North Initiative for Geospatial Open Source
 * Software GmbH
 *
 * 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 org.n52.svalbard.odata;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import javax.annotation.CheckReturnValue;

import org.apache.olingo.commons.api.edm.Edm;
import org.apache.olingo.commons.api.edm.EdmEnumType;
import org.apache.olingo.commons.api.edm.EdmType;
import org.apache.olingo.commons.api.ex.ODataException;
import org.apache.olingo.commons.core.edm.EdmProviderImpl;
import org.apache.olingo.server.api.uri.UriInfo;
import org.apache.olingo.server.api.uri.UriInfoResource;
import org.apache.olingo.server.api.uri.UriResource;
import org.apache.olingo.server.api.uri.queryoption.expression.BinaryOperatorKind;
import org.apache.olingo.server.api.uri.queryoption.expression.Expression;
import org.apache.olingo.server.api.uri.queryoption.expression.ExpressionVisitException;
import org.apache.olingo.server.api.uri.queryoption.expression.ExpressionVisitor;
import org.apache.olingo.server.api.uri.queryoption.expression.Literal;
import org.apache.olingo.server.api.uri.queryoption.expression.Member;
import org.apache.olingo.server.api.uri.queryoption.expression.MethodKind;
import org.apache.olingo.server.api.uri.queryoption.expression.UnaryOperatorKind;
import org.apache.olingo.server.core.ODataImpl;
import org.apache.olingo.server.core.uri.parser.Parser;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.PrecisionModel;
import org.locationtech.jts.io.ParseException;
import org.locationtech.jts.io.WKTReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.n52.janmayen.Optionals;
import org.n52.shetland.ogc.filter.BinaryLogicFilter;
import org.n52.shetland.ogc.filter.ComparisonFilter;
import org.n52.shetland.ogc.filter.Filter;
import org.n52.shetland.ogc.filter.FilterConstants.BinaryLogicOperator;
import org.n52.shetland.ogc.filter.FilterConstants.ComparisonOperator;
import org.n52.shetland.ogc.filter.FilterConstants.SpatialOperator;
import org.n52.shetland.ogc.filter.FilterConstants.UnaryLogicOperator;
import org.n52.shetland.ogc.filter.SpatialFilter;
import org.n52.shetland.ogc.filter.UnaryLogicFilter;
import org.n52.svalbard.decode.Decoder;
import org.n52.svalbard.decode.DecoderKey;
import org.n52.svalbard.decode.exception.DecodingException;
import org.n52.svalbard.odata.expr.BinaryExpr;
import org.n52.svalbard.odata.expr.BooleanBinaryExpr;
import org.n52.svalbard.odata.expr.BooleanExpr;
import org.n52.svalbard.odata.expr.BooleanUnaryExpr;
import org.n52.svalbard.odata.expr.ComparisonExpr;
import org.n52.svalbard.odata.expr.Expr;
import org.n52.svalbard.odata.expr.ExprVisitor;
import org.n52.svalbard.odata.expr.MemberExpr;
import org.n52.svalbard.odata.expr.MethodCallExpr;
import org.n52.svalbard.odata.expr.UnaryExpr;
import org.n52.svalbard.odata.expr.ValueExpr;

import com.google.common.escape.Escaper;
import com.google.common.net.PercentEscaper;

/**
 * Class to parse OData-based {@code $filter} expression into FES filters. See {@link ObservationCsdlEdmProvider} for
 * the available properties, their types and the resulting value references.
 *
 * @author Christian Autermann
 *
 * @see ObservationCsdlEdmProvider
 */
public class ODataFesParser implements Decoder<Filter<?>, String> {
    private static final String METHOD_CONTAINS = "contains";
    private static final String METHOD_STARTS_WITH = "startswith";
    private static final String METHOD_ENDS_WITH = "endswith";
    private static final String METHOD_GEO_INTERSECTS = "geo.intersects";
    private static final Logger LOG = LoggerFactory.getLogger(ODataFesParser.class);
    private static final String PATH = "/ObservationCollection";
    private static final String FRAGMENT = "";
    private static final String BASE_URI = "/";
    private static final String GEOGRAPHY_TYPE = "geography";
    private static final String SRID_PREFIX = "SRID=";
    private static final String GEOMETRY_TYPE = "geometry";
    private final Escaper urlEscaper;
    private final Edm edm;
    private final Parser parser;
    private final ObservationCsdlEdmProvider csdlProvider;
    private final ODataImpl odata;

    /**
     * Creates a new {@code ODataFesParser}.
     */
    public ODataFesParser() {
        this.urlEscaper = new PercentEscaper("-_.*", false);
        this.odata = new ODataImpl();
        this.csdlProvider = new ObservationCsdlEdmProvider();
        this.edm = new EdmProviderImpl(this.csdlProvider);
        // >=4.2.0
        // this.parser = new Parser(this.edm, this.odata);
        // >=4.0.0 <4.2.0
        this.parser = new Parser();

    }

    @Override
    public Filter<?> decode(String objectToDecode) throws DecodingException {
        LOG.debug("Parsing filter: {}", objectToDecode);
        if (objectToDecode == null || objectToDecode.isEmpty()) {
            return null;
        }
        try {
            String encode = urlEscaper.escape(objectToDecode);
            // >=4.4.0
            // UriInfo parseUri = parser.parseUri(PATH, "$filter=" + encode, FRAGMENT, BASE_URI);
            // >=4.2.0 <4.4.0
            // UriInfo parseUri = parser.parseUri(PATH, "$filter=" + encode, FRAGMENT);
            // >=4.0.0 <4.2.0
            UriInfo parseUri = parser.parseUri(PATH, "$filter=" + encode, FRAGMENT, this.edm);
            return parseUri.getFilterOption().getExpression().accept(new ExpressionGenerator())
                    .accept(new RenamingVisitor(csdlProvider::mapProperty)).accept(new FilterGenerator());
        } catch (ODataException ex) {
            throw new DecodingException(ex);
        }
    }

    @Override
    public Set<DecoderKey> getKeys() {
        // TODO implement ODataFesParser.getKeys()
        return Collections.emptySet();
    }

    /**
     * Parse the value expression as an {@code Geometry} in WKT or EWKT format. Geographies are handled as if they would
     * be geometries.
     *
     * @param val the geometry value
     *
     * @return the geometry
     *
     * @throws DecodingException if the geometry is invalid
     */
    private static Geometry parseGeometry(ValueExpr val) throws DecodingException {
        String value = val.getValue();
        if (value.startsWith(GEOGRAPHY_TYPE)) {
            value = value.substring(GEOGRAPHY_TYPE.length());
        }
        if (value.startsWith(GEOMETRY_TYPE)) {
            value = value.substring(GEOMETRY_TYPE.length());
        }
        value = stripQuotes(value).toUpperCase();
        int srid = 4326;
        if (value.startsWith(SRID_PREFIX)) {
            int sep = value.indexOf(';');
            if (sep > SRID_PREFIX.length() && value.length() > sep) {
                try {
                    srid = Integer.parseInt(value.substring(SRID_PREFIX.length(), sep));
                } catch (NumberFormatException ex) {
                    throw invalidGeometry(val, ex);
                }
                value = value.substring(sep + 1);
            } else {
                throw invalidGeometry(val);
            }
        }
        PrecisionModel precisionModel = new PrecisionModel(PrecisionModel.FLOATING);
        GeometryFactory geometryFactory = new GeometryFactory(precisionModel, srid);
        WKTReader wktReader = new WKTReader(geometryFactory);
        try {

            return wktReader.read(value);
        } catch (ParseException ex) {
            throw invalidGeometry(val, ex);
        }
    }

    /**
     * Get the the pair of value and member expression from the to expressions or {@code Optional.empty()} if the
     * expression do not match the types.
     *
     * @param first  the first expression
     * @param second the second expression
     *
     * @return the member-value-pair
     */
    private static Optional<MemberValueExprPair> getMemberValuePair(Expr first, Expr second) {
        return Optionals.or(first.asMember(), second.asMember()).flatMap(member -> Optionals
                .or(first.asValue(), second.asValue()).map(value -> new MemberValueExprPair(member, value)));
    }

    /**
     * Get the the pair of value and member expression from the to expressions or {@code Optional.empty()} if the
     * expression do not match the types.
     *
     * @param expr the binary expression
     *
     * @return the member-value-pair
     */
    private static Optional<MemberValueExprPair> getMemberValuePair(BinaryExpr<?> expr) {
        return getMemberValuePair(expr.getLeft(), expr.getRight());
    }

    /**
     * Get the the pair of value and member expression from the to expressions or {@code Optional.empty()} if the
     * expression do not match the types.
     *
     * @param expr the binary expression
     *
     * @return the member-value-pair
     */
    private static Optional<MemberValueExprPair> getMemberValuePair(List<Expr> expr) {
        if (expr.size() != 2) {
            return Optional.empty();
        }
        Iterator<Expr> iter = expr.iterator();
        return getMemberValuePair(iter.next(), iter.next());
    }

    /**
     * Strip any enclosing single quotes from the string.
     *
     * @param value the string value
     *
     * @return the string value without quotes
     */
    @CheckReturnValue
    private static String stripQuotes(String value) {
        return value != null && value.length() >= 2 && value.startsWith("'") && value.endsWith("'")
                ? value.substring(1, value.length() - 1)
                : value;
    }

    /**
     * Get the {@code ComparisonOperator} matching the supplied {@code BinaryOperatorKind}.
     *
     * @param op the operator
     *
     * @return the {@code ComparisonOperator} or {@code Optional.empty()} if none matches
     */
    private static Optional<ComparisonOperator> getComparisonOperator(BinaryOperatorKind op) {
        switch (op) {
        case EQ:
            return Optional.of(ComparisonOperator.PropertyIsEqualTo);
        case GE:
            return Optional.of(ComparisonOperator.PropertyIsGreaterThanOrEqualTo);
        case LE:
            return Optional.of(ComparisonOperator.PropertyIsLessThanOrEqualTo);
        case GT:
            return Optional.of(ComparisonOperator.PropertyIsGreaterThan);
        case LT:
            return Optional.of(ComparisonOperator.PropertyIsLessThan);
        case NE:
            return Optional.of(ComparisonOperator.PropertyIsNotEqualTo);
        default:
            return Optional.empty();
        }
    }

    /**
     * Get the {@code BinaryLogicOperator} matching the supplied {@code BinaryOperatorKind}.
     *
     * @param op the operator
     *
     * @return the {@code BinaryLogicOperator} or {@code Optional.empty()} if none matches
     */
    private static Optional<BinaryLogicOperator> getLogicOperator(BinaryOperatorKind op) {
        switch (op) {
        case AND:
            return Optional.of(BinaryLogicOperator.And);
        case OR:
            return Optional.of(BinaryLogicOperator.Or);
        default:
            return Optional.empty();
        }
    }

    /**
     * Createa new {@code DecodingException} indicating that the geometry in {@code val} is invalid.
     *
     * @param val the value containing the invalid geometry
     *
     * @return the exception
     */
    private static DecodingException invalidGeometry(ValueExpr val) {
        return invalidGeometry(val, null);
    }

    /**
     * Createa new {@code DecodingException} indicating that the geometry in {@code val} is invalid.
     *
     * @param val   the value containing the invalid geometry
     * @param cause the exception describing the invalidity
     *
     * @return the exception
     */
    private static DecodingException invalidGeometry(ValueExpr val, Throwable cause) {
        return new DecodingException(cause, "invalid geometry: %s", val.getValue());
    }

    /**
     * Class to hold a pair of member and value expressions.
     */
    private static final class MemberValueExprPair {
        private final MemberExpr member;
        private final ValueExpr value;

        /**
         * Create a new {@code MemberValueExprPair}.
         *
         * @param member the member
         * @param value  the value
         */
        MemberValueExprPair(MemberExpr member, ValueExpr value) {
            this.member = Objects.requireNonNull(member);
            this.value = Objects.requireNonNull(value);
        }

        /**
         * Get the member expression.
         *
         * @return the expression
         */
        MemberExpr getMember() {
            return member;
        }

        /**
         * Get the value expression.
         *
         * @return the expression
         */
        ValueExpr getValue() {
            return value;
        }
    }

    /**
     * Class to generate a {@code Expr} from the Olingo structures.
     */
    private static final class ExpressionGenerator implements ExpressionVisitor<Expr> {

        @Override
        public Expr visitBinaryOperator(BinaryOperatorKind op, Expr left, Expr right)
                throws ExpressionVisitException {
            Supplier<ExpressionVisitException> exceptionSupplier = () -> new ExpressionVisitException(
                    String.format("Operator %s is not supported: %s %s %s", op, left, op, right));
            switch (op) {
            case AND:
            case OR: {
                BinaryLogicOperator operator = getLogicOperator(op).orElseThrow(exceptionSupplier);
                BooleanExpr leftOperand = left.asBoolean().orElseThrow(exceptionSupplier);
                BooleanExpr rightOperand = right.asBoolean().orElseThrow(exceptionSupplier);
                return new BooleanBinaryExpr(operator, leftOperand, rightOperand);
            }
            case EQ:
            case NE:
            case GT:
            case GE:
            case LT:
            case LE: {
                MemberValueExprPair mv = getMemberValuePair(left, right).orElseThrow(exceptionSupplier);
                ComparisonOperator operator = getComparisonOperator(op).orElseThrow(exceptionSupplier);
                return new ComparisonExpr(operator, mv.getMember(), mv.getValue());
            }
            default:
                throw exceptionSupplier.get();
            }

        }

        @Override
        public ValueExpr visitLiteral(Literal literal) {
            return new ValueExpr(stripQuotes(literal.getText()));
        }

        @Override
        public MethodCallExpr visitMethodCall(MethodKind methodCall, List<Expr> parameters) {
            return new MethodCallExpr(methodCall.toString(), parameters);
        }

        @Override
        public UnaryExpr<?> visitUnaryOperator(UnaryOperatorKind op, Expr operand) throws ExpressionVisitException {
            Supplier<ExpressionVisitException> exceptionSupplier = () -> new ExpressionVisitException(
                    String.format("Operator is not supported: %s %s", op, operand));
            switch (op) {
            case NOT:
                return new BooleanUnaryExpr(UnaryLogicOperator.Not,
                        operand.asBoolean().orElseThrow(exceptionSupplier));
            case MINUS:
            default:
                throw exceptionSupplier.get();

            }
        }

        @Override
        public Expr visitLambdaExpression(String fun, String var, Expression expr) throws ExpressionVisitException {
            throw new ExpressionVisitException("Lambda expressions are not supported");
        }

        // >=4.2.0
        // @Override
        public Expr visitMember(Member member) throws ExpressionVisitException {
            return visitMember(member.getResourcePath());
        }

        // >=4.0.0<=4.2.0
        @Override
        public Expr visitMember(UriInfoResource member) throws ExpressionVisitException {
            return new MemberExpr(member.getUriResourceParts().stream().map(UriResource::getSegmentValue)
                    .collect(Collectors.joining("/")));
        }

        @Override
        public Expr visitAlias(String aliasName) throws ExpressionVisitException {
            throw new ExpressionVisitException("aliases are not supported");
        }

        @Override
        public Expr visitTypeLiteral(EdmType type) throws ExpressionVisitException {
            throw new ExpressionVisitException("type literals are not supported");
        }

        @Override
        public Expr visitLambdaReference(String variableName) throws ExpressionVisitException {
            throw new ExpressionVisitException("Lambda references are not supported");
        }

        @Override
        public Expr visitEnum(EdmEnumType type, List<String> enumValues) throws ExpressionVisitException {
            throw new ExpressionVisitException("enums are not supported");
        }
    }

    /**
     * Class to create a {@code Filter} from an {@code Expr}.
     */
    private static final class FilterGenerator implements ExprVisitor<Filter<?>, DecodingException> {
        private static final String WILDCARD = "%";

        @Override
        public Filter<?> visitBooleanBinary(BooleanBinaryExpr expr) throws DecodingException {
            return new BinaryLogicFilter(expr.getOperator(), expr.getLeft().accept(this),
                    expr.getRight().accept(this));
        }

        @Override
        public Filter<?> visitBooleanUnary(BooleanUnaryExpr expr) throws DecodingException {
            return new UnaryLogicFilter(expr.getOperand().accept(this));
        }

        @Override
        public Filter<?> visitComparison(ComparisonExpr expr) throws DecodingException {
            MemberValueExprPair memberValuePair = getMemberValuePair(expr).orElseThrow(this::unsupported);
            return new ComparisonFilter(expr.getOperator(), memberValuePair.getMember().getValue(),
                    memberValuePair.getValue().getValue());
        }

        @Override
        public Filter<?> visitMethodCall(MethodCallExpr expr) throws DecodingException {
            switch (expr.getName()) {
            case METHOD_CONTAINS: {
                MemberValueExprPair mv = getMemberValuePair(expr.getParameters()).orElseThrow(this::unsupported);
                String referenceValue = mv.getMember().getValue();
                String value = WILDCARD + mv.getValue().getValue() + WILDCARD;
                return new ComparisonFilter(ComparisonOperator.PropertyIsLike, referenceValue, value);
            }
            case METHOD_STARTS_WITH: {
                MemberValueExprPair mv = getMemberValuePair(expr.getParameters()).orElseThrow(this::unsupported);
                String referenceValue = mv.getMember().getValue();
                String value = mv.getValue().getValue() + WILDCARD;
                return new ComparisonFilter(ComparisonOperator.PropertyIsLike, referenceValue, value);
            }
            case METHOD_ENDS_WITH: {
                MemberValueExprPair mv = getMemberValuePair(expr.getParameters()).orElseThrow(this::unsupported);
                String referenceValue = mv.getMember().getValue();
                String value = WILDCARD + mv.getValue().getValue();
                return new ComparisonFilter(ComparisonOperator.PropertyIsLike, referenceValue, value);

            }
            case METHOD_GEO_INTERSECTS: {
                MemberValueExprPair mv = getMemberValuePair(expr.getParameters()).orElseThrow(this::unsupported);
                String referenceValue = mv.getMember().getValue();
                if (referenceValue.equals("om:featureOfInterest")) {
                    referenceValue += "/*/sams:shape";
                }
                Geometry geometry = parseGeometry(mv.getValue());
                return new SpatialFilter(SpatialOperator.Intersects, geometry, referenceValue);
            }
            default:
                throw new DecodingException("unsupported method '%s'", expr.getName());
            }
        }

        @Override
        public Filter<?> visitMember(MemberExpr expr) throws DecodingException {
            throw new DecodingException("unexpected member expression '%s'", expr.getValue());
        }

        @Override
        public Filter<?> visitValue(ValueExpr expr) throws DecodingException {
            throw new DecodingException("unexpected value expression '%s'", expr.getValue());
        }

        /**
         * Creates an {@code DecodingException} indicating that the supplied expression is not supported.
         *
         * @return the exception
         */
        private DecodingException unsupported() {
            return new DecodingException("unsupported expression");
        }
    }

    /**
     * Abstract transforming visitor that is able to modify expression.
     *
     * @param <T> The exception type
     */
    private static class AbstractExprTransformer<T extends Throwable> implements ExprVisitor<Expr, T> {

        @Override
        public Expr visitBooleanBinary(BooleanBinaryExpr expr) throws T {
            BinaryLogicOperator op = expr.getOperator();
            BooleanExpr left = expr.getLeft().accept(this).asBoolean().orElseThrow(Error::new);
            BooleanExpr right = expr.getRight().accept(this).asBoolean().orElseThrow(Error::new);
            return new BooleanBinaryExpr(op, left, right);
        }

        @Override
        public Expr visitBooleanUnary(BooleanUnaryExpr expr) throws T {
            UnaryLogicOperator op = expr.getOperator();
            BooleanExpr operand = expr.getOperand().accept(this).asBoolean().orElseThrow(Error::new);
            return new BooleanUnaryExpr(op, operand);
        }

        @Override
        public Expr visitComparison(ComparisonExpr expr) throws T {
            ComparisonOperator op = expr.getOperator();
            Expr left = expr.getLeft().accept(this);
            Expr right = expr.getRight().accept(this);
            return new ComparisonExpr(op, left, right);
        }

        @Override
        public Expr visitMethodCall(MethodCallExpr expr) throws T {
            String name = expr.getName();
            List<Expr> list = new ArrayList<>(expr.getParameters().size());
            for (Expr e : expr.getParameters()) {
                list.add(e.accept(this));
            }
            return new MethodCallExpr(name, list);
        }

        @Override
        public Expr visitMember(MemberExpr expr) {
            String value = expr.getValue();
            return new MemberExpr(value);
        }

        @Override
        public Expr visitValue(ValueExpr expr) {
            String value = expr.getValue();
            return new ValueExpr(value);
        }
    }

    /**
     * Transformer for expression that modifies the member referneces.
     */
    private static class RenamingVisitor extends AbstractExprTransformer<Error> {

        private final Function<String, String> mapper;

        /**
         * Create a new {@code RenamingVisitor}.
         *
         * @param mapper the mapper used to modifiy the member references
         */
        RenamingVisitor(Function<String, String> mapper) {
            this.mapper = mapper;
        }

        @Override
        public Expr visitMember(MemberExpr expr) {
            return new MemberExpr(mapper.apply(expr.getValue()));
        }
    }

}