Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,28 @@ jobs:
- name: Tests
run: |
npm run test
# only added temporarily to run integration tests on Java
integration-tests-java:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup node.js
uses: actions/setup-node@v4
- name: Install
run: |
npm install
- name: Build
run: |
npm run build
- name: Install Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
- name: Prepare for testing
run: |
npm run
npm run test:setup-java
- name: Integration tests
run: |
JAVA_ES_REQUEST_CONVERTER_JAR=/home/runner/work/request-converter/request-converter/tests/integration/.java-request-converter/build/libs/java-request-converter-1.0.0.jar npm run test:integration-java
24 changes: 24 additions & 0 deletions .github/workflows/daily.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,27 @@ jobs:
- name: Integration tests
run: |
npm run test:integration-ruby
integration-tests-java:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup node.js
uses: actions/setup-node@v4
- name: Install
run: |
npm install
- name: Build
run: |
npm run build
- name: Install Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
- name: Prepare for testing
run: |
npm run
npm run test:setup-java
- name: Integration tests
run: |
JAVA_ES_REQUEST_CONVERTER_JAR=/home/runner/work/request-converter/request-converter/tests/integration/.java-request-converter/build/libs/java-request-converter-1.0.0.jar npm run test:integration-java
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@
"test:setup-javascript": "./tests/integration/run-javascript.sh",
"test:setup-php": "./tests/integration/run-php.sh",
"test:setup-ruby": "./tests/integration/run-ruby.sh",
"test:setup-java": "./tests/integration/run-java.sh",
"test:integration": "jest tests/integration",
"test:integration-curl": "./scripts/test-format.sh curl",
"test:integration-python": "./scripts/test-format.sh python",
"test:integration-javascript": "./scripts/test-format.sh javascript",
"test:integration-php": "./scripts/test-format.sh php",
"test:integration-ruby": "./scripts/test-format.sh ruby",
"test:integration-java": "./scripts/test-format.sh java",
"test:example": "./scripts/test-example.sh",
"fix": "npm run fix:lint && npm run fix:prettier",
"fix:lint": "eslint src tests --fix --ignore-pattern tests/wasm/",
Expand Down
16 changes: 14 additions & 2 deletions src/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { CurlExporter } from "./exporters/curl";
import { JavaScriptExporter } from "./exporters/javascript";
import { PHPExporter } from "./exporters/php";
import { RubyExporter } from "./exporters/ruby";
import { JavaExporter } from "./exporters/java";
import util from "util";

