CustomMappingBuilder.java

/*
 * Copyright 2014-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.gringlobal.custom.elasticsearch;

import static org.elasticsearch.common.xcontent.XContentFactory.*;
import static org.springframework.util.StringUtils.*;

import java.io.IOException;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.time.Instant;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.UUID;

import javax.persistence.Column;
import javax.persistence.Lob;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentType;
import org.gringlobal.custom.validation.javax.CodeValueField;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.elasticsearch.annotations.CompletionContext;
import org.springframework.data.elasticsearch.annotations.CompletionField;
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.InnerField;
import org.springframework.data.elasticsearch.annotations.Mapping;
import org.springframework.data.elasticsearch.annotations.MultiField;
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.mapping.ElasticsearchPersistentEntity;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;

import com.fasterxml.jackson.annotation.JsonIdentityReference;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;

/**
 * Modified MappingBuilder that includes all simple (non-entity) types and
 * forces index to not use dynamic mappings.
 *
 * @author Rizwan Idrees
 * @author Mohsin Husen
 * @author Artur Konczak
 * @author Kevin Leturc
 * @author Alexander Volz
 * @author Dennis Maaß
 * @author Pavel Luhin
 * @author Mark Paluch
 * @author Sascha Woo
 * @author Nordine Bittich
 * @author Robert Gruendler
 * @author Petr Kukral
 * @author Peter-Josef Meisch
 * @author Matija Obreza
 * @author Maxym Borodenko
 */
@Slf4j
class CustomMappingBuilder {

	private static final String FIELD_DATA = "fielddata";
	private static final String FIELD_STORE = "store";
	private static final String FIELD_TYPE = "type";
	private static final String FIELD_INDEX = "index";
	private static final String FIELD_FORMAT = "format";
	private static final String FIELD_SEARCH_ANALYZER = "search_analyzer";
	private static final String FIELD_INDEX_ANALYZER = "analyzer";
	private static final String FIELD_NORMALIZER = "normalizer";
	private static final String FIELD_PROPERTIES = "properties";
	private static final String FIELD_PARENT = "_parent";
	private static final String FIELD_COPY_TO = "copy_to";
	private static final String FIELD_CONTEXT_NAME = "name";
	private static final String FIELD_CONTEXT_TYPE = "type";
	private static final String FIELD_CONTEXT_PRECISION = "precision";
	private static final String FIELD_DYNAMIC = "dynamic";
//	private static final String FIELD_DYNAMIC_TEMPLATES = "dynamic_templates";

	private static final String COMPLETION_PRESERVE_SEPARATORS = "preserve_separators";
	private static final String COMPLETION_PRESERVE_POSITION_INCREMENTS = "preserve_position_increments";
	private static final String COMPLETION_MAX_INPUT_LENGTH = "max_input_length";
	private static final String COMPLETION_CONTEXTS = "contexts";

	private static final String TYPE_VALUE_KEYWORD = "keyword";
	private static final String TYPE_VALUE_GEO_POINT = "geo_point";
	private static final String TYPE_VALUE_COMPLETION = "completion";
	private static final String ALL_TEXT_FIELD = "_texts";
	private static final String[] COPYTO_ALL_TEXT = { ALL_TEXT_FIELD };

	private final ElasticsearchConverter elasticsearchConverter;

	CustomMappingBuilder(ElasticsearchConverter elasticsearchConverter) {
		this.elasticsearchConverter = elasticsearchConverter;
	}

	/**
	 * builds the Elasticsearch mapping for the given clazz.
	 * @param object 
	 * @param indexType 
	 *
	 * @return JSON string
	 * @throws IOException
	 */
	XContentBuilder buildPropertyMapping(Class<?> clazz, String indexType, String parentType) throws IOException {

		ElasticsearchPersistentEntity<?> entity = elasticsearchConverter.getMappingContext()
				.getRequiredPersistentEntity(clazz);

		XContentBuilder builder = jsonBuilder().startObject().startObject(indexType);

		// Parent
		if (hasText(parentType)) {
			builder.startObject(FIELD_PARENT).field(FIELD_TYPE, parentType).endObject();
		}

		// Dynamic
		builder.field(FIELD_DYNAMIC, false);

		// We don't want the source stored in the index
		XContentBuilder ignoreSource = builder.startObject("_source");
		ignoreSource.field("enabled", false);
		ignoreSource.endObject();

		// Properties
		builder.startObject(FIELD_PROPERTIES);
		builder.startObject(ALL_TEXT_FIELD).field(FIELD_TYPE, "text").endObject();

		mapEntity(new HashSet<>(), builder, entity, true, "", false, FieldType.Auto, null, null);

		builder.endObject() // FIELD_PROPERTIES
				.endObject() // indexType
				.endObject() // root object
				.close();

		return builder;
	}

