Skip to content

A simple picocli/native-image ion-java CLI #988

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: ion-11-encoding
Choose a base branch
from
Draft
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
27 changes: 27 additions & 0 deletions ion-java-cli/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
plugins {
java
application
// Apply GraalVM Native Image plugin
id("org.graalvm.buildtools.native") version "0.10.3"
}

description = "A CLI that implements the standard interface defined by ion-test-driver."
Expand All @@ -15,8 +17,33 @@ repositories {
dependencies {
implementation("args4j:args4j:2.33")
implementation(rootProject)

implementation("info.picocli:picocli:4.7.6")
annotationProcessor("info.picocli:picocli-codegen:4.7.6")
}

tasks.withType<JavaCompile> {
options.compilerArgs.add("-Aproject=${project.group}/${project.name}")
}

application {
mainClass.set("com.amazon.tools.cli.IonJavaCli")
}

// Defines an ion-java-cli:nativeCompile task which produces ion-java-cli/build/native/nativeCompile/jion
// You need to have GRAALVM_HOME pointed at a GraalVM installation
// You can get one of those via e.g. `sdk install java 17.0.9-graalce`
// See: https://sdkman.io/
graalvmNative {
testSupport.set(false)
binaries {
named("main") {
imageName.set("jion")
mainClass.set("com.amazon.tools.cli.SimpleIonCli")
buildArgs.add("-O4")
}
}
binaries.all {
buildArgs.add("--verbose")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ private static IonWriter createIonWriter(OutputFormat format, OutputStream out,
switch (format) {
case TEXT: return IonTextWriterBuilder.standard().withImports(symbols).build(out);
case PRETTY: return IonTextWriterBuilder.pretty().withImports(symbols).build(out);
case EVENTS: return IonTextWriterBuilder.pretty().withImports(symbols).build(out);
case EVENTS: return IonTextWriterBuilder.standard().withImports(symbols).build(out);
case BINARY: return IonBinaryWriterBuilder.standard().withImports(symbols).build(out);
case NONE: return IonTextWriterBuilder.standard().withImports(symbols).build(new NoOpOutputStream());
default: throw new IllegalStateException("Unsupported output format: " + format);
Expand Down
159 changes: 159 additions & 0 deletions ion-java-cli/src/main/java/com/amazon/tools/cli/SimpleIonCli.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package com.amazon.tools.cli;


import com.amazon.ion.IonEncodingVersion;
import com.amazon.ion.IonReader;
import com.amazon.ion.IonWriter;
import com.amazon.ion.system.IonReaderBuilder;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.HelpCommand;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.SequenceInputStream;
import java.util.Arrays;

@Command(
name = SimpleIonCli.NAME,
version = SimpleIonCli.VERSION,
subcommands = {HelpCommand.class},
mixinStandardHelpOptions = true
)
class SimpleIonCli {

public static final String NAME = "jion";
public static final String VERSION = "2024-10-31";
//TODO: Replace with InputStream.nullInputStream in JDK 11+
public static final InputStream EMPTY = new ByteArrayInputStream(new byte[0]);

public static void main(String[] args) {
CommandLine commandLine = new CommandLine(new SimpleIonCli())
.setCaseInsensitiveEnumValuesAllowed(true)
.setUsageHelpAutoWidth(true);
System.exit(commandLine.execute(args));
}

@Option(names={"-v", "--ion-version"}, description = "Output Ion version", defaultValue = "1.0",
converter = IonEncodingVersionConverter.class, scope = CommandLine.ScopeType.INHERIT)
IonEncodingVersion ionVersion;

@Option(names={"-f", "--format", "--output-format"}, defaultValue = "pretty",
description = "Output format, from the set (text | pretty | binary | debug | none).",
paramLabel = "<format>",
scope = CommandLine.ScopeType.INHERIT)
OutputFormat outputFormat;

@Option(names={"-o", "--output"}, paramLabel = "FILE", description = "Output file",
scope = CommandLine.ScopeType.INHERIT)
File outputFile;

@Command(name = "cat", aliases = {"process"},
description = "concatenate FILE(s) in the requested Ion output format",
mixinStandardHelpOptions = true)
int cat( @Parameters(paramLabel = "FILE") File... files) {


//TODO: Handle stream cutoff- java.io.IOException: Broken pipe
//TODO: This is not resilient to problems with a single file. Should it be?
try (InputStream in = getInputStream(files);
IonReader reader = IonReaderBuilder.standard().build(in);
OutputStream out = getOutputStream(outputFile);
IonWriter writer = getWriter(ionVersion, outputFormat, out)) {
// getInputStream will look for stdin if we don't supply
writer.writeValues(reader);
} catch (IOException e) {
System.err.println(e.getMessage());
return CommandLine.ExitCode.SOFTWARE;
}

// process files
return CommandLine.ExitCode.OK;
}

private static InputStream getInputStream(File... files) {
if (files == null || files.length == 0) return new FileInputStream(FileDescriptor.in);

// As convenient as this formulation is I'm not sure of the ordering guarantees here
// Revisit if that is ever problematic
return Arrays.stream(files)
.map(SimpleIonCli::getInputStream)
.reduce(EMPTY, SequenceInputStream::new);
}

private static InputStream getInputStream(File inputFile) {
try {
return new FileInputStream(inputFile);
} catch (FileNotFoundException e) {
throw cloak(e);
}
}

// Removing some boilerplate from checked-exception consuming paths, without RuntimeException wrapping
// JLS Section 18.4 covers type inference for generic methods,
// including the rule that `throws T` is inferred as RuntimeException if possible.
// See e.g. https://www.rainerhahnekamp.com/en/ignoring-exceptions-in-java/
private static <T extends Throwable> T cloak(Throwable t) throws T {
@SuppressWarnings("unchecked")
T result = (T) t;
return result;
}

private static FileOutputStream getOutputStream(File outputFile) throws IOException {
// non-line-buffered stdout, or the requested file output
return outputFile == null ? new FileOutputStream(FileDescriptor.out) : new FileOutputStream(outputFile);
}

private static IonWriter getWriter(IonEncodingVersion<?,?> version, OutputFormat format, OutputStream out) {
if (version == IonEncodingVersion.ION_1_0) return getWriter_1_0(format, out);
if (version == IonEncodingVersion.ION_1_1) return getWriter_1_1(format, out);
throw new IllegalArgumentException("Unrecognized IonEncodingVersion: " + version);
}

private static IonWriter getWriter_1_0(OutputFormat format, OutputStream out) {
switch (format) {
case Pretty: return IonEncodingVersion.ION_1_0.textWriterBuilder().withPrettyPrinting().build(out);
case Text: return IonEncodingVersion.ION_1_0.textWriterBuilder().build(out);
case Binary: return IonEncodingVersion.ION_1_0.binaryWriterBuilder().build(out);
case Debug: throw new UnsupportedOperationException("Not yet supported, pending ion-java #1005");
case None: return IonEncodingVersion.ION_1_0.textWriterBuilder().build(new NoOpOutputStream());
default: throw new IllegalArgumentException("Unrecognized or unsupported output format: " + format);
}
}

private static IonWriter getWriter_1_1(OutputFormat format, OutputStream out) {
switch (format) {
case Pretty: return IonEncodingVersion.ION_1_1.textWriterBuilder().withPrettyPrinting().build(out);
case Text: return IonEncodingVersion.ION_1_1.textWriterBuilder().build(out);
case Binary: return IonEncodingVersion.ION_1_1.binaryWriterBuilder().build(out);
case Debug: throw new UnsupportedOperationException("Not yet supported, pending ion-java #1005");
case None: return IonEncodingVersion.ION_1_1.textWriterBuilder().build(new NoOpOutputStream());
default: throw new IllegalArgumentException("Unrecognized or unsupported output format: " + format);
}
}

private enum OutputFormat {
Pretty, Text, Binary, Debug, None
}

private static class IonEncodingVersionConverter implements CommandLine.ITypeConverter<IonEncodingVersion<?,?>> {

@Override
public IonEncodingVersion<?,?> convert(String ionVersion) {
switch (ionVersion) {
case "1.0": return IonEncodingVersion.ION_1_0;
case "1.1": return IonEncodingVersion.ION_1_1;
default: throw new IllegalArgumentException("Unrecognized or unsupported Ion version: " + ionVersion);
}
}
}
}
Loading