From 469194dedcd6fd448420dcda199d38ff85f96258 Mon Sep 17 00:00:00 2001 From: Tanguy Leroux Date: Thu, 15 Jun 2017 12:50:02 +0200 Subject: [PATCH] Use SPI in High Level Rest Client to load XContent parsers (#25098) This commit adds a NamedXContentProvider interface that can be implemented by plugins or modules using Java's SPI feature in order to provide additional NamedXContent parsers to external applications like the Java High Level Rest Client. --- .../client/RestHighLevelClient.java | 25 ++++-- .../client/RestHighLevelClientTests.java | 27 ++++++- .../plugins/spi/NamedXContentProvider.java | 35 ++++++++ .../plugins/spi/package-info.java | 25 ++++++ .../spi/NamedXContentProviderTests.java | 81 +++++++++++++++++++ ...icsearch.plugins.spi.NamedXContentProvider | 1 + .../spi/MatrixStatsNamedXContentProvider.java | 42 ++++++++++ ...icsearch.plugins.spi.NamedXContentProvider | 1 + .../spi/ParentJoinNamedXContentProvider.java | 42 ++++++++++ ...icsearch.plugins.spi.NamedXContentProvider | 1 + 10 files changed, 269 insertions(+), 11 deletions(-) create mode 100644 core/src/main/java/org/elasticsearch/plugins/spi/NamedXContentProvider.java create mode 100644 core/src/main/java/org/elasticsearch/plugins/spi/package-info.java create mode 100644 core/src/test/java/org/elasticsearch/plugins/spi/NamedXContentProviderTests.java create mode 100644 core/src/test/resources/META-INF/services/org.elasticsearch.plugins.spi.NamedXContentProvider create mode 100644 modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/spi/MatrixStatsNamedXContentProvider.java create mode 100644 modules/aggs-matrix-stats/src/main/resources/META-INF/services/org.elasticsearch.plugins.spi.NamedXContentProvider create mode 100644 modules/parent-join/src/main/java/org/elasticsearch/join/spi/ParentJoinNamedXContentProvider.java create mode 100644 modules/parent-join/src/main/resources/META-INF/services/org.elasticsearch.plugins.spi.NamedXContentProvider diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java index a354bdfb7ba5a..e40c3c223ebcb 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java @@ -49,8 +49,7 @@ import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; -import org.elasticsearch.join.aggregations.ChildrenAggregationBuilder; -import org.elasticsearch.join.aggregations.ParsedChildren; +import org.elasticsearch.plugins.spi.NamedXContentProvider; import org.elasticsearch.rest.BytesRestResponse; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.search.aggregations.Aggregation; @@ -92,8 +91,6 @@ import org.elasticsearch.search.aggregations.bucket.terms.ParsedLongTerms; import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms; import org.elasticsearch.search.aggregations.bucket.terms.StringTerms; -import org.elasticsearch.search.aggregations.matrix.stats.MatrixStatsAggregationBuilder; -import org.elasticsearch.search.aggregations.matrix.stats.ParsedMatrixStats; import org.elasticsearch.search.aggregations.metrics.avg.AvgAggregationBuilder; import org.elasticsearch.search.aggregations.metrics.avg.ParsedAvg; import org.elasticsearch.search.aggregations.metrics.cardinality.CardinalityAggregationBuilder; @@ -142,11 +139,13 @@ import org.elasticsearch.search.suggest.term.TermSuggestion; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.ServiceLoader; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -180,8 +179,9 @@ public RestHighLevelClient(RestClient restClient) { */ protected RestHighLevelClient(RestClient restClient, List namedXContentEntries) { this.client = Objects.requireNonNull(restClient); - this.registry = new NamedXContentRegistry(Stream.of(getDefaultNamedXContents().stream(), namedXContentEntries.stream()) - .flatMap(Function.identity()).collect(toList())); + this.registry = new NamedXContentRegistry( + Stream.of(getDefaultNamedXContents().stream(), getProvidedNamedXContents().stream(), namedXContentEntries.stream()) + .flatMap(Function.identity()).collect(toList())); } /** @@ -566,8 +566,6 @@ static List getDefaultNamedXContents() { map.put(SignificantLongTerms.NAME, (p, c) -> ParsedSignificantLongTerms.fromXContent(p, (String) c)); map.put(SignificantStringTerms.NAME, (p, c) -> ParsedSignificantStringTerms.fromXContent(p, (String) c)); map.put(ScriptedMetricAggregationBuilder.NAME, (p, c) -> ParsedScriptedMetric.fromXContent(p, (String) c)); - map.put(ChildrenAggregationBuilder.NAME, (p, c) -> ParsedChildren.fromXContent(p, (String) c)); - map.put(MatrixStatsAggregationBuilder.NAME, (p, c) -> ParsedMatrixStats.fromXContent(p, (String) c)); List entries = map.entrySet().stream() .map(entry -> new NamedXContentRegistry.Entry(Aggregation.class, new ParseField(entry.getKey()), entry.getValue())) .collect(Collectors.toList()); @@ -579,4 +577,15 @@ static List getDefaultNamedXContents() { (parser, context) -> CompletionSuggestion.fromXContent(parser, (String)context))); return entries; } + + /** + * Loads and returns the {@link NamedXContentRegistry.Entry} parsers provided by plugins. + */ + static List getProvidedNamedXContents() { + List entries = new ArrayList<>(); + for (NamedXContentProvider service : ServiceLoader.load(NamedXContentProvider.class)) { + entries.addAll(service.getNamedXContentParsers()); + } + return entries; + } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java index 7fc0733a7f0c7..bbc973e231588 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java @@ -56,10 +56,12 @@ import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.cbor.CborXContent; import org.elasticsearch.common.xcontent.smile.SmileXContent; +import org.elasticsearch.join.aggregations.ChildrenAggregationBuilder; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.search.SearchHits; import org.elasticsearch.search.aggregations.Aggregation; import org.elasticsearch.search.aggregations.InternalAggregations; +import org.elasticsearch.search.aggregations.matrix.stats.MatrixStatsAggregationBuilder; import org.elasticsearch.search.suggest.Suggest; import org.elasticsearch.test.ESTestCase; import org.junit.Before; @@ -69,6 +71,7 @@ import java.io.IOException; import java.net.SocketTimeoutException; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -613,9 +616,9 @@ public void testWrapResponseListenerOnResponseExceptionWithIgnoresErrorValidBody assertEquals("Elasticsearch exception [type=exception, reason=test error message]", elasticsearchException.getMessage()); } - public void testNamedXContents() { + public void testDefaultNamedXContents() { List namedXContents = RestHighLevelClient.getDefaultNamedXContents(); - assertEquals(45, namedXContents.size()); + assertEquals(43, namedXContents.size()); Map, Integer> categories = new HashMap<>(); for (NamedXContentRegistry.Entry namedXContent : namedXContents) { Integer counter = categories.putIfAbsent(namedXContent.categoryClass, 1); @@ -624,10 +627,28 @@ public void testNamedXContents() { } } assertEquals(2, categories.size()); - assertEquals(Integer.valueOf(42), categories.get(Aggregation.class)); + assertEquals(Integer.valueOf(40), categories.get(Aggregation.class)); assertEquals(Integer.valueOf(3), categories.get(Suggest.Suggestion.class)); } + public void testProvidedNamedXContents() { + List namedXContents = RestHighLevelClient.getProvidedNamedXContents(); + assertEquals(2, namedXContents.size()); + Map, Integer> categories = new HashMap<>(); + List names = new ArrayList<>(); + for (NamedXContentRegistry.Entry namedXContent : namedXContents) { + names.add(namedXContent.name.getPreferredName()); + Integer counter = categories.putIfAbsent(namedXContent.categoryClass, 1); + if (counter != null) { + categories.put(namedXContent.categoryClass, counter + 1); + } + } + assertEquals(1, categories.size()); + assertEquals(Integer.valueOf(2), categories.get(Aggregation.class)); + assertTrue(names.contains(ChildrenAggregationBuilder.NAME)); + assertTrue(names.contains(MatrixStatsAggregationBuilder.NAME)); + } + private static class TrackingActionListener implements ActionListener { private final AtomicInteger statusCode = new AtomicInteger(-1); private final AtomicReference exception = new AtomicReference<>(); diff --git a/core/src/main/java/org/elasticsearch/plugins/spi/NamedXContentProvider.java b/core/src/main/java/org/elasticsearch/plugins/spi/NamedXContentProvider.java new file mode 100644 index 0000000000000..ef511fcfeae35 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/plugins/spi/NamedXContentProvider.java @@ -0,0 +1,35 @@ +/* + * 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.plugins.spi; + +import org.elasticsearch.common.xcontent.NamedXContentRegistry; + +import java.util.List; + +/** + * Provides named XContent parsers. + */ +public interface NamedXContentProvider { + + /** + * @return a list of {@link NamedXContentRegistry.Entry} that this plugin provides. + */ + List getNamedXContentParsers(); +} diff --git a/core/src/main/java/org/elasticsearch/plugins/spi/package-info.java b/core/src/main/java/org/elasticsearch/plugins/spi/package-info.java new file mode 100644 index 0000000000000..7740e1424fb76 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/plugins/spi/package-info.java @@ -0,0 +1,25 @@ +/* + * 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. + */ + +/** + * This package contains interfaces for services provided by + * Elasticsearch plugins to external applications like the + * Java High Level Rest Client. + */ +package org.elasticsearch.plugins.spi; diff --git a/core/src/test/java/org/elasticsearch/plugins/spi/NamedXContentProviderTests.java b/core/src/test/java/org/elasticsearch/plugins/spi/NamedXContentProviderTests.java new file mode 100644 index 0000000000000..3b63d88f39298 --- /dev/null +++ b/core/src/test/java/org/elasticsearch/plugins/spi/NamedXContentProviderTests.java @@ -0,0 +1,81 @@ +/* + * 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.plugins.spi; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.Streams; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.search.aggregations.Aggregation; +import org.elasticsearch.search.aggregations.pipeline.ParsedSimpleValue; +import org.elasticsearch.search.suggest.Suggest; +import org.elasticsearch.search.suggest.term.TermSuggestion; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.ServiceLoader; +import java.util.function.Predicate; + +public class NamedXContentProviderTests extends ESTestCase { + + public void testSpiFileExists() throws IOException { + String serviceFile = "/META-INF/services/" + NamedXContentProvider.class.getName(); + List implementations = new ArrayList<>(); + try (InputStream input = NamedXContentProviderTests.class.getResourceAsStream(serviceFile)) { + Streams.readAllLines(input, implementations::add); + } + + assertEquals(1, implementations.size()); + assertEquals(TestNamedXContentProvider.class.getName(), implementations.get(0)); + } + + public void testNamedXContents() { + final List namedXContents = new ArrayList<>(); + for (NamedXContentProvider service : ServiceLoader.load(NamedXContentProvider.class)) { + namedXContents.addAll(service.getNamedXContentParsers()); + } + + assertEquals(2, namedXContents.size()); + + List> predicates = new ArrayList<>(2); + predicates.add(e -> Aggregation.class.equals(e.categoryClass) && "test_aggregation".equals(e.name.getPreferredName())); + predicates.add(e -> Suggest.Suggestion.class.equals(e.categoryClass) && "test_suggestion".equals(e.name.getPreferredName())); + predicates.forEach(predicate -> assertEquals(1, namedXContents.stream().filter(predicate).count())); + } + + public static class TestNamedXContentProvider implements NamedXContentProvider { + + public TestNamedXContentProvider() { + } + + @Override + public List getNamedXContentParsers() { + return Arrays.asList( + new NamedXContentRegistry.Entry(Aggregation.class, new ParseField("test_aggregation"), + (parser, context) -> ParsedSimpleValue.fromXContent(parser, (String) context)), + new NamedXContentRegistry.Entry(Suggest.Suggestion.class, new ParseField("test_suggestion"), + (parser, context) -> TermSuggestion.fromXContent(parser, (String) context)) + ); + } + } +} diff --git a/core/src/test/resources/META-INF/services/org.elasticsearch.plugins.spi.NamedXContentProvider b/core/src/test/resources/META-INF/services/org.elasticsearch.plugins.spi.NamedXContentProvider new file mode 100644 index 0000000000000..8ec7461c6676f --- /dev/null +++ b/core/src/test/resources/META-INF/services/org.elasticsearch.plugins.spi.NamedXContentProvider @@ -0,0 +1 @@ +org.elasticsearch.plugins.spi.NamedXContentProviderTests$TestNamedXContentProvider \ No newline at end of file diff --git a/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/spi/MatrixStatsNamedXContentProvider.java b/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/spi/MatrixStatsNamedXContentProvider.java new file mode 100644 index 0000000000000..bb71e3085de62 --- /dev/null +++ b/modules/aggs-matrix-stats/src/main/java/org/elasticsearch/search/aggregations/matrix/spi/MatrixStatsNamedXContentProvider.java @@ -0,0 +1,42 @@ +/* + * 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.matrix.spi; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ContextParser; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.plugins.spi.NamedXContentProvider; +import org.elasticsearch.search.aggregations.Aggregation; +import org.elasticsearch.search.aggregations.matrix.stats.MatrixStatsAggregationBuilder; +import org.elasticsearch.search.aggregations.matrix.stats.ParsedMatrixStats; + +import java.util.List; + +import static java.util.Collections.singletonList; + +public class MatrixStatsNamedXContentProvider implements NamedXContentProvider { + + @Override + public List getNamedXContentParsers() { + ParseField parseField = new ParseField(MatrixStatsAggregationBuilder.NAME); + ContextParser contextParser = (p, name) -> ParsedMatrixStats.fromXContent(p, (String) name); + return singletonList(new NamedXContentRegistry.Entry(Aggregation.class, parseField, contextParser)); + } +} diff --git a/modules/aggs-matrix-stats/src/main/resources/META-INF/services/org.elasticsearch.plugins.spi.NamedXContentProvider b/modules/aggs-matrix-stats/src/main/resources/META-INF/services/org.elasticsearch.plugins.spi.NamedXContentProvider new file mode 100644 index 0000000000000..a2d706a39a60c --- /dev/null +++ b/modules/aggs-matrix-stats/src/main/resources/META-INF/services/org.elasticsearch.plugins.spi.NamedXContentProvider @@ -0,0 +1 @@ +org.elasticsearch.search.aggregations.matrix.spi.MatrixStatsNamedXContentProvider \ No newline at end of file diff --git a/modules/parent-join/src/main/java/org/elasticsearch/join/spi/ParentJoinNamedXContentProvider.java b/modules/parent-join/src/main/java/org/elasticsearch/join/spi/ParentJoinNamedXContentProvider.java new file mode 100644 index 0000000000000..250241014613c --- /dev/null +++ b/modules/parent-join/src/main/java/org/elasticsearch/join/spi/ParentJoinNamedXContentProvider.java @@ -0,0 +1,42 @@ +/* + * 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.join.spi; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ContextParser; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.join.aggregations.ChildrenAggregationBuilder; +import org.elasticsearch.join.aggregations.ParsedChildren; +import org.elasticsearch.plugins.spi.NamedXContentProvider; +import org.elasticsearch.search.aggregations.Aggregation; + +import java.util.List; + +import static java.util.Collections.singletonList; + +public class ParentJoinNamedXContentProvider implements NamedXContentProvider { + + @Override + public List getNamedXContentParsers() { + ParseField parseField = new ParseField(ChildrenAggregationBuilder.NAME); + ContextParser contextParser = (p, name) -> ParsedChildren.fromXContent(p, (String) name); + return singletonList(new NamedXContentRegistry.Entry(Aggregation.class, parseField, contextParser)); + } +} diff --git a/modules/parent-join/src/main/resources/META-INF/services/org.elasticsearch.plugins.spi.NamedXContentProvider b/modules/parent-join/src/main/resources/META-INF/services/org.elasticsearch.plugins.spi.NamedXContentProvider new file mode 100644 index 0000000000000..48687c21c3250 --- /dev/null +++ b/modules/parent-join/src/main/resources/META-INF/services/org.elasticsearch.plugins.spi.NamedXContentProvider @@ -0,0 +1 @@ +org.elasticsearch.join.spi.ParentJoinNamedXContentProvider \ No newline at end of file