const isBrowser = typeof window !== "undefined";
Expand Down Expand Up @@ -35,6 +36,7 @@ export type ConvertOptions = {
* This interface defines the structure of a language exporter.
*/
export interface FormatExporter {
available?(): boolean;
check(requests: ParsedRequest[]): Promise<boolean>;
convert(requests: ParsedRequest[], options: ConvertOptions): Promise<string>;
}
Expand All @@ -44,9 +46,10 @@ const EXPORTERS: Record<string, FormatExporter> = {
php: new PHPExporter(),
python: new PythonExporter(),
ruby: new RubyExporter(),
java: new JavaExporter(),
curl: new CurlExporter(),
};
const LANGUAGES = ["JavaScript", "PHP", "Python", "Ruby", "curl"];
const LANGUAGES = ["JavaScript", "PHP", "Python", "Ruby", "Java", "curl"];

/**
* Return the list of available export formats.
Expand All @@ -55,7 +58,16 @@ const LANGUAGES = ["JavaScript", "PHP", "Python", "Ruby", "curl"];
* to use in the `convertRequests()` function.
*/
export function listFormats(): string[] {
return LANGUAGES;
return LANGUAGES.filter((lang) => {
const exporter = EXPORTERS[lang.toLowerCase()];
if (!exporter) {
return false;
}
if (!exporter.available) {
return true;
}
return exporter.available();
});
}

/**
Expand Down
88 changes: 88 additions & 0 deletions src/exporters/java.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import childProcess from "child_process";
import { FormatExporter, ConvertOptions } from "../convert";
import { ParsedRequest, JSONValue } from "../parse";
import { Request } from "../metamodel";
import util from "util";

const isBrowser = typeof window !== "undefined";
const execAsync = !isBrowser ? util.promisify(childProcess.exec) : undefined;

type JavaRequest = {
api?: string;
params: Record<string, string | undefined>;
query?: Record<string, string>;
body: JSONValue;
};

function getCodeGenParamNames(
params: Record<string, string | undefined>,
request: Request,
) {
for (const [key, value] of Object.entries(params)) {
if (request?.path) {
for (const prop of request.path) {
if (prop.name === key && prop.codegenName !== undefined) {
delete params[key];
params[prop.codegenName] = value;
}
}
}
}
return params;
}

export class JavaExporter implements FormatExporter {
available(): boolean {
return !isBrowser && !!process.env.JAVA_ES_REQUEST_CONVERTER_JAR;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
async check(requests: ParsedRequest[]): Promise<boolean> {
// only return true if all requests are for Elasticsearch
return requests
.map((req) => req.service == "es")
.reduce((prev, curr) => prev && curr, true);
}

async convert(
requests: ParsedRequest[],
options: ConvertOptions,
): Promise<string> {
const javaRequests: JavaRequest[] = [];
for (const request of requests) {
if (request.request) {
const correctParams = getCodeGenParamNames(
request.params,
request.request,
);
const body = request.body ?? {};

const javaRequest: JavaRequest = {
api: request.api,
params: correctParams,
query: request.query,
body: body,
};
javaRequests.push(javaRequest);
}
}

// escape single quotes that may appear in the payload
// (only for bash-style shells for now)
const req = JSON.stringify(javaRequests).replaceAll("'", "'\"'\"'");
// other arguments to the Java converter
const complete = options.complete ? "true" : "false";
const url = options.elasticsearchUrl ?? "";

if (execAsync === undefined) {
throw new Error("Cannot use exec()");
}
const { stdout, stderr } = await execAsync(
`java -jar ${process.env.JAVA_ES_REQUEST_CONVERTER_JAR} '${req}' '${complete}' '${url}'`,
);
if (!stdout) {
throw new Error(`Could not invoke exporter: ${stderr}`);
}
return stdout;
}
}
1 change: 1 addition & 0 deletions src/java-caller.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module "java-caller";
1 change: 1 addition & 0 deletions tests/integration/convert.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const TEST_FORMATS: Record<string, string> = {
php: "php",
curl: "sh",
ruby: "rb",
java: "java",
};

interface SchemaExample {
Expand Down
109 changes: 109 additions & 0 deletions tests/integration/java-app/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>app</artifactId>
<version>1.0-SNAPSHOT</version>

<name>app</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>17</maven.compiler.release>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>5.11.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<!-- Optionally: parameterized tests support -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>9.2.0</version>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<version>9.2.0</version>
<version>9.2.2</version>

Copy link
Contributor Author

@miguelgrinberg miguelgrinberg Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@l-trotta Here what I do for all the other languages is use a cloned copy of the source code. Because the CI runs on the main version of the spec, so it is more correct to run it on the unreleased clients that will eventually be 9.3 then on the released 9.2.

In the run-java.sh script I'm cloning the java client, but I haven't looked into how to include it here as a locally installed dependency. I hope it is possible?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it, ignore the change then. to get it locally the dependency must have been previously published to the local .m2 repo running mvn install in the project folder, then I think maven will try and get it by default if it's available, no modifications needed to the pom.

</dependency>
</dependencies>

<build>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.4.0</version>
</plugin>
<!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.1</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.3.0</version>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>3.1.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>3.1.2</version>
</plugin>
<!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
<plugin>
<artifactId>maven-site-plugin</artifactId>
<version>3.12.1</version>
</plugin>
<plugin>
<artifactId>maven-project-info-reports-plugin</artifactId>
<version>3.6.1</version>
</plugin>
<plugin>
Comment on lines +53 to +93
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are all of these used somewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this is a template pom.xml file that I generated a while ago, I believe using maven. We can prune all the stuff that isn't needed.

<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>org.example.App</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
36 changes: 36 additions & 0 deletions tests/integration/run-java.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/bin/bash
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
CURRENT_DIR=$(pwd)
BRANCH=$(jq -r .version package.json | grep -Eo "^[0-9]+\.[0-9]+")

if [[ "$1" == "" ]]; then
# the `test:setup` command runs this script without arguments to initialize
# the environment, so we first delete any previous one
rm -rf $SCRIPT_DIR/.java-request-converter
rm -rf $SCRIPT_DIR/.java
fi

if [[ ! -d $SCRIPT_DIR/.java-es-request-converter ]]; then
git clone https://github.com/elastic/java-request-converter $SCRIPT_DIR/.java-request-converter
cd $SCRIPT_DIR/.java-request-converter
./gradlew jar
cd $CURRENT_DIR
fi

if [[ ! -d $SCRIPT_DIR/.java ]]; then
mkdir $SCRIPT_DIR/.java
echo "Installing from branch $BRANCH."
git clone -b "$BRANCH" --depth=1 "https://github.com/elastic/elasticsearch-java.git" $SCRIPT_DIR/.java ||
(echo "Branch $BRANCH not found. Cloning main branch." &&
git clone -b "main" --depth=1 "https://github.com/elastic/elasticsearch-java.git" $SCRIPT_DIR/.java)
fi

if [[ "$1" != "" ]]; then
cd $SCRIPT_DIR/java-app
mkdir -p src/main/java/org/example
cp $CURRENT_DIR/$1 src/main/java/org/example/App.java
mvn clean compile assembly:single
if [[ "$?" == "0" ]]; then
java -jar target/app-1.0-SNAPSHOT-jar-with-dependencies.jar || true
fi
fi
Loading