DATAES-628 - Introduce Document API.

We now expose a Document API to encapsulate data interchanged with Elasticsearch (get responses, search hits, arbitrary maps) for a consistent API.
This commit is contained in:
Mark Paluch 2019-08-12 14:53:20 +02:00 committed by Peter-Josef Meisch
parent ce686b1f03
commit 29ecd484c5
6 changed files with 1739 additions and 0 deletions

View File

@ -0,0 +1,388 @@
/*
* Copyright 2019 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;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import java.util.function.IntSupplier;
import java.util.function.LongSupplier;
import java.util.function.Supplier;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* A representation of a Elasticsearch document as extended {@link Map} with {@link String} keys. All iterators preserve
* original insertion order.
* <p>
* Document does not allow {@code null} keys. It allows {@literal null} values.
* <p>
* Implementing classes can bei either mutable or immutable. In case a subclass is immutable, its methods may throw
* {@link UnsupportedOperationException} when calling modifying methods.
*
* @author Mark Paluch
* @since 4.0
*/
public interface Document extends Map<String, Object> {
/**
* Create a new mutable {@link Document}.
*
* @return a new {@link Document}.
*/
static Document create() {
return new MapDocument();
}
/**
* Create a {@link Document} from a {@link Map} containing key-value pairs and sub-documents.
*
* @param map source map containing key-value pairs and sub-documents.
* @return a new {@link Document}.
*/
static Document from(Map<String, Object> map) {
Assert.notNull(map, "Map must not be null");
if (map instanceof LinkedHashMap) {
return new MapDocument(map);
}
return new MapDocument(new LinkedHashMap<>(map));
}
/**
* Parse JSON to {@link Document}.
*
* @param json must not be {@literal null}.
* @return the parsed {@link Document}.
*/
static Document parse(String json) {
try {
return new MapDocument(MapDocument.OBJECT_MAPPER.readerFor(Map.class).readValue(json));
} catch (IOException e) {
throw new ElasticsearchException("Cannot parse JSON", e);
}
}
/**
* {@link #put(Object, Object)} the {@code key}/{@code value} tuple and return {@code this} {@link Document}.
*
* @param key key with which the specified value is to be associated.
* @param value value to be associated with the specified key.
* @return {@code this} {@link Document}.
*/
default Document append(String key, Object value) {
put(key, value);
return this;
}
/**
* Return {@literal true} if this {@link Document} is associated with an identifier.
*
* @return {@literal true} if this {@link Document} is associated with an identifier, {@literal false} otherwise.
*/
default boolean hasId() {
return false;
}
/**
* Retrieve the identifier associated with this {@link Document}.
* <p>
* The default implementation throws {@link UnsupportedOperationException}. It's recommended to check {@link #hasId()}
* prior to calling this method.
*
* @return the identifier associated with this {@link Document}.
* @throws IllegalStateException if the underlying implementation supports Id's but no Id was yet associated with the
* document.
*/
default String getId() {
throw new UnsupportedOperationException();
}
/**
* Set the identifier for this {@link Document}.
* <p>
* The default implementation throws {@link UnsupportedOperationException}.
*/
default void setId(String id) {
throw new UnsupportedOperationException();
}
/**
* Return {@literal true} if this {@link Document} is associated with a version.
*
* @return {@literal true} if this {@link Document} is associated with a version, {@literal false} otherwise.
*/
default boolean hasVersion() {
return false;
}
/**
* Retrieve the version associated with this {@link Document}.
* <p>
* The default implementation throws {@link UnsupportedOperationException}. It's recommended to check
* {@link #hasVersion()} prior to calling this method.
*
* @return the version associated with this {@link Document}.
* @throws IllegalStateException if the underlying implementation supports Id's but no Id was yet associated with the
* document.
*/
default long getVersion() {
throw new UnsupportedOperationException();
}
/**
* Set the version for this {@link Document}.
* <p>
* The default implementation throws {@link UnsupportedOperationException}.
*/
default void setVersion(long version) {
throw new UnsupportedOperationException();
}
/**
* Returns the value to which the specified {@code key} is mapped, or {@literal null} if this document contains no
* mapping for the key. The value is casted within the method which makes it useful for calling code as it does not
* require casting on the calling side. If the value type is not assignable to {@code type}, then this method throws
* {@link ClassCastException}.
*
* @param key the key whose associated value is to be returned
* @param type the expected return value type.
* @param <T> expected return type.
* @return the value to which the specified key is mapped, or {@literal null} if this document contains no mapping for
* the key.
* @throws ClassCastException if the value of the given key is not of {@code type T}.
*/
@Nullable
default <T> T get(Object key, Class<T> type) {
Assert.notNull(key, "Key must not be null");
Assert.notNull(type, "Type must not be null");
return type.cast(get(key));
}
/**
* Returns the value to which the specified {@code key} is mapped, or {@literal null} if this document contains no
* mapping for the key. If the value type is not a {@link Boolean}, then this method throws
* {@link ClassCastException}.
*
* @param key the key whose associated value is to be returned
* @return the value to which the specified key is mapped, or {@literal null} if this document contains no mapping for
* the key.
* @throws ClassCastException if the value of the given key is not a {@link Boolean}.
*/
@Nullable
default Boolean getBoolean(String key) {
return get(key, Boolean.class);
}
/**
* Returns the value to which the specified {@code key} is mapped or {@code defaultValue} if this document contains no
* mapping for the key. If the value type is not a {@link Boolean}, then this method throws
* {@link ClassCastException}.
*
* @param key the key whose associated value is to be returned
* @return the value to which the specified key is mapped or {@code defaultValue} if this document contains no mapping
* for the key.
* @throws ClassCastException if the value of the given key is not a {@link Boolean}.
*/
default boolean getBooleanOrDefault(String key, boolean defaultValue) {
return getBooleanOrDefault(key, () -> defaultValue);
}
/**
* Returns the value to which the specified {@code key} is mapped or the value from {@code defaultValue} if this
* document contains no mapping for the key. If the value type is not a {@link Boolean}, then this method throws
* {@link ClassCastException}.
*
* @param key the key whose associated value is to be returned
* @return the value to which the specified key is mapped or the value from {@code defaultValue} if this document
* contains no mapping for the key.
* @throws ClassCastException if the value of the given key is not a {@link Boolean}.
* @see BooleanSupplier
*/
default boolean getBooleanOrDefault(String key, BooleanSupplier defaultValue) {
Boolean value = getBoolean(key);
return value == null ? defaultValue.getAsBoolean() : value;
}
/**
* Returns the value to which the specified {@code key} is mapped, or {@literal null} if this document contains no
* mapping for the key. If the value type is not a {@link Integer}, then this method throws
* {@link ClassCastException}.
*
* @param key the key whose associated value is to be returned
* @return the value to which the specified key is mapped, or {@literal null} if this document contains no mapping for
* the key.
* @throws ClassCastException if the value of the given key is not a {@link Integer}.
*/
@Nullable
default Integer getInt(String key) {
return get(key, Integer.class);
}
/**
* Returns the value to which the specified {@code key} is mapped or {@code defaultValue} if this document contains no
* mapping for the key. If the value type is not a {@link Integer}, then this method throws
* {@link ClassCastException}.
*
* @param key the key whose associated value is to be returned
* @return the value to which the specified key is mapped or {@code defaultValue} if this document contains no mapping
* for the key.
* @throws ClassCastException if the value of the given key is not a {@link Integer}.
*/
default int getIntOrDefault(String key, int defaultValue) {
return getIntOrDefault(key, () -> defaultValue);
}
/**
* Returns the value to which the specified {@code key} is mapped or the value from {@code defaultValue} if this
* document contains no mapping for the key. If the value type is not a {@link Integer}, then this method throws
* {@link ClassCastException}.
*
* @param key the key whose associated value is to be returned
* @return the value to which the specified key is mapped or the value from {@code defaultValue} if this document
* contains no mapping for the key.
* @throws ClassCastException if the value of the given key is not a {@link Integer}.
* @see IntSupplier
*/
default int getIntOrDefault(String key, IntSupplier defaultValue) {
Integer value = getInt(key);
return value == null ? defaultValue.getAsInt() : value;
}
/**
* Returns the value to which the specified {@code key} is mapped, or {@literal null} if this document contains no
* mapping for the key. If the value type is not a {@link Long}, then this method throws {@link ClassCastException}.
*
* @param key the key whose associated value is to be returned
* @return the value to which the specified key is mapped, or {@literal null} if this document contains no mapping for
* the key.
* @throws ClassCastException if the value of the given key is not a {@link Long}.
*/
@Nullable
default Long getLong(String key) {
return get(key, Long.class);
}
/**
* Returns the value to which the specified {@code key} is mapped or {@code defaultValue} if this document contains no
* mapping for the key. If the value type is not a {@link Long}, then this method throws {@link ClassCastException}.
*
* @param key the key whose associated value is to be returned
* @return the value to which the specified key is mapped or {@code defaultValue} if this document contains no mapping
* for the key.
* @throws ClassCastException if the value of the given key is not a {@link Long}.
*/
default long getLongOrDefault(String key, long defaultValue) {
return getLongOrDefault(key, () -> defaultValue);
}
/**
* Returns the value to which the specified {@code key} is mapped or the value from {@code defaultValue} if this
* document contains no mapping for the key. If the value type is not a {@link Long}, then this method throws
* {@link ClassCastException}.
*
* @param key the key whose associated value is to be returned
* @return the value to which the specified key is mapped or the value from {@code defaultValue} if this document
* contains no mapping for the key.
* @throws ClassCastException if the value of the given key is not a {@link Long}.
* @see LongSupplier
*/
default long getLongOrDefault(String key, LongSupplier defaultValue) {
Long value = getLong(key);
return value == null ? defaultValue.getAsLong() : value;
}
/**
* Returns the value to which the specified {@code key} is mapped, or {@literal null} if this document contains no
* mapping for the key. If the value type is not a {@link String}, then this method throws {@link ClassCastException}.
*
* @param key the key whose associated value is to be returned
* @return the value to which the specified key is mapped, or {@literal null} if this document contains no mapping for
* the key.
* @throws ClassCastException if the value of the given key is not a {@link String}.
*/
@Nullable
default String getString(String key) {
return get(key, String.class);
}
/**
* Returns the value to which the specified {@code key} is mapped or {@code defaultValue} if this document contains no
* mapping for the key. If the value type is not a {@link String}, then this method throws {@link ClassCastException}.
*
* @param key the key whose associated value is to be returned
* @return the value to which the specified key is mapped or {@code defaultValue} if this document contains no mapping
* for the key.
* @throws ClassCastException if the value of the given key is not a {@link String}.
*/
default String getStringOrDefault(String key, String defaultValue) {
return getStringOrDefault(key, () -> defaultValue);
}
/**
* Returns the value to which the specified {@code key} is mapped or the value from {@code defaultValue} if this
* document contains no mapping for the key. If the value type is not a {@link String}, then this method throws
* {@link ClassCastException}.
*
* @param key the key whose associated value is to be returned
* @return the value to which the specified key is mapped or the value from {@code defaultValue} if this document
* contains no mapping for the key.
* @throws ClassCastException if the value of the given key is not a {@link String}.
* @see Supplier
*/
default String getStringOrDefault(String key, Supplier<String> defaultValue) {
String value = getString(key);
return value == null ? defaultValue.get() : value;
}
/**
* This method allows the application of a function to {@code this} {@link Document}. The function should expect a
* single {@link Document} argument and produce an {@code R} result.
* <p>
* Any exception thrown by the function will be propagated to the caller.
*
* @param transformer functional interface to a apply
* @param <R> class of the result
* @return the result of applying the function to this string
* @see java.util.function.Function
*/
default <R> R transform(Function<? super Document, ? extends R> transformer) {
return transformer.apply(this);
}
/**
* Render this {@link Document} to JSON. Auxiliary values such as Id and version are not considered within the JSON
* representation.
*
* @return a JSON representation of this document.
*/
String toJson();
}

