diff --git a/src/main/java/org/springframework/data/elasticsearch/core/MappingBuilder.java b/src/main/java/org/springframework/data/elasticsearch/core/MappingBuilder.java index 31947dd41..eaad002d5 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/MappingBuilder.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/MappingBuilder.java @@ -16,9 +16,9 @@ package org.springframework.data.elasticsearch.core; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.springframework.core.GenericCollectionTypeResolver; import org.springframework.data.annotation.Transient; import org.springframework.data.elasticsearch.annotations.*; -import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.core.facet.FacetRequest; import org.springframework.data.elasticsearch.core.geo.GeoPoint; import org.springframework.data.mapping.model.SimpleTypeHolder; @@ -26,7 +26,6 @@ import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; import java.io.IOException; -import java.lang.reflect.*; import java.util.Arrays; import java.util.Map; @@ -52,7 +51,6 @@ class MappingBuilder { public static final String INDEX_VALUE_NOT_ANALYZED = "not_analyzed"; public static final String TYPE_VALUE_STRING = "string"; - public static final String TYPE_VALUE_OBJECT = "object"; public static final String TYPE_VALUE_GEO_POINT = "geo_point"; private static SimpleTypeHolder SIMPLE_TYPE_HOLDER = new SimpleTypeHolder(); @@ -60,35 +58,47 @@ class MappingBuilder { static XContentBuilder buildMapping(Class clazz, String indexType, String idFieldName) throws IOException { XContentBuilder xContentBuilder = jsonBuilder().startObject().startObject(indexType).startObject(FIELD_PROPERTIES); - mapEntity(xContentBuilder, clazz, true, idFieldName, EMPTY); + mapEntity(xContentBuilder, clazz, true, idFieldName, EMPTY, false); return xContentBuilder.endObject().endObject().endObject(); } private static void mapEntity(XContentBuilder xContentBuilder, Class clazz, boolean isRootObject, String idFieldName, - String nestedObjectFieldName) throws IOException { + String nestedObjectFieldName, boolean nestedAnnotaion) throws IOException { java.lang.reflect.Field[] fields = clazz.getDeclaredFields(); - if (!isRootObject && isAnyPropertyAnnotatedAsField(fields)) { - xContentBuilder.startObject(nestedObjectFieldName).field(FIELD_TYPE, TYPE_VALUE_OBJECT).startObject(FIELD_PROPERTIES); + if (!isRootObject && (isAnyPropertyAnnotatedAsField(fields) || nestedAnnotaion)) { + String type = FieldType.Object.toString().toLowerCase(); + if(nestedAnnotaion){ + type = FieldType.Nested.toString().toLowerCase(); + } + xContentBuilder.startObject(nestedObjectFieldName).field(FIELD_TYPE, type).startObject(FIELD_PROPERTIES); } for (java.lang.reflect.Field field : fields) { - if (field.isAnnotationPresent(Transient.class)) { + if (field.isAnnotationPresent(Transient.class) || isInIgnoreFields(field)) { continue; - } - - if (isEntity(field) && !isInIgnoreFields(field) && !isAnnotated(field)) { - mapEntity(xContentBuilder, field.getType(), false, EMPTY, field.getName()); - } + } + + boolean isGeoField = isGeoField(field); Field singleField = field.getAnnotation(Field.class); - MultiField multiField = field.getAnnotation(MultiField.class); - GeoPointField geoPoint = field.getAnnotation(GeoPointField.class); + if (!isGeoField && isEntity(field) && !isAnnotated(field)) { + if(singleField == null){ + continue; + } + boolean nestedField = isNestedField(field); + mapEntity(xContentBuilder, getFieldType(field), false, EMPTY, field.getName(), nestedField); + if(nestedField){ + continue; + } + } - if (field.getType() == GeoPoint.class || geoPoint != null) { + MultiField multiField = field.getAnnotation(MultiField.class); + + if (isGeoField) { applyGeoPointFieldMapping(xContentBuilder, field); } @@ -238,12 +248,20 @@ class MappingBuilder { builder.endObject(); } - private static boolean isEntity(java.lang.reflect.Field field) { + protected static boolean isEntity(java.lang.reflect.Field field) { TypeInformation typeInformation = ClassTypeInformation.from(field.getType()); - TypeInformation actualType = typeInformation.getActualType(); - boolean isComplexType = actualType == null ? false : !SIMPLE_TYPE_HOLDER.isSimpleType(actualType.getType()); - return isComplexType && !actualType.isCollectionLike() && !Map.class.isAssignableFrom(typeInformation.getType()); - //return isComplexType && !actualType.isCollectionLike() && !Map.class.isAssignableFrom(typeInformation.getType()) && !Number.class.isAssignableFrom(typeInformation.getType()); + Class clazz = getFieldType(field); + boolean isComplexType = !SIMPLE_TYPE_HOLDER.isSimpleType(clazz); + return isComplexType && !Map.class.isAssignableFrom(typeInformation.getType()); + } + + protected static Class getFieldType(java.lang.reflect.Field field) { + Class clazz = field.getType(); + TypeInformation typeInformation = ClassTypeInformation.from(clazz); + if (typeInformation.isCollectionLike()) { + clazz = GenericCollectionTypeResolver.getCollectionFieldType(field) != null ? GenericCollectionTypeResolver.getCollectionFieldType(field) : typeInformation.getComponentType().getType(); + } + return clazz; } private static boolean isAnyPropertyAnnotatedAsField(java.lang.reflect.Field[] fields) { @@ -269,4 +287,14 @@ class MappingBuilder { } return false; } + + private static boolean isNestedField(java.lang.reflect.Field field) { + Field fieldAnnotation = field.getAnnotation(Field.class); + return fieldAnnotation != null && (FieldType.Nested == fieldAnnotation.type() || FieldType.Object == fieldAnnotation.type()); + } + + private static boolean isGeoField(java.lang.reflect.Field field) { + return field.getType() == GeoPoint.class || field.getAnnotation(GeoPointField.class) != null; + } + } diff --git a/src/test/java/org/springframework/data/elasticsearch/GirlFriend.java b/src/test/java/org/springframework/data/elasticsearch/GirlFriend.java new file mode 100644 index 000000000..6e1a5dcb2 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/GirlFriend.java @@ -0,0 +1,59 @@ +/* + * Copyright 2014 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 + * + * 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.springframework.data.elasticsearch; + +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; + +import java.util.List; + +/** + * @author Mohsin Husen + */ + +public class GirlFriend { + + private String name; + + private String type; + + @Field(type = FieldType.Nested) + private List cars; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public List getCars() { + return cars; + } + + public void setCars(List cars) { + this.cars = cars; + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/NestedObjectTests.java b/src/test/java/org/springframework/data/elasticsearch/NestedObjectTests.java index 0e7c37cff..f1cc0ce86 100644 --- a/src/test/java/org/springframework/data/elasticsearch/NestedObjectTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/NestedObjectTests.java @@ -15,12 +15,15 @@ */ package org.springframework.data.elasticsearch; +import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; import org.springframework.data.elasticsearch.core.ElasticsearchTemplate; +import org.springframework.data.elasticsearch.core.query.GetQuery; import org.springframework.data.elasticsearch.core.query.IndexQuery; import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder; import org.springframework.data.elasticsearch.core.query.SearchQuery; @@ -67,10 +70,14 @@ public class NestedObjectTests { elasticsearchTemplate.createIndex(Person.class); elasticsearchTemplate.putMapping(Person.class); elasticsearchTemplate.refresh(Person.class, true); + elasticsearchTemplate.deleteIndex(PersonMultipleLevelNested.class); + elasticsearchTemplate.createIndex(PersonMultipleLevelNested.class); + elasticsearchTemplate.putMapping(PersonMultipleLevelNested.class); + elasticsearchTemplate.refresh(PersonMultipleLevelNested.class,true); } @Test - public void shouldIndexNestedObject(){ + public void shouldIndexInitialLevelNestedObject(){ List cars = new ArrayList(); @@ -128,6 +135,48 @@ public class NestedObjectTests { assertThat(persons.size() , is(1)); + } + + @Test + public void shouldIndexMultipleLevelNestedObject(){ + //given + List indexQueries = createPerson(); + + //when + elasticsearchTemplate.putMapping(PersonMultipleLevelNested.class); + elasticsearchTemplate.bulkIndex(indexQueries); + elasticsearchTemplate.refresh(PersonMultipleLevelNested.class, true); + + //then + GetQuery getQuery = new GetQuery(); + getQuery.setId("1"); + PersonMultipleLevelNested personIndexed = elasticsearchTemplate.queryForObject(getQuery, PersonMultipleLevelNested.class); + assertThat(personIndexed, is(notNullValue())); + } + + @Test + public void shouldSearchUsingNestedQueryOnMultipleLevelNestedObject(){ + //given + List indexQueries = createPerson(); + + //when + elasticsearchTemplate.putMapping(PersonMultipleLevelNested.class); + elasticsearchTemplate.bulkIndex(indexQueries); + elasticsearchTemplate.refresh(PersonMultipleLevelNested.class, true); + + //then + BoolQueryBuilder builder = boolQuery(); + builder.must(nestedQuery("girlFriends", termQuery("girlFriends.type","temp"))) + .must(nestedQuery("girlFriends.cars", termQuery("girlFriends.cars.name", "Ford".toLowerCase()))); + + SearchQuery searchQuery = new NativeSearchQueryBuilder() + .withQuery(builder) + .build(); + + Page personIndexed = elasticsearchTemplate.queryForPage(searchQuery, PersonMultipleLevelNested.class); + assertThat(personIndexed, is(notNullValue())); + assertThat(personIndexed.getTotalElements(), is(1L)); + assertThat(personIndexed.getContent().get(0).getId(), is("1")); } @Test @@ -146,4 +195,63 @@ public class NestedObjectTests { // then assertThat(bookRepository.findOne(id), is(notNullValue())); } + + private List createPerson(){ + + PersonMultipleLevelNested person1 = new PersonMultipleLevelNested(); + + person1.setId("1"); + person1.setName("name"); + + Car saturn = new Car(); + saturn.setName("Saturn"); + saturn.setModel("SL"); + + Car subaru = new Car(); + subaru.setName("Subaru"); + subaru.setModel("Imprezza"); + + Car car = new Car(); + car.setName("Saturn"); + car.setModel("Imprezza"); + + Car ford = new Car(); + ford.setName("Ford"); + ford.setModel("Focus"); + + GirlFriend permanent = new GirlFriend(); + permanent.setName("permanent"); + permanent.setType("permanent"); + permanent.setCars(Arrays.asList(saturn, subaru)); + + GirlFriend temp = new GirlFriend(); + temp.setName("temp"); + temp.setType("temp"); + temp.setCars(Arrays.asList(car,ford)); + + person1.setGirlFriends(Arrays.asList(permanent, temp)); + + IndexQuery indexQuery1 = new IndexQuery(); + indexQuery1.setId(person1.getId()); + indexQuery1.setObject(person1); + + + PersonMultipleLevelNested person2 = new PersonMultipleLevelNested(); + + person2.setId("2"); + person2.setName("name"); + + person2.setGirlFriends(Arrays.asList(permanent)); + + IndexQuery indexQuery2 = new IndexQuery(); + indexQuery2.setId(person2.getId()); + indexQuery2.setObject(person2); + + List indexQueries = new ArrayList(); + indexQueries.add(indexQuery1); + indexQueries.add(indexQuery2); + + return indexQueries; + + } } diff --git a/src/test/java/org/springframework/data/elasticsearch/PersonMultipleLevelNested.java b/src/test/java/org/springframework/data/elasticsearch/PersonMultipleLevelNested.java new file mode 100644 index 000000000..0c7e54889 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/PersonMultipleLevelNested.java @@ -0,0 +1,66 @@ +/* + * Copyright 2014 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 + * + * 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.springframework.data.elasticsearch; + + +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.FieldType; + +import java.util.List; + +/** + * @author Rizwan Idrees + * @author Mohsin Husen + * @author Artur Konczak + */ + +@Document( indexName = "person-multiple-level-nested" , type = "user", indexStoreType = "memory", shards = 1, replicas = 0, refreshInterval = "-1") +public class PersonMultipleLevelNested { + + @Id + private String id; + + private String name; + + @Field(type = FieldType.Nested) + private List girlFriends; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getGirlFriends() { + return girlFriends; + } + + public void setGirlFriends(List girlFriends) { + this.girlFriends = girlFriends; + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTests.java b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTests.java index feaf9fd54..9953bc24b 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ElasticsearchTemplateTests.java @@ -869,7 +869,7 @@ public class ElasticsearchTemplateTests { indexQuery.setObject(sampleEntity); elasticsearchTemplate.index(indexQuery); - + elasticsearchTemplate.refresh(SampleEntity.class, true); // when elasticsearchTemplate.deleteType("test-index", "test-type"); diff --git a/src/test/java/org/springframework/data/elasticsearch/core/MappingBuilderTests.java b/src/test/java/org/springframework/data/elasticsearch/core/MappingBuilderTests.java index 955eb1a2f..997e5d74e 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/MappingBuilderTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/MappingBuilderTests.java @@ -4,8 +4,10 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.elasticsearch.*; -import org.springframework.data.elasticsearch.core.query.IndexQuery; +import org.springframework.data.elasticsearch.SampleTransientEntity; +import org.springframework.data.elasticsearch.SimpleRecursiveEntity; +import org.springframework.data.elasticsearch.StockPrice; +import org.springframework.data.elasticsearch.StockPriceBuilder; import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder; import org.springframework.data.elasticsearch.core.query.SearchQuery; import org.springframework.test.context.ContextConfiguration; @@ -18,8 +20,6 @@ import java.util.List; import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.eq; /** * @author Stuart Stevenson