diff --git a/src/main/java/org/springframework/data/elasticsearch/annotations/GeoShapeField.java b/src/main/java/org/springframework/data/elasticsearch/annotations/GeoShapeField.java new file mode 100644 index 000000000..ad8f4bfdc --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/annotations/GeoShapeField.java @@ -0,0 +1,44 @@ +/* + * Copyright 2017-2020 the original author or authors. + * + * 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 + * + * https://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.springframework.data.elasticsearch.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Lukas Vorisek + * @author Peter-Josef Meisch + * @since 4.1 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +@Documented +public @interface GeoShapeField { + Orientation orientation() default Orientation.ccw; + + boolean ignoreMalformed() default false; + + boolean ignoreZValue() default true; + + boolean coerce() default false; + + enum Orientation { + right, ccw, counterclockwise, left, cw, clockwise + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/index/GeoShapeMappingParameters.java b/src/main/java/org/springframework/data/elasticsearch/core/index/GeoShapeMappingParameters.java new file mode 100644 index 000000000..3f1cbdda6 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/GeoShapeMappingParameters.java @@ -0,0 +1,89 @@ +/* + * Copyright 2020 the original author or authors. + * + * 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 + * + * https://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.springframework.data.elasticsearch.core.index; + +import java.io.IOException; + +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.springframework.data.elasticsearch.annotations.GeoShapeField; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * @author Peter-Josef Meisch + */ +final class GeoShapeMappingParameters { + private static final String FIELD_PARAM_TYPE = "type"; + private static final String FIELD_PARAM_COERCE = "coerce"; + private static final String FIELD_PARAM_IGNORE_MALFORMED = "ignore_malformed"; + private static final String FIELD_PARAM_IGNORE_Z_VALUE = "ignore_z_value"; + private static final String FIELD_PARAM_ORIENTATION = "orientation"; + + private static final String TYPE_VALUE_GEO_SHAPE = "geo_shape"; + + private final boolean coerce; + private final boolean ignoreMalformed; + private final boolean ignoreZValue; + private final GeoShapeField.Orientation orientation; + + /** + * Creates a GeoShapeMappingParameters from the given annotation. + * + * @param annotation if null, default values are set in the returned object + * @return a parameters object + */ + public static GeoShapeMappingParameters from(@Nullable GeoShapeField annotation) { + + if (annotation == null) { + return new GeoShapeMappingParameters(false, false, true, GeoShapeField.Orientation.ccw); + } else { + return new GeoShapeMappingParameters(annotation.coerce(), annotation.ignoreMalformed(), annotation.ignoreZValue(), + annotation.orientation()); + } + } + + private GeoShapeMappingParameters(boolean coerce, boolean ignoreMalformed, boolean ignoreZValue, + GeoShapeField.Orientation orientation) { + this.coerce = coerce; + this.ignoreMalformed = ignoreMalformed; + this.ignoreZValue = ignoreZValue; + this.orientation = orientation; + } + + public void writeTypeAndParametersTo(XContentBuilder builder) throws IOException { + + Assert.notNull(builder, "builder must ot be null"); + + if (coerce) { + builder.field(FIELD_PARAM_COERCE, coerce); + } + + if (ignoreMalformed) { + builder.field(FIELD_PARAM_IGNORE_MALFORMED, ignoreMalformed); + } + + if (!ignoreZValue) { + builder.field(FIELD_PARAM_IGNORE_Z_VALUE, ignoreZValue); + } + + if (orientation != GeoShapeField.Orientation.ccw) { + builder.field(FIELD_PARAM_ORIENTATION, orientation.name()); + } + + builder.field(FIELD_PARAM_TYPE, TYPE_VALUE_GEO_SHAPE); + + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/index/MappingBuilder.java b/src/main/java/org/springframework/data/elasticsearch/core/index/MappingBuilder.java index 8f48c0d85..757888170 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/MappingBuilder.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/MappingBuilder.java @@ -35,10 +35,7 @@ import org.springframework.data.elasticsearch.ElasticsearchException; import org.springframework.data.elasticsearch.annotations.*; import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate; import org.springframework.data.elasticsearch.core.ResourceUtil; -import org.springframework.data.elasticsearch.core.completion.Completion; import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; -import org.springframework.data.elasticsearch.core.geo.GeoPoint; -import org.springframework.data.elasticsearch.core.join.JoinField; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity; import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty; import org.springframework.data.mapping.MappingException; @@ -203,17 +200,21 @@ public class MappingBuilder { } } - if (isGeoPointProperty(property)) { + if (property.isGeoPointProperty()) { applyGeoPointFieldMapping(builder, property); return; } - if (isJoinFieldProperty(property)) { + if (property.isGeoShapeProperty()) { + applyGeoShapeMapping(builder, property); + } + + if (property.isJoinFieldProperty()) { addJoinFieldMapping(builder, property); } Field fieldAnnotation = property.findAnnotation(Field.class); - boolean isCompletionProperty = isCompletionProperty(property); + boolean isCompletionProperty = property.isCompletionProperty(); boolean isNestedOrObjectProperty = isNestedOrObjectProperty(property); if (!isCompletionProperty && property.isEntity() && hasRelevantAnnotation(property)) { @@ -228,8 +229,8 @@ public class MappingBuilder { ? elasticsearchConverter.getMappingContext().getPersistentEntity(iterator.next()) : null; - mapEntity(builder, persistentEntity, false, property.getFieldName(), isNestedOrObjectProperty, - fieldAnnotation.type(), fieldAnnotation, property.findAnnotation(DynamicMapping.class)); + mapEntity(builder, persistentEntity, false, property.getFieldName(), true, fieldAnnotation.type(), + fieldAnnotation, property.findAnnotation(DynamicMapping.class)); return; } } @@ -259,10 +260,17 @@ public class MappingBuilder { private void applyGeoPointFieldMapping(XContentBuilder builder, ElasticsearchPersistentProperty property) throws IOException { - builder.startObject(property.getFieldName()).field(FIELD_PARAM_TYPE, TYPE_VALUE_GEO_POINT).endObject(); } + private void applyGeoShapeMapping(XContentBuilder builder, ElasticsearchPersistentProperty property) + throws IOException { + + builder.startObject(property.getFieldName()); + GeoShapeMappingParameters.from(property.findAnnotation(GeoShapeField.class)).writeTypeAndParametersTo(builder); + builder.endObject(); + } + private void applyCompletionFieldMapping(XContentBuilder builder, ElasticsearchPersistentProperty property, @Nullable CompletionField annotation) throws IOException { @@ -448,16 +456,4 @@ public class MappingBuilder { return fieldAnnotation != null && (FieldType.Nested == fieldAnnotation.type() || FieldType.Object == fieldAnnotation.type()); } - - private boolean isGeoPointProperty(ElasticsearchPersistentProperty property) { - return property.getActualType() == GeoPoint.class || property.isAnnotationPresent(GeoPointField.class); - } - - private boolean isJoinFieldProperty(ElasticsearchPersistentProperty property) { - return property.getActualType() == JoinField.class; - } - - private boolean isCompletionProperty(ElasticsearchPersistentProperty property) { - return property.getActualType() == Completion.class; - } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/index/MappingParameters.java b/src/main/java/org/springframework/data/elasticsearch/core/index/MappingParameters.java index 92750c872..189f21853 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/MappingParameters.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/MappingParameters.java @@ -195,6 +195,7 @@ public final class MappingParameters { * @param builder must not be {@literal null}. */ public void writeTypeAndParametersTo(XContentBuilder builder) throws IOException { + Assert.notNull(builder, "builder must ot be null"); if (fielddata) { diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentProperty.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentProperty.java index 1c922e7c8..63db55350 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentProperty.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/ElasticsearchPersistentProperty.java @@ -102,6 +102,30 @@ public interface ElasticsearchPersistentProperty extends PersistentProperty { INSTANCE; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java index 50665af2a..33ff1ad0e 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java @@ -26,10 +26,15 @@ import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.DateFormat; import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.FieldType; +import org.springframework.data.elasticsearch.annotations.GeoPointField; +import org.springframework.data.elasticsearch.annotations.GeoShapeField; import org.springframework.data.elasticsearch.annotations.MultiField; import org.springframework.data.elasticsearch.annotations.Parent; import org.springframework.data.elasticsearch.annotations.Score; +import org.springframework.data.elasticsearch.core.completion.Completion; import org.springframework.data.elasticsearch.core.convert.ElasticsearchDateConverter; +import org.springframework.data.elasticsearch.core.geo.GeoPoint; +import org.springframework.data.elasticsearch.core.join.JoinField; import org.springframework.data.elasticsearch.core.query.SeqNoPrimaryTerm; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.MappingException; @@ -215,66 +220,58 @@ public class SimpleElasticsearchPersistentProperty extends return StringUtils.hasText(name) ? name : null; } - /* - * (non-Javadoc) - * @see org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty#getFieldName() - */ @Override public String getFieldName() { return annotatedFieldName == null ? getProperty().getName() : annotatedFieldName; } - /* - * (non-Javadoc) - * @see org.springframework.data.mapping.model.AnnotationBasedPersistentProperty#isIdProperty() - */ @Override public boolean isIdProperty() { return isId; } - /* - * (non-Javadoc) - * @see org.springframework.data.mapping.model.AbstractPersistentProperty#createAssociation() - */ @Override protected Association createAssociation() { throw new UnsupportedOperationException(); } - /* - * (non-Javadoc) - * @see org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty#isScoreProperty() - */ @Override public boolean isScoreProperty() { return isScore; } - /* - * (non-Javadoc) - * @see org.springframework.data.mapping.model.AbstractPersistentProperty#isImmutable() - */ @Override public boolean isImmutable() { return false; } - /* - * (non-Javadoc) - * @see org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty#isParentProperty() - */ @Override public boolean isParentProperty() { return isParent; } - /* - * (non-Javadoc) - * @see org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty#isSeqNoPrimaryTermProperty() - */ @Override public boolean isSeqNoPrimaryTermProperty() { return isSeqNoPrimaryTerm; } + + @Override + public boolean isGeoPointProperty() { + return getActualType() == GeoPoint.class || isAnnotationPresent(GeoPointField.class); + } + + @Override + public boolean isGeoShapeProperty() { + return isAnnotationPresent(GeoShapeField.class); + } + + @Override + public boolean isJoinFieldProperty() { + return getActualType() == JoinField.class; + } + + @Override + public boolean isCompletionProperty() { + return getActualType() == Completion.class; + } } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderTests.java b/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderTests.java index 13d7cdd81..ae2ee2f18 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderTests.java @@ -210,13 +210,37 @@ public class MappingBuilderTests extends MappingContextBaseTests { assertThat(entry.getMessage()).isEqualTo(message); } - @Test // DATAES-568 - public void shouldBuildMappingsForGeoPoint() throws JSONException { + @Test // DATAES-568, DATAES-929 + @DisplayName("should build mappings for geo types") + void shouldBuildMappingsForGeoTypes() throws JSONException { // given - String expected = "{\"properties\": {" + "\"pointA\":{\"type\":\"geo_point\"}," - + "\"pointB\":{\"type\":\"geo_point\"}," + "\"pointC\":{\"type\":\"geo_point\"}," - + "\"pointD\":{\"type\":\"geo_point\"}" + "}}"; + String expected = "{\n" + // + " \"properties\": {\n" + // + " \"pointA\": {\n" + // + " \"type\": \"geo_point\"\n" + // + " },\n" + // + " \"pointB\": {\n" + // + " \"type\": \"geo_point\"\n" + // + " },\n" + // + " \"pointC\": {\n" + // + " \"type\": \"geo_point\"\n" + // + " },\n" + // + " \"pointD\": {\n" + // + " \"type\": \"geo_point\"\n" + // + " },\n" + // + " \"shape1\": {\n" + // + " \"type\": \"geo_shape\"\n" + // + " },\n" + // + " \"shape2\": {\n" + // + " \"type\": \"geo_shape\",\n" + // + " \"orientation\": \"clockwise\",\n" + // + " \"ignore_malformed\": true,\n" + // + " \"ignore_z_value\": false,\n" + // + " \"coerce\": true\n" + // + " }\n" + // + " }\n" + // + "}\n}"; // // when String mapping; @@ -961,9 +985,6 @@ public class MappingBuilderTests extends MappingContextBaseTests { } } - /** - * @author Artur Konczak - */ @Setter @Getter @NoArgsConstructor @@ -981,12 +1002,14 @@ public class MappingBuilderTests extends MappingContextBaseTests { // geo point - Custom implementation + Spring Data @GeoPointField private Point pointA; - private GeoPoint pointB; - @GeoPointField private String pointC; - @GeoPointField private double[] pointD; + + // geo shape, until e have the classes for this, us a strng + @GeoShapeField private String shape1; + @GeoShapeField(coerce = true, ignoreMalformed = true, ignoreZValue = false, + orientation = GeoShapeField.Orientation.clockwise) private String shape2; } /**