
org.elasticsearch.script.CtxMap Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of elasticsearch Show documentation
Show all versions of elasticsearch Show documentation
Elasticsearch - Open Source, Distributed, RESTful Search Engine
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.script;
import org.elasticsearch.common.util.set.Sets;
import java.util.AbstractCollection;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* A scripting ctx map with metadata for write ingest contexts. Delegates all metadata updates to metadata and
* all other updates to source. Implements the {@link Map} interface for backwards compatibility while performing
* validation via {@link Metadata}.
*/
public class CtxMap extends AbstractMap {
protected static final String SOURCE = "_source";
protected Map source;
protected final T metadata;
/**
* Create CtxMap from a source and metadata
*
* @param source the source document map
* @param metadata the metadata map
*/
public CtxMap(Map source, T metadata) {
this.source = source;
this.metadata = metadata;
Set badKeys = Sets.intersection(this.metadata.keySet(), this.source.keySet());
if (badKeys.size() > 0) {
throw new IllegalArgumentException(
"unexpected metadata ["
+ badKeys.stream().sorted().map(k -> k + ":" + this.source.get(k)).collect(Collectors.joining(", "))
+ "] in source"
);
}
}
/**
* Does this access to the internal {@link #source} map occur directly via ctx? ie {@code ctx['myField']}.
* Or does it occur via the {@link #SOURCE} key? ie {@code ctx['_source']['myField']}.
*
* Defaults to indirect, {@code ctx['_source']}
*/
protected boolean directSourceAccess() {
return false;
}
/**
* get the source map, if externally modified then the guarantees of this class are not enforced
*/
public final Map getSource() {
return source;
}
/**
* get the metadata map, if externally modified then the guarantees of this class are not enforced
*/
public T getMetadata() {
return metadata;
}
/**
* Returns an entrySet that respects the validators of the map.
*/
@Override
public Set> entrySet() {
// Make a copy of the Metadata.keySet() to avoid a ConcurrentModificationException when removing a value from the iterator
Set> sourceEntries = directSourceAccess() ? source.entrySet() : Set.of(new IndirectSourceEntry());
return new EntrySet(sourceEntries, new HashSet<>(metadata.keySet()));
}
/**
* Associate a key with a value. If the key has a validator, it is applied before association.
* @throws IllegalArgumentException if value does not pass validation for the given key
*/
@Override
public Object put(String key, Object value) {
if (metadata.isAvailable(key)) {
return metadata.put(key, value);
}
if (directSourceAccess()) {
return source.put(key, value);
} else if (SOURCE.equals(key)) {
return replaceSource(value);
}
throw new IllegalArgumentException("Cannot put key [" + key + "] with value [" + value + "] into ctx");
}
private Object replaceSource(Object value) {
if (value instanceof Map == false) {
throw new IllegalArgumentException(
"Expected ["
+ SOURCE
+ "] to be a Map, not ["
+ value
+ "]"
+ (value != null ? " with type [" + value.getClass().getName() + "]" : "")
);
}
var oldSource = source;
source = castSourceMap(value);
return oldSource;
}
@SuppressWarnings({ "unchecked", "raw" })
private static Map castSourceMap(Object value) {
return (Map) value;
}
/**
* Remove the mapping of key. If the key has a validator, it is checked before key removal.
* @throws IllegalArgumentException if the validator does not allow the key to be removed
*/
@Override
public Object remove(Object key) {
// uses map directly to avoid AbstractMaps linear time implementation using entrySet()
if (key instanceof String str) {
if (metadata.isAvailable(str)) {
return metadata.remove(str);
}
}
if (directSourceAccess()) {
return source.remove(key);
} else {
throw new UnsupportedOperationException("Cannot remove key " + key + " from ctx");
}
}
/**
* Clear entire map. For each key in the map with a validator, that validator is checked as in {@link #remove(Object)}.
* @throws IllegalArgumentException if any validator does not allow the key to be removed, in this case the map is unmodified
*/
@Override
public void clear() {
// AbstractMap uses entrySet().clear(), it should be quicker to run through the validators, then call the wrapped maps clear
for (String key : metadata.keySet()) {
metadata.remove(key);
}
// TODO: this is just bogus, there isn't any case where metadata won't trip a failure above?
source.clear();
}
@Override
public int size() {
// uses map directly to avoid creating an EntrySet via AbstractMaps implementation, which returns entrySet().size()
final int sourceSize = directSourceAccess() ? source.size() : 1;
return sourceSize + metadata.size();
}
@Override
public boolean containsValue(Object value) {
// uses map directly to avoid AbstractMaps linear time implementation using entrySet()
return metadata.containsValue(value) || (directSourceAccess() ? source.containsValue(value) : source.equals(value));
}
@Override
public boolean containsKey(Object key) {
// uses map directly to avoid AbstractMaps linear time implementation using entrySet()
if (key instanceof String str) {
if (metadata.isAvailable(str)) {
return metadata.containsKey(str);
}
return directSourceAccess() ? source.containsKey(key) : SOURCE.equals(key);
}
return false;
}
@Override
public Object get(Object key) {
// uses map directly to avoid AbstractMaps linear time implementation using entrySet()
if (key instanceof String str) {
if (metadata.isAvailable(str)) {
return metadata.get(str);
}
}
return directSourceAccess() ? source.get(key) : (SOURCE.equals(key) ? source : null);
}
/**
* Set of entries of the wrapped map that calls the appropriate validator before changing an entries value or removing an entry.
*
* Inherits {@link AbstractSet#removeAll(Collection)}, which calls the overridden {@link #remove(Object)} which performs validation.
*
* Inherits {@link AbstractCollection#retainAll(Collection)} and {@link AbstractCollection#clear()}, which both use
* {@link EntrySetIterator#remove()} for removal.
*/
class EntrySet extends AbstractSet> {
Set> sourceSet;
Set metadataKeys;
EntrySet(Set> sourceSet, Set metadataKeys) {
this.sourceSet = sourceSet;
this.metadataKeys = metadataKeys;
}
@Override
public Iterator> iterator() {
return new EntrySetIterator(sourceSet.iterator(), metadataKeys.iterator());
}
@Override
public int size() {
return sourceSet.size() + metadataKeys.size();
}
@Override
public boolean remove(Object o) {
if (o instanceof Map.Entry, ?> entry) {
if (entry.getKey() instanceof String key) {
if (metadata.containsKey(key)) {
if (Objects.equals(entry.getValue(), metadata.get(key))) {
metadata.remove(key);
return true;
}
}
}
}
return sourceSet.remove(o);
}
}
/**
* Iterator over the wrapped map that returns a validating {@link Entry} on {@link #next()} and validates on {@link #remove()}.
*
* {@link #remove()} is called by remove in {@link AbstractMap#values()}, {@link AbstractMap#keySet()}, {@link AbstractMap#clear()} via
* {@link AbstractSet#clear()}
*/
class EntrySetIterator implements Iterator> {
final Iterator> sourceIter;
final Iterator metadataKeyIter;
boolean sourceCur = true;
Map.Entry cur;
EntrySetIterator(Iterator> sourceIter, Iterator metadataKeyIter) {
this.sourceIter = sourceIter;
this.metadataKeyIter = metadataKeyIter;
}
@Override
public boolean hasNext() {
return sourceIter.hasNext() || metadataKeyIter.hasNext();
}
@Override
public Map.Entry next() {
sourceCur = sourceIter.hasNext();
return cur = sourceCur ? sourceIter.next() : new Entry(metadataKeyIter.next());
}
/**
* Remove current entry from the backing Map. Checks the Entry's key's validator, if one exists, before removal.
* @throws IllegalArgumentException if the validator does not allow the Entry to be removed
* @throws IllegalStateException if remove is called before {@link #next()}
*/
@Override
public void remove() {
if (cur == null) {
throw new IllegalStateException();
}
if (sourceCur) {
try {
sourceIter.remove();
} catch (UnsupportedOperationException e) {
// UnsupportedOperationException's message is "remove", rethrowing with more helpful message
throw new UnsupportedOperationException("Cannot remove key [" + cur.getKey() + "] from ctx");
}
} else {
metadata.remove(cur.getKey());
}
}
}
private class IndirectSourceEntry implements Map.Entry {
@Override
public String getKey() {
return SOURCE;
}
@Override
public Object getValue() {
return source;
}
@Override
public Object setValue(Object value) {
return replaceSource(value);
}
}
/**
* Map.Entry that stores metadata key and calls into {@link #metadata} for {@link #setValue}
*/
class Entry implements Map.Entry {
final String key;
Entry(String key) {
this.key = key;
}
@Override
public String getKey() {
return key;
}
@Override
public Object getValue() {
return metadata.get(key);
}
@Override
public Object setValue(Object value) {
return metadata.put(key, value);
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if ((o instanceof CtxMap) == false) return false;
if (super.equals(o) == false) return false;
CtxMap> ctxMap = (CtxMap>) o;
return source.equals(ctxMap.source) && metadata.equals(ctxMap.metadata);
}
@Override
public int hashCode() {
return Objects.hash(source, metadata);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy