decorators-squad / decorators-squad/eo-yaml

compiletime dependency on javax.json is a problem in modular context

Open
#636 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Java
Stars
272
Forks
55
PR merge metrics
No merged PRs in 30d

Description

eo-yaml has a compile-time dependency on javax.json-api. That is, code that *uses* eo-yaml must have a compile-time dependency on javax.json or this happens:

untitled.java:
```java
import com.amihaiemil.eoyaml.*;

void main() {
var ym = Yaml.createMutableYamlMappingBuilder()
.add("hello", "world")
.add("first", "last")
.build();
IO.println(ym);
}
```

```
$ javac -cp Downloads/eo-yaml-8.0.6.jar untitled.java
untitled.java:5: error: cannot access JsonStructure
.add("hello", "world")
^
class file for javax.json.JsonStructure not found
1 error
```

OK, so you can add it to the classpath and it works:

```
$ javac -cp Downloads/eo-yaml-8.0.6.jar:Downloads/javax.json-api-1.1.4.jar untitled.java
```
(no output)

And subsequently run it without javax.json-api and it's fine too.

```
$ java -cp .:Downloads/eo-yaml-8.0.6.jar untitled
hello: world
first: last
```

OK, but what about if your app is a modular app? ie: using the Java 9+ jigsaw module system? Here abbreviated to the same effect to reproduce on commandline with a compact source, but obviously the context in which I hit this was a full application project...

```
$ javac --module-path Downloads/eo-yaml-8.0.6.jar:Downloads/javax.json-api-1.1.4.jar \
--add-modules com.amihaiemil.eoyaml \
untitled.java
untitled.java:5: error: cannot access JsonStructure
.add("hello", "world")
^
class file for javax.json.JsonStructure not found
1 error
```

You can add javax.json-api to the module path, but it still fails! This is because the eo-yaml model does not *require* java.json.

Now, you can force it to read it with more commandlinery:

```
$ javac --module-path Downloads/eo-yaml-8.0.6.jar:Downloads/javax.json-api-1.1.4.jar \
--add-modules com.amihaiemil.eoyaml,java.json \
--add-reads com.amihaiemil.eoyaml=java.json \
untitled.java
```
(no output)

and again, it runs fine without the java.json dependency:

```
$ java --module-path Downloads/eo-yaml-8.0.6.jar --add-modules com.amihaiemil.eoyaml untitled
hello: world
first: last
```

But this isn't really ideal, especially in full application-dev settings, you shouldn't need to be messing about with `--add-reads` arguments to javaCompile if you can help it.

I *think* the correct solution is that the `module-info.java` file for the eo-yaml library needs to have an additional line:

```java
requires static java.json;
```

And then you still need to have javax.json-api as a compileOnly dependency, but this means you can now compile code that uses eg: YamlMappingBuilder.

NB: I think you'd have the same problem if you were actually *using* javax.json-api in your modular application and trying to use the eo-yaml methods that use them; you'd get the same errors at runtime even if you managed to get it compiled, because the eo-yaml module doesn't require java.json. You'd need to add the `--add-reads` to runtime args as well, or possibly more.

(I'm not using javax.json-api, I'm using jakarta.json-api and an impl thereof, which is another matter. If this library were to switch to that, this issue would still need to be resolved.)

As it happens, for my purposes I found a workaround that needs no javax.json-api dependency at any point, nor the `--add-reads` argument, but it does it by using the `java.lang.invoke` functionality:

untitled.java:
```java
import module com.amihaiemil.eoyaml;

void main() {
var ym = new MappingBuilder(Yaml.createMutableYamlMappingBuilder())
.add("hello", "world")
.add("first", "last")
.build();
IO.println(ym);
}

private static class MappingBuilder {
private static final Supplier ymbAdd = StableValue.supplier(() -> {
try {
return MethodHandles.lookup()
.findVirtual(YamlMappingBuilder.class, "add", MethodType.methodType(YamlMappingBuilder.class, String.class, String.class));
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new RuntimeException(e);
}
});

private YamlMappingBuilder ymb;
private final MethodHandle addHandle;

private MappingBuilder(YamlMappingBuilder ymb) {
this.ymb = ymb;
this.addHandle = ymbAdd.get();
}

private MappingBuilder add(String key, String value) {
try {
this.ymb = (YamlMappingBuilder) addHandle.invoke(this.ymb, key, value);
return this;
} catch (Throwable e) {
throw e instanceof RuntimeException re ? re : new RuntimeException(e);
}
}

private YamlMapping build() {
return ymb.build();
}
}
```

```
$ javac --enable-preview --release 25 --module-path Downloads/eo-yaml-8.0.6.jar \
--add-modules com.amihaiemil.eoyaml \
untitled.java
Note: untitled.java uses preview features of Java SE 25.
Note: Recompile with -Xlint:preview for details.
$ java --enable-preview --module-path Downloads/eo-yaml-8.0.6.jar --add-modules com.amihaiemil.eoyaml untitled
hello: world
first: last
```

By the way if you use reflection rather than invoke, ie: to get the method using something like:

```java
Method addMethod = YamlMappingBuilder.class.getMethod("add", String.class, String.class);
MethodHandle addHandle = MethodHandles.lookup().unreflect(addMethod);
```

... you'll just move that `class file for javax.json.JsonStructure not found` error to runtime instead of compiletime, as `Class::getMethod` actually gets all the methods, and will trip over the ones that depend on `JsonStructure` et al. Guess how I know this. 😀 Obviously this would apply to any use of reflection over these classes at runtime, for any purpose.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.