View File

@ -0,0 +1,285 @@
/*
* Copyright 2019 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;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import org.springframework.lang.Nullable;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* {@link Document} implementation backed by a {@link LinkedHashMap}.
*
* @author Mark Paluch
* @since 4.0
*/
class MapDocument implements Document {
static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private final LinkedHashMap<String, Object> documentAsMap;
private @Nullable String id;
private @Nullable Long version;
MapDocument() {
this(new LinkedHashMap<>());
}
MapDocument(Map<String, Object> documentAsMap) {
this.documentAsMap = new LinkedHashMap<>(documentAsMap);
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.Document#hasId()
*/
@Override
public boolean hasId() {
return this.id != null;
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.Document#getId()
*/
@Override
public String getId() {
if (!hasId()) {
throw new IllegalStateException("No Id associated with this Document");
}
return this.id;
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.Document#setId(java.lang.String)
*/
@Override
public void setId(String id) {
this.id = id;
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.Document#hasVersion()
*/
@Override
public boolean hasVersion() {
return this.version != null;
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.Document#getVersion()
*/
@Override
public long getVersion() {
if (!hasVersion()) {
throw new IllegalStateException("No version associated with this Document");
}
return this.version;
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.Document#setVersion(long)
*/
@Override
public void setVersion(long version) {
this.version = version;
}
/*
* (non-Javadoc)
* @see java.util.Map#size()
*/
@Override
public int size() {
return documentAsMap.size();
}
/*
* (non-Javadoc)
* @see java.util.Map#isEmpty()
*/
@Override
public boolean isEmpty() {
return documentAsMap.isEmpty();
}
/*
* (non-Javadoc)
* @see java.util.Map#containsKey(java.lang.Object)
*/
@Override
public boolean containsKey(Object key) {
return documentAsMap.containsKey(key);
}
/*
* (non-Javadoc)
* @see java.util.Map#containsValue(java.lang.Object)
*/
@Override
public boolean containsValue(Object value) {
return documentAsMap.containsValue(value);
}
/*
* (non-Javadoc)
* @see java.util.Map#get(java.lang.Object)
*/
@Override
public Object get(Object key) {
return documentAsMap.get(key);
}
/*
* (non-Javadoc)
* @see java.lang.Object#getOrDefault(java.lang.Object, java.lang.Object)
*/
@Override
public Object getOrDefault(Object key, Object defaultValue) {
return documentAsMap.getOrDefault(key, defaultValue);
}
/*
* (non-Javadoc)
* @see java.util.Map#put(java.lang.Object, java.lang.Object)
*/
@Override
public Object put(String key, Object value) {
return documentAsMap.put(key, value);
}
/*
* (non-Javadoc)
* @see java.util.Map#remove(java.lang.Object)
*/
@Override
public Object remove(Object key) {
return documentAsMap.remove(key);
}
/*
* (non-Javadoc)
* @see java.util.Map#putAll(Map)
*/
@Override
public void putAll(Map<? extends String, ?> m) {
documentAsMap.putAll(m);
}
/*
* (non-Javadoc)
* @see java.util.Map#clear()
*/
@Override
public void clear() {
documentAsMap.clear();
}
/*
* (non-Javadoc)
* @see java.util.Map#keySet()
*/
@Override
public Set<String> keySet() {
return documentAsMap.keySet();
}
/*
* (non-Javadoc)
* @see java.util.Map#values()
*/
@Override
public Collection<Object> values() {
return documentAsMap.values();
}
/*
* (non-Javadoc)
* @see java.util.Map#entrySet()
*/
@Override
public Set<Entry<String, Object>> entrySet() {
return documentAsMap.entrySet();
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object o) {
return documentAsMap.equals(o);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return documentAsMap.hashCode();
}
/*
* (non-Javadoc)
* @see java.util.Map#forEach(java.util.function.BiConsumer)
*/
@Override
public void forEach(BiConsumer<? super String, ? super Object> action) {
documentAsMap.forEach(action);
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.Document#toJson()
*/
@Override
public String toJson() {
try {
return OBJECT_MAPPER.writeValueAsString(this);
} catch (JsonProcessingException e) {
throw new ElasticsearchException("Cannot render document to JSON", e);
}
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
String id = hasId() ? getId() : "?";
String version = hasVersion() ? Long.toString(getVersion()) : "?";
return getClass().getSimpleName() + "@" + id + "#" + version + " " + toJson();
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright 2019 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;
/**
* Extension to {@link Document} exposing a search {@link #getScore() score}.
*
* @author Mark Paluch
* @since 4.0
* @see Document
*/
public interface SearchDocument extends Document {
/**
* Return the search {@code score}.
*
* @return the search {@code score}.
*/
float getScore();
}

View File

@ -0,0 +1,674 @@
/*
* Copyright 2019 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;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.document.DocumentField;
import org.elasticsearch.index.get.GetResult;
import org.elasticsearch.search.SearchHit;
import org.springframework.data.elasticsearch.Document;
import org.springframework.data.elasticsearch.ElasticsearchException;
import org.springframework.data.elasticsearch.SearchDocument;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
/**
* Utility class to adapt {@link org.elasticsearch.common.document.DocumentField} to
* {@link org.springframework.data.elasticsearch.Document}.
*
* @author Mark Paluch
* @since 4.0
*/
class DocumentAdapters {
/**
* Create a {@link Document} from {@link GetResponse}.
* <p>
* Returns a {@link Document} using the source if available.
*
* @param source the source {@link GetResponse}.
* @return the adapted {@link Document}.
*/
public static Document from(GetResponse source) {
Assert.notNull(source, "GetResponse must not be null");
if (source.isSourceEmpty()) {
return fromDocumentFields(source, source.getId(), source.getVersion());
}
Document document = Document.from(source.getSourceAsMap());
document.setId(source.getId());
document.setVersion(source.getVersion());
return document;
}
/**
* Create a {@link Document} from {@link GetResult}.
* <p>
* Returns a {@link Document} using the source if available.
*
* @param source the source {@link GetResult}.
* @return the adapted {@link Document}.
*/
public static Document from(GetResult source) {
Assert.notNull(source, "GetResult must not be null");
if (source.isSourceEmpty()) {
return fromDocumentFields(source, source.getId(), source.getVersion());
}
Document document = Document.from(source.getSource());
document.setId(source.getId());
document.setVersion(source.getVersion());
return document;
}
/**
* Create a {@link SearchDocument} from {@link SearchHit}.
* <p>
* Returns a {@link SearchDocument} using the source if available.
*
* @param source the source {@link SearchHit}.
* @return the adapted {@link SearchDocument}.
*/
public static SearchDocument from(SearchHit source) {
Assert.notNull(source, "SearchHit must not be null");
BytesReference sourceRef = source.getSourceRef();
if (sourceRef == null || sourceRef.length() == 0) {
return new SearchDocumentAdapter(source.getScore(),
fromDocumentFields(source, source.getId(), source.getVersion()));
}
Document document = Document.from(source.getSourceAsMap());
document.setId(source.getId());
if (source.getVersion() >= 0) {
document.setVersion(source.getVersion());
}
return new SearchDocumentAdapter(source.getScore(), document);
}
/**
* Create an unmodifiable {@link Document} from {@link Iterable} of {@link DocumentField}s.
*
* @param documentFields the {@link DocumentField}s backing the {@link Document}.
* @return the adapted {@link Document}.
*/
public static Document fromDocumentFields(Iterable<DocumentField> documentFields, String id, long version) {
if (documentFields instanceof Collection) {
return new DocumentFieldAdapter((Collection<DocumentField>) documentFields, id, version);
}
List<DocumentField> fields = new ArrayList<>();
for (DocumentField documentField : documentFields) {
fields.add(documentField);
}
return new DocumentFieldAdapter(fields, id, version);
}
// TODO: Performance regarding keys/values/entry-set
static class DocumentFieldAdapter implements Document {
private final Collection<DocumentField> documentFields;
private final String id;
private final long version;
DocumentFieldAdapter(Collection<DocumentField> documentFields, String id, long version) {
this.documentFields = documentFields;
this.id = id;
this.version = version;
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.Document#hasId()
*/
@Override
public boolean hasId() {
return id != null;
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.Document#getId()
*/
@Override
public String getId() {
return this.id;
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.Document#hasVersion()
*/
@Override
public boolean hasVersion() {
return this.version >= 0;
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.Document#getVersion()
*/
@Override
public long getVersion() {
if (!hasVersion()) {
throw new IllegalStateException("No version associated with this Document");
}
return this.version;
}
/*
* (non-Javadoc)
* @see java.util.Map#size()
*/
@Override
public int size() {
return documentFields.size();
}
/*
* (non-Javadoc)
* @see java.util.Map#isEmpty()
*/
@Override
public boolean isEmpty() {
return documentFields.isEmpty();
}
/*
* (non-Javadoc)
* @see java.util.Map#containsKey(java.lang.Object)
*/
@Override
public boolean containsKey(Object key) {
for (DocumentField documentField : documentFields) {
if (documentField.getName().equals(key)) {
return true;
}
}
return false;
}
/*
* (non-Javadoc)
* @see java.util.Map#containsValue(java.lang.Object)
*/
@Override
public boolean containsValue(Object value) {
for (DocumentField documentField : documentFields) {
Object fieldValue = getValue(documentField);
if (fieldValue != null && fieldValue.equals(value) || value == fieldValue) {
return true;
}
}
return false;
}
/*
* (non-Javadoc)
* @see java.util.Map#get(java.lang.Object)
*/
@Override
public Object get(Object key) {
for (DocumentField documentField : documentFields) {
if (documentField.getName().equals(key)) {
return getValue(documentField);
}
}
return null;
}
/*
* (non-Javadoc)
* @see java.util.Map#put(java.lang.Object, java.lang.Object)
*/
@Override
public Object put(String key, Object value) {
throw new UnsupportedOperationException();
}
/*
* (non-Javadoc)
* @see java.util.Map#remove(java.lang.Object)
*/
@Override
public Object remove(Object key) {
throw new UnsupportedOperationException();
}
/*
* (non-Javadoc)
* @see java.util.Map#putAll(Map)
*/
@Override
public void putAll(Map<? extends String, ?> m) {
throw new UnsupportedOperationException();
}
/*
* (non-Javadoc)
* @see java.util.Map#clear()
*/
@Override
public void clear() {
throw new UnsupportedOperationException();
}
/*
* (non-Javadoc)
* @see java.util.Map#keySet()
*/
@Override
public Set<String> keySet() {
return documentFields.stream().map(DocumentField::getName).collect(Collectors.toCollection(LinkedHashSet::new));
}
/*
* (non-Javadoc)
* @see java.util.Map#values()
*/
@Override
public Collection<Object> values() {
return documentFields.stream().map(DocumentFieldAdapter::getValue).collect(Collectors.toList());
}
/*
* (non-Javadoc)
* @see java.util.Map#entrySet()
*/
@Override
public Set<Entry<String, Object>> entrySet() {
return documentFields.stream().collect(Collectors.toMap(DocumentField::getName, DocumentFieldAdapter::getValue))
.entrySet();
}
/*
* (non-Javadoc)
* @see java.util.Map#forEach(java.util.function.BiConsumer)
*/
@Override
public void forEach(BiConsumer<? super String, ? super Object> action) {
Objects.requireNonNull(action);
documentFields.forEach(field -> {
action.accept(field.getName(), getValue(field));
});
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.Document#toJson()
*/
@Override
public String toJson() {
JsonFactory nodeFactory = new JsonFactory();
try {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
JsonGenerator generator = nodeFactory.createGenerator(stream, JsonEncoding.UTF8);
generator.writeStartObject();
for (DocumentField value : documentFields) {
if (value.getValues().size() > 1) {
generator.writeArrayFieldStart(value.getName());
for (Object val : value.getValues()) {
generator.writeObject(val);
}
generator.writeEndArray();
} else {
generator.writeObjectField(value.getName(), value.getValue());
}
}
generator.writeEndObject();
generator.flush();
return new String(stream.toByteArray(), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new ElasticsearchException("Cannot render JSON", e);
}
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return getClass().getSimpleName() + "@" + this.id + "#" + this.version + " " + toJson();
}
@Nullable
private static Object getValue(DocumentField documentField) {
if (documentField.getValues().isEmpty()) {
return null;
}
if (documentField.getValues().size() == 1) {
return documentField.getValue();
}
return documentField.getValues();
}
}
/**
* Adapter for a {@link SearchDocument}.
*/
static class SearchDocumentAdapter implements SearchDocument {
private final float score;
private final Document delegate;
SearchDocumentAdapter(float score, Document delegate) {
this.score = score;
this.delegate = delegate;
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.Document#append(java.lang.String, java.lang.Object)
*/
@Override
public SearchDocument append(String key, Object value) {
delegate.append(key, value);
return this;
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.SearchDocument#getScore()
*/
@Override
public float getScore() {
return score;
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.Document#hasId()
*/
@Override
public boolean hasId() {
return delegate.hasId();
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.Document#getId()
*/
@Override
public String getId() {
return delegate.getId();
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.Document#setId(java.lang.String)
*/
@Override
public void setId(String id) {
delegate.setId(id);
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.Document#hasVersion()
*/
@Override
public boolean hasVersion() {
return delegate.hasVersion();
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.Document#getVersion()
*/
@Override
public long getVersion() {
return delegate.getVersion();
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.Document#setVersion(long)
*/
@Override
public void setVersion(long version) {
delegate.setVersion(version);
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.Document#get(java.lang.Object, java.lang.Class)
*/
@Override
@Nullable
public <T> T get(Object key, Class<T> type) {
return delegate.get(key, type);
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.Document#toJson()
*/
@Override
public String toJson() {
return delegate.toJson();
}
/*
* (non-Javadoc)
* @see java.util.Map#size()
*/
@Override
public int size() {
return delegate.size();
}
/*
* (non-Javadoc)
* @see java.util.Map#isEmpty()
*/
@Override
public boolean isEmpty() {
return delegate.isEmpty();
}
/*
* (non-Javadoc)
* @see java.util.Map#containsKey(java.lang.Object)
*/
@Override
public boolean containsKey(Object key) {
return delegate.containsKey(key);
}
/*
* (non-Javadoc)
* @see java.util.Map#containsValue(java.lang.Object)
*/
@Override
public boolean containsValue(Object value) {
return delegate.containsValue(value);
}
/*
* (non-Javadoc)
* @see java.util.Map#get(java.lang.Object)
*/
@Override
public Object get(Object key) {
return delegate.get(key);
}
/*
* (non-Javadoc)
* @see java.util.Map#put(java.lang.Object, java.lang.Object)
*/
@Override
public Object put(String key, Object value) {
return delegate.put(key, value);
}
/*
* (non-Javadoc)
* @see java.util.Map#remove(java.lang.Object)
*/
@Override
public Object remove(Object key) {
return delegate.remove(key);
}
/*
* (non-Javadoc)
* @see java.util.Map#putAll(Map)
*/
@Override
public void putAll(Map<? extends String, ?> m) {
delegate.putAll(m);
}
/*
* (non-Javadoc)
* @see java.util.Map#clear()
*/
@Override
public void clear() {
delegate.clear();
}
/*
* (non-Javadoc)
* @see java.util.Map#keySet()
*/
@Override
public Set<String> keySet() {
return delegate.keySet();
}
/*
* (non-Javadoc)
* @see java.util.Map#values()
*/
@Override
public Collection<Object> values() {
return delegate.values();
}
/*
* (non-Javadoc)
* @see java.util.Map#entrySet()
*/
@Override
public Set<Entry<String, Object>> entrySet() {
return delegate.entrySet();
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof SearchDocumentAdapter))
return false;
SearchDocumentAdapter that = (SearchDocumentAdapter) o;
return Float.compare(that.score, score) == 0 && delegate.equals(that.delegate);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return delegate.hashCode();
}
/*
* (non-Javadoc)
* @see java.util.Map#forEach(java.util.function.BiConsumer)
*/
@Override
public void forEach(BiConsumer<? super String, ? super Object> action) {
delegate.forEach(action);
}
/*
* (non-Javadoc)
* @see java.util.Map#remove(java.lang.Object, java.lang.Object)
*/
@Override
public boolean remove(Object key, Object value) {
return delegate.remove(key, value);
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
String id = hasId() ? getId() : "?";
String version = hasVersion() ? Long.toString(getVersion()) : "?";
return getClass().getSimpleName() + "@" + id + "#" + version + " " + toJson();
}
}
}

View File

@ -0,0 +1,196 @@
/*
* Copyright 2019 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;
import static org.assertj.core.api.Assertions.*;
import java.util.Arrays;
import java.util.Collections;
import org.junit.Test;
/**
* Unit tests for {@link Document}.
*
* @author Mark Paluch
*/
public class DocumentUnitTests {
@Test // DATAES-628
public void shouldCreateNewDocument() {
Document document = Document.create().append("key", "value");
assertThat(document).containsEntry("key", "value");
}
@Test // DATAES-628
public void shouldCreateNewDocumentFromMap() {
Document document = Document.from(Collections.singletonMap("key", "value"));
assertThat(document).containsEntry("key", "value");
}
@Test // DATAES-628
public void shouldRenderDocumentToJson() {
Document document = Document.from(Collections.singletonMap("key", "value"));
assertThat(document.toJson()).isEqualTo("{\"key\":\"value\"}");
}
@Test // DATAES-628
public void shouldParseDocumentFromJson() {
Document document = Document.parse("{\"key\":\"value\"}");
assertThat(document).containsEntry("key", "value");
}
@Test // DATAES-628
public void shouldReturnContainsKey() {
Document document = Document.create().append("string", "value").append("bool", true).append("int", 43)
.append("long", 42L);
assertThat(document.containsKey("string")).isTrue();
assertThat(document.containsKey("not-set")).isFalse();
}
@Test // DATAES-628
public void shouldReturnContainsValue() {
Document document = Document.create().append("string", "value").append("bool", Arrays.asList(true, true, false))
.append("int", 43).append("long", 42L);
assertThat(document.containsValue("value")).isTrue();
assertThat(document.containsValue(43)).isTrue();
assertThat(document.containsValue(44)).isFalse();
assertThat(document.containsValue(Arrays.asList(true, true, false))).isTrue();
}
@Test // DATAES-628
public void shouldReturnTypedValue() {
Document document = Document.create().append("string", "value").append("bool", true).append("int", 43)
.append("long", 42L);
assertThat(document.get("string")).isEqualTo("value");
assertThat(document.getString("string")).isEqualTo("value");
assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> document.get("long", String.class));
}
@Test // DATAES-628
public void shouldReturnTypedValueString() {
Document document = Document.create().append("string", "value").append("bool", true).append("int", 43)
.append("long", 42L);
assertThat(document.getString("string")).isEqualTo("value");
assertThat(document.getStringOrDefault("not-set", "default")).isEqualTo("default");
assertThat(document.getStringOrDefault("not-set", () -> "default")).isEqualTo("default");
assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> document.getString("long"));
assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> document.get("long", String.class));
}
@Test // DATAES-628
public void shouldReturnTypedValueBoolean() {
Document document = Document.create().append("string", "value").append("bool", true).append("int", 43)
.append("long", 42L);
assertThat(document.getBoolean("bool")).isTrue();
assertThat(document.getBooleanOrDefault("not-set", true)).isTrue();
assertThat(document.getBooleanOrDefault("not-set", () -> true)).isTrue();
assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> document.getString("bool"));
assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> document.get("bool", String.class));
}
@Test // DATAES-628
public void shouldReturnTypedValueInt() {
Document document = Document.create().append("string", "value").append("bool", true).append("int", 43)
.append("long", 42L);
assertThat(document.getInt("int")).isEqualTo(43);
assertThat(document.getIntOrDefault("not-set", 44)).isEqualTo(44);
assertThat(document.getIntOrDefault("not-set", () -> 44)).isEqualTo(44);
assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> document.getString("int"));
assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> document.get("int", String.class));
}
@Test // DATAES-628
public void shouldReturnTypedValueLong() {
Document document = Document.create().append("string", "value").append("bool", true).append("int", 43)
.append("long", 42L);
assertThat(document.getLong("long")).isEqualTo(42);
assertThat(document.getLongOrDefault("not-set", 44)).isEqualTo(44);
assertThat(document.getLongOrDefault("not-set", () -> 44)).isEqualTo(44);
assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> document.getString("long"));
assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> document.get("long", String.class));
}
@Test // DATAES-628
public void shouldApplyTransformer() {
Document document = Document.create();
document.setId("value");
assertThat(document.transform(Document::getId)).isEqualTo("value");
}
@Test // DATAES-628
public void shouldSetId() {
Document document = Document.create();
assertThat(document.hasId()).isFalse();
assertThatIllegalStateException().isThrownBy(document::getId);
document.setId("foo");
assertThat(document.getId()).isEqualTo("foo");
}
@Test // DATAES-628
public void shouldSetVersion() {
Document document = Document.create();
assertThat(document.hasVersion()).isFalse();
assertThatIllegalStateException().isThrownBy(document::getVersion);
document.setVersion(14);
assertThat(document.getVersion()).isEqualTo(14);
}
@Test // DATAES-628
public void shouldRenderToString() {
Document document = Document.from(Collections.singletonMap("key", "value"));
document.setId("123");
document.setVersion(42);
assertThat(document).hasToString("MapDocument@123#42 {\"key\":\"value\"}");
}
}

View File

@ -0,0 +1,163 @@
/*
* Copyright 2019 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;
import static org.assertj.core.api.Assertions.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.document.DocumentField;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.index.get.GetResult;
import org.elasticsearch.search.SearchHit;
import org.junit.Test;
import org.springframework.data.elasticsearch.Document;
import org.springframework.data.elasticsearch.SearchDocument;
/**
* Unit tests for {@link DocumentAdapters}.
*
* @author Mark Paluch
*/
public class DocumentAdaptersUnitTests {
@Test // DATAES-628
public void shouldAdaptGetResponse() {
Map<String, DocumentField> fields = Collections.singletonMap("field",
new DocumentField("field", Arrays.asList("value")));
GetResult getResult = new GetResult("index", "type", "my-id", 1, 0, 42, true, null, fields);
GetResponse response = new GetResponse(getResult);
Document document = DocumentAdapters.from(response);
assertThat(document.hasId()).isTrue();
assertThat(document.getId()).isEqualTo("my-id");
assertThat(document.hasVersion()).isTrue();
assertThat(document.getVersion()).isEqualTo(42);
assertThat(document.get("field")).isEqualTo("value");
}
@Test // DATAES-628
public void shouldAdaptGetResponseSource() {
BytesArray source = new BytesArray("{\"field\":\"value\"}");
GetResult getResult = new GetResult("index", "type", "my-id", 1, 0, 42, true, source, Collections.emptyMap());
GetResponse response = new GetResponse(getResult);
Document document = DocumentAdapters.from(response);
assertThat(document.hasId()).isTrue();
assertThat(document.getId()).isEqualTo("my-id");
assertThat(document.hasVersion()).isTrue();
assertThat(document.getVersion()).isEqualTo(42);
assertThat(document.get("field")).isEqualTo("value");
}
@Test // DATAES-628
public void shouldAdaptSearchResponse() {
Map<String, DocumentField> fields = Collections.singletonMap("field",
new DocumentField("field", Arrays.asList("value")));
SearchHit searchHit = new SearchHit(123, "my-id", new Text("type"), fields);
searchHit.score(42);
SearchDocument document = DocumentAdapters.from(searchHit);
assertThat(document.hasId()).isTrue();
assertThat(document.getId()).isEqualTo("my-id");
assertThat(document.hasVersion()).isFalse();
assertThat(document.getScore()).isBetween(42f, 42f);
assertThat(document.get("field")).isEqualTo("value");
}
@Test // DATAES-628
public void searchResponseShouldReturnContainsKey() {
Map<String, DocumentField> fields = new LinkedHashMap<>();
fields.put("string", new DocumentField("string", Arrays.asList("value")));
fields.put("bool", new DocumentField("bool", Arrays.asList(true, true, false)));
SearchHit searchHit = new SearchHit(123, "my-id", new Text("type"), fields);
SearchDocument document = DocumentAdapters.from(searchHit);
assertThat(document.containsKey("string")).isTrue();
assertThat(document.containsKey("not-set")).isFalse();
}
@Test // DATAES-628
public void searchResponseShouldReturnContainsValue() {
Map<String, DocumentField> fields = new LinkedHashMap<>();
fields.put("string", new DocumentField("string", Arrays.asList("value")));
fields.put("bool", new DocumentField("bool", Arrays.asList(true, true, false)));
fields.put("null", new DocumentField("null", Collections.emptyList()));
SearchHit searchHit = new SearchHit(123, "my-id", new Text("type"), fields);
SearchDocument document = DocumentAdapters.from(searchHit);
assertThat(document.containsValue("value")).isTrue();
assertThat(document.containsValue(Arrays.asList(true, true, false))).isTrue();
assertThat(document.containsValue(null)).isTrue();
}
@Test // DATAES-628
public void shouldRenderToJson() {
Map<String, DocumentField> fields = new LinkedHashMap<>();
fields.put("string", new DocumentField("string", Arrays.asList("value")));
fields.put("bool", new DocumentField("bool", Arrays.asList(true, true, false)));
SearchHit searchHit = new SearchHit(123, "my-id", new Text("type"), fields);
SearchDocument document = DocumentAdapters.from(searchHit);
assertThat(document.toJson()).isEqualTo("{\"string\":\"value\",\"bool\":[true,true,false]}");
}
@Test // DATAES-628
public void shouldAdaptSearchResponseSource() {
BytesArray source = new BytesArray("{\"field\":\"value\"}");
SearchHit searchHit = new SearchHit(123, "my-id", new Text("type"), Collections.emptyMap());
searchHit.sourceRef(source).score(42);
searchHit.version(22);
SearchDocument document = DocumentAdapters.from(searchHit);
assertThat(document.hasId()).isTrue();
assertThat(document.getId()).isEqualTo("my-id");
assertThat(document.hasVersion()).isTrue();
assertThat(document.getVersion()).isEqualTo(22);
assertThat(document.getScore()).isBetween(42f, 42f);
assertThat(document.get("field")).isEqualTo("value");
}
}