Skip to content

Commit 469194d

Browse files
committed
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.
1 parent 8ede459 commit 469194d

File tree

10 files changed

+269
-11
lines changed

10 files changed

+269
-11
lines changed

client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,7 @@
4949
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
5050
import org.elasticsearch.common.xcontent.XContentParser;
5151
import org.elasticsearch.common.xcontent.XContentType;
52-
import org.elasticsearch.join.aggregations.ChildrenAggregationBuilder;
53-
import org.elasticsearch.join.aggregations.ParsedChildren;
52+
import org.elasticsearch.plugins.spi.NamedXContentProvider;
5453
import org.elasticsearch.rest.BytesRestResponse;
5554
import org.elasticsearch.rest.RestStatus;
5655
import org.elasticsearch.search.aggregations.Aggregation;
@@ -92,8 +91,6 @@
9291
import org.elasticsearch.search.aggregations.bucket.terms.ParsedLongTerms;
9392
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
9493
import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;
95-
import org.elasticsearch.search.aggregations.matrix.stats.MatrixStatsAggregationBuilder;
96-
import org.elasticsearch.search.aggregations.matrix.stats.ParsedMatrixStats;
9794
import org.elasticsearch.search.aggregations.metrics.avg.AvgAggregationBuilder;
9895
import org.elasticsearch.search.aggregations.metrics.avg.ParsedAvg;
9996
import org.elasticsearch.search.aggregations.metrics.cardinality.CardinalityAggregationBuilder;
@@ -142,11 +139,13 @@
142139
import org.elasticsearch.search.suggest.term.TermSuggestion;
143140

144141
import java.io.IOException;
142+
import java.util.ArrayList;
145143
import java.util.Collections;
146144
import java.util.HashMap;
147145
import java.util.List;
148146
import java.util.Map;
149147
import java.util.Objects;
148+
import java.util.ServiceLoader;
150149
import java.util.Set;
151150
import java.util.function.Function;
152151
import java.util.stream.Collectors;
@@ -180,8 +179,9 @@ public RestHighLevelClient(RestClient restClient) {
180179
*/
181180
protected RestHighLevelClient(RestClient restClient, List<NamedXContentRegistry.Entry> namedXContentEntries) {
182181
this.client = Objects.requireNonNull(restClient);
183-
this.registry = new NamedXContentRegistry(Stream.of(getDefaultNamedXContents().stream(), namedXContentEntries.stream())
184-
.flatMap(Function.identity()).collect(toList()));
182+
this.registry = new NamedXContentRegistry(
183+
Stream.of(getDefaultNamedXContents().stream(), getProvidedNamedXContents().stream(), namedXContentEntries.stream())
184+
.flatMap(Function.identity()).collect(toList()));
185185
}
186186

187187
/**
@@ -566,8 +566,6 @@ static List<NamedXContentRegistry.Entry> getDefaultNamedXContents() {
566566
map.put(SignificantLongTerms.NAME, (p, c) -> ParsedSignificantLongTerms.fromXContent(p, (String) c));
567567
map.put(SignificantStringTerms.NAME, (p, c) -> ParsedSignificantStringTerms.fromXContent(p, (String) c));
568568
map.put(ScriptedMetricAggregationBuilder.NAME, (p, c) -> ParsedScriptedMetric.fromXContent(p, (String) c));
569-
map.put(ChildrenAggregationBuilder.NAME, (p, c) -> ParsedChildren.fromXContent(p, (String) c));
570-
map.put(MatrixStatsAggregationBuilder.NAME, (p, c) -> ParsedMatrixStats.fromXContent(p, (String) c));
571569
List<NamedXContentRegistry.Entry> entries = map.entrySet().stream()
572570
.map(entry -> new NamedXContentRegistry.Entry(Aggregation.class, new ParseField(entry.getKey()), entry.getValue()))
573571
.collect(Collectors.toList());
@@ -579,4 +577,15 @@ static List<NamedXContentRegistry.Entry> getDefaultNamedXContents() {
579577
(parser, context) -> CompletionSuggestion.fromXContent(parser, (String)context)));
580578
return entries;
581579
}
580+
581+
/**
582+
* Loads and returns the {@link NamedXContentRegistry.Entry} parsers provided by plugins.
583+
*/
584+
static List<NamedXContentRegistry.Entry> getProvidedNamedXContents() {
585+
List<NamedXContentRegistry.Entry> entries = new ArrayList<>();
586+
for (NamedXContentProvider service : ServiceLoader.load(NamedXContentProvider.class)) {
587+
entries.addAll(service.getNamedXContentParsers());
588+
}
589+
return entries;
590+
}
582591
}