	private void mapEntity(Set<Class<?>> circularReferences, XContentBuilder builder, @Nullable ElasticsearchPersistentEntity<?> entity, boolean isRootObject,
			String nestedObjectFieldName, boolean nestedOrObjectField, FieldType fieldType,
			@Nullable Field parentFieldAnnotation, JsonIgnoreProperties jsonIgnoreProperties) throws IOException {

		if (entity != null && circularReferences.contains(entity.getTypeInformation().getType())) {
			log.info("Circular reference detected class={} in {}", entity.getIndexName(), circularReferences);
			return;
		} else if (entity != null) {
			circularReferences.add(entity.getTypeInformation().getType());
		}

		boolean writeNestedProperties = !isRootObject && (isAnyPropertyAnnotatedWithField(entity) || nestedOrObjectField);
		if (writeNestedProperties) {

			String type = nestedOrObjectField ? fieldType.toString().toLowerCase()
					: FieldType.Object.toString().toLowerCase();
			builder.startObject(nestedObjectFieldName).field(FIELD_TYPE, type);

			if (nestedOrObjectField && FieldType.Nested == fieldType && parentFieldAnnotation != null
					&& parentFieldAnnotation.includeInParent()) {

				builder.field("include_in_parent", parentFieldAnnotation.includeInParent());
			}

			builder.startObject(FIELD_PROPERTIES);
		}
		if (entity != null) {

			entity.doWithProperties((PropertyHandler<ElasticsearchPersistentProperty>) property -> {
				try {
					// if (property.isAnnotationPresent(Transient.class) || isInIgnoreFields(property, parentFieldAnnotation, jsonIgnoreProperties)) {
					if (isInIgnoreFields(property, parentFieldAnnotation, jsonIgnoreProperties)) {
						return;
					}

					buildPropertyMapping(circularReferences, builder, isRootObject, property);

					CodeValueField codeValueField = property.findAnnotation(CodeValueField.class);
					if (codeValueField != null && codeValueField.indexed()) {
						builder.startObject(property.getFieldName() + "_cv");
						builder.field(FIELD_TYPE, FieldType.Text.name().toLowerCase());
						builder.field(FIELD_STORE, false);
						builder.field(FIELD_COPY_TO, ALL_TEXT_FIELD);
						builder.endObject();
					}

				} catch (IOException e) {
					log.warn("error mapping property with name {}", property.getName(), e);
				}
			});

			// Add _class field to entities, don't store
			builder.startObject("_class").field(FIELD_TYPE, TYPE_VALUE_KEYWORD).field(FIELD_STORE, false).endObject();

			circularReferences.remove(entity.getTypeInformation().getType());
		}

		if (writeNestedProperties) {
			builder.endObject().endObject();
		}
	}

