diff --git a/core/src/main/java/org/elasticsearch/index/fielddata/ScriptDocValues.java b/core/src/main/java/org/elasticsearch/index/fielddata/ScriptDocValues.java index 9e0d0fcc40d..afaaa9a1df8 100644 --- a/core/src/main/java/org/elasticsearch/index/fielddata/ScriptDocValues.java +++ b/core/src/main/java/org/elasticsearch/index/fielddata/ScriptDocValues.java @@ -27,6 +27,7 @@ import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.unit.DistanceUnit; import org.joda.time.DateTimeZone; import org.joda.time.MutableDateTime; +import org.joda.time.ReadableDateTime; import java.util.AbstractList; import java.util.Collections; @@ -131,7 +132,7 @@ public interface ScriptDocValues extends List { return Collections.unmodifiableList(this); } - public MutableDateTime getDate() { + public ReadableDateTime getDate() { date.setMillis(getValue()); return date; } diff --git a/core/src/main/java/org/elasticsearch/script/ClassPermission.java b/core/src/main/java/org/elasticsearch/script/ClassPermission.java index 0effe85578a..f636a0190c4 100644 --- a/core/src/main/java/org/elasticsearch/script/ClassPermission.java +++ b/core/src/main/java/org/elasticsearch/script/ClassPermission.java @@ -69,6 +69,8 @@ import java.util.Set; *
  • {@link org.joda.time.DateTimeUtils}
  • *
  • {@link org.joda.time.DateTimeZone}
  • *
  • {@link org.joda.time.Instant}
  • + *
  • {@link org.joda.time.ReadableDateTime}
  • + *
  • {@link org.joda.time.ReadableInstant}
  • * */ public final class ClassPermission extends BasicPermission { @@ -102,7 +104,9 @@ public final class ClassPermission extends BasicPermission { org.joda.time.DateTime.class.getName(), org.joda.time.DateTimeUtils.class.getName(), org.joda.time.DateTimeZone.class.getName(), - org.joda.time.Instant.class.getName() + org.joda.time.Instant.class.getName(), + org.joda.time.ReadableDateTime.class.getName(), + org.joda.time.ReadableInstant.class.getName() ))); /** diff --git a/docs/reference/modules/scripting/expression.asciidoc b/docs/reference/modules/scripting/expression.asciidoc index 61e28e05298..fb7739261ef 100644 --- a/docs/reference/modules/scripting/expression.asciidoc +++ b/docs/reference/modules/scripting/expression.asciidoc @@ -42,6 +42,8 @@ scripts, simply set the `lang` parameter to `expression`. |`doc['field_name'].empty` |A boolean indicating if the field has no values within the doc. +|`doc['field_name'].length` |The number of values in this document. + |`doc['field_name'].min()` |The minimum value of the field in this document. |`doc['field_name'].max()` |The maximum value of the field in this document. @@ -51,8 +53,6 @@ values within the doc. |`doc['field_name'].avg()` |The average of the values in this document. |`doc['field_name'].sum()` |The sum of the values in this document. - -|`doc['field_name'].count()` |The number of values in this document. |======================================================================= When a document is missing the field completely, by default the value will be treated as `0`. @@ -69,27 +69,47 @@ For example: `doc['on_sale'].value ? doc['price'].value * 0.5 : doc['price'].val [float] === Date field API Date fields are treated as the number of milliseconds since January 1, 1970 and -support the Numeric Fields API above, with these additional methods: +support the Numeric Fields API above, plus access to some date-specific fields: [cols="<,<",options="header",] |======================================================================= |Expression |Description -|`doc['field_name'].getYear()` |Year component, e.g. `1970`. +|`doc['field_name'].date.centuryOfEra`|Century (1-2920000) -|`doc['field_name'].getMonth()` |Month component (0-11), e.g. `0` for January. +|`doc['field_name'].date.dayOfMonth`|Day (1-31), e.g. `1` for the first of the month. -|`doc['field_name'].getDayOfMonth()` |Day component, e.g. `1` for the first of the month. +|`doc['field_name'].date.dayOfWeek`|Day of the week (1-7), e.g. `1` for Monday. -|`doc['field_name'].getHourOfDay()` |Hour component (0-23) +|`doc['field_name'].date.dayOfYear`|Day of the year, e.g. `1` for January 1. -|`doc['field_name'].getMinutes()` |Minutes component (0-59) +|`doc['field_name'].date.era`|Era: `0` for BC, `1` for AD. -|`doc['field_name'].getSeconds()` |Seconds component (0-59) +|`doc['field_name'].date.hourOfDay`|Hour (0-23). + +|`doc['field_name'].date.millisOfDay`|Milliseconds within the day (0-86399999). + +|`doc['field_name'].date.millisOfSecond`|Milliseconds within the second (0-999). + +|`doc['field_name'].date.minuteOfDay`|Minute within the day (0-1439). + +|`doc['field_name'].date.minuteOfHour`|Minute within the hour (0-59). + +|`doc['field_name'].date.monthOfYear`|Month within the year (1-12), e.g. `1` for January. + +|`doc['field_name'].date.secondOfDay`|Second within the day (0-86399). + +|`doc['field_name'].date.secondOfMinute`|Second within the minute (0-59). + +|`doc['field_name'].date.year`|Year (-292000000 - 292000000). + +|`doc['field_name'].date.yearOfCentury`|Year within the century (1-100). + +|`doc['field_name'].date.yearOfEra`|Year within the era (1-292000000). |======================================================================= The following example shows the difference in years between the `date` fields date0 and date1: -`doc['date1'].getYear() - doc['date0'].getYear()` +`doc['date1'].date.year - doc['date0'].date.year` [float] === `geo_point` field API diff --git a/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/DateField.java b/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/DateField.java index e4648887772..5c769774f61 100644 --- a/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/DateField.java +++ b/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/DateField.java @@ -35,14 +35,20 @@ final class DateField { // supported variables static final String VALUE_VARIABLE = "value"; static final String EMPTY_VARIABLE = "empty"; + static final String LENGTH_VARIABLE = "length"; // supported methods + static final String GETVALUE_METHOD = "getValue"; + static final String ISEMPTY_METHOD = "isEmpty"; + static final String SIZE_METHOD = "size"; static final String MINIMUM_METHOD = "min"; static final String MAXIMUM_METHOD = "max"; static final String AVERAGE_METHOD = "avg"; static final String MEDIAN_METHOD = "median"; static final String SUM_METHOD = "sum"; static final String COUNT_METHOD = "count"; + + // date-specific static final String GET_YEAR_METHOD = "getYear"; static final String GET_MONTH_METHOD = "getMonth"; static final String GET_DAY_OF_MONTH_METHOD = "getDayOfMonth"; @@ -56,6 +62,8 @@ final class DateField { return new FieldDataValueSource(fieldData, MultiValueMode.MIN); case EMPTY_VARIABLE: return new EmptyMemberValueSource(fieldData); + case LENGTH_VARIABLE: + return new CountMethodValueSource(fieldData); default: throw new IllegalArgumentException("Member variable [" + variable + "] does not exist for date field [" + fieldName + "]."); } @@ -63,6 +71,12 @@ final class DateField { static ValueSource getMethod(IndexFieldData fieldData, String fieldName, String method) { switch (method) { + case GETVALUE_METHOD: + return new FieldDataValueSource(fieldData, MultiValueMode.MIN); + case ISEMPTY_METHOD: + return new EmptyMemberValueSource(fieldData); + case SIZE_METHOD: + return new CountMethodValueSource(fieldData); case MINIMUM_METHOD: return new FieldDataValueSource(fieldData, MultiValueMode.MIN); case MAXIMUM_METHOD: diff --git a/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/DateObject.java b/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/DateObject.java new file mode 100644 index 00000000000..66cc56370a3 --- /dev/null +++ b/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/DateObject.java @@ -0,0 +1,161 @@ +package org.elasticsearch.script.expression; + +/* + * 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. + */ + +import org.apache.lucene.queries.function.ValueSource; +import org.elasticsearch.index.fielddata.IndexFieldData; +import org.elasticsearch.search.MultiValueMode; +import org.joda.time.ReadableDateTime; + +/** + * Expressions API for date objects (.date) + */ +final class DateObject { + // no instance + private DateObject() {} + + // supported variables + static final String CENTURY_OF_ERA_VARIABLE = "centuryOfEra"; + static final String DAY_OF_MONTH_VARIABLE = "dayOfMonth"; + static final String DAY_OF_WEEK_VARIABLE = "dayOfWeek"; + static final String DAY_OF_YEAR_VARIABLE = "dayOfYear"; + static final String ERA_VARIABLE = "era"; + static final String HOUR_OF_DAY_VARIABLE = "hourOfDay"; + static final String MILLIS_OF_DAY_VARIABLE = "millisOfDay"; + static final String MILLIS_OF_SECOND_VARIABLE = "millisOfSecond"; + static final String MINUTE_OF_DAY_VARIABLE = "minuteOfDay"; + static final String MINUTE_OF_HOUR_VARIABLE = "minuteOfHour"; + static final String MONTH_OF_YEAR_VARIABLE = "monthOfYear"; + static final String SECOND_OF_DAY_VARIABLE = "secondOfDay"; + static final String SECOND_OF_MINUTE_VARIABLE = "secondOfMinute"; + static final String WEEK_OF_WEEK_YEAR_VARIABLE = "weekOfWeekyear"; + static final String WEEK_YEAR_VARIABLE = "weekyear"; + static final String YEAR_VARIABLE = "year"; + static final String YEAR_OF_CENTURY_VARIABLE = "yearOfCentury"; + static final String YEAR_OF_ERA_VARIABLE = "yearOfEra"; + + // supported methods + static final String GETCENTURY_OF_ERA_METHOD = "getCenturyOfEra"; + static final String GETDAY_OF_MONTH_METHOD = "getDayOfMonth"; + static final String GETDAY_OF_WEEK_METHOD = "getDayOfWeek"; + static final String GETDAY_OF_YEAR_METHOD = "getDayOfYear"; + static final String GETERA_METHOD = "getEra"; + static final String GETHOUR_OF_DAY_METHOD = "getHourOfDay"; + static final String GETMILLIS_OF_DAY_METHOD = "getMillisOfDay"; + static final String GETMILLIS_OF_SECOND_METHOD = "getMillisOfSecond"; + static final String GETMINUTE_OF_DAY_METHOD = "getMinuteOfDay"; + static final String GETMINUTE_OF_HOUR_METHOD = "getMinuteOfHour"; + static final String GETMONTH_OF_YEAR_METHOD = "getMonthOfYear"; + static final String GETSECOND_OF_DAY_METHOD = "getSecondOfDay"; + static final String GETSECOND_OF_MINUTE_METHOD = "getSecondOfMinute"; + static final String GETWEEK_OF_WEEK_YEAR_METHOD = "getWeekOfWeekyear"; + static final String GETWEEK_YEAR_METHOD = "getWeekyear"; + static final String GETYEAR_METHOD = "getYear"; + static final String GETYEAR_OF_CENTURY_METHOD = "getYearOfCentury"; + static final String GETYEAR_OF_ERA_METHOD = "getYearOfEra"; + + static ValueSource getVariable(IndexFieldData fieldData, String fieldName, String variable) { + switch (variable) { + case CENTURY_OF_ERA_VARIABLE: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, variable, ReadableDateTime::getCenturyOfEra); + case DAY_OF_MONTH_VARIABLE: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, variable, ReadableDateTime::getDayOfMonth); + case DAY_OF_WEEK_VARIABLE: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, variable, ReadableDateTime::getDayOfWeek); + case DAY_OF_YEAR_VARIABLE: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, variable, ReadableDateTime::getDayOfYear); + case ERA_VARIABLE: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, variable, ReadableDateTime::getEra); + case HOUR_OF_DAY_VARIABLE: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, variable, ReadableDateTime::getHourOfDay); + case MILLIS_OF_DAY_VARIABLE: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, variable, ReadableDateTime::getMillisOfDay); + case MILLIS_OF_SECOND_VARIABLE: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, variable, ReadableDateTime::getMillisOfSecond); + case MINUTE_OF_DAY_VARIABLE: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, variable, ReadableDateTime::getMinuteOfDay); + case MINUTE_OF_HOUR_VARIABLE: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, variable, ReadableDateTime::getMinuteOfHour); + case MONTH_OF_YEAR_VARIABLE: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, variable, ReadableDateTime::getMonthOfYear); + case SECOND_OF_DAY_VARIABLE: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, variable, ReadableDateTime::getSecondOfDay); + case SECOND_OF_MINUTE_VARIABLE: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, variable, ReadableDateTime::getSecondOfMinute); + case WEEK_OF_WEEK_YEAR_VARIABLE: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, variable, ReadableDateTime::getWeekOfWeekyear); + case WEEK_YEAR_VARIABLE: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, variable, ReadableDateTime::getWeekyear); + case YEAR_VARIABLE: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, variable, ReadableDateTime::getYear); + case YEAR_OF_CENTURY_VARIABLE: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, variable, ReadableDateTime::getYearOfCentury); + case YEAR_OF_ERA_VARIABLE: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, variable, ReadableDateTime::getYearOfEra); + default: + throw new IllegalArgumentException("Member variable [" + variable + + "] does not exist for date object on field [" + fieldName + "]."); + } + } + + static ValueSource getMethod(IndexFieldData fieldData, String fieldName, String method) { + switch (method) { + case GETCENTURY_OF_ERA_METHOD: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, method, ReadableDateTime::getCenturyOfEra); + case GETDAY_OF_MONTH_METHOD: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, method, ReadableDateTime::getDayOfMonth); + case GETDAY_OF_WEEK_METHOD: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, method, ReadableDateTime::getDayOfWeek); + case GETDAY_OF_YEAR_METHOD: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, method, ReadableDateTime::getDayOfYear); + case GETERA_METHOD: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, method, ReadableDateTime::getEra); + case GETHOUR_OF_DAY_METHOD: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, method, ReadableDateTime::getHourOfDay); + case GETMILLIS_OF_DAY_METHOD: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, method, ReadableDateTime::getMillisOfDay); + case GETMILLIS_OF_SECOND_METHOD: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, method, ReadableDateTime::getMillisOfSecond); + case GETMINUTE_OF_DAY_METHOD: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, method, ReadableDateTime::getMinuteOfDay); + case GETMINUTE_OF_HOUR_METHOD: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, method, ReadableDateTime::getMinuteOfHour); + case GETMONTH_OF_YEAR_METHOD: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, method, ReadableDateTime::getMonthOfYear); + case GETSECOND_OF_DAY_METHOD: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, method, ReadableDateTime::getSecondOfDay); + case GETSECOND_OF_MINUTE_METHOD: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, method, ReadableDateTime::getSecondOfMinute); + case GETWEEK_OF_WEEK_YEAR_METHOD: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, method, ReadableDateTime::getWeekOfWeekyear); + case GETWEEK_YEAR_METHOD: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, method, ReadableDateTime::getWeekyear); + case GETYEAR_METHOD: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, method, ReadableDateTime::getYear); + case GETYEAR_OF_CENTURY_METHOD: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, method, ReadableDateTime::getYearOfCentury); + case GETYEAR_OF_ERA_METHOD: + return new DateObjectValueSource(fieldData, MultiValueMode.MIN, method, ReadableDateTime::getYearOfEra); + default: + throw new IllegalArgumentException("Member method [" + method + + "] does not exist for date object on field [" + fieldName + "]."); + } + } +} diff --git a/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/DateObjectValueSource.java b/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/DateObjectValueSource.java new file mode 100644 index 00000000000..a9f2018d398 --- /dev/null +++ b/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/DateObjectValueSource.java @@ -0,0 +1,91 @@ +/* + * 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.script.expression; + +import java.io.IOException; +import java.util.Map; +import java.util.Objects; +import java.util.function.ToIntFunction; + +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.queries.function.FunctionValues; +import org.apache.lucene.queries.function.docvalues.DoubleDocValues; +import org.elasticsearch.index.fielddata.AtomicNumericFieldData; +import org.elasticsearch.index.fielddata.IndexFieldData; +import org.elasticsearch.index.fielddata.NumericDoubleValues; +import org.elasticsearch.search.MultiValueMode; +import org.joda.time.DateTimeZone; +import org.joda.time.MutableDateTime; +import org.joda.time.ReadableDateTime; + +/** Extracts a portion of a date field with joda time */ +class DateObjectValueSource extends FieldDataValueSource { + + final String methodName; + final ToIntFunction function; + + DateObjectValueSource(IndexFieldData indexFieldData, MultiValueMode multiValueMode, + String methodName, ToIntFunction function) { + super(indexFieldData, multiValueMode); + + Objects.requireNonNull(methodName); + + this.methodName = methodName; + this.function = function; + } + + @Override + @SuppressWarnings("rawtypes") // ValueSource uses a rawtype + public FunctionValues getValues(Map context, LeafReaderContext leaf) throws IOException { + AtomicNumericFieldData leafData = (AtomicNumericFieldData) fieldData.load(leaf); + MutableDateTime joda = new MutableDateTime(0, DateTimeZone.UTC); + NumericDoubleValues docValues = multiValueMode.select(leafData.getDoubleValues(), 0d); + return new DoubleDocValues(this) { + @Override + public double doubleVal(int docId) { + long millis = (long)docValues.get(docId); + joda.setMillis(millis); + return function.applyAsInt(joda); + } + }; + } + + @Override + public String description() { + return methodName + ": field(" + fieldData.getFieldName() + ")"; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + DateObjectValueSource that = (DateObjectValueSource) o; + return methodName.equals(that.methodName); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + methodName.hashCode(); + return result; + } +} diff --git a/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/ExpressionScriptEngineService.java b/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/ExpressionScriptEngineService.java index 5c4ff3ca94d..dce8d8a7800 100644 --- a/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/ExpressionScriptEngineService.java +++ b/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/ExpressionScriptEngineService.java @@ -51,6 +51,7 @@ import java.security.AccessController; import java.security.PrivilegedAction; import java.text.ParseException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; @@ -148,6 +149,7 @@ public class ExpressionScriptEngineService extends AbstractComponent implements String fieldname = null; String methodname = null; String variablename = "value"; // .value is the default for doc['field'], its optional. + boolean dateAccessor = false; // true if the variable is of type doc['field'].date.xxx VariableContext[] parts = VariableContext.parse(variable); if (parts[0].text.equals("doc") == false) { throw new ParseException("Unknown variable [" + parts[0].text + "]", 0); @@ -167,7 +169,19 @@ public class ExpressionScriptEngineService extends AbstractComponent implements } } if (parts.length > 3) { - throw new IllegalArgumentException("Variable [" + variable + "] does not follow an allowed format of either doc['field'] or doc['field'].method()"); + // access to the .date "object" within the field + if (parts.length == 4 && ("date".equals(parts[2].text) || "getDate".equals(parts[2].text))) { + if (parts[3].type == VariableContext.Type.METHOD) { + methodname = parts[3].text; + dateAccessor = true; + } else if (parts[3].type == VariableContext.Type.MEMBER) { + variablename = parts[3].text; + dateAccessor = true; + } + } + if (!dateAccessor) { + throw new IllegalArgumentException("Variable [" + variable + "] does not follow an allowed format of either doc['field'] or doc['field'].method()"); + } } MappedFieldType fieldType = mapper.fullName(fieldname); @@ -191,11 +205,20 @@ public class ExpressionScriptEngineService extends AbstractComponent implements } } else if (fieldType instanceof LegacyDateFieldMapper.DateFieldType || fieldType instanceof DateFieldMapper.DateFieldType) { - // date - if (methodname == null) { - valueSource = DateField.getVariable(fieldData, fieldname, variablename); + if (dateAccessor) { + // date object + if (methodname == null) { + valueSource = DateObject.getVariable(fieldData, fieldname, variablename); + } else { + valueSource = DateObject.getMethod(fieldData, fieldname, methodname); + } } else { - valueSource = DateField.getMethod(fieldData, fieldname, methodname); + // date field itself + if (methodname == null) { + valueSource = DateField.getVariable(fieldData, fieldname, variablename); + } else { + valueSource = DateField.getMethod(fieldData, fieldname, methodname); + } } } else if (fieldData instanceof IndexNumericFieldData) { // number diff --git a/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/GeoField.java b/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/GeoField.java index e830813e3a3..0ce74a187df 100644 --- a/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/GeoField.java +++ b/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/GeoField.java @@ -34,6 +34,11 @@ final class GeoField { static final String LAT_VARIABLE = "lat"; static final String LON_VARIABLE = "lon"; + // supported methods + static final String ISEMPTY_METHOD = "isEmpty"; + static final String GETLAT_METHOD = "getLat"; + static final String GETLON_METHOD = "getLon"; + static ValueSource getVariable(IndexFieldData fieldData, String fieldName, String variable) { switch (variable) { case EMPTY_VARIABLE: @@ -48,6 +53,15 @@ final class GeoField { } static ValueSource getMethod(IndexFieldData fieldData, String fieldName, String method) { - throw new IllegalArgumentException("Member method [" + method + "] does not exist for geo field [" + fieldName + "]."); + switch (method) { + case ISEMPTY_METHOD: + return new GeoEmptyValueSource(fieldData); + case GETLAT_METHOD: + return new GeoLatitudeValueSource(fieldData); + case GETLON_METHOD: + return new GeoLongitudeValueSource(fieldData); + default: + throw new IllegalArgumentException("Member method [" + method + "] does not exist for geo field [" + fieldName + "]."); + } } } diff --git a/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/NumericField.java b/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/NumericField.java index 4147d0e97d9..7cd918cf914 100644 --- a/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/NumericField.java +++ b/modules/lang-expression/src/main/java/org/elasticsearch/script/expression/NumericField.java @@ -33,8 +33,12 @@ final class NumericField { // supported variables static final String VALUE_VARIABLE = "value"; static final String EMPTY_VARIABLE = "empty"; + static final String LENGTH_VARIABLE = "length"; // supported methods + static final String GETVALUE_METHOD = "getValue"; + static final String ISEMPTY_METHOD = "isEmpty"; + static final String SIZE_METHOD = "size"; static final String MINIMUM_METHOD = "min"; static final String MAXIMUM_METHOD = "max"; static final String AVERAGE_METHOD = "avg"; @@ -48,6 +52,8 @@ final class NumericField { return new FieldDataValueSource(fieldData, MultiValueMode.MIN); case EMPTY_VARIABLE: return new EmptyMemberValueSource(fieldData); + case LENGTH_VARIABLE: + return new CountMethodValueSource(fieldData); default: throw new IllegalArgumentException("Member variable [" + variable + "] does not exist for " + "numeric field [" + fieldName + "]."); @@ -56,6 +62,12 @@ final class NumericField { static ValueSource getMethod(IndexFieldData fieldData, String fieldName, String method) { switch (method) { + case GETVALUE_METHOD: + return new FieldDataValueSource(fieldData, MultiValueMode.MIN); + case ISEMPTY_METHOD: + return new EmptyMemberValueSource(fieldData); + case SIZE_METHOD: + return new CountMethodValueSource(fieldData); case MINIMUM_METHOD: return new FieldDataValueSource(fieldData, MultiValueMode.MIN); case MAXIMUM_METHOD: diff --git a/modules/lang-expression/src/test/java/org/elasticsearch/script/expression/MoreExpressionTests.java b/modules/lang-expression/src/test/java/org/elasticsearch/script/expression/MoreExpressionTests.java index 3f2466e52e9..da2f117c78f 100644 --- a/modules/lang-expression/src/test/java/org/elasticsearch/script/expression/MoreExpressionTests.java +++ b/modules/lang-expression/src/test/java/org/elasticsearch/script/expression/MoreExpressionTests.java @@ -169,6 +169,34 @@ public class MoreExpressionTests extends ESIntegTestCase { assertEquals(1985.0, hits.getAt(0).field("foo").getValue(), 0.0D); assertEquals(1983.0, hits.getAt(1).field("foo").getValue(), 0.0D); } + + public void testDateObjectMethods() throws Exception { + ElasticsearchAssertions.assertAcked(prepareCreate("test").addMapping("doc", "date0", "type=date", "date1", "type=date")); + ensureGreen("test"); + indexRandom(true, + client().prepareIndex("test", "doc", "1").setSource("date0", "2015-04-28T04:02:07Z", "date1", "1985-09-01T23:11:01Z"), + client().prepareIndex("test", "doc", "2").setSource("date0", "2013-12-25T11:56:45Z", "date1", "1983-10-13T23:15:00Z")); + SearchResponse rsp = buildRequest("doc['date0'].date.secondOfMinute - doc['date0'].date.minuteOfHour").get(); + assertEquals(2, rsp.getHits().getTotalHits()); + SearchHits hits = rsp.getHits(); + assertEquals(5.0, hits.getAt(0).field("foo").getValue(), 0.0D); + assertEquals(-11.0, hits.getAt(1).field("foo").getValue(), 0.0D); + rsp = buildRequest("doc['date0'].date.getHourOfDay() + doc['date1'].date.dayOfMonth").get(); + assertEquals(2, rsp.getHits().getTotalHits()); + hits = rsp.getHits(); + assertEquals(5.0, hits.getAt(0).field("foo").getValue(), 0.0D); + assertEquals(24.0, hits.getAt(1).field("foo").getValue(), 0.0D); + rsp = buildRequest("doc['date1'].date.monthOfYear + 1").get(); + assertEquals(2, rsp.getHits().getTotalHits()); + hits = rsp.getHits(); + assertEquals(10.0, hits.getAt(0).field("foo").getValue(), 0.0D); + assertEquals(11.0, hits.getAt(1).field("foo").getValue(), 0.0D); + rsp = buildRequest("doc['date1'].date.year").get(); + assertEquals(2, rsp.getHits().getTotalHits()); + hits = rsp.getHits(); + assertEquals(1985.0, hits.getAt(0).field("foo").getValue(), 0.0D); + assertEquals(1983.0, hits.getAt(1).field("foo").getValue(), 0.0D); + } public void testMultiValueMethods() throws Exception { ElasticsearchAssertions.assertAcked(prepareCreate("test").addMapping("doc", "double0", "type=double", "double1", "type=double", "double2", "type=double")); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/Definition.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/Definition.java index 0923cffc59d..669323da803 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/Definition.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/Definition.java @@ -50,7 +50,8 @@ public final class Definition { "java.time.zone.txt", "java.util.txt", "java.util.function.txt", - "java.util.stream.txt")); + "java.util.stream.txt", + "joda.time.txt")); private static final Definition INSTANCE = new Definition(); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/Variables.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/Variables.java index 4443b1dc0f8..f50ab29096f 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/Variables.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/Variables.java @@ -99,7 +99,7 @@ public final class Variables { // Method variables. // This reference. Internal use only. - addVariable("[" + Reserved.THIS + "]", Definition.getType("Executable"), Reserved.THIS, true, true); + addVariable("[" + Reserved.THIS + "]", Definition.getType("Object"), Reserved.THIS, true, true); // Input map of variables passed to the script. addVariable("[" + Reserved.PARAMS + "]", Definition.getType("Map"), Reserved.PARAMS, true, true); diff --git a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/java.util.txt b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/java.util.txt index ffa80da79c1..5e240d0d45a 100644 --- a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/java.util.txt +++ b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/java.util.txt @@ -643,9 +643,11 @@ class GregorianCalendar -> java.util.GregorianCalendar extends Calendar,Comparab GregorianCalendar (int,int,int,int,int,int) GregorianCalendar (TimeZone) GregorianCalendar (TimeZone,Locale) + GregorianCalendar from(ZonedDateTime) Date getGregorianChange() boolean isLeapYear(int) void setGregorianChange(Date) + ZonedDateTime toZonedDateTime() } class HashMap -> java.util.HashMap extends AbstractMap,Map,Object { diff --git a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/joda.time.txt b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/joda.time.txt new file mode 100644 index 00000000000..02d95921534 --- /dev/null +++ b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/joda.time.txt @@ -0,0 +1,60 @@ +# +# 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. +# + +# +# Painless definition file. This defines the hierarchy of classes, +# what methods and fields they have, etc. +# + +# NOTE: this just minimal whitelisting of joda time, just to provide +# convenient access via the scripting API. classes are fully qualified to avoid +# any confusion with java.time + +class org.joda.time.ReadableInstant -> org.joda.time.ReadableInstant extends Comparable { + boolean equals(Object) + long getMillis() + int hashCode() + boolean isAfter(org.joda.time.ReadableInstant) + boolean isBefore(org.joda.time.ReadableInstant) + boolean isEqual(org.joda.time.ReadableInstant) + String toString() +} + +class org.joda.time.ReadableDateTime -> org.joda.time.ReadableDateTime extends org.joda.time.ReadableInstant,Comparable { + int getCenturyOfEra() + int getDayOfMonth() + int getDayOfWeek() + int getDayOfYear() + int getEra() + int getHourOfDay() + int getMillisOfDay() + int getMillisOfSecond() + int getMinuteOfDay() + int getMinuteOfHour() + int getMonthOfYear() + int getSecondOfDay() + int getSecondOfMinute() + int getWeekOfWeekyear() + int getWeekyear() + int getYear() + int getYearOfCentury() + int getYearOfEra() + String toString(String) + String toString(String,Locale) +} diff --git a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.txt b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.txt index a4af7c318ec..8627de26d26 100644 --- a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.txt +++ b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.txt @@ -59,28 +59,29 @@ class def -> java.lang.Object { #### ES Scripting API -class GeoPoint -> org.elasticsearch.common.geo.GeoPoint extends Object { +class org.elasticsearch.common.geo.GeoPoint -> org.elasticsearch.common.geo.GeoPoint extends Object { double getLat() double getLon() } -class Strings -> org.elasticsearch.index.fielddata.ScriptDocValues$Strings extends List,Collection,Iterable,Object { +class org.elasticsearch.index.fielddata.ScriptDocValues.Strings -> org.elasticsearch.index.fielddata.ScriptDocValues$Strings extends List,Collection,Iterable,Object { String getValue() List getValues() } -class Longs -> org.elasticsearch.index.fielddata.ScriptDocValues$Longs extends List,Collection,Iterable,Object { +class org.elasticsearch.index.fielddata.ScriptDocValues.Longs -> org.elasticsearch.index.fielddata.ScriptDocValues$Longs extends List,Collection,Iterable,Object { long getValue() List getValues() + org.joda.time.ReadableDateTime getDate() } -class Doubles -> org.elasticsearch.index.fielddata.ScriptDocValues$Doubles extends List,Collection,Iterable,Object { +class org.elasticsearch.index.fielddata.ScriptDocValues.Doubles -> org.elasticsearch.index.fielddata.ScriptDocValues$Doubles extends List,Collection,Iterable,Object { double getValue() List getValues() } -class GeoPoints -> org.elasticsearch.index.fielddata.ScriptDocValues$GeoPoints extends List,Collection,Iterable,Object { - GeoPoint getValue() +class org.elasticsearch.index.fielddata.ScriptDocValues.GeoPoints -> org.elasticsearch.index.fielddata.ScriptDocValues$GeoPoints extends List,Collection,Iterable,Object { + org.elasticsearch.common.geo.GeoPoint getValue() List getValues() double getLat() double getLon() @@ -111,9 +112,9 @@ class GeoPoints -> org.elasticsearch.index.fielddata.ScriptDocValues$GeoPoints e # for testing. # currently FeatureTest exposes overloaded constructor, field load store, and overloaded static methods -class FeatureTest -> org.elasticsearch.painless.FeatureTest extends Object { - FeatureTest () - FeatureTest (int,int) +class org.elasticsearch.painless.FeatureTest -> org.elasticsearch.painless.FeatureTest extends Object { + org.elasticsearch.painless.FeatureTest () + org.elasticsearch.painless.FeatureTest (int,int) int getX() int getY() void setX(int) @@ -121,7 +122,3 @@ class FeatureTest -> org.elasticsearch.painless.FeatureTest extends Object { boolean overloadedStatic() boolean overloadedStatic(boolean) } - -# currently needed internally -class Executable -> org.elasticsearch.painless.Executable { -} diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/OverloadTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/OverloadTests.java index 6662aa95fbd..00c86836a67 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/OverloadTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/OverloadTests.java @@ -41,12 +41,14 @@ public class OverloadTests extends ScriptTestCase { } public void testConstructor() { - assertEquals(true, exec("FeatureTest f = new FeatureTest(); return f.x == 0 && f.y == 0;")); - assertEquals(true, exec("FeatureTest f = new FeatureTest(1, 2); return f.x == 1 && f.y == 2;")); + assertEquals(true, exec("org.elasticsearch.painless.FeatureTest f = new org.elasticsearch.painless.FeatureTest();" + + "return f.x == 0 && f.y == 0;")); + assertEquals(true, exec("org.elasticsearch.painless.FeatureTest f = new org.elasticsearch.painless.FeatureTest(1, 2);" + + "return f.x == 1 && f.y == 2;")); } public void testStatic() { - assertEquals(true, exec("return FeatureTest.overloadedStatic();")); - assertEquals(false, exec("return FeatureTest.overloadedStatic(false);")); + assertEquals(true, exec("return org.elasticsearch.painless.FeatureTest.overloadedStatic();")); + assertEquals(false, exec("return org.elasticsearch.painless.FeatureTest.overloadedStatic(false);")); } }