/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ /* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch licenses this file to you 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. */ /* * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ package org.opensearch.index.mapper; import org.apache.lucene.document.FieldType; import org.opensearch.OpenSearchParseException; import org.opensearch.common.CheckedBiFunction; import org.opensearch.common.Explicit; import org.opensearch.core.ParseField; import org.opensearch.common.geo.GeoPoint; import org.opensearch.common.geo.GeometryFormat; import org.opensearch.common.geo.GeometryParser; import org.opensearch.common.xcontent.LoggingDeprecationHandler; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.core.xcontent.XContentParser; import org.opensearch.geometry.Geometry; import org.opensearch.geometry.Point; import java.io.IOException; import java.text.ParseException; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.function.Supplier; import static org.opensearch.index.mapper.TypeParsers.parseField; /** * Base class for spatial fields that only support indexing points * * @opensearch.internal */ public abstract class AbstractPointGeometryFieldMapper extends AbstractGeometryFieldMapper { /** * Common parameter names for the base point geometry field mapper * * @opensearch.internal */ public static class Names extends AbstractGeometryFieldMapper.Names { public static final ParseField NULL_VALUE = new ParseField("null_value"); } public static final FieldType DEFAULT_FIELD_TYPE = new FieldType(); static { DEFAULT_FIELD_TYPE.setDimensions(2, Integer.BYTES); DEFAULT_FIELD_TYPE.setStored(false); DEFAULT_FIELD_TYPE.freeze(); } /** * Base builder for the base point geometry field mapper * * @opensearch.internal */ public abstract static class Builder, FT extends AbstractPointGeometryFieldType> extends AbstractGeometryFieldMapper.Builder { protected ParsedPoint nullValue; public Builder(String name, FieldType fieldType) { super(name, fieldType); } public void setNullValue(ParsedPoint nullValue) { this.nullValue = nullValue; } public abstract AbstractPointGeometryFieldMapper build( BuilderContext context, String simpleName, FieldType fieldType, MultiFields multiFields, Explicit ignoreMalformed, Explicit ignoreZValue, ParsedPoint nullValue, CopyTo copyTo ); @Override public AbstractPointGeometryFieldMapper build(BuilderContext context) { return build( context, name, fieldType, multiFieldsBuilder.build(this, context), ignoreMalformed(context), ignoreZValue(context), nullValue, copyTo ); } } /** * Base type parser for the base point geometry field mapper * * @opensearch.internal */ public abstract static class TypeParser extends AbstractGeometryFieldMapper.TypeParser { protected abstract ParsedPoint parseNullValue(Object nullValue, boolean ignoreZValue, boolean ignoreMalformed); @Override public T parse(String name, Map node, Map params, ParserContext parserContext) { T builder = (T) (super.parse(name, node, params, parserContext)); parseField(builder, name, node, parserContext); Object nullValue = null; for (Iterator> iterator = node.entrySet().iterator(); iterator.hasNext();) { Map.Entry entry = iterator.next(); String propName = entry.getKey(); Object propNode = entry.getValue(); if (Names.NULL_VALUE.match(propName, LoggingDeprecationHandler.INSTANCE)) { nullValue = propNode; iterator.remove(); } } if (nullValue != null) { builder.setNullValue( parseNullValue(nullValue, (Boolean) builder.ignoreZValue().value(), (Boolean) builder.ignoreMalformed().value()) ); } return builder; } } ParsedPoint nullValue; /** * Base field type for the base point geometry field mapper * * @opensearch.internal */ public abstract static class AbstractPointGeometryFieldType extends AbstractGeometryFieldType { protected AbstractPointGeometryFieldType( String name, boolean indexed, boolean stored, boolean hasDocValues, Map meta ) { super(name, indexed, stored, hasDocValues, true, meta); } } protected AbstractPointGeometryFieldMapper( String simpleName, FieldType fieldType, MappedFieldType mappedFieldType, MultiFields multiFields, Explicit ignoreMalformed, Explicit ignoreZValue, ParsedPoint nullValue, CopyTo copyTo ) { super(simpleName, fieldType, mappedFieldType, ignoreMalformed, ignoreZValue, multiFields, copyTo); this.nullValue = nullValue; } @Override public final boolean parsesArrayValue() { return true; } @Override protected void mergeOptions(FieldMapper other, List conflicts) { super.mergeOptions(other, conflicts); AbstractPointGeometryFieldMapper gpfm = (AbstractPointGeometryFieldMapper) other; // TODO make this un-updateable if (gpfm.nullValue != null) { this.nullValue = gpfm.nullValue; } } @Override public void doXContentBody(XContentBuilder builder, boolean includeDefaults, Params params) throws IOException { super.doXContentBody(builder, includeDefaults, params); if (nullValue != null || includeDefaults) { builder.field(Names.NULL_VALUE.getPreferredName(), nullValue); } } public ParsedPoint nullValue() { return nullValue; } /** * represents a Point that has been parsed by {@link PointParser} * * @opensearch.internal */ public interface ParsedPoint { void validate(String fieldName); void normalize(String fieldName); void resetCoords(double x, double y); Point asGeometry(); default boolean isNormalizable(double coord) { return Double.isNaN(coord) == false && Double.isInfinite(coord) == false; } } /** * A parser implementation that can parse the various point formats * * @opensearch.internal */ public static class PointParser

extends Parser> { /** * Note that this parser is only used for formatting values. */ private final GeometryParser geometryParser; private final String field; private final Supplier

pointSupplier; private final CheckedBiFunction objectParser; private final P nullValue; private final boolean ignoreZValue; private final boolean ignoreMalformed; public PointParser( String field, Supplier

pointSupplier, CheckedBiFunction objectParser, P nullValue, boolean ignoreZValue, boolean ignoreMalformed ) { this.field = field; this.pointSupplier = pointSupplier; this.objectParser = objectParser; this.nullValue = nullValue; this.ignoreZValue = ignoreZValue; this.ignoreMalformed = ignoreMalformed; this.geometryParser = new GeometryParser(true, true, true); } private P process(P in) { if (ignoreMalformed == false) { in.validate(field); } else { in.normalize(field); } return in; } @Override public List

parse(XContentParser parser) throws IOException, ParseException { if (parser.currentToken() == XContentParser.Token.START_ARRAY) { XContentParser.Token token = parser.nextToken(); P point = pointSupplier.get(); ArrayList

points = new ArrayList<>(); if (token == XContentParser.Token.VALUE_NUMBER) { double x = parser.doubleValue(); parser.nextToken(); double y = parser.doubleValue(); token = parser.nextToken(); if (token == XContentParser.Token.VALUE_NUMBER) { GeoPoint.assertZValue(ignoreZValue, parser.doubleValue()); } else if (token != XContentParser.Token.END_ARRAY) { throw new OpenSearchParseException("field type does not accept > 3 dimensions"); } point.resetCoords(x, y); points.add(process(point)); } else { while (token != XContentParser.Token.END_ARRAY) { points.add(process(objectParser.apply(parser, point))); point = pointSupplier.get(); token = parser.nextToken(); } } return points; } else if (parser.currentToken() == XContentParser.Token.VALUE_NULL) { if (nullValue == null) { return null; } else { return Collections.singletonList(nullValue); } } else { return Collections.singletonList(process(objectParser.apply(parser, pointSupplier.get()))); } } @Override public Object format(List

points, String format) { List result = new ArrayList<>(); GeometryFormat geometryFormat = geometryParser.geometryFormat(format); for (ParsedPoint point : points) { Geometry geometry = point.asGeometry(); result.add(geometryFormat.toXContentAsObject(geometry)); } return result; } } }