diff --git a/core/src/main/java/org/elasticsearch/search/aggregations/ParsedAggregation.java b/core/src/main/java/org/elasticsearch/search/aggregations/ParsedAggregation.java new file mode 100644 index 0000000000000..f1f9705214977 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/search/aggregations/ParsedAggregation.java @@ -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; + +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * An implementation of {@link Aggregation} that is parsed from a REST response. + * Serves as a base class for all aggregation implementations that are parsed from REST. + */ +public abstract class ParsedAggregation implements Aggregation, ToXContent { + + //TODO move CommonFields out of InternalAggregation + protected static void declareCommonFields(ObjectParser objectParser) { + objectParser.declareObject((parsedAgg, metadata) -> parsedAgg.metadata.putAll(metadata), + (parser, context) -> parser.map(), InternalAggregation.CommonFields.META); + } + + String name; + final Map metadata = new HashMap<>(); + + @Override + public final String getName() { + return name; + } + + @Override + public final Map getMetaData() { + return Collections.unmodifiableMap(metadata); + } + + /** + * Returns a string representing the type of the aggregation. This type is added to + * the aggregation name in the response, so that it can later be used by REST clients + * to determine the internal type of the aggregation. + */ + //TODO it may make sense to move getType to the Aggregation interface given that we are duplicating it in both implementations + protected abstract String getType(); + + //TODO the only way to avoid duplicating this method is making Aggregation extend ToXContent + //and declare toXContent as a default method in it. Doesn't sound like the right thing to do. + public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { + //TODO move TYPED_KEYS_DELIMITER constant out of InternalAggregation + // Concatenates the type and the name of the aggregation (ex: top_hits#foo) + builder.startObject(String.join(InternalAggregation.TYPED_KEYS_DELIMITER, getType(), name)); + if (metadata.isEmpty() == false) { + builder.field(InternalAggregation.CommonFields.META.getPreferredName()); + builder.map(metadata); + } + doXContentBody(builder, params); + builder.endObject(); + return builder; + } + + protected abstract XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException; +} diff --git a/core/src/test/java/org/elasticsearch/search/aggregations/ParsedAggregationTests.java b/core/src/test/java/org/elasticsearch/search/aggregations/ParsedAggregationTests.java new file mode 100644 index 0000000000000..4b75bf1990c58 --- /dev/null +++ b/core/src/test/java/org/elasticsearch/search/aggregations/ParsedAggregationTests.java @@ -0,0 +1,145 @@ +/* + * 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.ParseField; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.rest.FakeRestRequest; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent; +import static org.hamcrest.CoreMatchers.instanceOf; + +public class ParsedAggregationTests extends ESTestCase { + + //TODO maybe this test will no longer be needed once we have real tests for ParsedAggregation subclasses + public void testParse() throws IOException { + String name = randomAlphaOfLengthBetween(5, 10); + int numMetas = randomIntBetween(0, 5); + Map meta = new HashMap<>(numMetas); + for (int i = 0; i < numMetas; i++) { + meta.put(randomAlphaOfLengthBetween(3, 10), randomAlphaOfLengthBetween(3, 10)); + } + TestInternalAggregation testAgg = new TestInternalAggregation(name, meta); + XContentType xContentType = randomFrom(XContentType.values()); + FakeRestRequest params = new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY) + .withParams(Collections.singletonMap("typed_keys", "true")).build(); + BytesReference bytesAgg = XContentHelper.toXContent(testAgg, xContentType, params, randomBoolean()); + try (XContentParser parser = createParser(xContentType.xContent(), bytesAgg)) { + parser.nextToken(); + assert parser.currentToken() == XContentParser.Token.START_OBJECT; + parser.nextToken(); + assert parser.currentToken() == XContentParser.Token.FIELD_NAME; + String currentName = parser.currentName(); + int i = currentName.indexOf(InternalAggregation.TYPED_KEYS_DELIMITER); + String aggType = currentName.substring(0, i); + String aggName = currentName.substring(i + 1); + Aggregation parsedAgg = parser.namedObject(Aggregation.class, aggType, aggName); + assertThat(parsedAgg, instanceOf(TestParsedAggregation.class)); + assertEquals(testAgg.getName(), parsedAgg.getName()); + assertEquals(testAgg.getMetaData(), parsedAgg.getMetaData()); + BytesReference finalAgg = XContentHelper.toXContent((ToXContent) parsedAgg, xContentType, randomBoolean()); + assertToXContentEquivalent(bytesAgg, finalAgg, xContentType); + } + } + + @Override + protected NamedXContentRegistry xContentRegistry() { + //TODO we may want to un-deprecate this Entry constructor if we are going to use it extensively, which I think we are + NamedXContentRegistry.Entry entry = new NamedXContentRegistry.Entry(Aggregation.class, new ParseField("type"), + (parser, name) -> TestParsedAggregation.fromXContent(parser, (String)name)); + return new NamedXContentRegistry(Collections.singletonList(entry)); + } + + private static class TestParsedAggregation extends ParsedAggregation { + private static ObjectParser PARSER = new ObjectParser<>("testAggParser", TestParsedAggregation::new); + + static { + ParsedAggregation.declareCommonFields(PARSER); + } + + @Override + protected String getType() { + return "type"; + } + + @Override + protected XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException { + return builder; + } + + public static TestParsedAggregation fromXContent(XContentParser parser, String name) throws IOException { + TestParsedAggregation parsedAgg = PARSER.parse(parser, null); + parsedAgg.name = name; + return parsedAgg; + } + } + + private static class TestInternalAggregation extends InternalAggregation { + + private TestInternalAggregation(String name, Map metaData) { + super(name, Collections.emptyList(), metaData); + } + + @Override + public String getWriteableName() { + throw new UnsupportedOperationException(); + } + + @Override + protected String getType() { + return "type"; + } + + @Override + protected void doWriteTo(StreamOutput out) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { + throw new UnsupportedOperationException(); + } + + @Override + public Object getProperty(List path) { + throw new UnsupportedOperationException(); + } + + @Override + public XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException { + return builder; + } + } +}