nodes = new ArrayList<>();
- while (token != XContentParser.Token.END_ARRAY) {
- nodes.add(parseCoordinates(parser));
- token = parser.nextToken();
+ protected static Coordinate readFromStream(StreamInput in) throws IOException {
+ return new Coordinate(in.readDouble(), in.readDouble());
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ out.writeVInt(coordinates.size());
+ for (Coordinate point : coordinates) {
+ writeCoordinateTo(point, out);
}
+ }
+
+ protected static void writeCoordinateTo(Coordinate coordinate, StreamOutput out) throws IOException {
+ out.writeDouble(coordinate.x);
+ out.writeDouble(coordinate.y);
+ }
- return new CoordinateNode(nodes);
+ @SuppressWarnings("unchecked")
+ private E thisRef() {
+ return (E)this;
}
/**
- * Create a new {@link ShapeBuilder} from {@link XContent}
- * @param parser parser to read the GeoShape from
- * @return {@link ShapeBuilder} read from the parser or null
- * if the parsers current token has been null
- * @throws IOException if the input could not be read
+ * Add a new coordinate to the collection
+ * @param longitude longitude of the coordinate
+ * @param latitude latitude of the coordinate
+ * @return this
*/
- public static ShapeBuilder parse(XContentParser parser) throws IOException {
- return GeoShapeType.parse(parser, null);
+ public E coordinate(double longitude, double latitude) {
+ return this.coordinate(new Coordinate(longitude, latitude));
}
/**
- * Create a new {@link ShapeBuilder} from {@link XContent}
- * @param parser parser to read the GeoShape from
- * @param geoDocMapper document field mapper reference required for spatial parameters relevant
- * to the shape construction process (e.g., orientation)
- * todo: refactor to place build specific parameters in the SpatialContext
- * @return {@link ShapeBuilder} read from the parser or null
- * if the parsers current token has been null
- * @throws IOException if the input could not be read
+ * Add a new coordinate to the collection
+ * @param coordinate coordinate of the point
+ * @return this
*/
- public static ShapeBuilder parse(XContentParser parser, GeoShapeFieldMapper geoDocMapper) throws IOException {
- return GeoShapeType.parse(parser, geoDocMapper);
+ public E coordinate(Coordinate coordinate) {
+ this.coordinates.add(coordinate);
+ return thisRef();
}
- protected static XContentBuilder toXContent(XContentBuilder builder, Coordinate coordinate) throws IOException {
- return builder.startArray().value(coordinate.x).value(coordinate.y).endArray();
+ /**
+ * Add a array of coordinates to the collection
+ *
+ * @param coordinates array of {@link Coordinate}s to add
+ * @return this
+ */
+ public E coordinates(Coordinate...coordinates) {
+ return this.coordinates(Arrays.asList(coordinates));
}
- protected static void writeCoordinateTo(Coordinate coordinate, StreamOutput out) throws IOException {
- out.writeDouble(coordinate.x);
- out.writeDouble(coordinate.y);
+ /**
+ * Add a collection of coordinates to the collection
+ *
+ * @param coordinates array of {@link Coordinate}s to add
+ * @return this
+ */
+ public E coordinates(Collection extends Coordinate> coordinates) {
+ this.coordinates.addAll(coordinates);
+ return thisRef();
}
- protected static Coordinate readFromStream(StreamInput in) throws IOException {
- return new Coordinate(in.readDouble(), in.readDouble());
+ /**
+ * Copy all coordinate to a new Array
+ *
+ * @param closed if set to true the first point of the array is repeated as last element
+ * @return Array of coordinates
+ */
+ protected Coordinate[] coordinates(boolean closed) {
+ Coordinate[] result = coordinates.toArray(new Coordinate[coordinates.size() + (closed?1:0)]);
+ if(closed) {
+ result[result.length-1] = result[0];
+ }
+ return result;
+ }
+
+ protected JtsGeometry jtsGeometry(Geometry geom) {
+ //dateline180Check is false because ElasticSearch does it's own dateline wrapping
+ JtsGeometry jtsGeometry = new JtsGeometry(geom, SPATIAL_CONTEXT, false, MULTI_POLYGON_MAY_OVERLAP);
+ if (AUTO_VALIDATE_JTS_GEOMETRY)
+ jtsGeometry.validate();
+ if (AUTO_INDEX_JTS_GEOMETRY)
+ jtsGeometry.index();
+ return jtsGeometry;
}
+ /**
+ * Create a new Shape from this builder. Since calling this method could change the
+ * defined shape. (by inserting new coordinates or change the position of points)
+ * the builder looses its validity. So this method should only be called once on a builder
+ * @return new {@link Shape} defined by the builder
+ */
+ public abstract T build();
+
protected static Coordinate shift(Coordinate coordinate, double dateline) {
if (dateline == 0) {
return coordinate;
@@ -255,58 +274,6 @@ protected static int intersections(double dateline, Edge[] edges) {
return numIntersections;
}
- /**
- * Node used to represent a tree of coordinates.
- *
- * Can either be a leaf node consisting of a Coordinate, or a parent with
- * children
- */
- protected static class CoordinateNode implements ToXContentObject {
-
- protected final Coordinate coordinate;
- protected final List children;
-
- /**
- * Creates a new leaf CoordinateNode
- *
- * @param coordinate
- * Coordinate for the Node
- */
- protected CoordinateNode(Coordinate coordinate) {
- this.coordinate = coordinate;
- this.children = null;
- }
-
- /**
- * Creates a new parent CoordinateNode
- *
- * @param children
- * Children of the Node
- */
- protected CoordinateNode(List children) {
- this.children = children;
- this.coordinate = null;
- }
-
- protected boolean isEmpty() {
- return (coordinate == null && (children == null || children.isEmpty()));
- }
-
- @Override
- public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
- if (children == null) {
- builder.startArray().value(coordinate.x).value(coordinate.y).endArray();
- } else {
- builder.startArray();
- for (CoordinateNode child : children) {
- child.toXContent(builder, params);
- }
- builder.endArray();
- }
- return builder;
- }
- }
-
/**
* This helper class implements a linked list for {@link Coordinate}. It contains
* fields for a dateline intersection and component id
@@ -415,293 +382,50 @@ public static Orientation fromString(String orientation) {
}
}
- public static final String FIELD_TYPE = "type";
- public static final String FIELD_COORDINATES = "coordinates";
- public static final String FIELD_GEOMETRIES = "geometries";
- public static final String FIELD_ORIENTATION = "orientation";
-
protected static final boolean debugEnabled() {
return LOGGER.isDebugEnabled() || DEBUG;
}
+ protected static XContentBuilder toXContent(XContentBuilder builder, Coordinate coordinate) throws IOException {
+ return builder.startArray().value(coordinate.x).value(coordinate.y).endArray();
+ }
+
/**
- * Enumeration that lists all {@link GeoShapeType}s that can be handled
+ * builds an array of coordinates to a {@link XContentBuilder}
+ *
+ * @param builder builder to use
+ * @param closed repeat the first point at the end of the array if it's not already defines as last element of the array
+ * @return the builder
*/
- public enum GeoShapeType {
- POINT("point"),
- MULTIPOINT("multipoint"),
- LINESTRING("linestring"),
- MULTILINESTRING("multilinestring"),
- POLYGON("polygon"),
- MULTIPOLYGON("multipolygon"),
- GEOMETRYCOLLECTION("geometrycollection"),
- ENVELOPE("envelope"),
- CIRCLE("circle");
-
- private final String shapename;
-
- GeoShapeType(String shapename) {
- this.shapename = shapename;
- }
-
- protected String shapeName() {
- return shapename;
- }
-
- public static GeoShapeType forName(String geoshapename) {
- String typename = geoshapename.toLowerCase(Locale.ROOT);
- for (GeoShapeType type : values()) {
- if(type.shapename.equals(typename)) {
- return type;
- }
- }
- throw new IllegalArgumentException("unknown geo_shape ["+geoshapename+"]");
- }
-
- public static ShapeBuilder parse(XContentParser parser) throws IOException {
- return parse(parser, null);
- }
-
- /**
- * Parse the geometry specified by the source document and return a ShapeBuilder instance used to
- * build the actual geometry
- * @param parser - parse utility object including source document
- * @param shapeMapper - field mapper needed for index specific parameters
- * @return ShapeBuilder - a builder instance used to create the geometry
- */
- public static ShapeBuilder parse(XContentParser parser, GeoShapeFieldMapper shapeMapper) throws IOException {
- if (parser.currentToken() == XContentParser.Token.VALUE_NULL) {
- return null;
- } else if (parser.currentToken() != XContentParser.Token.START_OBJECT) {
- throw new ElasticsearchParseException("shape must be an object consisting of type and coordinates");
- }
-
- GeoShapeType shapeType = null;
- Distance radius = null;
- CoordinateNode node = null;
- GeometryCollectionBuilder geometryCollections = null;
-
- Orientation requestedOrientation = (shapeMapper == null) ? Orientation.RIGHT : shapeMapper.fieldType().orientation();
- boolean coerce = (shapeMapper == null) ? GeoShapeFieldMapper.Defaults.COERCE.value() : shapeMapper.coerce().value();
-
- XContentParser.Token token;
- while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
- if (token == XContentParser.Token.FIELD_NAME) {
- String fieldName = parser.currentName();
-
- if (FIELD_TYPE.equals(fieldName)) {
- parser.nextToken();
- shapeType = GeoShapeType.forName(parser.text());
- } else if (FIELD_COORDINATES.equals(fieldName)) {
- parser.nextToken();
- node = parseCoordinates(parser);
- } else if (FIELD_GEOMETRIES.equals(fieldName)) {
- parser.nextToken();
- geometryCollections = parseGeometries(parser, shapeMapper);
- } else if (CircleBuilder.FIELD_RADIUS.equals(fieldName)) {
- parser.nextToken();
- radius = Distance.parseDistance(parser.text());
- } else if (FIELD_ORIENTATION.equals(fieldName)) {
- parser.nextToken();
- requestedOrientation = Orientation.fromString(parser.text());
- } else {
- parser.nextToken();
- parser.skipChildren();
- }
- }
- }
-
- if (shapeType == null) {
- throw new ElasticsearchParseException("shape type not included");
- } else if (node == null && GeoShapeType.GEOMETRYCOLLECTION != shapeType) {
- throw new ElasticsearchParseException("coordinates not included");
- } else if (geometryCollections == null && GeoShapeType.GEOMETRYCOLLECTION == shapeType) {
- throw new ElasticsearchParseException("geometries not included");
- } else if (radius != null && GeoShapeType.CIRCLE != shapeType) {
- throw new ElasticsearchParseException("field [{}] is supported for [{}] only", CircleBuilder.FIELD_RADIUS,
- CircleBuilder.TYPE);
- }
-
- switch (shapeType) {
- case POINT: return parsePoint(node);
- case MULTIPOINT: return parseMultiPoint(node);
- case LINESTRING: return parseLineString(node);
- case MULTILINESTRING: return parseMultiLine(node);
- case POLYGON: return parsePolygon(node, requestedOrientation, coerce);
- case MULTIPOLYGON: return parseMultiPolygon(node, requestedOrientation, coerce);
- case CIRCLE: return parseCircle(node, radius);
- case ENVELOPE: return parseEnvelope(node);
- case GEOMETRYCOLLECTION: return geometryCollections;
- default:
- throw new ElasticsearchParseException("shape type [{}] not included", shapeType);
- }
- }
-
- protected static void validatePointNode(CoordinateNode node) {
- if (node.isEmpty()) {
- throw new ElasticsearchParseException(
- "invalid number of points (0) provided when expecting a single coordinate ([lat, lng])");
- } else if (node.coordinate == null) {
- if (node.children.isEmpty() == false) {
- throw new ElasticsearchParseException("multipoint data provided when single point data expected.");
- }
- }
- }
-
- protected static PointBuilder parsePoint(CoordinateNode node) {
- validatePointNode(node);
- return ShapeBuilders.newPoint(node.coordinate);
- }
-
- protected static CircleBuilder parseCircle(CoordinateNode coordinates, Distance radius) {
- return ShapeBuilders.newCircleBuilder().center(coordinates.coordinate).radius(radius);
- }
-
- protected static EnvelopeBuilder parseEnvelope(CoordinateNode coordinates) {
- // validate the coordinate array for envelope type
- if (coordinates.children.size() != 2) {
- throw new ElasticsearchParseException(
- "invalid number of points [{}] provided for geo_shape [{}] when expecting an array of 2 coordinates",
- coordinates.children.size(), GeoShapeType.ENVELOPE.shapename);
- }
- // verify coordinate bounds, correct if necessary
- Coordinate uL = coordinates.children.get(0).coordinate;
- Coordinate lR = coordinates.children.get(1).coordinate;
- if (((lR.x < uL.x) || (uL.y < lR.y))) {
- Coordinate uLtmp = uL;
- uL = new Coordinate(Math.min(uL.x, lR.x), Math.max(uL.y, lR.y));
- lR = new Coordinate(Math.max(uLtmp.x, lR.x), Math.min(uLtmp.y, lR.y));
+ protected XContentBuilder coordinatesToXcontent(XContentBuilder builder, boolean closed) throws IOException {
+ builder.startArray();
+ for(Coordinate coord : coordinates) {
+ toXContent(builder, coord);
+ }
+ if(closed) {
+ Coordinate start = coordinates.get(0);
+ Coordinate end = coordinates.get(coordinates.size()-1);
+ if(start.x != end.x || start.y != end.y) {
+ toXContent(builder, coordinates.get(0));
}
- return ShapeBuilders.newEnvelope(uL, lR);
- }
-
- protected static void validateMultiPointNode(CoordinateNode coordinates) {
- if (coordinates.children == null || coordinates.children.isEmpty()) {
- if (coordinates.coordinate != null) {
- throw new ElasticsearchParseException("single coordinate found when expecting an array of " +
- "coordinates. change type to point or change data to an array of >0 coordinates");
- }
- throw new ElasticsearchParseException("no data provided for multipoint object when expecting " +
- ">0 points (e.g., [[lat, lng]] or [[lat, lng], ...])");
- } else {
- for (CoordinateNode point : coordinates.children) {
- validatePointNode(point);
- }
- }
- }
-
- protected static MultiPointBuilder parseMultiPoint(CoordinateNode coordinates) {
- validateMultiPointNode(coordinates);
- CoordinatesBuilder points = new CoordinatesBuilder();
- for (CoordinateNode node : coordinates.children) {
- points.coordinate(node.coordinate);
- }
- return new MultiPointBuilder(points.build());
- }
-
- protected static LineStringBuilder parseLineString(CoordinateNode coordinates) {
- /**
- * Per GeoJSON spec (http://geojson.org/geojson-spec.html#linestring)
- * "coordinates" member must be an array of two or more positions
- * LineStringBuilder should throw a graceful exception if < 2 coordinates/points are provided
- */
- if (coordinates.children.size() < 2) {
- throw new ElasticsearchParseException("invalid number of points in LineString (found [{}] - must be >= 2)",
- coordinates.children.size());
- }
-
- CoordinatesBuilder line = new CoordinatesBuilder();
- for (CoordinateNode node : coordinates.children) {
- line.coordinate(node.coordinate);
- }
- return ShapeBuilders.newLineString(line);
- }
-
- protected static MultiLineStringBuilder parseMultiLine(CoordinateNode coordinates) {
- MultiLineStringBuilder multiline = ShapeBuilders.newMultiLinestring();
- for (CoordinateNode node : coordinates.children) {
- multiline.linestring(parseLineString(node));
- }
- return multiline;
- }
-
- protected static LineStringBuilder parseLinearRing(CoordinateNode coordinates, boolean coerce) {
- /**
- * Per GeoJSON spec (http://geojson.org/geojson-spec.html#linestring)
- * A LinearRing is closed LineString with 4 or more positions. The first and last positions
- * are equivalent (they represent equivalent points). Though a LinearRing is not explicitly
- * represented as a GeoJSON geometry type, it is referred to in the Polygon geometry type definition.
- */
- if (coordinates.children == null) {
- String error = "Invalid LinearRing found.";
- error += (coordinates.coordinate == null) ?
- " No coordinate array provided" : " Found a single coordinate when expecting a coordinate array";
- throw new ElasticsearchParseException(error);
- }
-
- int numValidPts = coerce ? 3 : 4;
- if (coordinates.children.size() < numValidPts) {
- throw new ElasticsearchParseException("invalid number of points in LinearRing (found [{}] - must be >= [{}])",
- coordinates.children.size(), numValidPts);
- }
-
- if (!coordinates.children.get(0).coordinate.equals(
- coordinates.children.get(coordinates.children.size() - 1).coordinate)) {
- if (coerce) {
- coordinates.children.add(coordinates.children.get(0));
- } else {
- throw new ElasticsearchParseException("invalid LinearRing found (coordinates are not closed)");
- }
- }
- return parseLineString(coordinates);
- }
-
- protected static PolygonBuilder parsePolygon(CoordinateNode coordinates, final Orientation orientation, final boolean coerce) {
- if (coordinates.children == null || coordinates.children.isEmpty()) {
- throw new ElasticsearchParseException(
- "invalid LinearRing provided for type polygon. Linear ring must be an array of coordinates");
- }
-
- LineStringBuilder shell = parseLinearRing(coordinates.children.get(0), coerce);
- PolygonBuilder polygon = new PolygonBuilder(shell, orientation);
- for (int i = 1; i < coordinates.children.size(); i++) {
- polygon.hole(parseLinearRing(coordinates.children.get(i), coerce));
- }
- return polygon;
}
+ builder.endArray();
+ return builder;
+ }
- protected static MultiPolygonBuilder parseMultiPolygon(CoordinateNode coordinates, final Orientation orientation,
- final boolean coerce) {
- MultiPolygonBuilder polygons = ShapeBuilders.newMultiPolygon(orientation);
- for (CoordinateNode node : coordinates.children) {
- polygons.polygon(parsePolygon(node, orientation, coerce));
- }
- return polygons;
- }
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof ShapeBuilder)) return false;
- /**
- * Parse the geometries array of a GeometryCollection
- *
- * @param parser Parser that will be read from
- * @return Geometry[] geometries of the GeometryCollection
- * @throws IOException Thrown if an error occurs while reading from the XContentParser
- */
- protected static GeometryCollectionBuilder parseGeometries(XContentParser parser, GeoShapeFieldMapper mapper) throws
- IOException {
- if (parser.currentToken() != XContentParser.Token.START_ARRAY) {
- throw new ElasticsearchParseException("geometries must be an array of geojson objects");
- }
+ ShapeBuilder,?> that = (ShapeBuilder,?>) o;
- XContentParser.Token token = parser.nextToken();
- GeometryCollectionBuilder geometryCollection = ShapeBuilders.newGeometryCollection();
- while (token != XContentParser.Token.END_ARRAY) {
- ShapeBuilder shapeBuilder = GeoShapeType.parse(parser);
- geometryCollection.shape(shapeBuilder);
- token = parser.nextToken();
- }
+ return Objects.equals(coordinates, that.coordinates);
+ }
- return geometryCollection;
- }
+ @Override
+ public int hashCode() {
+ return Objects.hash(coordinates);
}
@Override
diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/ShapeBuilders.java b/core/src/main/java/org/elasticsearch/common/geo/builders/ShapeBuilders.java
deleted file mode 100644
index e0afa4c20d50c..0000000000000
--- a/core/src/main/java/org/elasticsearch/common/geo/builders/ShapeBuilders.java
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * 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.
- */
-
-package org.elasticsearch.common.geo.builders;
-
-import java.util.List;
-
-import com.vividsolutions.jts.geom.Coordinate;
-import org.elasticsearch.common.io.stream.NamedWriteableRegistry.Entry;
-
-/**
- * A collection of static methods for creating ShapeBuilders.
- */
-public class ShapeBuilders {
-
- /**
- * Create a new point
- *
- * @param longitude longitude of the point
- * @param latitude latitude of the point
- * @return a new {@link PointBuilder}
- */
- public static PointBuilder newPoint(double longitude, double latitude) {
- return ShapeBuilders.newPoint(new Coordinate(longitude, latitude));
- }
-
- /**
- * Create a new {@link PointBuilder} from a {@link Coordinate}
- * @param coordinate coordinate defining the position of the point
- * @return a new {@link PointBuilder}
- */
- public static PointBuilder newPoint(Coordinate coordinate) {
- return new PointBuilder().coordinate(coordinate);
- }
-
- /**
- * Create a new set of points
- * @return new {@link MultiPointBuilder}
- */
- public static MultiPointBuilder newMultiPoint(List points) {
- return new MultiPointBuilder(points);
- }
-
- /**
- * Create a new lineString
- * @return a new {@link LineStringBuilder}
- */
- public static LineStringBuilder newLineString(List list) {
- return new LineStringBuilder(list);
- }
-
- /**
- * Create a new lineString
- * @return a new {@link LineStringBuilder}
- */
- public static LineStringBuilder newLineString(CoordinatesBuilder coordinates) {
- return new LineStringBuilder(coordinates);
- }
-
- /**
- * Create a new Collection of lineStrings
- * @return a new {@link MultiLineStringBuilder}
- */
- public static MultiLineStringBuilder newMultiLinestring() {
- return new MultiLineStringBuilder();
- }
-
- /**
- * Create a new PolygonBuilder
- * @return a new {@link PolygonBuilder}
- */
- public static PolygonBuilder newPolygon(List shell) {
- return new PolygonBuilder(new CoordinatesBuilder().coordinates(shell));
- }
-
- /**
- * Create a new PolygonBuilder
- * @return a new {@link PolygonBuilder}
- */
- public static PolygonBuilder newPolygon(CoordinatesBuilder shell) {
- return new PolygonBuilder(shell);
- }
-
- /**
- * Create a new Collection of polygons
- * @return a new {@link MultiPolygonBuilder}
- */
- public static MultiPolygonBuilder newMultiPolygon() {
- return new MultiPolygonBuilder();
- }
-
- /**
- * Create a new Collection of polygons
- * @return a new {@link MultiPolygonBuilder}
- */
- public static MultiPolygonBuilder newMultiPolygon(ShapeBuilder.Orientation orientation) {
- return new MultiPolygonBuilder(orientation);
- }
-
- /**
- * Create a new GeometryCollection
- * @return a new {@link GeometryCollectionBuilder}
- */
- public static GeometryCollectionBuilder newGeometryCollection() {
- return new GeometryCollectionBuilder();
- }
-
- /**
- * create a new Circle
- *
- * @return a new {@link CircleBuilder}
- */
- public static CircleBuilder newCircleBuilder() {
- return new CircleBuilder();
- }
-
- /**
- * create a new rectangle
- *
- * @return a new {@link EnvelopeBuilder}
- */
- public static EnvelopeBuilder newEnvelope(Coordinate topLeft, Coordinate bottomRight) {
- return new EnvelopeBuilder(topLeft, bottomRight);
- }
-
- public static void register(List namedWriteables) {
- namedWriteables.add(new Entry(ShapeBuilder.class, PointBuilder.TYPE.shapeName(), PointBuilder::new));
- namedWriteables.add(new Entry(ShapeBuilder.class, CircleBuilder.TYPE.shapeName(), CircleBuilder::new));
- namedWriteables.add(new Entry(ShapeBuilder.class, EnvelopeBuilder.TYPE.shapeName(), EnvelopeBuilder::new));
- namedWriteables.add(new Entry(ShapeBuilder.class, MultiPointBuilder.TYPE.shapeName(), MultiPointBuilder::new));
- namedWriteables.add(new Entry(ShapeBuilder.class, LineStringBuilder.TYPE.shapeName(), LineStringBuilder::new));
- namedWriteables.add(new Entry(ShapeBuilder.class, MultiLineStringBuilder.TYPE.shapeName(), MultiLineStringBuilder::new));
- namedWriteables.add(new Entry(ShapeBuilder.class, PolygonBuilder.TYPE.shapeName(), PolygonBuilder::new));
- namedWriteables.add(new Entry(ShapeBuilder.class, MultiPolygonBuilder.TYPE.shapeName(), MultiPolygonBuilder::new));
- namedWriteables.add(new Entry(ShapeBuilder.class, GeometryCollectionBuilder.TYPE.shapeName(), GeometryCollectionBuilder::new));
- }
-}
diff --git a/core/src/main/java/org/elasticsearch/common/geo/parsers/CoordinateNode.java b/core/src/main/java/org/elasticsearch/common/geo/parsers/CoordinateNode.java
new file mode 100644
index 0000000000000..d766d75d5ec1a
--- /dev/null
+++ b/core/src/main/java/org/elasticsearch/common/geo/parsers/CoordinateNode.java
@@ -0,0 +1,81 @@
+/*
+ * 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.
+ */
+package org.elasticsearch.common.geo.parsers;
+
+import com.vividsolutions.jts.geom.Coordinate;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Node used to represent a tree of coordinates.
+ *
+ * Can either be a leaf node consisting of a Coordinate, or a parent with
+ * children
+ */
+public class CoordinateNode implements ToXContentObject {
+ public final Coordinate coordinate;
+ public final List children;
+
+ /**
+ * Creates a new leaf CoordinateNode
+ *
+ * @param coordinate
+ * Coordinate for the Node
+ */
+ protected CoordinateNode(Coordinate coordinate) {
+ this.coordinate = coordinate;
+ this.children = null;
+ }
+
+ /**
+ * Creates a new parent CoordinateNode
+ *
+ * @param children
+ * Children of the Node
+ */
+ protected CoordinateNode(List children) {
+ this.children = children;
+ this.coordinate = null;
+ }
+
+ public boolean isEmpty() {
+ return (coordinate == null && (children == null || children.isEmpty()));
+ }
+
+ public boolean isMultiPoint() {
+ return children != null && children.size() > 1;
+ }
+
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ if (children == null) {
+ builder.startArray().value(coordinate.x).value(coordinate.y).endArray();
+ } else {
+ builder.startArray();
+ for (CoordinateNode child : children) {
+ child.toXContent(builder, params);
+ }
+ builder.endArray();
+ }
+ return builder;
+ }
+}
diff --git a/core/src/main/java/org/elasticsearch/common/geo/parsers/GeoJsonParser.java b/core/src/main/java/org/elasticsearch/common/geo/parsers/GeoJsonParser.java
new file mode 100644
index 0000000000000..90145448be326
--- /dev/null
+++ b/core/src/main/java/org/elasticsearch/common/geo/parsers/GeoJsonParser.java
@@ -0,0 +1,194 @@
+/*
+ * 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.
+ */
+package org.elasticsearch.common.geo.parsers;
+
+import com.vividsolutions.jts.geom.Coordinate;
+import org.elasticsearch.ElasticsearchParseException;
+import org.elasticsearch.common.Explicit;
+import org.elasticsearch.common.geo.GeoShapeType;
+import org.elasticsearch.common.geo.builders.CircleBuilder;
+import org.elasticsearch.common.geo.builders.GeometryCollectionBuilder;
+import org.elasticsearch.common.geo.builders.ShapeBuilder;
+import org.elasticsearch.common.unit.DistanceUnit;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.index.mapper.GeoShapeFieldMapper;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Parses shape geometry represented in geojson
+ *
+ * complies with geojson specification: https://tools.ietf.org/html/rfc7946
+ */
+abstract class GeoJsonParser {
+ protected static ShapeBuilder parse(XContentParser parser, GeoShapeFieldMapper shapeMapper)
+ throws IOException {
+ GeoShapeType shapeType = null;
+ DistanceUnit.Distance radius = null;
+ CoordinateNode coordinateNode = null;
+ GeometryCollectionBuilder geometryCollections = null;
+
+ ShapeBuilder.Orientation requestedOrientation =
+ (shapeMapper == null) ? ShapeBuilder.Orientation.RIGHT : shapeMapper.fieldType().orientation();
+ Explicit coerce = (shapeMapper == null) ? GeoShapeFieldMapper.Defaults.COERCE : shapeMapper.coerce();
+
+ String malformedException = null;
+
+ XContentParser.Token token;
+ while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
+ if (token == XContentParser.Token.FIELD_NAME) {
+ String fieldName = parser.currentName();
+
+ if (ShapeParser.FIELD_TYPE.match(fieldName)) {
+ parser.nextToken();
+ final GeoShapeType type = GeoShapeType.forName(parser.text());
+ if (shapeType != null && shapeType.equals(type) == false) {
+ malformedException = ShapeParser.FIELD_TYPE + " already parsed as ["
+ + shapeType + "] cannot redefine as [" + type + "]";
+ } else {
+ shapeType = type;
+ }
+ } else if (ShapeParser.FIELD_COORDINATES.match(fieldName)) {
+ parser.nextToken();
+ coordinateNode = parseCoordinates(parser);
+ } else if (ShapeParser.FIELD_GEOMETRIES.match(fieldName)) {
+ if (shapeType == null) {
+ shapeType = GeoShapeType.GEOMETRYCOLLECTION;
+ } else if (shapeType.equals(GeoShapeType.GEOMETRYCOLLECTION) == false) {
+ malformedException = "cannot have [" + ShapeParser.FIELD_GEOMETRIES + "] with type set to ["
+ + shapeType + "]";
+ }
+ parser.nextToken();
+ geometryCollections = parseGeometries(parser, shapeMapper);
+ } else if (CircleBuilder.FIELD_RADIUS.match(fieldName)) {
+ if (shapeType == null) {
+ shapeType = GeoShapeType.CIRCLE;
+ } else if (shapeType != null && shapeType.equals(GeoShapeType.CIRCLE) == false) {
+ malformedException = "cannot have [" + CircleBuilder.FIELD_RADIUS + "] with type set to ["
+ + shapeType + "]";
+ }
+ parser.nextToken();
+ radius = DistanceUnit.Distance.parseDistance(parser.text());
+ } else if (ShapeParser.FIELD_ORIENTATION.match(fieldName)) {
+ if (shapeType != null
+ && (shapeType.equals(GeoShapeType.POLYGON) || shapeType.equals(GeoShapeType.MULTIPOLYGON)) == false) {
+ malformedException = "cannot have [" + ShapeParser.FIELD_ORIENTATION + "] with type set to [" + shapeType + "]";
+ }
+ parser.nextToken();
+ requestedOrientation = ShapeBuilder.Orientation.fromString(parser.text());
+ } else {
+ parser.nextToken();
+ parser.skipChildren();
+ }
+ }
+ }
+
+ if (malformedException != null) {
+ throw new ElasticsearchParseException(malformedException);
+ } else if (shapeType == null) {
+ throw new ElasticsearchParseException("shape type not included");
+ } else if (coordinateNode == null && GeoShapeType.GEOMETRYCOLLECTION != shapeType) {
+ throw new ElasticsearchParseException("coordinates not included");
+ } else if (geometryCollections == null && GeoShapeType.GEOMETRYCOLLECTION == shapeType) {
+ throw new ElasticsearchParseException("geometries not included");
+ } else if (radius != null && GeoShapeType.CIRCLE != shapeType) {
+ throw new ElasticsearchParseException("field [{}] is supported for [{}] only", CircleBuilder.FIELD_RADIUS,
+ CircleBuilder.TYPE);
+ }
+
+ if (shapeType == null) {
+ throw new ElasticsearchParseException("shape type [{}] not included", shapeType);
+ }
+
+ if (shapeType.equals(GeoShapeType.GEOMETRYCOLLECTION)) {
+ return geometryCollections;
+ }
+
+ return shapeType.getBuilder(coordinateNode, radius, requestedOrientation, coerce.value());
+ }
+
+ /**
+ * Recursive method which parses the arrays of coordinates used to define
+ * Shapes
+ *
+ * @param parser
+ * Parser that will be read from
+ * @return CoordinateNode representing the start of the coordinate tree
+ * @throws IOException
+ * Thrown if an error occurs while reading from the
+ * XContentParser
+ */
+ private static CoordinateNode parseCoordinates(XContentParser parser) throws IOException {
+ XContentParser.Token token = parser.nextToken();
+ // Base cases
+ if (token != XContentParser.Token.START_ARRAY &&
+ token != XContentParser.Token.END_ARRAY &&
+ token != XContentParser.Token.VALUE_NULL) {
+ return new CoordinateNode(parseCoordinate(parser));
+ } else if (token == XContentParser.Token.VALUE_NULL) {
+ throw new IllegalArgumentException("coordinates cannot contain NULL values)");
+ }
+
+ List nodes = new ArrayList<>();
+ while (token != XContentParser.Token.END_ARRAY) {
+ nodes.add(parseCoordinates(parser));
+ token = parser.nextToken();
+ }
+
+ return new CoordinateNode(nodes);
+ }
+
+ private static Coordinate parseCoordinate(XContentParser parser) throws IOException {
+ double lon = parser.doubleValue();
+ parser.nextToken();
+ double lat = parser.doubleValue();
+ XContentParser.Token token = parser.nextToken();
+ while (token == XContentParser.Token.VALUE_NUMBER) {
+ token = parser.nextToken();
+ }
+ // todo support z/alt
+ return new Coordinate(lon, lat);
+ }
+
+ /**
+ * Parse the geometries array of a GeometryCollection
+ *
+ * @param parser Parser that will be read from
+ * @return Geometry[] geometries of the GeometryCollection
+ * @throws IOException Thrown if an error occurs while reading from the XContentParser
+ */
+ static GeometryCollectionBuilder parseGeometries(XContentParser parser, GeoShapeFieldMapper mapper) throws
+ IOException {
+ if (parser.currentToken() != XContentParser.Token.START_ARRAY) {
+ throw new ElasticsearchParseException("geometries must be an array of geojson objects");
+ }
+
+ XContentParser.Token token = parser.nextToken();
+ GeometryCollectionBuilder geometryCollection = new GeometryCollectionBuilder();
+ while (token != XContentParser.Token.END_ARRAY) {
+ ShapeBuilder shapeBuilder = ShapeParser.parse(parser);
+ geometryCollection.shape(shapeBuilder);
+ token = parser.nextToken();
+ }
+
+ return geometryCollection;
+ }
+}
diff --git a/core/src/main/java/org/elasticsearch/common/geo/parsers/ShapeParser.java b/core/src/main/java/org/elasticsearch/common/geo/parsers/ShapeParser.java
new file mode 100644
index 0000000000000..39540f902fedf
--- /dev/null
+++ b/core/src/main/java/org/elasticsearch/common/geo/parsers/ShapeParser.java
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+package org.elasticsearch.common.geo.parsers;
+
+import org.elasticsearch.ElasticsearchParseException;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.geo.builders.ShapeBuilder;
+import org.elasticsearch.common.xcontent.XContent;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.index.mapper.GeoShapeFieldMapper;
+
+import java.io.IOException;
+
+/**
+ * first point of entry for a shape parser
+ */
+public interface ShapeParser {
+ ParseField FIELD_TYPE = new ParseField("type");
+ ParseField FIELD_COORDINATES = new ParseField("coordinates");
+ ParseField FIELD_GEOMETRIES = new ParseField("geometries");
+ ParseField FIELD_ORIENTATION = new ParseField("orientation");
+
+ /**
+ * Create a new {@link ShapeBuilder} from {@link XContent}
+ * @param parser parser to read the GeoShape from
+ * @param shapeMapper document field mapper reference required for spatial parameters relevant
+ * to the shape construction process (e.g., orientation)
+ * todo: refactor to place build specific parameters in the SpatialContext
+ * @return {@link ShapeBuilder} read from the parser or null
+ * if the parsers current token has been null
+ * @throws IOException if the input could not be read
+ */
+ static ShapeBuilder parse(XContentParser parser, GeoShapeFieldMapper shapeMapper) throws IOException {
+ if (parser.currentToken() == XContentParser.Token.VALUE_NULL) {
+ return null;
+ } if (parser.currentToken() == XContentParser.Token.START_OBJECT) {
+ return GeoJsonParser.parse(parser, shapeMapper);
+ }
+ throw new ElasticsearchParseException("shape must be an object consisting of type and coordinates");
+ }
+
+ /**
+ * Create a new {@link ShapeBuilder} from {@link XContent}
+ * @param parser parser to read the GeoShape from
+ * @return {@link ShapeBuilder} read from the parser or null
+ * if the parsers current token has been null
+ * @throws IOException if the input could not be read
+ */
+ static ShapeBuilder parse(XContentParser parser) throws IOException {
+ return parse(parser, null);
+ }
+}
diff --git a/core/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java b/core/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java
index 61f32c67c20cb..38eaef1d14df9 100644
--- a/core/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java
+++ b/core/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java
@@ -264,20 +264,16 @@ public synchronized void addSettingsUpdateConsumer(Setting setting, Consu
}
/**
- * Validates that all settings in the builder are registered and valid
+ * Validates that all given settings are registered and valid
+ * @param settings the settings to validate
+ * @param validateDependencies if true
settings dependencies are validated as well.
+ * @see Setting#getSettingsDependencies(String)
*/
- public final void validate(Settings.Builder settingsBuilder) {
- validate(settingsBuilder.build());
- }
-
- /**
- * * Validates that all given settings are registered and valid
- */
- public final void validate(Settings settings) {
+ public final void validate(Settings settings, boolean validateDependencies) {
List exceptions = new ArrayList<>();
for (String key : settings.keySet()) { // settings iterate in deterministic fashion
try {
- validate(key, settings);
+ validate(key, settings, validateDependencies);
} catch (RuntimeException ex) {
exceptions.add(ex);
}
@@ -285,12 +281,11 @@ public final void validate(Settings settings) {
ExceptionsHelper.rethrowAndSuppress(exceptions);
}
-
/**
* Validates that the setting is valid
*/
- public final void validate(String key, Settings settings) {
- Setting setting = get(key);
+ void validate(String key, Settings settings, boolean validateDependencies) {
+ Setting setting = getRaw(key);
if (setting == null) {
LevensteinDistance ld = new LevensteinDistance();
List> scoredKeys = new ArrayList<>();
@@ -315,6 +310,20 @@ public final void validate(String key, Settings settings) {
"settings";
}
throw new IllegalArgumentException(msg);
+ } else {
+ Set settingsDependencies = setting.getSettingsDependencies(key);
+ if (setting.hasComplexMatcher()) {
+ setting = setting.getConcreteSetting(key);
+ }
+ if (validateDependencies && settingsDependencies.isEmpty() == false) {
+ Set settingKeys = settings.keySet();
+ for (String requiredSetting : settingsDependencies) {
+ if (settingKeys.contains(requiredSetting) == false) {
+ throw new IllegalArgumentException("Missing required setting ["
+ + requiredSetting + "] for setting [" + setting.getKey() + "]");
+ }
+ }
+ }
}
setting.get(settings);
}
@@ -375,7 +384,18 @@ default Runnable updater(Settings current, Settings previous) {
/**
* Returns the {@link Setting} for the given key or null
if the setting can not be found.
*/
- public Setting> get(String key) {
+ public final Setting> get(String key) {
+ Setting> raw = getRaw(key);
+ if (raw == null) {
+ return null;
+ } if (raw.hasComplexMatcher()) {
+ return raw.getConcreteSetting(key);
+ } else {
+ return raw;
+ }
+ }
+
+ private Setting> getRaw(String key) {
Setting> setting = keySettings.get(key);
if (setting != null) {
return setting;
@@ -383,7 +403,8 @@ public Setting> get(String key) {
for (Map.Entry> entry : complexMatchers.entrySet()) {
if (entry.getValue().match(key)) {
assert assertMatcher(key, 1);
- return entry.getValue().getConcreteSetting(key);
+ assert entry.getValue().hasComplexMatcher();
+ return entry.getValue();
}
}
return null;
@@ -513,7 +534,7 @@ private boolean updateSettings(Settings toApply, Settings.Builder target, Settin
} else if (get(key) == null) {
throw new IllegalArgumentException(type + " setting [" + key + "], not recognized");
} else if (isNull == false && canUpdate.test(key)) {
- validate(key, toApply);
+ validate(key, toApply, false); // we might not have a full picture here do to a dependency validation
settingsBuilder.copy(key, toApply);
updates.copy(key, toApply);
changed = true;
@@ -654,7 +675,7 @@ public String setValue(String value) {
* representation. Otherwise false
*/
// TODO this should be replaced by Setting.Property.HIDDEN or something like this.
- protected boolean isPrivateSetting(String key) {
+ public boolean isPrivateSetting(String key) {
return false;
}
}
diff --git a/core/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java b/core/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java
index 962e61b5c3c68..d40488eaa34f8 100644
--- a/core/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java
+++ b/core/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java
@@ -191,7 +191,7 @@ protected void validateSettingKey(Setting setting) {
}
@Override
- protected boolean isPrivateSetting(String key) {
+ public boolean isPrivateSetting(String key) {
switch (key) {
case IndexMetaData.SETTING_CREATION_DATE:
case IndexMetaData.SETTING_INDEX_UUID:
diff --git a/core/src/main/java/org/elasticsearch/common/settings/Setting.java b/core/src/main/java/org/elasticsearch/common/settings/Setting.java
index 9b99e67c8c4da..abc589aedafc3 100644
--- a/core/src/main/java/org/elasticsearch/common/settings/Setting.java
+++ b/core/src/main/java/org/elasticsearch/common/settings/Setting.java
@@ -42,6 +42,7 @@
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
@@ -126,7 +127,7 @@ public enum Property {
private static final EnumSet EMPTY_PROPERTIES = EnumSet.noneOf(Property.class);
private Setting(Key key, @Nullable Setting fallbackSetting, Function defaultValue, Function parser,
- Validator validator, Property... properties) {
+ Validator validator, Property... properties) {
assert this instanceof SecureSetting || this.isGroupSetting() || parser.apply(defaultValue.apply(Settings.EMPTY)) != null
: "parser returned null";
this.key = key;
@@ -457,6 +458,14 @@ public Setting getConcreteSetting(String key) {
return this;
}
+ /**
+ * Returns a set of settings that are required at validation time. Unless all of the dependencies are present in the settings
+ * object validation of setting must fail.
+ */
+ public Set getSettingsDependencies(String key) {
+ return Collections.emptySet();
+ }
+
/**
* Build a new updater with a noop validator.
*/
@@ -519,11 +528,13 @@ public String toString() {
public static class AffixSetting extends Setting {
private final AffixKey key;
private final Function> delegateFactory;
+ private final Set dependencies;
- public AffixSetting(AffixKey key, Setting delegate, Function> delegateFactory) {
+ public AffixSetting(AffixKey key, Setting delegate, Function> delegateFactory, AffixSetting... dependencies) {
super(key, delegate.defaultValue, delegate.parser, delegate.properties.toArray(new Property[0]));
this.key = key;
this.delegateFactory = delegateFactory;
+ this.dependencies = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(dependencies)));
}
boolean isGroupSetting() {
@@ -534,6 +545,15 @@ private Stream matchStream(Settings settings) {
return settings.keySet().stream().filter((key) -> match(key)).map(settingKey -> key.getConcreteString(settingKey));
}
+ public Set getSettingsDependencies(String settingsKey) {
+ if (dependencies.isEmpty()) {
+ return Collections.emptySet();
+ } else {
+ String namespace = key.getNamespace(settingsKey);
+ return dependencies.stream().map(s -> s.key.toConcreteKey(namespace).key).collect(Collectors.toSet());
+ }
+ }
+
AbstractScopedSettings.SettingUpdater