/*
* 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.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.FieldMapper;
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.ScriptParameterParser;
import org.elasticsearch.script.ScriptParameterParser.ScriptParameterValue;
import org.elasticsearch.script.ScriptService;
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 java.io.IOException;
import java.util.Map;
/**
*
*/
public class ValuesSourceParser<VS extends ValuesSource> {
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);
}
private static class Input {
String field = null;
String script = null;
ScriptService.ScriptType scriptType = null;
String lang = null;
Map<String, Object> params = null;
ValueType valueType = null;
String format = null;
}
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 ValueType targetValueType = null;
private boolean requiresSortedValues = false;
private boolean requiresUniqueValues = false;
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 (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 (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 + "]");
}
} else if (!scriptParameterParser.token(currentFieldName, token, parser)) {
return false;
}
return true;
} else {
return false;
}
return true;
}
if (scriptable && token == XContentParser.Token.START_OBJECT) {
if ("params".equals(currentFieldName)) {
input.params = parser.map();
return true;
}
return false;
}
return false;
}
public ValuesSourceConfig<VS> config() {
ScriptParameterValue scriptValue = scriptParameterParser.getDefaultScriptParameterValue();
if (scriptValue != null) {
input.script = scriptValue.script();
input.scriptType = scriptValue.scriptType();
}
input.lang = scriptParameterParser.lang();
ValueType valueType = input.valueType != null ? input.valueType : targetValueType;
if (input.field == null) {
if (input.script == null) {
return new ValuesSourceConfig(ValuesSource.class);
}
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.format = resolveFormat(input.format, valueType);
config.script = createScript();
config.scriptValueType = valueType;
return config;
}
FieldMapper<?> mapper = context.smartNameFieldMapper(input.field);
if (mapper == null) {
Class<VS> valuesSourceType = valueType != null ? (Class<VS>) valueType.getValuesSourceType() : this.valuesSourceType;
ValuesSourceConfig<VS> config = new ValuesSourceConfig<>(valuesSourceType);
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(mapper);
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, mapper);
config.script = createScript();
config.format = resolveFormat(input.format, mapper);
return config;
}
private SearchScript createScript() {
return input.script == null ? null : context.scriptService().search(context.lookup(), input.lang, input.script, input.scriptType, input.params);
}
private static ValueFormat resolveFormat(@Nullable String format, @Nullable ValueType valueType) {
if (valueType == null) {
return null; // 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, FieldMapper mapper) {
if (mapper instanceof DateFieldMapper) {
return format != null ? ValueFormat.DateTime.format(format) : ValueFormat.DateTime.mapper((DateFieldMapper) mapper);
}
if (mapper instanceof IpFieldMapper) {
return ValueFormat.IPv4;
}
if (mapper instanceof NumberFieldMapper) {
return format != null ? ValueFormat.Number.format(format) : ValueFormat.RAW;
}
return null;
}
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> targetValueType(ValueType valueType) {
parser.targetValueType = valueType;
return this;
}
public ValuesSourceParser<VS> build() {
return parser;
}
}
}