/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you 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.elasticsearch.index.fielddata;
import org.apache.lucene.index.SortedNumericDocValues;
import org.apache.lucene.util.ArrayUtil;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.BytesRefBuilder;
import org.elasticsearch.common.geo.GeoHashUtils;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.geo.GeoUtils;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.common.logging.ESLoggerFactory;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.MutableDateTime;
import org.joda.time.ReadableDateTime;
import java.io.IOException;
import java.util.AbstractList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.function.UnaryOperator;
/**
* Script level doc values, the assumption is that any implementation will implement a <code>getValue</code>
* and a <code>getValues</code> that return the relevant type that then can be used in scripts.
*/
public abstract class ScriptDocValues<T> extends AbstractList<T> {
/**
* Set the current doc ID.
*/
public abstract void setNextDocId(int docId) throws IOException;
/**
* Return a copy of the list of the values for the current document.
*/
public final List<T> getValues() {
return this;
}
// Throw meaningful exceptions if someone tries to modify the ScriptDocValues.
@Override
public final void add(int index, T element) {
throw new UnsupportedOperationException("doc values are unmodifiable");
}
@Override
public final boolean remove(Object o) {
throw new UnsupportedOperationException("doc values are unmodifiable");
}
@Override
public final void replaceAll(UnaryOperator<T> operator) {
throw new UnsupportedOperationException("doc values are unmodifiable");
}
@Override
public final T set(int index, T element) {
throw new UnsupportedOperationException("doc values are unmodifiable");
}
@Override
public final void sort(Comparator<? super T> c) {
throw new UnsupportedOperationException("doc values are unmodifiable");
}
public static final class Strings extends ScriptDocValues<String> {
private final SortedBinaryDocValues in;
private BytesRefBuilder[] values = new BytesRefBuilder[0];
private int count;
public Strings(SortedBinaryDocValues in) {
this.in = in;
}
@Override
public void setNextDocId(int docId) throws IOException {
if (in.advanceExact(docId)) {
resize(in.docValueCount());
for (int i = 0; i < count; i++) {
values[i].copyBytes(in.nextValue());
}
} else {
resize(0);
}
}
/**
* Set the {@link #size()} and ensure that the {@link #values} array can
* store at least that many entries.
*/
protected void resize(int newSize) {
count = newSize;
if (newSize > values.length) {
final int oldLength = values.length;
values = ArrayUtil.grow(values, count);
for (int i = oldLength; i < values.length; ++i) {
values[i] = new BytesRefBuilder();
}
}
}
public SortedBinaryDocValues getInternalValues() {
return this.in;
}
public BytesRef getBytesValue() {
if (size() > 0) {
return values[0].get();
} else {
return null;
}
}
public String getValue() {
BytesRef value = getBytesValue();
if (value == null) {
return null;
} else {
return value.utf8ToString();
}
}
@Override
public String get(int index) {
return values[index].get().utf8ToString();
}
@Override
public int size() {
return count;
}
}
public static final class Longs extends ScriptDocValues<Long> {
protected static final DeprecationLogger deprecationLogger = new DeprecationLogger(ESLoggerFactory.getLogger(Longs.class));
private final SortedNumericDocValues in;
private long[] values = new long[0];
private int count;
private Dates dates;
private int docId = -1;
public Longs(SortedNumericDocValues in) {
this.in = in;
}
@Override
public void setNextDocId(int docId) throws IOException {
this.docId = docId;
if (in.advanceExact(docId)) {
resize(in.docValueCount());
for (int i = 0; i < count; i++) {
values[i] = in.nextValue();
}
} else {
resize(0);
}
if (dates != null) {
dates.setNextDocId(docId);
}
}
/**
* Set the {@link #size()} and ensure that the {@link #values} array can
* store at least that many entries.
*/
protected void resize(int newSize) {
count = newSize;
values = ArrayUtil.grow(values, count);
}
public SortedNumericDocValues getInternalValues() {
return this.in;
}
public long getValue() {
if (count == 0) {
return 0L;
}
return values[0];
}
@Deprecated
public ReadableDateTime getDate() throws IOException {
deprecationLogger.deprecated("getDate on numeric fields is deprecated. Use a date field to get dates.");
if (dates == null) {
dates = new Dates(in);
dates.setNextDocId(docId);
}
return dates.getValue();
}
@Deprecated
public List<ReadableDateTime> getDates() throws IOException {
deprecationLogger.deprecated("getDates on numeric fields is deprecated. Use a date field to get dates.");
if (dates == null) {
dates = new Dates(in);
dates.setNextDocId(docId);
}
return dates;
}
@Override
public Long get(int index) {
return values[index];
}
@Override
public int size() {
return count;
}
}
public static final class Dates extends ScriptDocValues<ReadableDateTime> {
protected static final DeprecationLogger deprecationLogger = new DeprecationLogger(ESLoggerFactory.getLogger(Dates.class));
private static final ReadableDateTime EPOCH = new DateTime(0, DateTimeZone.UTC);
private final SortedNumericDocValues in;
/**
* Values wrapped in {@link MutableDateTime}. Null by default an allocated on first usage so we allocate a reasonably size. We keep
* this array so we don't have allocate new {@link MutableDateTime}s on every usage. Instead we reuse them for every document.
*/
private MutableDateTime[] dates;
private int count;
public Dates(SortedNumericDocValues in) {
this.in = in;
}
/**
* Fetch the first field value or 0 millis after epoch if there are no
* in.
*/
public ReadableDateTime getValue() {
if (count == 0) {
return EPOCH;
}
return get(0);
}
/**
* Fetch the first value. Added for backwards compatibility with 5.x when date fields were {@link Longs}.
*/
@Deprecated
public ReadableDateTime getDate() {
deprecationLogger.deprecated("getDate is no longer necisary on date fields as the value is now a date.");
return getValue();
}
/**
* Fetch all the values. Added for backwards compatibility with 5.x when date fields were {@link Longs}.
*/
@Deprecated
public List<ReadableDateTime> getDates() {
deprecationLogger.deprecated("getDates is no longer necisary on date fields as the values are now dates.");
return this;
}
@Override
public ReadableDateTime get(int index) {
if (index >= count) {
throw new IndexOutOfBoundsException(
"attempted to fetch the [" + index + "] date when there are only ["
+ count + "] dates.");
}
return dates[index];
}
@Override
public int size() {
return count;
}
@Override
public void setNextDocId(int docId) throws IOException {
if (in.advanceExact(docId)) {
count = in.docValueCount();
} else {
count = 0;
}
refreshArray();
}
/**
* Refresh the backing array. Package private so it can be called when {@link Longs} loads dates.
*/
void refreshArray() throws IOException {
if (count == 0) {
return;
}
if (dates == null) {
// Happens for the document. We delay allocating dates so we can allocate it with a reasonable size.
dates = new MutableDateTime[count];
for (int i = 0; i < dates.length; i++) {
dates[i] = new MutableDateTime(in.nextValue(), DateTimeZone.UTC);
}
return;
}
if (count > dates.length) {
// Happens when we move to a new document and it has more dates than any documents before it.
MutableDateTime[] backup = dates;
dates = new MutableDateTime[count];
System.arraycopy(backup, 0, dates, 0, backup.length);
for (int i = 0; i < backup.length; i++) {
dates[i].setMillis(in.nextValue());
}
for (int i = backup.length; i < dates.length; i++) {
dates[i] = new MutableDateTime(in.nextValue(), DateTimeZone.UTC);
}
return;
}
for (int i = 0; i < count; i++) {
dates[i].setMillis(in.nextValue());
}
}
}
public static final class Doubles extends ScriptDocValues<Double> {
private final SortedNumericDoubleValues in;
private double[] values = new double[0];
private int count;
public Doubles(SortedNumericDoubleValues in) {
this.in = in;
}
@Override
public void setNextDocId(int docId) throws IOException {
if (in.advanceExact(docId)) {
resize(in.docValueCount());
for (int i = 0; i < count; i++) {
values[i] = in.nextValue();
}
} else {
resize(0);
}
}
/**
* Set the {@link #size()} and ensure that the {@link #values} array can
* store at least that many entries.
*/
protected void resize(int newSize) {
count = newSize;
values = ArrayUtil.grow(values, count);
}
public SortedNumericDoubleValues getInternalValues() {
return this.in;
}
public double getValue() {
if (count == 0) {
return 0d;
}
return values[0];
}
@Override
public Double get(int index) {
return values[index];
}
@Override
public int size() {
return count;
}
}
public static final class GeoPoints extends ScriptDocValues<GeoPoint> {
private final MultiGeoPointValues in;
private GeoPoint[] values = new GeoPoint[0];
private int count;
public GeoPoints(MultiGeoPointValues in) {
this.in = in;
}
@Override
public void setNextDocId(int docId) throws IOException {
if (in.advanceExact(docId)) {
resize(in.docValueCount());
for (int i = 0; i < count; i++) {
GeoPoint point = in.nextValue();
values[i].reset(point.lat(), point.lon());
}
} else {
resize(0);
}
}
/**
* Set the {@link #size()} and ensure that the {@link #values} array can
* store at least that many entries.
*/
protected void resize(int newSize) {
count = newSize;
if (newSize > values.length) {
int oldLength = values.length;
values = ArrayUtil.grow(values, count);
for (int i = oldLength; i < values.length; ++i) {
values[i] = new GeoPoint();
}
}
}
public GeoPoint getValue() {
if (count == 0) {
return null;
}
return values[0];
}
public double getLat() {
return getValue().lat();
}
public double[] getLats() {
List<GeoPoint> points = getValues();
double[] lats = new double[points.size()];
for (int i = 0; i < points.size(); i++) {
lats[i] = points.get(i).lat();
}
return lats;
}
public double[] getLons() {
List<GeoPoint> points = getValues();
double[] lons = new double[points.size()];
for (int i = 0; i < points.size(); i++) {
lons[i] = points.get(i).lon();
}
return lons;
}
public double getLon() {
return getValue().lon();
}
@Override
public GeoPoint get(int index) {
final GeoPoint point = values[index];
return new GeoPoint(point.lat(), point.lon());
}
@Override
public int size() {
return count;
}
public double arcDistance(double lat, double lon) {
GeoPoint point = getValue();
return GeoUtils.arcDistance(point.lat(), point.lon(), lat, lon);
}
public double arcDistanceWithDefault(double lat, double lon, double defaultValue) {
if (isEmpty()) {
return defaultValue;
}
return arcDistance(lat, lon);
}
public double planeDistance(double lat, double lon) {
GeoPoint point = getValue();
return GeoUtils.planeDistance(point.lat(), point.lon(), lat, lon);
}
public double planeDistanceWithDefault(double lat, double lon, double defaultValue) {
if (isEmpty()) {
return defaultValue;
}
return planeDistance(lat, lon);
}
public double geohashDistance(String geohash) {
GeoPoint point = getValue();
return GeoUtils.arcDistance(point.lat(), point.lon(), GeoHashUtils.decodeLatitude(geohash),
GeoHashUtils.decodeLongitude(geohash));
}
public double geohashDistanceWithDefault(String geohash, double defaultValue) {
if (isEmpty()) {
return defaultValue;
}
return geohashDistance(geohash);
}
}
public static final class Booleans extends ScriptDocValues<Boolean> {
private final SortedNumericDocValues in;
private boolean[] values = new boolean[0];
private int count;
public Booleans(SortedNumericDocValues in) {
this.in = in;
}
@Override
public void setNextDocId(int docId) throws IOException {
if (in.advanceExact(docId)) {
resize(in.docValueCount());
for (int i = 0; i < count; i++) {
values[i] = in.nextValue() == 1;
}
} else {
resize(0);
}
}
/**
* Set the {@link #size()} and ensure that the {@link #values} array can
* store at least that many entries.
*/
protected void resize(int newSize) {
count = newSize;
values = grow(values, count);
}
public boolean getValue() {
return count != 0 && values[0];
}
@Override
public Boolean get(int index) {
return values[index];
}
@Override
public int size() {
return count;
}
private static boolean[] grow(boolean[] array, int minSize) {
assert minSize >= 0 : "size must be positive (got " + minSize
+ "): likely integer overflow?";
if (array.length < minSize) {
return Arrays.copyOf(array, ArrayUtil.oversize(minSize, 1));
} else
return array;
}
}
public static final class BytesRefs extends ScriptDocValues<BytesRef> {
private final SortedBinaryDocValues in;
private BytesRef[] values;
private int count;
public BytesRefs(SortedBinaryDocValues in) {
this.in = in;
}
@Override
public void setNextDocId(int docId) throws IOException {
if (in.advanceExact(docId)) {
resize(in.docValueCount());
for (int i = 0; i < count; i++) {
values[i] = in.nextValue();
}
} else {
resize(0);
}
}
/**
* Set the {@link #size()} and ensure that the {@link #values} array can
* store at least that many entries.
*/
protected void resize(int newSize) {
count = newSize;
if (values == null) {
values = new BytesRef[newSize];
} else {
values = ArrayUtil.grow(values, count);
}
}
public SortedBinaryDocValues getInternalValues() {
return this.in;
}
public BytesRef getValue() {
if (count == 0) {
return new BytesRef();
}
return values[0];
}
@Override
public BytesRef get(int index) {
return values[index];
}
@Override
public int size() {
return count;
}
}
}