jbangdev / jbang

Unleash the power of Java - JBang Lets Students, Educators and Professional Developers create, edit and run self-contained source-only Java programs with unprecedented ease.
https://jbang.dev
MIT License
1.42k stars 157 forks source link

Provide a simplified and light jbang flavor #237

Open rmannibucau opened 4 years ago

rmannibucau commented 4 years ago

Hi,

I have some use cases for a very light jbang flavor, here are the thing I'd like to drop from jbang and things I need:

  1. It must be able to resolve maven dependencies to add them in the script classloader/cp
  2. It must not use SW resolver (TomEE resolver is enough with slight changes: https://github.com/apache/tomee/blob/master/container/openejb-loader/src/main/java/org/apache/openejb/loader/provisining/)
  3. It must support a simplified entry point "à la python" (ie instead of public static void main(String...), just void main() and use the script as the class content.
  4. In terms of dependency it should be a vanilla jbang, ie no opiniated predefined choice like picocli or so, just let user write a custom entry point (main) - goal being to let all script use that technic
  5. It must be officially delivered as a fatjar (it is not in download area today, guess it is just a matter of uploading the fatjar but a small todo to add)

Sample script I'm using today (simplified):

#!/usr/bin/env -S java -jar /opt/rmannibucau/dev/javabien/target/javabien-2.0.0-SNAPSHOT.jar

// javabien set-var johnzonVersion 1.2.8
// javabien dependency mvn:org.apache.geronimo.specs:geronimo-json_1.1_spec:1.4
// javabien dependency mvn:org.apache.geronimo.specs:geronimo-jsonb_1.0_spec:1.3
// javabien dependency mvn:org.apache.johnzon:johnzon-core:${var:johnzonVersion}
// javabien dependency mvn:org.apache.johnzon:johnzon-mapper:${var:johnzonVersion}
// javabien dependency mvn:org.apache.johnzon:johnzon-jsonb:${var:johnzonVersion}

import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonArrayBuilder;
import javax.json.JsonBuilderFactory;
import javax.json.JsonObject;
import javax.json.JsonValue;
import javax.json.bind.Jsonb;
import javax.json.bind.JsonbBuilder;
import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import static java.util.stream.Collectors.joining;
import java.util.stream.Stream;

import static java.util.Collections.emptyMap;
import static java.util.Locale.ROOT;

String JSON_RPC_ENDPOINT = "http://localhost:3333/jsonrpc";
String OAUTH2_ENDPOINT = "http://localhost:1111/oauth2/token";
String USERNAME = "xxx";
String PASSWORD = "xxx";
String CLIENT_ID = "xxx";
String CLIENT_SECRET = "xxx";

class JsonRpc {
  private final URI endpoint = URI.create(JSON_RPC_ENDPOINT);
  private final JsonBuilderFactory json = Json.createBuilderFactory(emptyMap());
  private final Jsonb jsonb = JsonbBuilder.create();
  private final HttpClient client = HttpClient.newHttpClient();

  private String authorization;

  private JsonObject execute(final JsonValue data) throws Exception {
    final var request = data.toString();
    final var response = client.send(
      HttpRequest.newBuilder()
        .method("POST", HttpRequest.BodyPublishers.ofString(request))
        .uri(endpoint)
        .header("Authorization", authorization == null ? authorization = getAuthorization() : authorization)
        .build(),
      HttpResponse.BodyHandlers.ofString());
    if (response.statusCode() != 200) {
      throw new IllegalArgumentException("Invalid response: HTTP " + response.statusCode() + "\n" + response.body());
    }
    return jsonb.fromJson(response.body(), JsonObject.class);
  }

  private String getAuthorization() throws Exception {
    final String payload = Map.of(
      "grant_type", "password",
      "username", USERNAME,
      "password", PASSWORD,
      "client_id", CLIENT_ID,
      "client_secret", CLIENT_SECRET)
      .entrySet().stream()
      .map(entry -> Stream.of(
        URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8),
        URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8))
        .collect(joining("=")))
      .collect(joining("&"));
    final var response = client.send(
      HttpRequest.newBuilder()
        .method("POST", HttpRequest.BodyPublishers.ofString(payload))
        .uri(URI.create(OAUTH2_ENDPOINT))
        .header("Content-Type", "application/x-www-form-urlencoded")
        .header("Accept", "application/json")
        .build(),
      HttpResponse.BodyHandlers.ofString());
    if (response.statusCode() != 200) {
      throw new IllegalArgumentException("Invalid response: HTTP " + response.statusCode());
    }
    return "Bearer " + jsonb.fromJson(response.body(), JsonObject.class).getString("access_token");
  }

  private JsonRpc create(final String desc, final String name) throws Exception {
    final var result = execute(createJpaBulk(createBulks(desc, name).build()));
    println(result);
    return this;
  }

  private JsonObject createJpaBulk(final JsonArray bulks) {
    return json.createObjectBuilder(); // cut to make it shorter
  }

  private JsonArrayBuilder createBulks(final String name, final String desc) {
    return json.createArrayBuilder(); // cut to make it shorter
  }
}

void main() throws Exception {
  new JsonRpc()
    .create("Cars", "All about cars.")
    .create("Pizza", "All about italian pizze.")
    .create("Music", "All about music.");
}
maxandersen commented 4 years ago

Having a tinyjbang as one that does the same or similar but without full mvn dep resolution could be interesting; bascially user would have to list all dependencies explicitly and not rely on any transitive deps.