	private void buildPropertyMapping(Set<Class<?>> circularReferences, XContentBuilder builder, boolean isRootObject,
			ElasticsearchPersistentProperty property) throws IOException {

		if (property.isAnnotationPresent(Mapping.class)) {

			String mappingPath = property.getRequiredAnnotation(Mapping.class).mappingPath();
			if (!StringUtils.isEmpty(mappingPath)) {

				ClassPathResource mappings = new ClassPathResource(mappingPath);
				if (mappings.exists()) {
					builder.rawField(property.getFieldName(), mappings.getInputStream(), XContentType.JSON);
					return;
				}
			}
		}

		boolean isGeoPointProperty = isGeoPointProperty(property);
		boolean isCompletionProperty = isCompletionProperty(property);
		boolean isNestedOrObjectProperty = isNestedOrObjectProperty(property);

		Field fieldAnnotation = property.findAnnotation(Field.class);
		if (!isGeoPointProperty && !isCompletionProperty && hasRelevantAnnotation(property)) {

			JsonIdentityReference idRef = property.findAnnotation(JsonIdentityReference.class);
			if (idRef != null && idRef.alwaysAsId()) {
				log.debug("@JsonIdentityReference for {}#{}", property.getActualType(), property.getFieldName());
				applyDefaultIdFieldMapping(builder, property);
				return;
			}
			if (fieldAnnotation == null) {
				log.info("Skipping {}#{}", property.getActualType(), property.getFieldName());
				return;
			}

			Iterator<? extends TypeInformation<?>> iterator = property.getPersistentEntityTypeInformation().iterator();
			ElasticsearchPersistentEntity<?> persistentEntity = iterator.hasNext()
					? elasticsearchConverter.getMappingContext().getPersistentEntity(iterator.next())
					: null;

			JsonIgnoreProperties jsonIgnoreProperties = property.findAnnotation(JsonIgnoreProperties.class);
			mapEntity(circularReferences, builder, persistentEntity, false, property.getFieldName(), isNestedOrObjectProperty,
					fieldAnnotation.type(), fieldAnnotation, jsonIgnoreProperties);

			if (isNestedOrObjectProperty) {
				return;
			}
		}

		MultiField multiField = property.findAnnotation(MultiField.class);

		if (isGeoPointProperty) {
			applyGeoPointFieldMapping(builder, property);
			return;
		}

		if (isCompletionProperty) {
			CompletionField completionField = property.findAnnotation(CompletionField.class);
			applyCompletionFieldMapping(builder, property, completionField);
		}

		if (isRootObject && fieldAnnotation != null && property.isIdProperty()) {
			applyDefaultIdFieldMapping(builder, property);
		} else if (multiField != null) {
			addMultiFieldMapping(builder, property, multiField, isNestedOrObjectProperty);
		} else if (fieldAnnotation != null) {
			addSingleFieldMapping(builder, property, fieldAnnotation, isNestedOrObjectProperty);
		} else if (!property.isEntity() && !property.isMap() && !isConstant(property.getField())) {
			// This includes all simple property
			addSingleFieldMapping(builder, property, null, false);
		} else {
			Iterator<? extends TypeInformation<?>> entityTypes = property.getPersistentEntityTypeInformation().iterator();
			if (entityTypes.hasNext()) {
				TypeInformation<?> entityType = entityTypes.next();
				Class<?> type1 = entityType.getType();
				if (UUID.class.isAssignableFrom(type1)) {
					log.info("Force adding {}.{} {}", property.getOwner().getName(), property.getFieldName(), property.getActualType());
					addSingleFieldMapping(builder, property, null, false);
				} else {
					log.info("Skipping {}.{} {}", property.getOwner().getName(), property.getFieldName(), property.getActualType());
				}
			} else {
				log.info("Skipping {}.{} {}", property.getOwner().getName(), property.getFieldName(), property.getActualType());
			}
		}
	}

	/// Things that generally don't change much
	private boolean isConstant(java.lang.reflect.Field field) {
		return field != null && (Modifier.isStatic(field.getModifiers()) || Modifier.isFinal(field.getModifiers()));
	}

	private boolean hasRelevantAnnotation(ElasticsearchPersistentProperty property) {

		return property.findAnnotation(Field.class) != null || property.findAnnotation(MultiField.class) != null
				|| property.findAnnotation(GeoPointField.class) != null
				|| property.findAnnotation(CompletionField.class) != null;
	}

	private void applyGeoPointFieldMapping(XContentBuilder builder, ElasticsearchPersistentProperty property)
			throws IOException {

		builder.startObject(property.getFieldName()).field(FIELD_TYPE, TYPE_VALUE_GEO_POINT).endObject();
	}

