Java bindings (#147)

This commit is contained in:
Burak
2024-02-18 17:21:58 +01:00
committed by GitHub
parent 8d282f1ffd
commit bdb2aba596
10 changed files with 677 additions and 1 deletions

1
bindings/java/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
target/

17
bindings/java/Cargo.toml Normal file
View File

@@ -0,0 +1,17 @@
[package]
name = "regorus-java"
version = "0.1.0"
edition = "2021"
repository = "https://github.com/microsoft/regorus/bindings/java"
description = "Java bindings for Regorus - a fast, lightweight Rego interpreter written in Rust"
keywords = ["interpreter", "opa", "policy-as-code", "rego"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib"]
[dependencies]
anyhow = "1.0.79"
serde_json = "1.0.112"
jni = "0.21.1"
regorus = { path = "../.." }

49
bindings/java/README.md Normal file
View File

@@ -0,0 +1,49 @@
# Regorus Java
**Regorus** is
- *Rego*-*Rus(t)* - A fast, light-weight [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/)
interpreter written in Rust.
- *Rigorous* - A rigorous enforcer of well-defined Rego semantics.
See main [Regorus page](https://github.com/microsoft/regorus) for more details about the project.
Regorus can be used in Java via `com.microsoft.regorus` package. (It is not yet available in Maven Central, but can be manually built.)
## Building
You can build this binding using [Maven](https://maven.apache.org/):
```shell
$ mvn package
$ file target/regorus-java-0.0.1*
target/regorus-java-0.0.1-osx-aarch_64.jar: Zip archive data, at least v1.0 to extract, compression method=deflate
target/regorus-java-0.0.1.jar: Zip archive data, at least v1.0 to extract, compression method=deflate
```
## Usage
```java
import com.microsoft.regorus.Engine;
public class Test {
public static void main(String[] args) {
try (Engine engine = new Engine()) {
engine.pubAddPolicy(
"hello.rego",
"package test\nmessage = concat(\", \", [input.message, data.message])"
);
engine.pubAddDataJson("{\"message\":\"World!\"}");
engine.pubSetInputJson("{\"message\":\"Hello\"}");
String resJson = engine.pubEvalQuery("data.test.message");
System.out.println(resJson);
}
}
}
```
and run it with:
```shell
$ java -cp target/regorus-java-0.0.1.jar:target/regorus-java-0.0.1-osx-aarch_64.jar Test.java
{"result":[{"expressions":[{"value":"Hello, World!","text":"data.test.message","location":{"row":1,"col":1}}]}]}
```

View File

@@ -0,0 +1,85 @@
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_microsoft_regorus_Engine */
#ifndef _Included_com_microsoft_regorus_Engine
#define _Included_com_microsoft_regorus_Engine
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_microsoft_regorus_Engine
* Method: newEngine
* Signature: ()J
*/
JNIEXPORT jlong JNICALL Java_com_microsoft_regorus_Engine_newEngine
(JNIEnv *, jclass);
/*
* Class: com_microsoft_regorus_Engine
* Method: addPolicy
* Signature: (JLjava/lang/String;Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_com_microsoft_regorus_Engine_addPolicy
(JNIEnv *, jclass, jlong, jstring, jstring);
/*
* Class: com_microsoft_regorus_Engine
* Method: addPolicyFromFile
* Signature: (JLjava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_com_microsoft_regorus_Engine_addPolicyFromFile
(JNIEnv *, jclass, jlong, jstring);
/*
* Class: com_microsoft_regorus_Engine
* Method: addDataJson
* Signature: (JLjava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_com_microsoft_regorus_Engine_addDataJson
(JNIEnv *, jclass, jlong, jstring);
/*
* Class: com_microsoft_regorus_Engine
* Method: addDataJsonFromFile
* Signature: (JLjava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_com_microsoft_regorus_Engine_addDataJsonFromFile
(JNIEnv *, jclass, jlong, jstring);
/*
* Class: com_microsoft_regorus_Engine
* Method: setInputJson
* Signature: (JLjava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_com_microsoft_regorus_Engine_setInputJson
(JNIEnv *, jclass, jlong, jstring);
/*
* Class: com_microsoft_regorus_Engine
* Method: setInputJsonFromFile
* Signature: (JLjava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_com_microsoft_regorus_Engine_setInputJsonFromFile
(JNIEnv *, jclass, jlong, jstring);
/*
* Class: com_microsoft_regorus_Engine
* Method: evalQuery
* Signature: (JLjava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_microsoft_regorus_Engine_evalQuery
(JNIEnv *, jclass, jlong, jstring);
/*
* Class: com_microsoft_regorus_Engine
* Method: destroyEngine
* Signature: (J)V
*/
JNIEXPORT void JNICALL Java_com_microsoft_regorus_Engine_destroyEngine
(JNIEnv *, jclass, jlong);
#ifdef __cplusplus
}
#endif
#endif

158
bindings/java/pom.xml Normal file
View File

@@ -0,0 +1,158 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) Microsoft Corporation.
Licensed under the MIT License.
-->
<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>com.microsoft.regorus</groupId>
<artifactId>regorus-java</artifactId>
<version>0.0.1</version>
<name>Regorus Java</name>
<description>Java bindings for Regorus - a fast, lightweight Rego interpreter written in Rust</description>
<url>https://github.com/microsoft/regorus/bindings/java</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<plugin>
<artifactId>maven-site-plugin</artifactId>
<version>3.7.1</version>
</plugin>
<plugin>
<artifactId>maven-project-info-reports-plugin</artifactId>
<version>3.0.0</version>
</plugin>
<!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
</plugins>
</pluginManagement>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.7.0</version>
</extension>
</extensions>
<!-- Adapted from https://github.com/apache/opendal/blob/93e5f65bbf30df2fed4bdd95bb0685c73c6418c2/bindings/java/pom.xml -->
<plugins>
<plugin>
<artifactId>exec-maven-plugin</artifactId>
<groupId>org.codehaus.mojo</groupId>
<version>3.1.0</version>
<executions>
<execution>
<id>compile-native-code</id>
<phase>compile</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>python3</executable>
<arguments>
<argument>${project.basedir}/tools/build.py</argument>
<argument>--classifier</argument>
<argument>${os.detected.classifier}</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<executions>
<!-- Generate the fallback JAR that does not contain the native library. -->
<execution>
<id>default-jar</id>
<configuration>
<excludes>
<exclude>native/**</exclude>
</excludes>
</configuration>
</execution>
<!-- Generate the JAR that contains the native library in it. -->
<execution>
<id>native-jar</id>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<classifier>${os.detected.classifier}</classifier>
<includes>
<include>native/**</include>
</includes>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<reporting>
<plugins>
<plugin>
<artifactId>maven-project-info-reports-plugin</artifactId>
</plugin>
</plugins>
</reporting>
</project>

150
bindings/java/src/lib.rs Normal file
View File

@@ -0,0 +1,150 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use anyhow::Result;
use jni::objects::{JClass, JObject, JString};
use jni::sys::{jlong, jstring};
use jni::JNIEnv;
use regorus::{Engine, Value};
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_newEngine(
_env: JNIEnv,
_class: JClass,
) -> jlong {
let engine = Engine::new();
Box::into_raw(Box::new(engine)) as jlong
}
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_addPolicy(
env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
path: JString,
rego: JString,
) {
let _ = throw_err(env, |env| {
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
let path: String = env.get_string(&path)?.into();
let rego: String = env.get_string(&rego)?.into();
engine.add_policy(path, rego)?;
Ok(())
});
}
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_addPolicyFromFile(
env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
path: JString,
) {
let _ = throw_err(env, |env| {
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
let path: String = env.get_string(&path)?.into();
engine.add_policy_from_file(path)?;
Ok(())
});
}
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_addDataJson(
env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
data: JString,
) {
let _ = throw_err(env, |env| {
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
let data: String = env.get_string(&data)?.into();
engine.add_data_json(&data)?;
Ok(())
});
}
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_addDataJsonFromFile(
env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
path: JString,
) {
let _ = throw_err(env, |env| {
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
let path: String = env.get_string(&path)?.into();
engine.add_data(Value::from_json_file(path)?)?;
Ok(())
});
}
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_setInputJson(
env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
input: JString,
) {
let _ = throw_err(env, |env| {
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
let input: String = env.get_string(&input)?.into();
engine.set_input_json(&input)?;
Ok(())
});
}
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_setInputJsonFromFile(
env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
path: JString,
) {
let _ = throw_err(env, |env| {
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
let path: String = env.get_string(&path)?.into();
engine.set_input(Value::from_json_file(&path)?);
Ok(())
});
}
#[no_mangle]
pub extern "system" fn Java_com_microsoft_regorus_Engine_evalQuery(
env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
query: JString,
) -> jstring {
let res = throw_err(env, |env| {
let engine = unsafe { &mut *(engine_ptr as *mut Engine) };
let query: String = env.get_string(&query)?.into();
let results = engine.eval_query(query, false)?;
let output = env.new_string(serde_json::to_string(&results)?)?;
Ok(output.into_raw())
});
match res {
Ok(val) => val,
Err(_) => JObject::null().into_raw(),
}
}
#[no_mangle]
pub unsafe extern "system" fn Java_com_microsoft_regorus_Engine_destroyEngine(
_env: JNIEnv,
_class: JClass,
engine_ptr: jlong,
) {
let _engine = Box::from_raw(engine_ptr as *mut Engine);
}
fn throw_err<T>(mut env: JNIEnv, mut f: impl FnMut(&mut JNIEnv) -> Result<T>) -> Result<T> {
match f(&mut env) {
Ok(val) => Ok(val),
Err(err) => {
env.throw(err.to_string())?;
Err(err)
}
}
}

View File

@@ -0,0 +1,114 @@
/**
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
**/
package com.microsoft.regorus;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.concurrent.atomic.AtomicReference;
public class Engine implements AutoCloseable {
// Methods exposed from Rust side, you can run
// `javac -h . src/main/java/com/microsoft/regorus/Engine.java` to update
// expected native header at `bindings/java/com_microsoft_regorus_Engine.h`
// if you update the native API.
private static native long newEngine();
private static native void addPolicy(long enginePtr, String path, String rego);
private static native void addPolicyFromFile(long enginePtr, String path);
private static native void addDataJson(long enginePtr, String data);
private static native void addDataJsonFromFile(long enginePtr, String path);
private static native void setInputJson(long enginePtr, String input);
private static native void setInputJsonFromFile(long enginePtr, String path);
private static native String evalQuery(long enginePtr, String query);
private static native void destroyEngine(long enginePtr);
// Pointer to Engine allocated on Rust's heap, all native methods works on
// engine expects this pointer. It is free'd in `close` method.
private final long enginePtr;
public Engine() {
enginePtr = newEngine();
}
public void pubAddPolicy(String path, String rego) {
addPolicy(enginePtr, path, rego);
}
public void pubAddDataJson(String path) {
addDataJson(enginePtr, path);
}
public void pubSetInputJson(String path) {
setInputJson(enginePtr, path);
}
public String pubEvalQuery(String path) {
return evalQuery(enginePtr, path);
}
@Override
public void close() {
destroyEngine(enginePtr);
}
// Loading native library from jar is adapted from:
// https://github.com/apache/opendal/blob/93e5f65bbf30df2fed4bdd95bb0685c73c6418c2/bindings/java/src/main/java/org/apache/opendal/NativeLibrary.java
// https://github.com/apache/opendal/blob/93e5f65bbf30df2fed4bdd95bb0685c73c6418c2/bindings/java/src/main/java/org/apache/opendal/Environment.java
private static final String classifier;
static {
final StringBuilder classifierBuilder = new StringBuilder();
final String os = System.getProperty("os.name").toLowerCase();
if (os.startsWith("windows")) {
classifierBuilder.append("windows");
} else if (os.startsWith("mac")) {
classifierBuilder.append("osx");
} else {
classifierBuilder.append("linux");
}
classifierBuilder.append("-");
final String arch = System.getProperty("os.arch").toLowerCase();
if (arch.equals("aarch64")) {
classifierBuilder.append("aarch_64");
} else {
classifierBuilder.append("x86_64");
}
classifier = classifierBuilder.toString();
loadNativeLibrary();
}
private static void loadNativeLibrary() {
try {
// try dynamic library - the search path can be configured via "-Djava.library.path"
System.loadLibrary("regorus_java");
return;
} catch (UnsatisfiedLinkError ignore) {
// ignore - try from classpath
}
final String libraryPath = bundledLibraryPath();
try (final InputStream is = Engine.class.getResourceAsStream(libraryPath)) {
if (is == null) {
throw new RuntimeException("cannot find " + libraryPath);
}
final int dot = libraryPath.indexOf('.');
final File tmpFile = File.createTempFile(libraryPath.substring(0, dot), libraryPath.substring(dot));
tmpFile.deleteOnExit();
Files.copy(is, tmpFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
System.load(tmpFile.getAbsolutePath());
} catch (IOException exception) {
throw new RuntimeException(exception);
}
}
private static String bundledLibraryPath() {
final String libraryName = System.mapLibraryName("regorus_java");
return "/native/" + classifier + "/" + libraryName;
}
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
**/
package com.microsoft.regorus;
import java.util.Map;
import java.util.ArrayList;
import junit.framework.TestCase;
import junit.framework.Assert;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
public class EngineTest extends TestCase
{
public void test_engine()
{
String resJson;
try (Engine engine = new Engine()) {
engine.pubAddPolicy(
"hello.rego",
"package test\nmessage = concat(\", \", [input.message, data.message])"
);
engine.pubAddDataJson("{\"message\":\"World!\"}");
engine.pubSetInputJson("{\"message\":\"Hello\"}");
resJson = engine.pubEvalQuery("data.test.message");
}
Gson gson = new Gson();
Map res = gson.fromJson(resJson, Map.class);
ArrayList results = (ArrayList) res.get("result");
ArrayList expressions = (ArrayList) ((Map) results.get(0)).get("expressions");
Map expression = (Map) expressions.get(0);
Assert.assertEquals("Hello, World!", expression.get("value"));
}
}

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env python3
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
#
# This script adapted from:
# https://github.com/apache/opendal/blob/93e5f65bbf30df2fed4bdd95bb0685c73c6418c2/bindings/java/tools/build.py
from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser
from pathlib import Path
import shutil
import subprocess
def classifier_to_target(classifier: str) -> str:
if classifier == 'osx-aarch_64':
return 'aarch64-apple-darwin'
if classifier == 'osx-x86_64':
return 'x86_64-apple-darwin'
if classifier == 'linux-aarch_64':
return 'aarch64-unknown-linux-gnu'
if classifier == 'linux-x86_64':
return 'x86_64-unknown-linux-gnu'
if classifier == 'windows-x86_64':
return 'x86_64-pc-windows-msvc'
raise Exception(f'Unsupported classifier: {classifier}')
def get_cargo_artifact_name(classifier: str) -> str:
if classifier.startswith('osx'):
return 'libregorus_java.dylib'
if classifier.startswith('linux'):
return 'libregorus_java.so'
if classifier.startswith('windows'):
return 'regorus_java.dll'
raise Exception(f'Unsupported classifier: {classifier}')
if __name__ == '__main__':
basedir = Path(__file__).parent.parent
parser = ArgumentParser()
parser.add_argument('--classifier', type=str, required=True)
args = parser.parse_args()
target = classifier_to_target(args.classifier)
# Setup target.
command = ['rustup', 'target', 'add', target]
print('$ ' + subprocess.list2cmdline(command))
subprocess.run(command, cwd=basedir, check=True)
cmd = ['cargo', 'build', '--target', target]
output = basedir / 'target' / 'bindings'
Path(output).mkdir(exist_ok=True, parents=True)
cmd += ['--target-dir', str(output)]
print('$ ' + subprocess.list2cmdline(cmd))
subprocess.run(cmd, cwd=basedir, check=True)
# History reason of cargo profiles.
profile = 'debug'
artifact = get_cargo_artifact_name(args.classifier)
src = output / target / profile / artifact
dst = basedir / 'target' / 'classes' / 'native' / args.classifier / artifact
dst.parent.mkdir(exist_ok=True, parents=True)
shutil.copy2(src, dst)