diff --git a/.ci/Dockerfile b/.ci/Dockerfile index 9ec81b90b..1fcb0eeff 100644 --- a/.ci/Dockerfile +++ b/.ci/Dockerfile @@ -34,6 +34,7 @@ COPY LICENSE.txt NOTICE.txt ./ # Prefetch dependencies COPY build.gradle.kts settings.gradle.kts ./ COPY buildSrc ./buildSrc/ +COPY config ./config/ COPY java-client/build.gradle.kts ./java-client/ RUN ./gradlew resolveDependencies diff --git a/.github/workflows/checkstyle.yml b/.github/workflows/checkstyle.yml index 9af5602ef..89c3972f4 100644 --- a/.github/workflows/checkstyle.yml +++ b/.github/workflows/checkstyle.yml @@ -4,14 +4,17 @@ on: [pull_request] jobs: build: runs-on: ubuntu-latest + strategy: + matrix: + java-version: [ 11 ] steps: - uses: actions/checkout@v2 - - name: Set up JDK 11 + - name: Set up JDK ${{ matrix.java-version }} uses: actions/setup-java@v2 with: - java-version: '11' - distribution: 'adopt' + java-version: ${{ matrix.java-version }} + distribution: 'temurin' - name: Check style and license headers run: | diff --git a/README.md b/README.md index 898f42831..bcfd92bbc 100644 --- a/README.md +++ b/README.md @@ -22,16 +22,11 @@ The `docs/design` folder contains records of the major decisions in the design o ### Installing the library -While it's a work in progress, snapshots of this library are published to a Maven repository hosted on [GitHub Packages](https://github.com/elastic/elasticsearch-java/packages/). To access it [you need a personal access token](https://github.com/settings/tokens) on your GitHub account that has the `read:packages` permission. This token should then be added to `~/.gradle/gradle.properties`: - -```properties -ESJavaGithubPackagesUsername=YOUR_GITHUB_USERNAME -ESJavaGithubPackagesPassword=YOUR_GITHUB_TOKEN -``` +This library requires at least Java 8. Along with this library, you also need a JSON/object mapping library. `elasticsearch-java` has built-in support for [Jackson](https://github.com/FasterXML/jackson) and [JSON-B](http://json-b.net/) implementations such as [Eclipse Yasson](https://github.com/eclipse-ee4j/yasson). -This library requires at least Java 8. +While it's a work in progress, snapshots of this library are published to Elastic's snapshot repository. Snapshots are currently available for the upcoming version 7.15.0, built from the `7.x` branch. Gradle project (Groovy flavor) setup using Jackson: @@ -39,52 +34,37 @@ Gradle project (Groovy flavor) setup using Jackson: repositories { mavenCentral() maven { - name = "ESJavaGithubPackages" - url = uri("https://maven.pkg.github.com/elastic/elasticsearch-java") - credentials(PasswordCredentials) + name = "Elastic-Snapshots" + url = uri("https://snapshots.elastic.co/maven") } } dependencies { - implementation 'co.elastic.clients:elasticsearch-java:8.0.0-SNAPSHOT' + implementation 'co.elastic.clients:elasticsearch-java:7.15.0-SNAPSHOT' implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.3' } ``` -If you are using Maven, you need to add the credentials in your `~/.m2/settings.xml`: - -```xml - - - - ESJavaGithubPackages - YOUR_GITHUB_USERNAME - YOUR_GITHUB_TOKEN - - - -``` - In the `pom.xml` for your project add the following repository definition and dependencies: ```xml - + - ESJavaGithubPackages - https://maven.pkg.github.com/elastic/elasticsearch-java + Elastic-Snapshots + https://snapshots.elastic.co/maven true - + co.elastic.clients elasticsearch-java - 8.0.0-SNAPSHOT + 7.15.0-SNAPSHOT com.fasterxml.jackson.core @@ -92,7 +72,7 @@ In the `pom.xml` for your project add the following repository definition and de 2.12.3 - + ``` @@ -125,14 +105,14 @@ if (search.hits().hits().isEmpty()) { ## Compatibility -The `main` branch targets the upcoming Elasticsearch 8.0. Support is still incomplete as the API code is generated from the [Elasticsearch Specification](https://github.com/elastic/elasticsearch-specification) that is also still a work in progress. +The `main` branch targets the next major release (8.0) and the `7.x` branch targets the next minor release. Support is still incomplete as the API code is generated from the [Elasticsearch Specification](https://github.com/elastic/elasticsearch-specification) that is also still a work in progress. -As the work on the specification comes to completion, an additional `7.x` branch will provide support for the corresponding versions of Elasticsearch. +The Elasticsearch Java client is forward compatible; meaning that the client supports communicating with greater minor versions of Elasticsearch. Elastic language clients are also backwards compatible with lesser supported minor Elasticsearch versions. ## Current status While not complete, this library is already fairly usable. What's missing falls in two main categories, related to the [Elasticsearch specification](https://github.com/elastic/elasticsearch-specification): -* incomplete support for some data types used in specification (e.g. unions). Until they have been implemented in the code generator, they are represented as raw `JsonValue` objects. +* incomplete support for some data types used in specification (e.g. unions). Until they have been implemented in the code generator, they are represented as raw `JsonValue` objects. * incomplete APIs: as the API specification is still incomplete, so are their implementations in this library since their code is entirely generated from the spec. ## Contributing diff --git a/build.gradle.kts b/build.gradle.kts index 6855300ee..d4d8903b9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,7 +19,9 @@ allprojects { group = "co.elastic.clients" - version = System.getenv("VERSION") ?: "8.0.0-SNAPSHOT" + // Release manager provides a $VERSION. If not present, it's a local or CI snapshot build. + version = System.getenv("VERSION") ?: + (File(project.rootDir, "config/version.txt").readText().trim() + "-SNAPSHOT") repositories { mavenCentral() diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 9b768e9ac..96ccb95f8 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -39,12 +39,14 @@ + type Indices = IndexName | IndexName[] + assertGetterType(List.class, SearchRequest.class, "index"); + } + + @Test + public void testSpanGapQuery() { + // Hand-written class + + SpanGapQuery q = new SpanGapQuery.Builder() + .field("a-field") + .spanWidth(12) + .build(); + + q = checkJsonRoundtrip(q, "{\"a-field\":12}"); + + assertEquals("a-field", q.field()); + assertEquals(12, q.spanWidth()); + } + + @Test + public void testErrorResponse() { + String json = "{\"error\":{\"root_cause\":[{\"type\":\"index_not_found_exception\",\"reason\":\"no such index [doesnotexist]\"," + + "\"resource.type\":\"index_expression\",\"resource.id\":\"doesnotexist\",\"index_uuid\":\"_na_\"," + + "\"index\":\"doesnotexist\"}],\"type\":\"index_not_found_exception\",\"reason\":\"no such index [doesnotexist]\",\"resource" + + ".type\":\"index_expression\",\"resource.id\":\"doesnotexist\",\"index_uuid\":\"_na_\",\"index\":\"doesnotexist\"}," + + "\"status\":404}"; + + System.out.println(json); + + ElasticsearchError error = fromJson(json, ElasticsearchError.class); + + assertEquals(404, error.status()); + assertEquals("index_not_found_exception", error.error().asJsonObject().getString("type")); + } +} diff --git a/java-client/src/test/java/co/elastic/clients/elasticsearch/model/ModelTestCase.java b/java-client/src/test/java/co/elastic/clients/elasticsearch/model/ModelTestCase.java new file mode 100644 index 000000000..c3b517a85 --- /dev/null +++ b/java-client/src/test/java/co/elastic/clients/elasticsearch/model/ModelTestCase.java @@ -0,0 +1,113 @@ +/* + * 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.elasticsearch.model; + +import co.elastic.clients.json.JsonpMapper; +import co.elastic.clients.json.jackson.JacksonJsonpMapper; +import co.elastic.clients.json.jsonb.JsonbJsonpMapper; +import jakarta.json.spi.JsonProvider; +import jakarta.json.stream.JsonGenerator; +import jakarta.json.stream.JsonParser; +import org.junit.Assert; + +import java.io.StringReader; +import java.io.StringWriter; +import java.lang.reflect.Method; +import java.util.Random; + +/** + * Base class for tests that encode/decode json + */ +public abstract class ModelTestCase extends Assert { + + // Same value for all tests in a test run + private static final int RAND = new Random().nextInt(100); + + protected final JsonpMapper mapper; + + private JsonpMapper setupMapper(int rand) { + // Randomly choose json-b or jackson + if (rand % 2 == 0) {; + System.out.println("Using a JsonB mapper (rand = " + rand + ")."); + return new JsonbJsonpMapper() { + @Override + public boolean ignoreUnknownFields() { + return false; + } + }; + } else { + System.out.println("Using a Jackson mapper (rand = " + rand + ")."); + return new JacksonJsonpMapper() { + @Override + public boolean ignoreUnknownFields() { + return false; + } + }; + } + } + + protected ModelTestCase() { + this(RAND); + } + + protected ModelTestCase(int rand) { + mapper = setupMapper(rand); + } + + protected String toJson(T value) { + return toJson(value, mapper); + } + + public static String toJson(T value, JsonpMapper mapper) { + StringWriter sw = new StringWriter(); + JsonProvider provider = mapper.jsonProvider(); + JsonGenerator generator = provider.createGenerator(sw); + mapper.serialize(value, generator); + generator.close(); + return sw.toString(); + } + + public static T fromJson(String json, Class clazz, JsonpMapper mapper) { + JsonParser parser = mapper.jsonProvider().createParser(new StringReader(json)); + return mapper.deserialize(parser, clazz); + } + + protected T fromJson(String json, Class clazz) { + return fromJson(json, clazz, mapper); + } + + @SuppressWarnings("unchecked") + protected T checkJsonRoundtrip(T value, String expectedJson) { + assertEquals(expectedJson, toJson(value)); + return fromJson(expectedJson, (Class)value.getClass()); + } + + public static void assertGetterType(Class expected, Class clazz, String name) { + Method method; + try { + method = clazz.getMethod(name); + } catch (NoSuchMethodException e) { + Assert.fail("Getter '" + clazz.getName() + "." + name + "' doesn't exist"); + return; + } + + Assert.assertSame(expected, method.getReturnType()); + } +} diff --git a/java-client/src/test/java/co/elastic/clients/elasticsearch/model/RequestEncodingTest.java b/java-client/src/test/java/co/elastic/clients/elasticsearch/model/RequestEncodingTest.java new file mode 100644 index 000000000..5bd3f4f38 --- /dev/null +++ b/java-client/src/test/java/co/elastic/clients/elasticsearch/model/RequestEncodingTest.java @@ -0,0 +1,59 @@ +/* + * 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.elasticsearch.model; + +import co.elastic.clients.elasticsearch._core.SearchRequest; +import co.elastic.clients.elasticsearch._types.query_dsl.Query; +import co.elastic.clients.json.jsonb.JsonbJsonpMapper; +import org.junit.Test; + +public class RequestEncodingTest extends ModelTestCase { + + @Test + public void testParametersNotInJson() { + + // This checks that path parameters ("q") are not serialized as json + // and variant containers ser/deser + + SearchRequest request = new SearchRequest.Builder() + .q("blah") + .query(b1 -> b1 + .type(b2 -> b2 + .value("foo")) + ) + .putAggs("myagg", b1 -> b1 + .avg(b2 -> b2 + .field("foo")) + ) + .build(); + + JsonbJsonpMapper mapper = new JsonbJsonpMapper(); + String str = toJson(request, mapper); + + assertEquals("{\"aggs\":{\"myagg\":{\"avg\":{\"field\":\"foo\"}}},\"query\":{\"type\":{\"value\":\"foo\"}}}", str); + + request = fromJson(str, SearchRequest.class, mapper); + + assertTrue(request.query()._is(Query.TYPE)); + assertEquals("foo", request.query().type().value()); + assertNull(request.q()); + + } +} diff --git a/java-client/src/test/java/co/elastic/clients/elasticsearch/model/SerializationTest.java b/java-client/src/test/java/co/elastic/clients/elasticsearch/model/SerializationTest.java new file mode 100644 index 000000000..97ffc3588 --- /dev/null +++ b/java-client/src/test/java/co/elastic/clients/elasticsearch/model/SerializationTest.java @@ -0,0 +1,132 @@ +/* + * 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.elasticsearch.model; + +import co.elastic.clients.elasticsearch._core.GetSourceResponse; +import co.elastic.clients.elasticsearch.cat.NodesResponse; +import co.elastic.clients.json.JsonpDeserializable; +import co.elastic.clients.json.JsonpDeserializer; +import co.elastic.clients.json.JsonpMapperBase; +import co.elastic.clients.json.JsonpUtils; +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ClassInfoList; +import io.github.classgraph.ScanResult; +import jakarta.json.Json; +import jakarta.json.JsonValue; +import jakarta.json.stream.JsonParser; +import jakarta.json.stream.JsonParsingException; +import org.junit.Test; + +import java.io.StringReader; + +public class SerializationTest extends ModelTestCase { + + @Test + public void loadAllDeserializers() throws Exception { + + ScanResult scan = new ClassGraph() + .acceptPackages("co.elastic.clients") + .enableAnnotationInfo() + .enableFieldInfo() + .scan(); + + ClassInfoList withAnnotation = scan.getClassesWithAnnotation(JsonpDeserializable.class.getName()); + + assertFalse("No JsonpDeserializable classes", withAnnotation.isEmpty()); + + for (ClassInfo info: withAnnotation) { + Class clazz = Class.forName(info.getName()); + JsonpDeserializer deserializer = JsonpMapperBase.findDeserializer(clazz); + assertNotNull(deserializer); + + // Deserialize something dummy to resolve lazy deserializers + JsonParser parser = mapper.jsonProvider().createParser(new StringReader("-")); + assertThrows(JsonParsingException.class, () -> deserializer.deserialize(parser, mapper)); + } + + // Check that all classes that have a _DESERIALIZER field also have the annotation + ClassInfoList withDeserializer = scan.getAllClasses().filter((c) -> c.hasDeclaredField("_DESERIALIZER")); + assertFalse("No classes with a _DESERIALIZER field", withDeserializer.isEmpty()); + +// Disabled for now, empty response classes still need a deserializer +// Set annotationNames = withAnnotation.stream().map(c -> c.getName()).collect(Collectors.toSet()); +// Set withFieldNames = withDeserializer.stream().map(c -> c.getName()).collect(Collectors.toSet()); +// +// withFieldNames.removeAll(annotationNames); +// +// assertFalse("Some classes with the field but not the annotation: " + withFieldNames, !withFieldNames.isEmpty()); + + } + + @Test + public void testArrayValueBody() { + + NodesResponse nr = new NodesResponse(_0 -> _0 + .addValueBody(_1 -> _1 + .bulkTotalOperations("1") + ) + .addValueBody(_1 -> _1 + .bulkTotalOperations("2") + ) + ); + + checkJsonRoundtrip(nr, "[{\"bulk.total_operations\":\"1\"},{\"bulk.total_operations\":\"2\"}]"); + + assertEquals(2, nr.valueBody().size()); + assertEquals("1", nr.valueBody().get(0).bulkTotalOperations()); + assertEquals("2", nr.valueBody().get(1).bulkTotalOperations()); + } + + @Test + public void testGenericValueBody() { + + GetSourceResponse r = new GetSourceResponse<>(_0 -> _0 + .valueBody("The value") + ); + + String json = toJson(r); + assertEquals("\"The value\"", json); + + JsonpDeserializer> deserializer = + GetSourceResponse.createGetSourceResponseDeserializer(JsonpDeserializer.stringDeserializer()); + + r = deserializer.deserialize(mapper.jsonProvider().createParser(new StringReader(json)), mapper); + + assertEquals("The value", r.valueBody()); + + } + + @Test + public void testJsonpValuesToString() { + + assertEquals("foo", JsonpUtils.toString(Json.createValue("foo"))); + assertEquals("42", JsonpUtils.toString(Json.createValue(42))); + assertEquals("42.1337", JsonpUtils.toString(Json.createValue(42.1337))); + assertEquals("true", JsonpUtils.toString(JsonValue.TRUE)); + assertEquals("false", JsonpUtils.toString(JsonValue.FALSE)); + assertEquals("null", JsonpUtils.toString(JsonValue.NULL)); + assertEquals("a,b,c", JsonpUtils.toString(Json.createArrayBuilder().add("a").add("b").add("c").build())); + + assertThrows(IllegalArgumentException.class, () -> { + JsonpUtils.toString(Json.createObjectBuilder().build()); + }); + } +} diff --git a/java-client/src/test/java/co/elastic/clients/elasticsearch/model/VariantsTest.java b/java-client/src/test/java/co/elastic/clients/elasticsearch/model/VariantsTest.java new file mode 100644 index 000000000..c37dffffa --- /dev/null +++ b/java-client/src/test/java/co/elastic/clients/elasticsearch/model/VariantsTest.java @@ -0,0 +1,96 @@ +/* + * 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.elasticsearch.model; + +import co.elastic.clients.elasticsearch._types.mapping.Property; +import co.elastic.clients.elasticsearch._types.query_dsl.Query; +import org.junit.Test; + +public class VariantsTest extends ModelTestCase { + + @Test + public void testNested() { + // nested containers: query > intervals > interval + // intervals is a single key dictionary + // query has container properties + + Query q = new Query(_0 -> _0 + .intervals(_1 -> _1 + .queryName("my-query") + .field("a_field") + .anyOf(_2 -> _2 + .addIntervals(_3 -> _3 + .match(_4 -> _4 + .query("match-query") + .analyzer("lowercase") + ) + ) + ) + ) + ); + + assertEquals(Query.INTERVALS, q._type()); + assertNotNull(q.intervals()); + assertEquals("a_field", q.intervals().field()); + assertEquals(1, q.intervals().anyOf().intervals().size()); + assertEquals("lowercase", q.intervals().anyOf().intervals().get(0).match().analyzer()); + + String json = toJson(q); + + assertEquals("{\"intervals\":{\"a_field\":{\"_name\":\"my-query\"," + + "\"any_of\":{\"intervals\":[{\"match\":{\"analyzer\":\"lowercase\",\"query\":\"match-query\"}}]}}}}", json); + + Query q2 = fromJson(json, Query.class); + assertEquals(json, toJson(q2)); + + assertEquals(Query.INTERVALS, q2._type()); + assertNotNull(q2.intervals()); + assertEquals("a_field", q2.intervals().field()); + assertEquals(1, q2.intervals().anyOf().intervals().size()); + assertEquals("lowercase", q2.intervals().anyOf().intervals().get(0).match().analyzer()); + + } + + @Test + public void testInternalTag() { + String expected = "{\"type\":\"ip\",\"fields\":{\"a-field\":{\"type\":\"float\",\"coerce\":true}},\"boost\":1" + + ".0,\"index\":true}"; + + Property p = new Property(_0 -> _0 + .ip(_1 -> _1 + .index(true) + .boost(1.0) + .fields("a-field", _2 -> _2 + .float_(_3 -> _3 + .coerce(true) + ) + ) + ) + ); + + assertEquals(expected, toJson(p)); + + Property property = fromJson(expected, Property.class); + assertTrue(property.ip().index()); + assertEquals(1.0, property.ip().boost().doubleValue(), 0.09); + + assertTrue(property.ip().fields().get("a-field").float_().coerce()); + } +} diff --git a/java-client/src/test/java/co/elastic/clients/elasticsearch/model/package-info.java b/java-client/src/test/java/co/elastic/clients/elasticsearch/model/package-info.java new file mode 100644 index 000000000..e58c3560b --- /dev/null +++ b/java-client/src/test/java/co/elastic/clients/elasticsearch/model/package-info.java @@ -0,0 +1,24 @@ +/* + * 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. + */ + +/** + * Tests that verify correct types and serialization/deserialization of the API specification model using API + * structures that cover the various model features + */ +package co.elastic.clients.elasticsearch.model;