client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,12 @@
5656
import org.elasticsearch.common.xcontent.XContentParser;
5757
import org.elasticsearch.common.xcontent.cbor.CborXContent;
5858
import org.elasticsearch.common.xcontent.smile.SmileXContent;
59+
import org.elasticsearch.join.aggregations.ChildrenAggregationBuilder;
5960
import org.elasticsearch.rest.RestStatus;
6061
import org.elasticsearch.search.SearchHits;
6162
import org.elasticsearch.search.aggregations.Aggregation;
6263
import org.elasticsearch.search.aggregations.InternalAggregations;
64+
import org.elasticsearch.search.aggregations.matrix.stats.MatrixStatsAggregationBuilder;
6365
import org.elasticsearch.search.suggest.Suggest;
6466
import org.elasticsearch.test.ESTestCase;
6567
import org.junit.Before;
@@ -69,6 +71,7 @@
6971

7072
import java.io.IOException;
7173
import java.net.SocketTimeoutException;
74+
import java.util.ArrayList;
7275
import java.util.Collections;
7376
import java.util.HashMap;
7477
import java.util.List;
@@ -613,9 +616,9 @@ public void testWrapResponseListenerOnResponseExceptionWithIgnoresErrorValidBody
613616
assertEquals("Elasticsearch exception [type=exception, reason=test error message]", elasticsearchException.getMessage());
614617
}
615618