	private void applyCompletionFieldMapping(XContentBuilder builder, ElasticsearchPersistentProperty property,
			@Nullable CompletionField annotation) throws IOException {

		builder.startObject(property.getFieldName());
		builder.field(FIELD_TYPE, TYPE_VALUE_COMPLETION);

		if (annotation != null) {

			builder.field(COMPLETION_MAX_INPUT_LENGTH, annotation.maxInputLength());
			builder.field(COMPLETION_PRESERVE_POSITION_INCREMENTS, annotation.preservePositionIncrements());
			builder.field(COMPLETION_PRESERVE_SEPARATORS, annotation.preserveSeparators());
			if (!StringUtils.isEmpty(annotation.searchAnalyzer())) {
				builder.field(FIELD_SEARCH_ANALYZER, annotation.searchAnalyzer());
			}
			if (!StringUtils.isEmpty(annotation.analyzer())) {
				builder.field(FIELD_INDEX_ANALYZER, annotation.analyzer());
			}

			if (annotation.contexts().length > 0) {

				builder.startArray(COMPLETION_CONTEXTS);
				for (CompletionContext context : annotation.contexts()) {

					builder.startObject();
					builder.field(FIELD_CONTEXT_NAME, context.name());
					builder.field(FIELD_CONTEXT_TYPE, context.type().name().toLowerCase());
					if (context.precision().length() > 0) {
						builder.field(FIELD_CONTEXT_PRECISION, context.precision());
					}
					builder.endObject();
				}
				builder.endArray();
			}

		}
		builder.endObject();
	}

	private void applyDefaultIdFieldMapping(XContentBuilder builder, ElasticsearchPersistentProperty property)
			throws IOException {

		builder.startObject(property.getFieldName()).field(FIELD_TYPE, TYPE_VALUE_KEYWORD).field(FIELD_INDEX, true)
				.endObject();
	}

	/**
	 * Add mapping for @Field annotation
	 *
	 * @throws IOException
	 */
	private void addSingleFieldMapping(XContentBuilder builder, ElasticsearchPersistentProperty property, Field annotation, boolean nestedOrObjectField) throws IOException {
		log.trace("addSingleFieldMapping {}#{}", property.getActualType(), property.getFieldName());

		builder.startObject(property.getFieldName());
		addFieldMappingParameters(builder, property, annotation, nestedOrObjectField);
		builder.endObject();
	}

	/**
	 * Add mapping for @MultiField annotation
	 *
	 * @throws IOException
	 */
	private void addMultiFieldMapping(XContentBuilder builder, ElasticsearchPersistentProperty property,
			MultiField annotation, boolean nestedOrObjectField) throws IOException {

		// main field
		builder.startObject(property.getFieldName());
		addFieldMappingParameters(builder, property, annotation.mainField(), nestedOrObjectField);

		// inner fields
		builder.startObject("fields");
		for (InnerField innerField : annotation.otherFields()) {
			builder.startObject(innerField.suffix());
			addFieldMappingParameters(builder, property, innerField, false);
			builder.endObject();
		}
		builder.endObject();

		builder.endObject();
	}

