/*
* 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.search.aggregations.support;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.index.fielddata.IndexFieldData;
import org.elasticsearch.index.fielddata.IndexGeoPointFieldData;
import org.elasticsearch.index.fielddata.IndexNumericFieldData;
import org.elasticsearch.index.mapper.MappedFieldType;
import org.elasticsearch.index.mapper.core.BooleanFieldMapper;
import org.elasticsearch.index.mapper.core.DateFieldMapper;
import org.elasticsearch.index.mapper.core.NumberFieldMapper;
import org.elasticsearch.index.mapper.ip.IpFieldMapper;
import org.elasticsearch.script.Script;
import org.elasticsearch.script.Script.ScriptField;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.ScriptParameterParser;
import org.elasticsearch.script.ScriptParameterParser.ScriptParameterValue;
import org.elasticsearch.script.SearchScript;
import org.elasticsearch.search.SearchParseException;
import org.elasticsearch.search.aggregations.InternalAggregation;
import org.elasticsearch.search.aggregations.support.format.ValueFormat;
import org.elasticsearch.search.internal.SearchContext;
import org.joda.time.DateTimeZone;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import static com.google.common.collect.Maps.newHashMap;
/**
*
*/
public class ValuesSourceParser<VS extends ValuesSource> {
static final ParseField TIME_ZONE = new ParseField("time_zone");
public static Builder any(String aggName, InternalAggregation.Type aggType, SearchContext context) {
return new Builder<>(aggName, aggType, context, ValuesSource.class);
}
public static Builder<ValuesSource.Numeric> numeric(String aggName, InternalAggregation.Type aggType, SearchContext context) {
return new Builder<>(aggName, aggType, context, ValuesSource.Numeric.class).targetValueType(ValueType.NUMERIC);
}
public static Builder<ValuesSource.Bytes> bytes(String aggName, InternalAggregation.Type aggType, SearchContext context) {
return new Builder<>(aggName, aggType, context, ValuesSource.Bytes.class).targetValueType(ValueType.STRING);
}
public static Builder<ValuesSource.GeoPoint> geoPoint(String aggName, InternalAggregation.Type aggType, SearchContext context) {
return new Builder<>(aggName, aggType, context, ValuesSource.GeoPoint.class).targetValueType(ValueType.GEOPOINT).scriptable(false);
}
public static class Input {
private String field = null;
private Script script = null;
@Deprecated
private Map<String, Object> params = null; // TODO Remove in 3.0
private ValueType valueType = null;
private String format = null;
private Object missing = null;
private DateTimeZone timezone = DateTimeZone.UTC;
public DateTimeZone timezone() {
return this.timezone;
}
}
private final String aggName;
private final InternalAggregation.Type aggType;
private final SearchContext context;
private final Class<VS> valuesSourceType;
private boolean scriptable = true;
private boolean formattable = false;
private boolean timezoneAware = false;
private ValueType targetValueType = null;
private ScriptParameterParser scriptParameterParser = new ScriptParameterParser();
private Input input = new Input();
private ValuesSourceParser(String aggName, InternalAggregation.Type aggType, SearchContext context, Class<VS> valuesSourceType) {
this.aggName = aggName;
this.aggType = aggType;
this.context = context;
this.valuesSourceType = valuesSourceType;
}
public boolean token(String currentFieldName, XContentParser.Token token, XContentParser parser) throws IOException {
if ("missing".equals(currentFieldName) && token.isValue()) {
input.missing = parser.objectText();
return true;
}
if (token == XContentParser.Token.VALUE_STRING) {
if ("field".equals(currentFieldName)) {
input.field = parser.text();
} else if (formattable && "format".equals(currentFieldName)) {
input.format = parser.text();
} else if (timezoneAware && context.parseFieldMatcher().match(currentFieldName, TIME_ZONE)) {
input.timezone = DateTimeZone.forID(parser.text());
} else if (scriptable) {
if ("value_type".equals(currentFieldName) || "valueType".equals(currentFieldName)) {
input.valueType = ValueType.resolveForScript(parser.text());
if (targetValueType != null && input.valueType.isNotA(targetValueType)) {
throw new SearchParseException(context, aggType.name() + " aggregation [" + aggName +
"] was configured with an incompatible value type [" + input.valueType + "]. [" + aggType +
"] aggregation can only work on value of type [" + targetValueType + "]",
parser.getTokenLocation());
}
} else if (!scriptParameterParser.token(currentFieldName, token, parser, context.parseFieldMatcher())) {
return false;
}
return true;
} else {
return false;
}
return true;
}
if (token == XContentParser.Token.VALUE_NUMBER) {
if (timezoneAware && context.parseFieldMatcher().match(currentFieldName, TIME_ZONE)) {
input.timezone = DateTimeZone.forOffsetHours(parser.intValue());
} else {
return false;
}
return true;
}
if (scriptable && token == XContentParser.Token.START_OBJECT) {
if (context.parseFieldMatcher().match(currentFieldName, ScriptField.SCRIPT)) {
input.script = Script.parse(parser, context.parseFieldMatcher());
return true;
} else if ("params".equals(currentFieldName)) {
input.params = parser.map();
return true;
}
return false;
}
return false;
}
public ValuesSourceConfig<VS> config() {
if (input.script == null) { // Didn't find anything using the new API so try using the old one instead
ScriptParameterValue scriptValue = scriptParameterParser.getDefaultScriptParameterValue();
if (scriptValue != null) {
if (input.params == null) {
input.params = newHashMap();
}
input.script = new Script(scriptValue.script(), scriptValue.scriptType(), scriptParameterParser.lang(), input.params);
}
}
ValueType valueType = input.valueType != null ? input.valueType : targetValueType;
if (input.field == null) {
if (input.script == null) {
ValuesSourceConfig<VS> config = new ValuesSourceConfig(ValuesSource.class);
config.format = resolveFormat(null, valueType);
return config;
}
Class valuesSourceType = valueType != null ? (Class<VS>) valueType.getValuesSourceType() : this.valuesSourceType;
if (valuesSourceType == null || valuesSourceType == ValuesSource.class) {
// the specific value source type is undefined, but for scripts, we need to have a specific value source
// type to know how to handle the script values, so we fallback on Bytes
valuesSourceType = ValuesSource.Bytes.class;
}
ValuesSourceConfig<VS> config = new ValuesSourceConfig<VS>(valuesSourceType);
config.missing = input.missing;
config.format = resolveFormat(input.format, valueType);
config.script = createScript();
config.scriptValueType = valueType;
return config;
}
MappedFieldType fieldType = context.smartNameFieldTypeFromAnyType(input.field);
if (fieldType == null) {
Class<VS> valuesSourceType = valueType != null ? (Class<VS>) valueType.getValuesSourceType() : this.valuesSourceType;
ValuesSourceConfig<VS> config = new ValuesSourceConfig<>(valuesSourceType);
config.missing = input.missing;
config.format = resolveFormat(input.format, valueType);
config.unmapped = true;
if (valueType != null) {
// todo do we really need this for unmapped?
config.scriptValueType = valueType;
}
return config;
}
IndexFieldData<?> indexFieldData = context.fieldData().getForField(fieldType);
ValuesSourceConfig config;
if (valuesSourceType == ValuesSource.class) {
if (indexFieldData instanceof IndexNumericFieldData) {
config = new ValuesSourceConfig<>(ValuesSource.Numeric.class);
} else if (indexFieldData instanceof IndexGeoPointFieldData) {
config = new ValuesSourceConfig<>(ValuesSource.GeoPoint.class);
} else {
config = new ValuesSourceConfig<>(ValuesSource.Bytes.class);
}
} else {
config = new ValuesSourceConfig(valuesSourceType);
}
config.fieldContext = new FieldContext(input.field, indexFieldData, fieldType);
config.missing = input.missing;
config.script = createScript();
config.format = resolveFormat(input.format, input.timezone, fieldType);
return config;
}
private SearchScript createScript() {
return input.script == null ? null : context.scriptService().search(context.lookup(), input.script, ScriptContext.Standard.AGGS, Collections.<String, String>emptyMap());
}
private static ValueFormat resolveFormat(@Nullable String format, @Nullable ValueType valueType) {
if (valueType == null) {
return ValueFormat.RAW; // we can't figure it out
}
ValueFormat valueFormat = valueType.defaultFormat;
if (valueFormat != null && valueFormat instanceof ValueFormat.Patternable && format != null) {
return ((ValueFormat.Patternable) valueFormat).create(format);
}
return valueFormat;
}
private static ValueFormat resolveFormat(@Nullable String format, @Nullable DateTimeZone timezone, MappedFieldType fieldType) {
if (fieldType instanceof DateFieldMapper.DateFieldType) {
return format != null ? ValueFormat.DateTime.format(format, timezone) : ValueFormat.DateTime.mapper((DateFieldMapper.DateFieldType) fieldType, timezone);
}
if (fieldType instanceof IpFieldMapper.IpFieldType) {
return ValueFormat.IPv4;
}
if (fieldType instanceof BooleanFieldMapper.BooleanFieldType) {
return ValueFormat.BOOLEAN;
}
if (fieldType instanceof NumberFieldMapper.NumberFieldType) {
return format != null ? ValueFormat.Number.format(format) : ValueFormat.RAW;
}
return ValueFormat.RAW;
}
public Input input() {
return this.input;
}
public static class Builder<VS extends ValuesSource> {
private final ValuesSourceParser<VS> parser;
private Builder(String aggName, InternalAggregation.Type aggType, SearchContext context, Class<VS> valuesSourceType) {
parser = new ValuesSourceParser<>(aggName, aggType, context, valuesSourceType);
}
public Builder<VS> scriptable(boolean scriptable) {
parser.scriptable = scriptable;
return this;
}
public Builder<VS> formattable(boolean formattable) {
parser.formattable = formattable;
return this;
}
public Builder<VS> timezoneAware(boolean timezoneAware) {
parser.timezoneAware = timezoneAware;
return this;
}
public Builder<VS> targetValueType(ValueType valueType) {
parser.targetValueType = valueType;
return this;
}
public ValuesSourceParser<VS> build() {
return parser;
}
}
}