616-
public void testNamedXContents() {
619+
public void testDefaultNamedXContents() {
617620
List<NamedXContentRegistry.Entry> namedXContents = RestHighLevelClient.getDefaultNamedXContents();
618-
assertEquals(45, namedXContents.size());
621+
assertEquals(43, namedXContents.size());
619622
Map<Class<?>, Integer> categories = new HashMap<>();
620623
for (NamedXContentRegistry.Entry namedXContent : namedXContents) {
621624
Integer counter = categories.putIfAbsent(namedXContent.categoryClass, 1);
@@ -624,10 +627,28 @@ public void testNamedXContents() {
624627
}
625628
}
626629
assertEquals(2, categories.size());
627-
assertEquals(Integer.valueOf(42), categories.get(Aggregation.class));
630+
assertEquals(Integer.valueOf(40), categories.get(Aggregation.class));
628631
assertEquals(Integer.valueOf(3), categories.get(Suggest.Suggestion.class));
629632
}
630633

634+
public void testProvidedNamedXContents() {
635+
List<NamedXContentRegistry.Entry> namedXContents = RestHighLevelClient.getProvidedNamedXContents();
636+
assertEquals(2, namedXContents.size());
637+
Map<Class<?>, Integer> categories = new HashMap<>();
638+
List<String> names = new ArrayList<>();
639+
for (NamedXContentRegistry.Entry namedXContent : namedXContents) {
640+
names.add(namedXContent.name.getPreferredName());
641+
Integer counter = categories.putIfAbsent(namedXContent.categoryClass, 1);
642+
if (counter != null) {
643+
categories.put(namedXContent.categoryClass, counter + 1);
644+
}
645+
}
646+
assertEquals(1, categories.size());
647+
assertEquals(Integer.valueOf(2), categories.get(Aggregation.class));
648+
assertTrue(names.contains(ChildrenAggregationBuilder.NAME));
649+
assertTrue(names.contains(MatrixStatsAggregationBuilder.NAME));
650+
}
651+
631652
private static class TrackingActionListener implements ActionListener<Integer> {
632653
private final AtomicInteger statusCode = new AtomicInteger(-1);
633654
private final AtomicReference<Exception> exception = new AtomicReference<>();
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.plugins.spi;
21+
22+
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
23+
24+
import java.util.List;
25+
26+
/**
27+
* Provides named XContent parsers.
28+
*/
29+
public interface NamedXContentProvider {
30+
31+
/**
32+
* @return a list of {@link NamedXContentRegistry.Entry} that this plugin provides.
33+
*/
34+
List<NamedXContentRegistry.Entry> getNamedXContentParsers();
35+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
/**
21+
* This package contains interfaces for services provided by
22+
* Elasticsearch plugins to external applications like the
23+
* Java High Level Rest Client.
24+
*/
25+
package org.elasticsearch.plugins.spi;
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.plugins.spi;
21+
22+
import org.elasticsearch.common.ParseField;
23+
import org.elasticsearch.common.io.Streams;
24+
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
25+
import org.elasticsearch.search.aggregations.Aggregation;
26+
import org.elasticsearch.search.aggregations.pipeline.ParsedSimpleValue;
27+
import org.elasticsearch.search.suggest.Suggest;
28+
import org.elasticsearch.search.suggest.term.TermSuggestion;
29+
import org.elasticsearch.test.ESTestCase;
30+
31+
import java.io.IOException;
32+
import java.io.InputStream;
33+
import java.util.ArrayList;
34+
import java.util.Arrays;
35+
import java.util.List;
36+
import java.util.ServiceLoader;
37+
import java.util.function.Predicate;
38+
39+
public class NamedXContentProviderTests extends ESTestCase {
40+
41+
public void testSpiFileExists() throws IOException {
42+
String serviceFile = "/META-INF/services/" + NamedXContentProvider.class.getName();
43+
List<String> implementations = new ArrayList<>();
44+
try (InputStream input = NamedXContentProviderTests.class.getResourceAsStream(serviceFile)) {
45+
Streams.readAllLines(input, implementations::add);
46+
}
47+
48+
assertEquals(1, implementations.size());
49+
assertEquals(TestNamedXContentProvider.class.getName(), implementations.get(0));
50+
}
51+
52+
public void testNamedXContents() {
53+
final List<NamedXContentRegistry.Entry> namedXContents = new ArrayList<>();
54+
for (NamedXContentProvider service : ServiceLoader.load(NamedXContentProvider.class)) {
55+
namedXContents.addAll(service.getNamedXContentParsers());
56+
}
57+
58+
assertEquals(2, namedXContents.size());
59+
60+
List<Predicate<NamedXContentRegistry.Entry>> predicates = new ArrayList<>(2);
61+
predicates.add(e -> Aggregation.class.equals(e.categoryClass) && "test_aggregation".equals(e.name.getPreferredName()));
62+
predicates.add(e -> Suggest.Suggestion.class.equals(e.categoryClass) && "test_suggestion".equals(e.name.getPreferredName()));
63+
predicates.forEach(predicate -> assertEquals(1, namedXContents.stream().filter(predicate).count()));
64+
}
65+
66+
public static class TestNamedXContentProvider implements NamedXContentProvider {
67+
68+
public TestNamedXContentProvider() {
69+
}
70+
71+
@Override
72+
public List<NamedXContentRegistry.Entry> getNamedXContentParsers() {
73+
return Arrays.asList(
74+
new NamedXContentRegistry.Entry(Aggregation.class, new ParseField("test_aggregation"),
75+
(parser, context) -> ParsedSimpleValue.fromXContent(parser, (String) context)),
76+
new NamedXContentRegistry.Entry(Suggest.Suggestion.class, new ParseField("test_suggestion"),
77+
(parser, context) -> TermSuggestion.fromXContent(parser, (String) context))
78+
);
79+
}
80+
}
81+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.elasticsearch.plugins.spi.NamedXContentProviderTests$TestNamedXContentProvider
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.search.aggregations.matrix.spi;
21+
22+
import org.elasticsearch.common.ParseField;
23+
import org.elasticsearch.common.xcontent.ContextParser;
24+
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
25+
import org.elasticsearch.plugins.spi.NamedXContentProvider;
26+
import org.elasticsearch.search.aggregations.Aggregation;
27+
import org.elasticsearch.search.aggregations.matrix.stats.MatrixStatsAggregationBuilder;
28+
import org.elasticsearch.search.aggregations.matrix.stats.ParsedMatrixStats;
29+
30+
import java.util.List;
31+
32+
import static java.util.Collections.singletonList;
33+
34+
public class MatrixStatsNamedXContentProvider implements NamedXContentProvider {
35+
36+
@Override
37+
public List<NamedXContentRegistry.Entry> getNamedXContentParsers() {
38+
ParseField parseField = new ParseField(MatrixStatsAggregationBuilder.NAME);
39+
ContextParser<Object, Aggregation> contextParser = (p, name) -> ParsedMatrixStats.fromXContent(p, (String) name);
40+
return singletonList(new NamedXContentRegistry.Entry(Aggregation.class, parseField, contextParser));
41+
}
42+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.elasticsearch.search.aggregations.matrix.spi.MatrixStatsNamedXContentProvider
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.join.spi;
21+
22+
import org.elasticsearch.common.ParseField;
23+
import org.elasticsearch.common.xcontent.ContextParser;
24+
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
25+
import org.elasticsearch.join.aggregations.ChildrenAggregationBuilder;
26+
import org.elasticsearch.join.aggregations.ParsedChildren;
27+
import org.elasticsearch.plugins.spi.NamedXContentProvider;
28+
import org.elasticsearch.search.aggregations.Aggregation;
29+
30+
import java.util.List;
31+
32+
import static java.util.Collections.singletonList;
33+
34+
public class ParentJoinNamedXContentProvider implements NamedXContentProvider {
35+
36+
@Override
37+
public List<NamedXContentRegistry.Entry> getNamedXContentParsers() {
38+
ParseField parseField = new ParseField(ChildrenAggregationBuilder.NAME);
39+
ContextParser<Object, Aggregation> contextParser = (p, name) -> ParsedChildren.fromXContent(p, (String) name);
40+
return singletonList(new NamedXContentRegistry.Entry(Aggregation.class, parseField, contextParser));
41+
}
42+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.elasticsearch.join.spi.ParentJoinNamedXContentProvider

0 commit comments

Comments
 (0)