	private void addFieldMappingParameters(XContentBuilder builder, ElasticsearchPersistentProperty property, Object annotation, boolean nestedOrObjectField) throws IOException {
		boolean index;
		boolean store = false;
		boolean fielddata = false;
		FieldType type;
		DateFormat dateFormat;
		String datePattern = null;
		String analyzer = null;
		String searchAnalyzer = null;
		String normalizer = null;
		String[] copyTo = null;

		if (annotation == null) {
			// Auto-detect
			type = typeForField(property);
			index = true; // indexForField(property);
			// What fields need fielddata? We are using for indexed text for now.
//			if (index && type.equals(FieldType.Text)) {
//				fielddata = Boolean.TRUE;
//			}
			dateFormat = DateFormat.none;
		} else if (annotation instanceof Field) {
			// @Field
			Field fieldAnnotation = (Field) annotation;
			index = fieldAnnotation.index();
			store = fieldAnnotation.store();
			fielddata = fieldAnnotation.fielddata();
			type = fieldAnnotation.type();
			dateFormat = fieldAnnotation.format();
			datePattern = fieldAnnotation.pattern();
			analyzer = fieldAnnotation.analyzer();
			searchAnalyzer = fieldAnnotation.searchAnalyzer();
			normalizer = fieldAnnotation.normalizer();
			copyTo = fieldAnnotation.copyTo();
		} else if (annotation instanceof InnerField) {
			// @InnerField
			InnerField fieldAnnotation = (InnerField) annotation;
			index = fieldAnnotation.index();
			store = fieldAnnotation.store();
			fielddata = fieldAnnotation.fielddata();
			type = fieldAnnotation.type();
			dateFormat = fieldAnnotation.format();
			datePattern = fieldAnnotation.pattern();
			analyzer = fieldAnnotation.analyzer();
			searchAnalyzer = fieldAnnotation.searchAnalyzer();
			normalizer = fieldAnnotation.normalizer();
		} else {
			throw new IllegalArgumentException("annotation must be an instance of @Field or @InnerField");
		}

		if (!nestedOrObjectField && store) {
			builder.field(FIELD_STORE, store);
		}
		if (fielddata) {
			builder.field(FIELD_DATA, fielddata);
		}
		if (type != FieldType.Auto) {
			builder.field(FIELD_TYPE, type.name().toLowerCase());

			if (type == FieldType.Date && dateFormat != DateFormat.none) {
				builder.field(FIELD_FORMAT, dateFormat == DateFormat.custom ? datePattern : dateFormat.toString());
			}
			
			// Add indexed version of keywords for full-text search
			if (type == FieldType.Keyword && annotation == null) {
				builder.startObject("fields");
				builder.startObject("i"); // for indexed
				builder.field(FIELD_TYPE, FieldType.Text.name().toLowerCase());
				builder.endObject();
				builder.endObject();
			}
			
		} else {
			builder.field(FIELD_TYPE, typeForField(property).name().toLowerCase());
		}
		if (!index) {
			builder.field(FIELD_INDEX, index);
		}
		if (!StringUtils.isEmpty(analyzer)) {
			builder.field(FIELD_INDEX_ANALYZER, analyzer);
		}
		if (!StringUtils.isEmpty(searchAnalyzer)) {
			builder.field(FIELD_SEARCH_ANALYZER, searchAnalyzer);
		}
		if (!StringUtils.isEmpty(normalizer)) {
			builder.field(FIELD_NORMALIZER, normalizer);
		}

		SearchField searchField = property.getField().getAnnotation(SearchField.class);
		if (searchField != null) {
			if (copyTo == null) {
				copyTo = COPYTO_ALL_TEXT;
			} else {
				copyTo = ArrayUtils.add(copyTo, ALL_TEXT_FIELD);
			}
		}

		if (copyTo != null && copyTo.length > 0) {
			builder.field(FIELD_COPY_TO, copyTo);
		}
	}

	private FieldType typeForField(@NonNull ElasticsearchPersistentProperty property) {
		if (property.isCollectionLike()) {
			var field = property.getField();
			if (field.getType().equals(byte[].class)) {
				return FieldType.Text;
			}
			var type = field.getGenericType();
//			if (type instanceof ParameterizedType) {
				ParameterizedType paramType = (ParameterizedType) type;
				Class<?> paramClass = (Class<?>) paramType.getActualTypeArguments()[0];
				return typeForClass(paramClass, property.getField());
//			}
		} else {
			return typeForClass(property.getActualType(), property.getField());
		}
	}

