diff --git a/java-client/src/main/java/co/elastic/clients/transport/endpoints/BinaryEndpoint.java b/java-client/src/main/java/co/elastic/clients/transport/endpoints/BinaryEndpoint.java new file mode 100644 index 000000000..76d74d6d4 --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/transport/endpoints/BinaryEndpoint.java @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.clients.transport.endpoints; + +import java.util.Map; +import java.util.function.Function; + +public class BinaryEndpoint extends EndpointBase { + + public BinaryEndpoint( + String id, + Function method, + Function requestUrl, + Function> queryParameters, + Function> headers, + boolean hasRequestBody, + Object ignored // same number of arguments as SimpleEndpoint + ) { + super(id, method, requestUrl, queryParameters, headers, hasRequestBody); + } + + @Override + public boolean isError(int statusCode) { + return statusCode >= 400; + } +} + diff --git a/java-client/src/main/java/co/elastic/clients/transport/endpoints/BinaryResponse.java b/java-client/src/main/java/co/elastic/clients/transport/endpoints/BinaryResponse.java new file mode 100644 index 000000000..773141f38 --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/transport/endpoints/BinaryResponse.java @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.clients.transport.endpoints; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Response for API endpoints that return non-JSON content. + *

+ * Note: binary responses hold a reference to the transport layer's response body. As such, they must be closed + * to ensure that any associated resources are released. Alternatively you can also close the {@link #content()} stream. + */ +public interface BinaryResponse extends AutoCloseable { + + /** + * The response content type. If not known, defaults to {@code application/octet-stream}. + */ + String contentType(); + + /** + * The content length, or {@code -1} if not known. + */ + long contentLength(); + + /** + * The response body. This method can be called only once and will throw an {@link IllegalStateException} on subsequent calls. + *

+ * Calling {@link InputStream#close()} on the result has the same effect as calling {@link #close()} on this object. + * + * @throws IOException if the stream could not be created + * @throws IllegalStateException if this method has already been called + */ + InputStream content() throws IOException; + + /** + * Releases any resources associated with this response. + */ + @Override + void close() throws IOException; +} diff --git a/java-client/src/main/java/co/elastic/clients/transport/endpoints/BooleanEndpoint.java b/java-client/src/main/java/co/elastic/clients/transport/endpoints/BooleanEndpoint.java index 9ffdc9f9a..398ccc386 100644 --- a/java-client/src/main/java/co/elastic/clients/transport/endpoints/BooleanEndpoint.java +++ b/java-client/src/main/java/co/elastic/clients/transport/endpoints/BooleanEndpoint.java @@ -19,12 +19,10 @@ package co.elastic.clients.transport.endpoints; -import co.elastic.clients.json.JsonpDeserializer; - import java.util.Map; import java.util.function.Function; -public class BooleanEndpoint extends SimpleEndpoint { +public class BooleanEndpoint extends EndpointBase { public BooleanEndpoint( String id, @@ -34,9 +32,9 @@ public BooleanEndpoint( Map> queryParameters, Function> headers, boolean hasRequestBody, - JsonpDeserializer responseParser // always null + Object ignored // same number of arguments as SimpleEndpoint ) { - super(id, method, requestUrl, queryParameters, headers, hasRequestBody, null); + super(id, method, requestUrl, queryParameters, headers, hasRequestBody); } @Override diff --git a/java-client/src/main/java/co/elastic/clients/transport/endpoints/EndpointBase.java b/java-client/src/main/java/co/elastic/clients/transport/endpoints/EndpointBase.java new file mode 100644 index 000000000..eac712e09 --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/transport/endpoints/EndpointBase.java @@ -0,0 +1,131 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.clients.transport.endpoints; + +import co.elastic.clients.elasticsearch._types.ErrorResponse; +import co.elastic.clients.json.JsonpDeserializer; +import co.elastic.clients.transport.Endpoint; +import org.apache.http.client.utils.URLEncodedUtils; + +import java.util.Collections; +import java.util.Map; +import java.util.function.Function; + +public class EndpointBase implements Endpoint { + + private static final Function> EMPTY_MAP = x -> Collections.emptyMap(); + + /** + * Returns a function that always returns an empty String to String map. Useful to avoid creating lots of + * duplicate lambdas in endpoints that don't have headers or parameters. + */ + @SuppressWarnings("unchecked") + public static Function> emptyMap() { + return (Function>) EMPTY_MAP; + } + + protected final String id; + protected final Function method; + protected final Function requestUrl; + protected final Function> queryParameters; + protected final Function> headers; + protected final boolean hasRequestBody; + + public EndpointBase( + String id, + Function method, + Function requestUrl, + Function> queryParameters, + Function> headers, + boolean hasRequestBody + ) { + this.id = id; + this.method = method; + this.requestUrl = requestUrl; + this.queryParameters = queryParameters; + this.headers = headers; + this.hasRequestBody = hasRequestBody; + } + + @Override + public String id() { + return this.id; + } + + @Override + public String method(RequestT request) { + return this.method.apply(request); + } + + @Override + public String requestUrl(RequestT request) { + return this.requestUrl.apply(request); + } + + @Override + public Map queryParameters(RequestT request) { + return this.queryParameters.apply(request); + } + + @Override + public Map headers(RequestT request) { + return this.headers.apply(request); + } + + @Override + public boolean hasRequestBody() { + return this.hasRequestBody; + } + + // ES-specific + @Override + public boolean isError(int statusCode) { + return statusCode >= 400; + } + + @Override + public JsonpDeserializer errorDeserializer(int statusCode) { + return ErrorResponse._DESERIALIZER; + } + + public SimpleEndpoint withResponseDeserializer( + JsonpDeserializer newResponseParser + ) { + return new SimpleEndpoint<>( + id, + method, + requestUrl, + queryParameters, + headers, + hasRequestBody, + newResponseParser + ); + } + + public static RuntimeException noPathTemplateFound(String what) { + return new RuntimeException("Could not find a request " + what + " with this set of properties. " + + "Please check the API documentation, or raise an issue if this should be a valid request."); + } + + public static void pathEncode(String src, StringBuilder dest) { + // TODO: avoid dependency on HttpClient here (and use something more efficient) + dest.append(URLEncodedUtils.formatSegments(src).substring(1)); + } +} diff --git a/java-client/src/main/java/co/elastic/clients/transport/endpoints/SimpleEndpoint.java b/java-client/src/main/java/co/elastic/clients/transport/endpoints/SimpleEndpoint.java index bf3fa1520..97c774aa1 100644 --- a/java-client/src/main/java/co/elastic/clients/transport/endpoints/SimpleEndpoint.java +++ b/java-client/src/main/java/co/elastic/clients/transport/endpoints/SimpleEndpoint.java @@ -28,7 +28,8 @@ import java.util.Map; import java.util.function.Function; -public class SimpleEndpoint implements JsonEndpoint { +public class SimpleEndpoint extends EndpointBase + implements JsonEndpoint { private static final Function> EMPTY_MAP = x -> Collections.emptyMap(); @@ -41,12 +42,6 @@ public static Function> emptyMap() { return (Function>) EMPTY_MAP; } - private final String id; - private final Function method; - private final Function requestUrl; - private final Function> queryParameters; - private final Function> headers; - private final boolean hasRequestBody; private final JsonpDeserializer responseParser; public SimpleEndpoint( @@ -58,56 +53,15 @@ public SimpleEndpoint( boolean hasRequestBody, JsonpDeserializer responseParser ) { - this.id = id; - this.method = method; - this.requestUrl = requestUrl; - this.queryParameters = queryParameters; - this.headers = headers; - this.hasRequestBody = hasRequestBody; + super(id, method, requestUrl, queryParameters, headers, hasRequestBody); this.responseParser = responseParser; } - @Override - public String id() { - return this.id; - } - - @Override - public String method(RequestT request) { - return this.method.apply(request); - } - - @Override - public String requestUrl(RequestT request) { - return this.requestUrl.apply(request); - } - - @Override - public Map queryParameters(RequestT request) { - return this.queryParameters.apply(request); - } - - @Override - public Map headers(RequestT request) { - return this.headers.apply(request); - } - - @Override - public boolean hasRequestBody() { - return this.hasRequestBody; - } - @Override public JsonpDeserializer responseDeserializer() { return this.responseParser; } - // ES-specific - @Override - public boolean isError(int statusCode) { - return statusCode >= 400; - } - @Override public JsonpDeserializer errorDeserializer(int statusCode) { return ErrorResponse._DESERIALIZER; diff --git a/java-client/src/main/java/co/elastic/clients/transport/endpoints/SimpleJsonEndpoint.java b/java-client/src/main/java/co/elastic/clients/transport/endpoints/SimpleJsonEndpoint.java new file mode 100644 index 000000000..c97a03675 --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/transport/endpoints/SimpleJsonEndpoint.java @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.clients.transport.endpoints; + +import co.elastic.clients.elasticsearch._types.ErrorResponse; +import co.elastic.clients.json.JsonpDeserializer; +import co.elastic.clients.transport.JsonEndpoint; + +import java.util.Map; +import java.util.function.Function; + +public class SimpleJsonEndpoint extends SimpleEndpoint + implements JsonEndpoint { + + public SimpleJsonEndpoint( + String id, + Function method, + Function requestUrl, + Function> queryParameters, + Function> headers, + boolean hasRequestBody, + JsonpDeserializer responseParser + ) { + super(id, method, requestUrl, queryParameters, headers, hasRequestBody, responseParser); + } +} diff --git a/java-client/src/main/java/co/elastic/clients/transport/rest_client/HttpClientBinaryResponse.java b/java-client/src/main/java/co/elastic/clients/transport/rest_client/HttpClientBinaryResponse.java new file mode 100644 index 000000000..27e4dad15 --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/transport/rest_client/HttpClientBinaryResponse.java @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.clients.transport.rest_client; + +import co.elastic.clients.transport.endpoints.BinaryResponse; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.util.EntityUtils; + +import java.io.IOException; +import java.io.InputStream; + +class HttpClientBinaryResponse implements BinaryResponse { + private final HttpEntity entity; + private boolean consumed = false; + + HttpClientBinaryResponse(HttpEntity entity) { + this.entity = entity; + } + + @Override + public String contentType() { + Header h = entity.getContentType(); + return h == null ? "application/octet-stream" : h.getValue(); + } + + @Override + public long contentLength() { + long len = entity.getContentLength(); + return len < 0 ? -1 : entity.getContentLength(); + } + + @Override + public InputStream content() throws IOException { + if (consumed) { + throw new IllegalStateException("Response content has already been consumed"); + } + consumed = true; + return entity.getContent(); + } + + @Override + public void close() throws IOException { + consumed = true; + EntityUtils.consume(entity); + } +} diff --git a/java-client/src/main/java/co/elastic/clients/transport/rest_client/RestClientTransport.java b/java-client/src/main/java/co/elastic/clients/transport/rest_client/RestClientTransport.java index 7b8682129..7b198d854 100644 --- a/java-client/src/main/java/co/elastic/clients/transport/rest_client/RestClientTransport.java +++ b/java-client/src/main/java/co/elastic/clients/transport/rest_client/RestClientTransport.java @@ -27,6 +27,7 @@ import co.elastic.clients.transport.JsonEndpoint; import co.elastic.clients.transport.TransportException; import co.elastic.clients.transport.Version; +import co.elastic.clients.transport.endpoints.BinaryEndpoint; import co.elastic.clients.transport.endpoints.BooleanEndpoint; import co.elastic.clients.transport.endpoints.BooleanResponse; import co.elastic.clients.transport.ElasticsearchTransport; @@ -246,8 +247,8 @@ private ResponseT getHighLevelResponse( Endpoint endpoint ) throws IOException { + int statusCode = clientResp.getStatusLine().getStatusCode(); try { - int statusCode = clientResp.getStatusLine().getStatusCode(); if (statusCode == 200) { checkProductHeader(clientResp, endpoint); @@ -294,7 +295,10 @@ private ResponseT getHighLevelResponse( return decodeResponse(statusCode, clientResp.getEntity(), clientResp, endpoint); } } finally { - EntityUtils.consume(clientResp.getEntity()); + // Consume the entity unless this is a successful binary endpoint, where the user must consume the entity + if (!(endpoint instanceof BinaryEndpoint && !endpoint.isError(statusCode))) { + EntityUtils.consume(clientResp.getEntity()); + } } } @@ -302,16 +306,9 @@ private ResponseT decodeResponse( int statusCode, @Nullable HttpEntity entity, Response clientResp, Endpoint endpoint ) throws IOException { - if (endpoint instanceof BooleanEndpoint) { - BooleanEndpoint bep = (BooleanEndpoint) endpoint; - - @SuppressWarnings("unchecked") - ResponseT response = (ResponseT) new BooleanResponse(bep.getResult(statusCode)); - return response; - - } else if (endpoint instanceof JsonEndpoint){ + if (endpoint instanceof JsonEndpoint) { @SuppressWarnings("unchecked") - JsonEndpoint jsonEndpoint = (JsonEndpoint)endpoint; + JsonEndpoint jsonEndpoint = (JsonEndpoint) endpoint; // Successful response ResponseT response = null; JsonpDeserializer responseParser = jsonEndpoint.responseDeserializer(); @@ -326,9 +323,26 @@ private ResponseT decodeResponse( InputStream content = entity.getContent(); try (JsonParser parser = mapper.jsonProvider().createParser(content)) { response = responseParser.deserialize(parser, mapper); - }; + } + ; } return response; + + } else if(endpoint instanceof BooleanEndpoint) { + BooleanEndpoint bep = (BooleanEndpoint) endpoint; + + @SuppressWarnings("unchecked") + ResponseT response = (ResponseT) new BooleanResponse(bep.getResult(statusCode)); + return response; + + + } else if (endpoint instanceof BinaryEndpoint) { + BinaryEndpoint bep = (BinaryEndpoint) endpoint; + + @SuppressWarnings("unchecked") + ResponseT response = (ResponseT) new HttpClientBinaryResponse(entity); + return response; + } else { throw new TransportException("Unhandled endpoint type: '" + endpoint.getClass().getName() + "'", endpoint.id()); }