Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,9 @@ public static Object parseStoredFieldsValue(XContentParser parser) throws IOExce
}

/**
* This method expects that the current token is a {@code XContentParser.Token.FIELD_NAME} and
* that the current field name is the concatenation of a type, delimiter and name (ex: terms#foo
* where "terms" refers to the type of a registered {@link NamedXContentRegistry.Entry}, "#" is
* the delimiter and "foo" the name of the object to parse).
* This method expects that the current field name is the concatenation of a type, a delimiter and a name
* (ex: terms#foo where "terms" refers to the type of a registered {@link NamedXContentRegistry.Entry},
* "#" is the delimiter and "foo" the name of the object to parse).
*
* The method splits the field's name to extract the type and name and then parses the object
* using the {@link XContentParser#namedObject(Class, String, Object)} method.
Expand All @@ -128,7 +127,6 @@ public static Object parseStoredFieldsValue(XContentParser parser) throws IOExce
* from the field's name
*/
public static <T> T parseTypedKeysObject(XContentParser parser, String delimiter, Class<T> objectClass) throws IOException {
ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.currentToken(), parser::getTokenLocation);
String currentFieldName = parser.currentName();
if (Strings.hasLength(currentFieldName)) {
int position = currentFieldName.indexOf(delimiter);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/*
* 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;

import org.elasticsearch.common.CheckedFunction;
import org.elasticsearch.common.xcontent.ObjectParser;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentParserUtils;
import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;

import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken;

public abstract class ParsedMultiBucketAggregation extends ParsedAggregation implements MultiBucketsAggregation {

protected final List<ParsedBucket<?>> buckets = new ArrayList<>();
protected boolean keyed;

@Override
protected XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException {
if (keyed) {
builder.startObject(CommonFields.BUCKETS.getPreferredName());
} else {
builder.startArray(CommonFields.BUCKETS.getPreferredName());
}
for (ParsedBucket<?> bucket : buckets) {
bucket.toXContent(builder, params);
}
if (keyed) {
builder.endObject();
} else {
builder.endArray();
}
return builder;
}

protected static void declareMultiBucketAggregationFields(final ObjectParser<? extends ParsedMultiBucketAggregation, Void> objectParser,
final CheckedFunction<XContentParser, ParsedBucket<?>, IOException> bucketParser,
final CheckedFunction<XContentParser, ParsedBucket<?>, IOException> keyedBucketParser) {
declareAggregationFields(objectParser);
objectParser.declareField((parser, aggregation, context) -> {
XContentParser.Token token = parser.currentToken();
if (token == XContentParser.Token.START_OBJECT) {
aggregation.keyed = true;
while (parser.nextToken() != XContentParser.Token.END_OBJECT) {
aggregation.buckets.add(keyedBucketParser.apply(parser));
}
} else if (token == XContentParser.Token.START_ARRAY) {
aggregation.keyed = false;
while (parser.nextToken() != XContentParser.Token.END_ARRAY) {
aggregation.buckets.add(bucketParser.apply(parser));
}
}
}, CommonFields.BUCKETS, ObjectParser.ValueType.OBJECT_ARRAY);
}

public static class ParsedBucket<T> implements MultiBucketsAggregation.Bucket {

private Aggregations aggregations;
private T key;
private String keyAsString;
private long docCount;
private boolean keyed;

protected void setKey(T key) {
this.key = key;
}

@Override
public Object getKey() {
return key;
}

protected void setKeyAsString(String keyAsString) {
this.keyAsString = keyAsString;
}

@Override
public String getKeyAsString() {
return keyAsString;
}

protected void setDocCount(long docCount) {
this.docCount = docCount;
}

@Override
public long getDocCount() {
return docCount;
}

public void setKeyed(boolean keyed) {
this.keyed = keyed;
}

protected void setAggregations(Aggregations aggregations) {
this.aggregations = aggregations;
}

@Override
public Aggregations getAggregations() {
return aggregations;
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
if (keyed) {
// Subclasses can override the getKeyAsString method to handle specific cases like
// keyed bucket with RAW doc value format where the key_as_string field is not printed
// out but we still need to have a string version of the key to use as the bucket's name.
builder.startObject(getKeyAsString());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but then why don't we use the getter below too?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be aligned - both now use getKeyAsString()

} else {
builder.startObject();
}
if (keyAsString != null) {
builder.field(CommonFields.KEY_AS_STRING.getPreferredName(), getKeyAsString());
}
builder.field(CommonFields.KEY.getPreferredName(), key);
builder.field(CommonFields.DOC_COUNT.getPreferredName(), docCount);
aggregations.toXContentInternal(builder, params);
builder.endObject();
return builder;
}

protected static <T, B extends ParsedBucket<T>> B parseXContent(final XContentParser parser,
final boolean keyed,
final Supplier<B> bucketSupplier,
final CheckedFunction<XContentParser, T, IOException> keyParser)
throws IOException {
final B bucket = bucketSupplier.get();
bucket.setKeyed(keyed);
XContentParser.Token token = parser.currentToken();
String currentFieldName = parser.currentName();
if (keyed) {
ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser::getTokenLocation);
ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser::getTokenLocation);
}

List<Aggregation> aggregations = new ArrayList<>();
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if (token.isValue()) {
if (CommonFields.KEY_AS_STRING.getPreferredName().equals(currentFieldName)) {
bucket.setKeyAsString(parser.text());
} else if (CommonFields.KEY.getPreferredName().equals(currentFieldName)) {
bucket.setKey(keyParser.apply(parser));
} else if (CommonFields.DOC_COUNT.getPreferredName().equals(currentFieldName)) {
bucket.setDocCount(parser.longValue());
}
} else if (token == XContentParser.Token.START_OBJECT) {
aggregations.add(XContentParserUtils.parseTypedKeysObject(parser, Aggregation.TYPED_KEYS_DELIMITER, Aggregation.class));
}
}
bucket.setAggregations(new Aggregations(aggregations));
return bucket;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* 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.bucket.histogram;

import org.elasticsearch.common.xcontent.ObjectParser;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.search.DocValueFormat;
import org.elasticsearch.search.aggregations.ParsedMultiBucketAggregation;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;

import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;

public class ParsedDateHistogram extends ParsedMultiBucketAggregation implements Histogram {

@Override
protected String getType() {
return DateHistogramAggregationBuilder.NAME;
}

@Override
public List<? extends Histogram.Bucket> getBuckets() {
return buckets.stream().map(bucket -> (Histogram.Bucket) bucket).collect(Collectors.toList());
}

private static ObjectParser<ParsedDateHistogram, Void> PARSER =
new ObjectParser<>(ParsedDateHistogram.class.getSimpleName(), true, ParsedDateHistogram::new);
static {
declareMultiBucketAggregationFields(PARSER,
parser -> ParsedBucket.fromXContent(parser, false),
parser -> ParsedBucket.fromXContent(parser, true));
}

public static ParsedDateHistogram fromXContent(XContentParser parser, String name) throws IOException {
ParsedDateHistogram aggregation = PARSER.parse(parser, null);
aggregation.setName(name);
return aggregation;
}

public static class ParsedBucket extends ParsedMultiBucketAggregation.ParsedBucket<Long> implements Histogram.Bucket {

@Override
public Object getKey() {
return new DateTime(super.getKey(), DateTimeZone.UTC);
}

@Override
public String getKeyAsString() {
String keyAsString = super.getKeyAsString();
if (keyAsString != null) {
return keyAsString;
} else {
return DocValueFormat.RAW.format((Long) super.getKey());
}
}

static ParsedBucket fromXContent(XContentParser parser, boolean keyed) throws IOException {
return parseXContent(parser, keyed, ParsedBucket::new, XContentParser::longValue);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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.bucket.histogram;

import org.elasticsearch.common.xcontent.ObjectParser;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.search.DocValueFormat;
import org.elasticsearch.search.aggregations.ParsedMultiBucketAggregation;

import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;

public class ParsedHistogram extends ParsedMultiBucketAggregation implements Histogram {

@Override
protected String getType() {
return HistogramAggregationBuilder.NAME;
}

@Override
public List<? extends Histogram.Bucket> getBuckets() {
return buckets.stream().map(bucket -> (Histogram.Bucket) bucket).collect(Collectors.toList());
}

private static ObjectParser<ParsedHistogram, Void> PARSER =
new ObjectParser<>(ParsedHistogram.class.getSimpleName(), true, ParsedHistogram::new);
static {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks very similar to the static block in ParsedDateHistogram, maybe we can have a static helper method here that takes a parser and the bucket parser function as arguments?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is a good suggestion

declareMultiBucketAggregationFields(PARSER,
parser -> ParsedBucket.fromXContent(parser, false),
parser -> ParsedBucket.fromXContent(parser, true));
}

public static ParsedHistogram fromXContent(XContentParser parser, String name) throws IOException {
ParsedHistogram aggregation = PARSER.parse(parser, null);
aggregation.setName(name);
return aggregation;
}

static class ParsedBucket extends ParsedMultiBucketAggregation.ParsedBucket<Double> implements Histogram.Bucket {

@Override
public String getKeyAsString() {
String keyAsString = super.getKeyAsString();
if (keyAsString != null) {
return keyAsString;
} else {
return DocValueFormat.RAW.format((Double) getKey());
}
}

static ParsedBucket fromXContent(XContentParser parser, boolean keyed) throws IOException {
return parseXContent(parser, keyed, ParsedBucket::new, XContentParser::doubleValue);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws

@SuppressWarnings("unchecked")
public static Suggestion<? extends Entry<? extends Option>> fromXContent(XContentParser parser) throws IOException {
ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.currentToken(), parser::getTokenLocation);
return XContentParserUtils.parseTypedKeysObject(parser, Aggregation.TYPED_KEYS_DELIMITER, Suggestion.class);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,10 @@ public void testParseTypedKeysObject() throws IOException {

BytesReference bytes = toXContent((builder, params) -> builder.field("test", 0), xContentType, randomBoolean());
try (XContentParser parser = xContentType.xContent().createParser(namedXContentRegistry, bytes)) {
parser.nextToken();
ParsingException e = expectThrows(ParsingException.class, () -> parseTypedKeysObject(parser, delimiter, Boolean.class));
assertEquals("Failed to parse object: expecting token of type [FIELD_NAME] but found [START_OBJECT]", e.getMessage());
ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser::getTokenLocation);
ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.nextToken(), parser::getTokenLocation);

parser.nextToken();
e = expectThrows(ParsingException.class, () -> parseTypedKeysObject(parser, delimiter, Boolean.class));
ParsingException e = expectThrows(ParsingException.class, () -> parseTypedKeysObject(parser, delimiter, Boolean.class));
assertEquals("Cannot parse object of class [Boolean] without type information. Set [typed_keys] parameter " +
"on the request to ensure the type information is added to the response output", e.getMessage());
}
Expand Down
Loading