	private FieldType typeForClass(Class<?> clazz, java.lang.reflect.Field field) {
		final int targetTypeIndex = 1;
		// Handle @JsonSerialize(converter)
		JsonSerialize jsonSerialize = field.getAnnotation(JsonSerialize.class);
		if (jsonSerialize != null) {
			Class<?> converter = jsonSerialize.converter();
			if (converter != null) {
				ParameterizedType paramType = (ParameterizedType) converter.getGenericSuperclass();
				clazz = (Class<?>) paramType.getActualTypeArguments()[targetTypeIndex];
				log.info("Field {}.{} is serialized using {} to {}", field.getDeclaringClass(), field.getName(), converter.getName(), clazz);
			}
		}

		if (String.class.equals(clazz)) {
			return isAnalyzed(clazz, field) ? FieldType.Text : FieldType.Keyword;
		} else if (Boolean.TYPE.equals(clazz) || Boolean.class.equals(clazz)) {
			return FieldType.Boolean;
		} else if (Long.TYPE.equals(clazz) || Long.class.equals(clazz)) {
			return FieldType.Long;
		} else if (Double.TYPE.equals(clazz) || Double.class.equals(clazz)) {
			return FieldType.Double;
		} else if (Float.TYPE.equals(clazz) || Float.class.equals(clazz)) {
			return FieldType.Float;
		} else if (Integer.TYPE.equals(clazz) || Integer.class.equals(clazz)) {
			return FieldType.Integer;
		} else if (Date.class.isAssignableFrom(clazz)) {
			return FieldType.Date;
		} else if (Instant.class.isAssignableFrom(clazz)) {
			return FieldType.Date;
		} else if (LocalDate.class.isAssignableFrom(clazz)) {
			return FieldType.Date;
		} else if (Number.class.isAssignableFrom(clazz)) {
			return FieldType.Double;
		} else if (UUID.class.isAssignableFrom(clazz)) {
			return FieldType.Keyword;
		} else if (clazz.isEnum()) {
			return FieldType.Keyword;
		} else {
			log.warn("Default mapping for class={} as {} for field={}#{}", clazz, FieldType.Text, field.getDeclaringClass(), field.getName());
			return FieldType.Text;
		}
	}

//	private Boolean indexForField(@NonNull ElasticsearchPersistentProperty property) {
//		if (property.isCollectionLike()) {
//			ParameterizedType paramType = (ParameterizedType) property.getField().getGenericType();
//			Class<?> paramClass = (Class<?>) paramType.getActualTypeArguments()[0];
//			return isAnalyzed(paramClass, property.getField());
//		} else {
//			return isAnalyzed(property.getActualType(), property.getField());
//		}
//	}

	private Boolean isAnalyzed(Class<?> clazz, java.lang.reflect.Field field) {
		if (String.class.equals(clazz)) {
			Lob jpaLob = field.getAnnotation(Lob.class);
			Column jpaColumn = field.getAnnotation(Column.class);
			boolean analyzed = false;
			if (jpaLob != null) {
				analyzed = true;
			} else if (jpaColumn != null) {
				if (jpaColumn.length() > 100) {
					analyzed = true;
				}
			}
			return analyzed ? Boolean.TRUE : Boolean.FALSE;
		}
		else if (clazz.isEnum()) {
			return Boolean.FALSE;
		} else {
			log.debug("Using default indexing for class={}", clazz);
			return Boolean.TRUE;
		}
	}

	private boolean isAnyPropertyAnnotatedWithField(@Nullable ElasticsearchPersistentEntity<?> entity) {

		return entity != null && entity.getPersistentProperty(Field.class) != null;
	}

	private boolean isInIgnoreFields(ElasticsearchPersistentProperty property, @Nullable Field parentFieldAnnotation, JsonIgnoreProperties jsonIgnoreProperties) {
		if (property.getFieldName().equals("serialVersionUID")) {
			return true;
		}

		IgnoreField ignoreFieldAnnotation = property.findAnnotation(IgnoreField.class);
		if (ignoreFieldAnnotation != null) {
			return ignoreFieldAnnotation.value();
		} 

		JsonIgnore ignoreAnnotation = property.findAnnotation(JsonIgnore.class);
		if (ignoreAnnotation != null) {
			return ignoreAnnotation.value();
		}

		if (null != parentFieldAnnotation) {
			String[] ignoreFields = parentFieldAnnotation.ignoreFields();
			if (Arrays.asList(ignoreFields).contains(property.getFieldName())) {
				return true;
			}
		}

		if (null != jsonIgnoreProperties) {
			String[] ignoreFields = jsonIgnoreProperties.value();
			if (Arrays.asList(ignoreFields).contains(property.getFieldName())) {
				return true;
			}
		}

		return false;
	}

	private boolean isNestedOrObjectProperty(ElasticsearchPersistentProperty property) {

		Field fieldAnnotation = property.findAnnotation(Field.class);
		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 isCompletionProperty(ElasticsearchPersistentProperty property) {
		return property.getActualType() == Completion.class;
	}
}