About your specific points I want to grok if you think these are not somehow supported to day or not...comments below.

  • It must be able to resolve maven dependencies to add them in the script classloader/cp

Yes - we do that today afaik. If not let me know what you think is different/missing.

Yes - this would be the new "feature"; allowing to choose which way you want to resolve these with the cost of lost functionallity but the gain of jbang being much smaller.

  • It must support a simplified entry point "à la python" (ie instead of public static void main(String...), just void main() and use the script as the class content.

I don't understand what this is. What is the win here over what is there today of simply just running any "bare" java code via jshell ?

  • In terms of dependency it should be a vanilla jbang, ie no opiniated predefined choice like picocli or so, just let user write a custom entry point (main) - goal being to let all script use that technic

jbang uses picocli for its own command line parsing but there are zero requirements to users on what their scripts uses to parse command line arguments. Thus not sure what you are referring to here ?

  • It must be officially delivered as a fatjar (it is not in download area today, guess it is just a matter of uploading the fatjar but a small todo to add)

and this is because you only want/need it to work on bash/linux based scripts; you don't care about Windows enablement and the optional automatic download of java etc. ?

Would require jbang itself launches the external java process though ... loosing quite of flexibility.... I assume your javabien.jar above launches such process or are you dynamically compile and loading the class ?

rmannibucau commented 4 years ago

@maxandersen to answer your questions:

  1. yes today jbang supports to setup the classloader properly from mvn coordinates
  2. agree replacing SW by the openejb like code will loose a few feature but will be lighter (my jar does 35K there)
  3. the main point is about starting to align script on common scripts (python as a ref there) + not have to use jshell at all - my code runs on j8 too, it is just a matter of defining an entry point, main being the simplest flavor I think
  4. There is no need to parse much options but just rewrite the script to create the env and run it after having compiled it (uing javac for ex), options needed are very light (I can only think about a --debug one which does not need any cli parser)
  5. the fatjar works well on windows, it is just not launched with a chbang like script but the main drops # lines so it works

jbang would just use contextual java to compile/launch the script.

maxandersen commented 4 years ago
  1. the main point is about starting to align script on common scripts (python as a ref there) + not have to use jshell at all - my code runs on j8 too, it is just a matter of defining an entry point, main being the simplest flavor I think

again - not fully following what you are getting at here (except that jshell needs java9). Can you give the example code you want to have executed ? It sounds to me you want this to be treated as valid to execute:

import x.y.z
...

void main(String args[]) {

}

i.e you'll need to parse/split the code to know what it needs/does.

Do I have it right?

  • There is no need to parse much options but just rewrite the script to create the env and run it after having compiled it (uing javac for ex), options needed are very light (I can only think about a --debug one which does not need any cli parser)

Your issue is with jbang depending on picocli because it adds ~300K to the jar then.

I would still like tinyjbang to have the edit and a few other facilities ... would hate to handle that without picocli ...lets see how bad it gets before I commit to try that ;)

  • the fatjar works well on windows, it is just not launched with a chbang like script but the main drops # lines so it works

sure; so you'll have to know how to call:

java -jar /opt/rmannibucau/dev/javabien/target/javabien-2.0.0-SNAPSHOT.jar script.java instead of tinyjbang script.java

Is your script launching 2 java processes or dynamic loading it ?

maxandersen commented 4 years ago

note, if we remove the mvn dependency need tinyjbang could more easily just be built as a native binary....would be bigger though ;)

rmannibucau commented 4 years ago

More exactly this (with args being optional):

import x.y.z
...

void main() {
}

My current impl 1. compiles the script having having rewritten part of it and parsed the metadata, 2. load the class in a custom classloader and 3. execute the entrypoint by reflection.

My launcher is not forking.

maxandersen commented 4 years ago

Ok. Not exactly a fan of that format as it won't work in any Ide. Jsh and java syntax does.

Nor sure how you would handle method and classes nicely.

I guess could just strip lines with import/package/comments and wrap everything else in a class...but not seeing the big win here. You loose a lot by this.

I don't dynamically load in jbang to allow script to tweak launch options.

rmannibucau commented 4 years ago

@maxandersen advantage is really about scripting and not just doing deployment. Jshell is quite limited today so inheriting from it is not a thing. Most IDE support this kind of partial classes "correctly" and it enables a better readability in full script mode (several scripts will not even use an IDE, if it becomes too complex it will become an app with a real build system). About classes, since a class can have inner classes then it is not a big deal and works as in my sample. Same for methods - you are in a class so you have methods ;)). Once again it is not perfect but does what it intends for, for only 35k.

maxandersen commented 4 years ago

Hmm.... what Ide setup do you have that supports such syntax ? By support I mean content assist.

rmannibucau commented 4 years ago

@maxandersen Intellij, VSCode and vim with java support (didn't test vanilla eclipse or netbeans). It does not support this syntax by itself but it is close enough to keep completion working (intellisense). Didn't test enough to say it was due to a previous indexation or not but didn't get an issue while it stays a single file (multiple files would make it "too red" ;)).

maxandersen commented 4 years ago

I get red syntax errors and it's not picking up the imports. At best just content assist on jdk classes.

rmannibucau commented 4 years ago

Hmm, guess you didnt set the classpath in the project (project settings for ex)?

maxandersen commented 2 years ago

This is old issue but I still can't get any of my IDE's to stop complaining about top level methods unless you treat it as jshell thus not buying into introduing a third syntax.