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 .client ;
21+
22+ import org .apache .http .Header ;
23+ import org .apache .http .HttpEntity ;
24+ import org .apache .http .HttpHost ;
25+ import org .apache .http .HttpResponse ;
26+ import org .apache .http .ProtocolVersion ;
27+ import org .apache .http .RequestLine ;
28+ import org .apache .http .client .methods .HttpGet ;
29+ import org .apache .http .entity .ByteArrayEntity ;
30+ import org .apache .http .entity .ContentType ;
31+ import org .apache .http .message .BasicHeader ;
32+ import org .apache .http .message .BasicHttpResponse ;
33+ import org .apache .http .message .BasicRequestLine ;
34+ import org .apache .http .message .BasicStatusLine ;
35+ import org .apache .lucene .util .BytesRef ;
36+ import org .elasticsearch .Build ;
37+ import org .elasticsearch .Version ;
38+ import org .elasticsearch .action .ActionListener ;
39+ import org .elasticsearch .action .main .MainRequest ;
40+ import org .elasticsearch .action .main .MainResponse ;
41+ import org .elasticsearch .cluster .ClusterName ;
42+ import org .elasticsearch .common .SuppressForbidden ;
43+ import org .elasticsearch .common .xcontent .XContentHelper ;
44+ import org .elasticsearch .common .xcontent .XContentType ;
45+ import org .elasticsearch .test .ESTestCase ;
46+ import org .junit .Before ;
47+
48+ import java .io .IOException ;
49+ import java .lang .reflect .Method ;
50+ import java .lang .reflect .Modifier ;
51+
52+ import static java .util .Collections .emptyMap ;
53+ import static java .util .Collections .emptySet ;
54+ import static org .elasticsearch .client .ESRestHighLevelClientTestCase .execute ;
55+ import static org .mockito .Matchers .any ;
56+ import static org .mockito .Matchers .anyMapOf ;
57+ import static org .mockito .Matchers .anyObject ;
58+ import static org .mockito .Matchers .anyVararg ;
59+ import static org .mockito .Matchers .eq ;
60+ import static org .mockito .Mockito .doAnswer ;
61+ import static org .mockito .Mockito .mock ;
62+
63+ /**
64+ * Test and demonstrates how {@link RestHighLevelClient} can be extended to support custom endpoints.
65+ */
66+ public class CustomRestHighLevelClientTests extends ESTestCase {
67+
68+ private static final String ENDPOINT = "/_custom" ;
69+
70+ private CustomRestClient restHighLevelClient ;
71+
72+ @ Before
73+ @ SuppressWarnings ("unchecked" )
74+ public void initClients () throws IOException {
75+ if (restHighLevelClient == null ) {
76+ final RestClient restClient = mock (RestClient .class );
77+ restHighLevelClient = new CustomRestClient (restClient );
78+
79+ doAnswer (mock -> mockPerformRequest ((Header ) mock .getArguments ()[4 ]))
80+ .when (restClient )
81+ .performRequest (eq (HttpGet .METHOD_NAME ), eq (ENDPOINT ), anyMapOf (String .class , String .class ), anyObject (), anyVararg ());
82+
83+ doAnswer (mock -> mockPerformRequestAsync ((Header ) mock .getArguments ()[5 ], (ResponseListener ) mock .getArguments ()[4 ]))
84+ .when (restClient )
85+ .performRequestAsync (eq (HttpGet .METHOD_NAME ), eq (ENDPOINT ), anyMapOf (String .class , String .class ),
86+ any (HttpEntity .class ), any (ResponseListener .class ), anyVararg ());
87+ }
88+ }
89+
90+ public void testCustomEndpoint () throws IOException {
91+ final MainRequest request = new MainRequest ();
92+ final Header header = new BasicHeader ("node_name" , randomAlphaOfLengthBetween (1 , 10 ));
93+
94+ MainResponse response = execute (request , restHighLevelClient ::custom , restHighLevelClient ::customAsync , header );
95+ assertEquals (header .getValue (), response .getNodeName ());
96+
97+ response = execute (request , restHighLevelClient ::customAndParse , restHighLevelClient ::customAndParseAsync , header );
98+ assertEquals (header .getValue (), response .getNodeName ());
99+ }
100+
101+ /**
102+ * The {@link RestHighLevelClient} must declare the following execution methods using the <code>protected</code> modifier
103+ * so that they can be used by subclasses to implement custom logic.
104+ */
105+ @ SuppressForbidden (reason = "We're forced to uses Class#getDeclaredMethods() here because this test checks protected methods" )
106+ public void testMethodsVisibility () throws ClassNotFoundException {
107+ String [] methodNames = new String []{"performRequest" , "performRequestAndParseEntity" , "performRequestAsync" ,
108+ "performRequestAsyncAndParseEntity" };
109+ for (String methodName : methodNames ) {
110+ boolean found = false ;
111+ for (Method method : RestHighLevelClient .class .getDeclaredMethods ()) {
112+ if (method .getName ().equals (methodName )) {
113+ assertTrue ("Method " + methodName + " must be protected" , Modifier .isProtected (method .getModifiers ()));
114+ found = true ;
115+ }
116+ }
117+ assertTrue ("Failed to find method " + methodName , found );
118+ }
119+ }
120+
121+ /**
122+ * Mocks the asynchronous request execution by calling the {@link #mockPerformRequest(Header)} method.
123+ */
124+ private Void mockPerformRequestAsync (Header httpHeader , ResponseListener responseListener ) {
125+ try {
126+ responseListener .onSuccess (mockPerformRequest (httpHeader ));
127+ } catch (IOException e ) {
128+ responseListener .onFailure (e );
129+ }
130+ return null ;
131+ }
132+
133+ /**
134+ * Mocks the synchronous request execution like if it was executed by Elasticsearch.
135+ */
136+ private Response mockPerformRequest (Header httpHeader ) throws IOException {
137+ ProtocolVersion protocol = new ProtocolVersion ("HTTP" , 1 , 1 );
138+ HttpResponse httpResponse = new BasicHttpResponse (new BasicStatusLine (protocol , 200 , "OK" ));
139+
140+ MainResponse response = new MainResponse (httpHeader .getValue (), Version .CURRENT , ClusterName .DEFAULT , "_na" , Build .CURRENT , true );
141+ BytesRef bytesRef = XContentHelper .toXContent (response , XContentType .JSON , false ).toBytesRef ();
142+ httpResponse .setEntity (new ByteArrayEntity (bytesRef .bytes , ContentType .APPLICATION_JSON ));
143+
144+ RequestLine requestLine = new BasicRequestLine (HttpGet .METHOD_NAME , ENDPOINT , protocol );
145+ return new Response (requestLine , new HttpHost ("localhost" , 9200 ), httpResponse );
146+ }
147+
148+ /**
149+ * A custom high level client that provides custom methods to execute a request and get its associate response back.
150+ */
151+ static class CustomRestClient extends RestHighLevelClient {
152+
153+ private CustomRestClient (RestClient restClient ) {
154+ super (restClient );
155+ }
156+
157+ MainResponse custom (MainRequest mainRequest , Header ... headers ) throws IOException {
158+ return performRequest (mainRequest , this ::toRequest , this ::toResponse , emptySet (), headers );
159+ }
160+
161+ MainResponse customAndParse (MainRequest mainRequest , Header ... headers ) throws IOException {
162+ return performRequestAndParseEntity (mainRequest , this ::toRequest , MainResponse ::fromXContent , emptySet (), headers );
163+ }
164+
165+ void customAsync (MainRequest mainRequest , ActionListener <MainResponse > listener , Header ... headers ) {
166+ performRequestAsync (mainRequest , this ::toRequest , this ::toResponse , listener , emptySet (), headers );
167+ }
168+
169+ void customAndParseAsync (MainRequest mainRequest , ActionListener <MainResponse > listener , Header ... headers ) {
170+ performRequestAsyncAndParseEntity (mainRequest , this ::toRequest , MainResponse ::fromXContent , listener , emptySet (), headers );
171+ }
172+
173+ Request toRequest (MainRequest mainRequest ) throws IOException {
174+ return new Request (HttpGet .METHOD_NAME , ENDPOINT , emptyMap (), null );
175+ }
176+
177+ MainResponse toResponse (Response response ) throws IOException {
178+ return parseEntity (response .getEntity (), MainResponse ::fromXContent );
179+ }
180+ }
181+ }
0 commit comments