Rework stores and add some documentation

This commit is contained in:
Christopher Schnick 2022-07-19 22:49:07 +02:00
parent 4eb0c80d60
commit 0f37600c96
29 changed files with 725 additions and 187 deletions

View file

@ -10,8 +10,21 @@ import lombok.extern.jackson.Jacksonized;
@Jacksonized @Jacksonized
@AllArgsConstructor @AllArgsConstructor
public class Choice { public class Choice {
/**
* The optional character which can be used to enter a choice.
*/
Character character; Character character;
/**
* The shown description of this choice.
*/
String description; String description;
/**
* A Boolean indicating whether this choice is disabled or not.
* Disabled choices are still shown but can't be selected.
*/
boolean disabled; boolean disabled;
public Choice(String description) { public Choice(String description) {

View file

@ -8,8 +8,25 @@ import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.function.Supplier; import java.util.function.Supplier;
/**
* A Dialog is a sequence of questions and answers.
*
* The dialogue API is only used for the command line interface.
* Therefore, the actual implementation is handled by the command line component.
* This API provides a way of creating server-side dialogues which makes
* it possible to create extensions that provide a commandline configuration component.
*
* When a Dialog is completed, it can also be optionally evaluated to a value, which can be queried by calling {@link #getResult()}.
* The evaluation function can be set with {@link #evaluateTo(Supplier)}.
* Alternatively, a dialogue can also copy the evaluation function of another dialogue with {@link #evaluateTo(Dialog)}.
* An evaluation result can also be mapped to another type with {@link #map(Function)}.
* It is also possible to listen for the completion of this dialogue with {@link #onCompletion(Consumer)}.
*/
public abstract class Dialog { public abstract class Dialog {
/**
* Creates an empty dialogue. This dialogue completes immediately and does not handle any questions or answers.
*/
public static Dialog empty() { public static Dialog empty() {
return new Dialog() { return new Dialog() {
@Override @Override
@ -53,12 +70,29 @@ public abstract class Dialog {
} }
} }
/**
* Creates a choice dialogue.
*
* @param description the shown question description
* @param elements the available elements to choose from
* @param required signals whether a choice is required or can be left empty
* @param selected the selected element index
*/
public static Dialog.Choice choice(String description, List<io.xpipe.core.dialog.Choice> elements, boolean required, int selected) { public static Dialog.Choice choice(String description, List<io.xpipe.core.dialog.Choice> elements, boolean required, int selected) {
Dialog.Choice c = new Dialog.Choice(description, elements, required, selected); Dialog.Choice c = new Dialog.Choice(description, elements, required, selected);
c.evaluateTo(c::getSelected); c.evaluateTo(c::getSelected);
return c; return c;
} }
/**
* Creates a choice dialogue from a set of objects.
*
* @param description the shown question description
* @param toString a function that maps the objects to a string
* @param required signals whether choices required or can be left empty
* @param def the element which is selected by default
* @param vals the range of possible elements
*/
@SafeVarargs @SafeVarargs
public static <T> Dialog.Choice choice(String description, Function<T, String> toString, boolean required, T def, T... vals) { public static <T> Dialog.Choice choice(String description, Function<T, String> toString, boolean required, T def, T... vals) {
var elements = Arrays.stream(vals).map(v -> new io.xpipe.core.dialog.Choice(null, toString.apply(v))).toList(); var elements = Arrays.stream(vals).map(v -> new io.xpipe.core.dialog.Choice(null, toString.apply(v))).toList();
@ -85,11 +119,6 @@ public abstract class Dialog {
this.element = new QueryElement(description, newLine, required, quiet, value, converter, hidden); this.element = new QueryElement(description, newLine, required, quiet, value, converter, hidden);
} }
@Override
public Optional<Map.Entry<String, String>> toValue() {
return Optional.of(new AbstractMap.SimpleEntry<>(element.getDescription(), element.getValue()));
}
@Override @Override
public DialogElement start() throws Exception { public DialogElement start() throws Exception {
return element; return element;
@ -109,17 +138,38 @@ public abstract class Dialog {
} }
} }
/**
* Creates a simple query dialogue.
*
* @param description the shown question description
* @param newLine signals whether the query should be done on a new line or not
* @param required signals whether the query can be left empty or not
* @param quiet signals whether the user should be explicitly queried for the value.
* In case the user is not queried, a value can still be set using the command line arguments
* that allow to set the specific value for a configuration query parameter
* @param value the default value
* @param converter the converter
*/
public static <T> Dialog.Query query(String description, boolean newLine, boolean required, boolean quiet, T value, QueryConverter<T> converter) { public static <T> Dialog.Query query(String description, boolean newLine, boolean required, boolean quiet, T value, QueryConverter<T> converter) {
var q = new <T>Dialog.Query(description, newLine, required, quiet, value, converter, false); var q = new <T>Dialog.Query(description, newLine, required, quiet, value, converter, false);
q.evaluateTo(q::getConvertedValue); q.evaluateTo(q::getConvertedValue);
return q; return q;
} }
/**
* A special wrapper for secret values of {@link #query(String, boolean, boolean, boolean, Object, QueryConverter)}.
*/
public static Dialog.Query querySecret(String description, boolean newLine, boolean required, Secret value) { public static Dialog.Query querySecret(String description, boolean newLine, boolean required, Secret value) {
var q = new Dialog.Query(description, newLine, required, false, value, QueryConverter.SECRET, true); var q = new Dialog.Query(description, newLine, required, false, value, QueryConverter.SECRET, true);
q.evaluateTo(q::getConvertedValue); q.evaluateTo(q::getConvertedValue);
return q; return q;
} }
/**
* Chains multiple dialogues together.
*
* @param ds the dialogues
*/
public static Dialog chain(Dialog... ds) { public static Dialog chain(Dialog... ds) {
return new Dialog() { return new Dialog() {
@ -156,6 +206,9 @@ public abstract class Dialog {
}.evaluateTo(ds[ds.length - 1]); }.evaluateTo(ds[ds.length - 1]);
} }
/**
* Creates a dialogue that starts from the beginning if the repeating condition is true.
*/
public static <T> Dialog repeatIf(Dialog d, Predicate<T> shouldRepeat) { public static <T> Dialog repeatIf(Dialog d, Predicate<T> shouldRepeat) {
return new Dialog() { return new Dialog() {
@ -180,10 +233,16 @@ public abstract class Dialog {
}.evaluateTo(d).onCompletion(d.completion); }.evaluateTo(d).onCompletion(d.completion);
} }
/**
* Create a simple dialogue that will print a message.
*/
public static Dialog header(String msg) { public static Dialog header(String msg) {
return of(new HeaderElement(msg)).evaluateTo(() -> msg); return of(new HeaderElement(msg)).evaluateTo(() -> msg);
} }
/**
* Create a simple dialogue that will print a message.
*/
public static Dialog header(Supplier<String> msg) { public static Dialog header(Supplier<String> msg) {
final String[] msgEval = {null}; final String[] msgEval = {null};
return new Dialog() { return new Dialog() {
@ -200,7 +259,9 @@ public abstract class Dialog {
}.evaluateTo(() -> msgEval[0]); }.evaluateTo(() -> msgEval[0]);
} }
/**
* Creates a dialogue that will show a loading icon until the next dialogue question is sent.
*/
public static Dialog busy() { public static Dialog busy() {
return of(new BusyElement()); return of(new BusyElement());
} }
@ -210,6 +271,10 @@ public abstract class Dialog {
T get() throws Exception; T get() throws Exception;
} }
/**
* Creates a dialogue that will only evaluate when needed.
* This allows a dialogue to incorporate completion information about a previous dialogue.
*/
public static Dialog lazy(FailableSupplier<Dialog> d) { public static Dialog lazy(FailableSupplier<Dialog> d) {
return new Dialog() { return new Dialog() {
@ -231,7 +296,7 @@ public abstract class Dialog {
}; };
} }
public static Dialog of(DialogElement e) { private static Dialog of(DialogElement e) {
return new Dialog() { return new Dialog() {
@ -253,6 +318,9 @@ public abstract class Dialog {
} }
/**
* Creates a dialogue that will not be executed if the condition is true.
*/
public static Dialog skipIf(Dialog d, Supplier<Boolean> check) { public static Dialog skipIf(Dialog d, Supplier<Boolean> check) {
return new Dialog() { return new Dialog() {
@ -271,6 +339,9 @@ public abstract class Dialog {
}.evaluateTo(d).onCompletion(d.completion); }.evaluateTo(d).onCompletion(d.completion);
} }
/**
* Creates a dialogue that will repeat with an error message if the condition is met.
*/
public static <T> Dialog retryIf(Dialog d, Function<T, String> msg) { public static <T> Dialog retryIf(Dialog d, Function<T, String> msg) {
return new Dialog() { return new Dialog() {
@ -303,6 +374,15 @@ public abstract class Dialog {
}.evaluateTo(d.evaluation).onCompletion(d.completion); }.evaluateTo(d.evaluation).onCompletion(d.completion);
} }
/**
* Creates a dialogue that will fork the control flow.
*
* @param description the shown question description
* @param elements the available elements to choose from
* @param required signals whether a choice is required or not
* @param selected the index of the element that is selected by default
* @param c the dialogue index mapping function
*/
public static Dialog fork(String description, List<io.xpipe.core.dialog.Choice> elements, boolean required, int selected, Function<Integer, Dialog> c) { public static Dialog fork(String description, List<io.xpipe.core.dialog.Choice> elements, boolean required, int selected, Function<Integer, Dialog> c) {
var choice = new ChoiceElement(description, elements, required, selected); var choice = new ChoiceElement(description, elements, required, selected);
return new Dialog() { return new Dialog() {
@ -343,10 +423,6 @@ public abstract class Dialog {
public abstract DialogElement start() throws Exception; public abstract DialogElement start() throws Exception;
public Optional<Map.Entry<String, String>> toValue() {
return Optional.empty();
}
public Dialog evaluateTo(Dialog d) { public Dialog evaluateTo(Dialog d) {
evaluation = () -> d.evaluation.get(); evaluation = () -> d.evaluation.get();
return this; return this;

View file

@ -1,5 +1,8 @@
package io.xpipe.core.dialog; package io.xpipe.core.dialog;
/**
* An exception indicating that the user aborted the dialogue.
*/
public class DialogCancelException extends Exception { public class DialogCancelException extends Exception {
public DialogCancelException() { public DialogCancelException() {

View file

@ -6,6 +6,9 @@ import lombok.Value;
import java.util.UUID; import java.util.UUID;
/**
* A reference to a dialogue instance that will be exchanged whenever a dialogue is started.
*/
@Value @Value
public class DialogReference { public class DialogReference {

View file

@ -16,10 +16,22 @@ import java.util.Optional;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
public interface DataStore { public interface DataStore {
default boolean isComplete() { /**
return true; * Performs a validation of this data store.
} *
* This validation can include one of multiple things:
* - Sanity checks of individual properties
* - Existence checks
* - Connection checks
*
* All in all, a successful execution of this method should almost guarantee
* that the data store can be successfully accessed in the near future.
*
* Note that some checks may take a long time, for example if a connection has to be validated.
* The caller should therefore expect a runtime of multiple seconds when calling this method.
*
* @throws Exception if any part of the validation went wrong
*/
default void validate() throws Exception { default void validate() throws Exception {
} }
@ -27,6 +39,10 @@ public interface DataStore {
return false; return false;
} }
/**
* Creates a display string of this store.
* This can be a multiline string.
*/
default String toDisplay() { default String toDisplay() {
return null; return null;
} }

View file

@ -8,6 +8,9 @@ import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.file.Path; import java.nio.file.Path;
/**
* Represents a file located on a certain machine.
*/
@Value @Value
@JsonTypeName("file") @JsonTypeName("file")
public class FileStore implements StreamDataStore, FilenameStore { public class FileStore implements StreamDataStore, FilenameStore {
@ -16,6 +19,9 @@ public class FileStore implements StreamDataStore, FilenameStore {
return new FileStore(MachineFileStore.local(), p.toString()); return new FileStore(MachineFileStore.local(), p.toString());
} }
/**
* Creates a file store for a file that is local to the callers machine.
*/
public static FileStore local(String p) { public static FileStore local(String p) {
return new FileStore(MachineFileStore.local(), p); return new FileStore(MachineFileStore.local(), p);
} }

View file

@ -2,6 +2,10 @@ package io.xpipe.core.store;
import java.util.Optional; import java.util.Optional;
/**
* Represents a store that has a filename.
* Note that this does not only apply to file stores but any other store as well that has some kind of file name.
*/
public interface FilenameStore extends DataStore { public interface FilenameStore extends DataStore {
@Override @Override

View file

@ -8,21 +8,29 @@ import java.io.ByteArrayInputStream;
import java.io.InputStream; import java.io.InputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
/**
* A store whose contents are stored in memory.
*/
@Value @Value
@JsonTypeName("string") @JsonTypeName("inMemory")
public class StringStore implements StreamDataStore { public class InMemoryStore implements StreamDataStore {
byte[] value; byte[] value;
@JsonCreator @JsonCreator
public StringStore(byte[] value) { public InMemoryStore(byte[] value) {
this.value = value; this.value = value;
} }
public StringStore(String s) { public InMemoryStore(String s) {
value = s.getBytes(StandardCharsets.UTF_8); value = s.getBytes(StandardCharsets.UTF_8);
} }
@Override
public boolean isLocalToApplication() {
return true;
}
@Override @Override
public InputStream openInput() throws Exception { public InputStream openInput() throws Exception {
return new ByteArrayInputStream(value); return new ByteArrayInputStream(value);
@ -30,6 +38,6 @@ public class StringStore implements StreamDataStore {
@Override @Override
public String toDisplay() { public String toDisplay() {
return "string"; return "inMemory";
} }
} }

View file

@ -1,21 +1,14 @@
package io.xpipe.core.store; package io.xpipe.core.store;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream;
/** /**
* A data store that is only represented by an InputStream. * A data store that is only represented by an InputStream.
* One common use case of this class are piped inputs. * This can be useful for development.
*
* As the data in a pipe can only be read once, this implementation
* internally uses a BufferedInputStream to support mark/rest.
*/ */
public class InputStreamDataStore implements StreamDataStore { public class InputStreamDataStore implements StreamDataStore {
private final InputStream in; private final InputStream in;
private BufferedInputStream bufferedInputStream;
public InputStreamDataStore(InputStream in) { public InputStreamDataStore(InputStream in) {
this.in = in; this.in = in;
@ -23,89 +16,7 @@ public class InputStreamDataStore implements StreamDataStore {
@Override @Override
public InputStream openInput() throws Exception { public InputStream openInput() throws Exception {
if (bufferedInputStream != null) { return in;
bufferedInputStream.reset();
return bufferedInputStream;
}
bufferedInputStream = new BufferedInputStream(in);
bufferedInputStream.mark(Integer.MAX_VALUE);
return new InputStream() {
@Override
public int read() throws IOException {
return bufferedInputStream.read();
}
@Override
public int read(byte[] b) throws IOException {
return bufferedInputStream.read(b);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return bufferedInputStream.read(b, off, len);
}
@Override
public byte[] readAllBytes() throws IOException {
return bufferedInputStream.readAllBytes();
}
@Override
public byte[] readNBytes(int len) throws IOException {
return bufferedInputStream.readNBytes(len);
}
@Override
public int readNBytes(byte[] b, int off, int len) throws IOException {
return bufferedInputStream.readNBytes(b, off, len);
}
@Override
public long skip(long n) throws IOException {
return bufferedInputStream.skip(n);
}
@Override
public void skipNBytes(long n) throws IOException {
bufferedInputStream.skipNBytes(n);
}
@Override
public int available() throws IOException {
return bufferedInputStream.available();
}
@Override
public void close() throws IOException {
reset();
}
@Override
public synchronized void mark(int readlimit) {
bufferedInputStream.mark(readlimit);
}
@Override
public synchronized void reset() throws IOException {
bufferedInputStream.reset();
}
@Override
public boolean markSupported() {
return bufferedInputStream.markSupported();
}
@Override
public long transferTo(OutputStream out) throws IOException {
return bufferedInputStream.transferTo(out);
}
};
}
@Override
public OutputStream openOutput() throws Exception {
throw new UnsupportedOperationException("No output available");
} }
@Override @Override

View file

@ -3,16 +3,74 @@ package io.xpipe.core.store;
import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonTypeName;
import lombok.Value; import lombok.Value;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List; import java.util.List;
@JsonTypeName("local") @JsonTypeName("local")
@Value @Value
public class LocalStore implements ShellStore { public class LocalStore implements ShellProcessStore {
static class LocalProcessControl extends ProcessControl {
private final InputStream input;
private final ProcessBuilder builder;
private Process process;
LocalProcessControl(InputStream input, List<String> cmd) {
this.input = input;
var l = new ArrayList<String>();
l.add("cmd");
l.add("/c");
l.addAll(cmd);
builder = new ProcessBuilder(l);
}
@Override
public void start() throws IOException {
process = builder.start();
var t = new Thread(() -> {
try {
input.transferTo(process.getOutputStream());
process.getOutputStream().close();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
t.setDaemon(true);
t.start();
}
@Override
public int waitFor() throws Exception {
return process.waitFor();
}
@Override
public InputStream getStdout() {
return process.getInputStream();
}
@Override
public InputStream getStderr() {
return process.getErrorStream();
}
@Override
public Charset getCharset() {
return StandardCharsets.UTF_8;
}
}
@Override @Override
public boolean exists(String file) { public boolean exists(String file) {
@ -37,17 +95,17 @@ public class LocalStore implements ShellStore {
} }
@Override @Override
public String executeAndRead(List<String> cmd) throws Exception { public ProcessControl prepareCommand(InputStream input, List<String> cmd) {
var p = prepare(cmd).redirectErrorStream(true); return new LocalProcessControl(input, cmd);
var proc = p.start();
var b = proc.getInputStream().readAllBytes();
proc.waitFor();
//TODO
return new String(b, StandardCharsets.UTF_16LE);
} }
@Override @Override
public List<String> createCommand(List<String> cmd) { public ProcessControl preparePrivilegedCommand(InputStream input, List<String> cmd) throws Exception {
return cmd; return new LocalProcessControl(input, cmd);
}
@Override
public ShellType determineType() {
return ShellTypes.CMD;
} }
} }

View file

@ -7,6 +7,10 @@ import lombok.Getter;
import java.time.Instant; import java.time.Instant;
import java.util.Optional; import java.util.Optional;
/**
* A store that refers to another store in the X-Pipe storage.
* The referenced store has to be resolved by the caller manually, as this class does not act as a resolver.
*/
@JsonTypeName("named") @JsonTypeName("named")
public final class NamedStore implements DataStore { public final class NamedStore implements DataStore {

View file

@ -0,0 +1,79 @@
package io.xpipe.core.store;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.nio.charset.Charset;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
public abstract class ProcessControl {
public String readOutOnly() throws Exception {
start();
var errT = discardErr();
var string = new String(getStdout().readAllBytes(), getCharset());
errT.join();
waitFor();
return string;
}
public Optional<String> readErrOnly() throws Exception {
start();
var outT = discardOut();
AtomicReference<String> read = new AtomicReference<>();
var t = new Thread(() -> {
try {
read.set(new String(getStderr().readAllBytes(), getCharset()));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
t.setDaemon(true);
t.start();
outT.join();
t.join();
var ec = waitFor();
return ec != 0 ? Optional.of(read.get()) : Optional.empty();
}
public Thread discardOut() {
var t = new Thread(() -> {
try {
getStdout().transferTo(OutputStream.nullOutputStream());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
t.setDaemon(true);
t.start();
return t;
}
public Thread discardErr() {
var t = new Thread(() -> {
try {
getStderr().transferTo(OutputStream.nullOutputStream());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
t.setDaemon(true);
t.start();
return t;
}
public abstract void start() throws Exception;
public abstract int waitFor() throws Exception;
public abstract InputStream getStdout();
public abstract InputStream getStderr();
public abstract Charset getCharset();
}

View file

@ -0,0 +1,8 @@
package io.xpipe.core.store;
import java.io.OutputStream;
public interface ProcessHandler {
void handle(OutputStream out, OutputStream err);
}

View file

@ -0,0 +1,37 @@
package io.xpipe.core.store;
import java.io.InputStream;
import java.io.OutputStream;
public interface ShellProcessStore extends StandardShellStore {
ShellType determineType() throws Exception;
@Override
default InputStream openInput(String file) throws Exception {
var type = determineType();
var cmd = type.createFileReadCommand(file);
var p = prepareCommand(InputStream.nullInputStream(), cmd);
p.start();
return p.getStdout();
}
@Override
default OutputStream openOutput(String file) throws Exception {
return null;
// var type = determineType();
// var cmd = type.createFileWriteCommand(file);
// var p = prepare(cmd).redirectErrorStream(true);
// var proc = p.start();
// return proc.getOutputStream();
}
@Override
default boolean exists(String file) throws Exception {
var type = determineType();
var cmd = type.createFileExistsCommand(file);
var p = prepareCommand(InputStream.nullInputStream(), cmd);
p.start();
return p.waitFor() == 0;
}
}

View file

@ -1,19 +1,83 @@
package io.xpipe.core.store; package io.xpipe.core.store;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
public interface ShellStore extends MachineFileStore { public interface ShellStore extends MachineFileStore {
static ShellStore local() { static StandardShellStore local() {
return new LocalStore(); return new LocalStore();
} }
default ProcessBuilder prepare(List<String> cmd) throws Exception { default String executeAndRead(List<String> cmd) throws Exception {
var toExec = createCommand(cmd); var pc = prepareCommand(InputStream.nullInputStream(), cmd);
return new ProcessBuilder(toExec); pc.start();
pc.discardErr();
var string = new String(pc.getStdout().readAllBytes(), pc.getCharset());
return string;
} }
String executeAndRead(List<String> cmd) throws Exception; default Optional<String> executeAndCheckOut(InputStream in, List<String> cmd) throws Exception {
var pc = prepareCommand(in, cmd);
pc.start();
var outT = pc.discardErr();
List<String> createCommand(List<String> cmd); AtomicReference<String> read = new AtomicReference<>();
var t = new Thread(() -> {
try {
read.set(new String(pc.getStdout().readAllBytes(), pc.getCharset()));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
t.setDaemon(true);
t.start();
outT.join();
t.join();
var ec = pc.waitFor();
return ec == 0 ? Optional.of(read.get()) : Optional.empty();
}
default Optional<String> executeAndCheckErr(InputStream in, List<String> cmd) throws Exception {
var pc = prepareCommand(in, cmd);
pc.start();
var outT = pc.discardOut();
AtomicReference<String> read = new AtomicReference<>();
var t = new Thread(() -> {
try {
read.set(new String(pc.getStderr().readAllBytes(), pc.getCharset()));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
t.setDaemon(true);
t.start();
outT.join();
t.join();
var ec = pc.waitFor();
return ec != 0 ? Optional.of(read.get()) : Optional.empty();
}
default ProcessControl prepareCommand(List<String> cmd) throws Exception {
return prepareCommand(InputStream.nullInputStream(), cmd);
}
ProcessControl prepareCommand(InputStream input, List<String> cmd) throws Exception;
default ProcessControl preparePrivilegedCommand(List<String> cmd) throws Exception {
return preparePrivilegedCommand(InputStream.nullInputStream(), cmd);
}
default ProcessControl preparePrivilegedCommand(InputStream input, List<String> cmd) throws Exception {
throw new UnsupportedOperationException();
}
} }

View file

@ -0,0 +1,195 @@
package io.xpipe.core.store;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
public class ShellTypes {
public static StandardShellStore.ShellType determine(ShellStore store) throws Exception {
var o = store.executeAndCheckOut(InputStream.nullInputStream(), List.of("echo", "$0"));
if (o.isPresent() && !o.get().equals("$0")) {
return SH;
} else {
o = store.executeAndCheckOut(InputStream.nullInputStream(), List.of("(dir 2>&1 *`|echo CMD);&<# rem #>echo PowerShell"));
if (o.isPresent() && o.get().equals("PowerShell")) {
return POWERSHELL;
} else {
return CMD;
}
}
}
public static StandardShellStore.ShellType[] getAvailable(ShellStore store) throws Exception {
var o = store.executeAndCheckOut(InputStream.nullInputStream(), List.of("echo", "$0"));
if (o.isPresent() && !o.get().trim().equals("$0")) {
return getLinuxShells();
} else {
return getWindowsShells();
}
}
public static final StandardShellStore.ShellType POWERSHELL = new StandardShellStore.ShellType() {
@Override
public List<String> switchTo(List<String> cmd) {
var l = new ArrayList<>(cmd);
l.add(0, "powershell.exe");
return l;
}
@Override
public List<String> createFileReadCommand(String file) {
return List.of("Get-Content", file);
}
@Override
public List<String> createFileWriteCommand(String file) {
return List.of("Out-File", "-FilePath", file);
}
@Override
public List<String> createFileExistsCommand(String file) {
return List.of("Test-Path", "-path", file);
}
@Override
public Charset getCharset() {
return StandardCharsets.UTF_16LE;
}
@Override
public String getName() {
return "powershell";
}
@Override
public String getDisplayName() {
return "PowerShell";
}
};
public static final StandardShellStore.ShellType CMD = new StandardShellStore.ShellType() {
@Override
public List<String> switchTo(List<String> cmd) {
var l = new ArrayList<>(cmd);
l.add(0, "cmd.exe");
l.add(1, "/c");
return l;
}
@Override
public ProcessControl prepareElevatedCommand(ShellStore st, InputStream in, List<String> cmd, String pw) throws Exception {
var l = List.of("net", "session", ";", "if", "%errorLevel%", "!=", "0");
return st.prepareCommand(InputStream.nullInputStream(), l);
}
@Override
public List<String> createFileReadCommand(String file) {
return List.of("type", file);
}
@Override
public List<String> createFileWriteCommand(String file) {
return List.of("Out-File", "-FilePath", file);
}
@Override
public List<String> createFileExistsCommand(String file) {
return List.of("if", "exist", file, "echo", "hi");
}
@Override
public Charset getCharset() {
return StandardCharsets.UTF_16LE;
}
@Override
public String getName() {
return "cmd";
}
@Override
public String getDisplayName() {
return "cmd.exe";
}
};
public static final StandardShellStore.ShellType SH = new StandardShellStore.ShellType() {
@Override
public List<String> switchTo(List<String> cmd) {
return cmd;
}
@Override
public ProcessControl prepareElevatedCommand(ShellStore st, InputStream in, List<String> cmd, String pw) throws Exception {
var l = new ArrayList<>(cmd);
l.add(0, "sudo");
l.add(1, "-S");
var pws = new ByteArrayInputStream(pw.getBytes(getCharset()));
return st.prepareCommand(pws, l);
}
@Override
public List<String> createFileReadCommand(String file) {
return List.of("cat", file);
}
@Override
public List<String> createFileWriteCommand(String file) {
return List.of(file);
}
@Override
public List<String> createFileExistsCommand(String file) {
return List.of("test", "-f", file, "||", "test", "-d", file);
}
@Override
public Charset getCharset() {
return StandardCharsets.UTF_8;
}
@Override
public String getName() {
return "sh";
}
@Override
public String getDisplayName() {
return "/bin/sh";
}
};
public static StandardShellStore.ShellType getDefault() {
if (System.getProperty("os.name").startsWith("Windows")) {
return CMD;
} else {
return SH;
}
}
public static StandardShellStore.ShellType[] getWindowsShells() {
return new StandardShellStore.ShellType[]{CMD, POWERSHELL};
}
public static StandardShellStore.ShellType[] getLinuxShells() {
return new StandardShellStore.ShellType[]{SH};
}
private final String name;
ShellTypes(String name) {
this.name = name;
}
public String getName() {
return name;
}
}

View file

@ -1,7 +1,6 @@
package io.xpipe.core.store; package io.xpipe.core.store;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.List; import java.util.List;
@ -9,6 +8,12 @@ public interface StandardShellStore extends ShellStore {
static interface ShellType { static interface ShellType {
List<String> switchTo(List<String> cmd);
default ProcessControl prepareElevatedCommand(ShellStore st, InputStream in, List<String> cmd, String pw) throws Exception {
return st.prepareCommand(in, cmd);
}
List<String> createFileReadCommand(String file); List<String> createFileReadCommand(String file);
List<String> createFileWriteCommand(String file); List<String> createFileWriteCommand(String file);
@ -18,44 +23,9 @@ public interface StandardShellStore extends ShellStore {
Charset getCharset(); Charset getCharset();
String getName(); String getName();
String getDisplayName();
} }
default String executeAndRead(List<String> cmd) throws Exception { ShellType determineType() throws Exception;
var type = determineType();
var p = prepare(cmd).redirectErrorStream(true);
var proc = p.start();
var s = new String(proc.getInputStream().readAllBytes(), type.getCharset());
return s;
}
List<String> createCommand(List<String> cmd);
ShellType determineType();
@Override
default InputStream openInput(String file) throws Exception {
var type = determineType();
var cmd = type.createFileReadCommand(file);
var p = prepare(cmd).redirectErrorStream(true);
var proc = p.start();
return proc.getInputStream();
}
@Override
default OutputStream openOutput(String file) throws Exception {
var type = determineType();
var cmd = type.createFileWriteCommand(file);
var p = prepare(cmd).redirectErrorStream(true);
var proc = p.start();
return proc.getOutputStream();
}
@Override
default boolean exists(String file) throws Exception {
var type = determineType();
var cmd = type.createFileExistsCommand(file);
var p = prepare(cmd).redirectErrorStream(true);
var proc = p.start();
return proc.waitFor() == 0;
}
} }

View file

@ -16,6 +16,7 @@ public class StdinDataStore implements StreamDataStore {
@Override @Override
public InputStream openInput() throws Exception { public InputStream openInput() throws Exception {
var in = System.in; var in = System.in;
// Prevent closing the standard in when the returned input stream is closed
return new InputStream() { return new InputStream() {
@Override @Override
public int read() throws IOException { public int read() throws IOException {
@ -87,9 +88,4 @@ public class StdinDataStore implements StreamDataStore {
} }
}; };
} }
@Override
public boolean canOpen() {
return false;
}
} }

View file

@ -14,6 +14,7 @@ public class StdoutDataStore implements StreamDataStore {
@Override @Override
public OutputStream openOutput() throws Exception { public OutputStream openOutput() throws Exception {
// Create an output stream that will write to standard out but will not close it
return new OutputStream() { return new OutputStream() {
@Override @Override
public void write(int b) throws IOException { public void write(int b) throws IOException {
@ -40,9 +41,4 @@ public class StdoutDataStore implements StreamDataStore {
} }
}; };
} }
@Override
public boolean canOpen() {
return false;
}
} }

View file

@ -6,21 +6,30 @@ import java.io.OutputStream;
/** /**
* A data store that can be accessed using InputStreams and/or OutputStreams. * A data store that can be accessed using InputStreams and/or OutputStreams.
* These streams must support mark/reset.
*/ */
public interface StreamDataStore extends DataStore { public interface StreamDataStore extends DataStore {
default boolean isLocalOnly() { /**
* Indicates whether this data store can only be accessed by the current running application.
* One example are standard in and standard out stores.
*
* @see StdinDataStore
* @see StdoutDataStore
*/
default boolean isLocalToApplication() {
return true; return true;
} }
/** /**
* Opens an input stream. This input stream does not necessarily have to be a new instance. * Opens an input stream that can be used to read its data.
*/ */
default InputStream openInput() throws Exception { default InputStream openInput() throws Exception {
throw new UnsupportedOperationException("Can't open store input"); throw new UnsupportedOperationException("Can't open store input");
} }
/**
* Opens an input stream that is guaranteed to be buffered.
*/
default InputStream openBufferedInput() throws Exception { default InputStream openBufferedInput() throws Exception {
var in = openInput(); var in = openInput();
if (in.markSupported()) { if (in.markSupported()) {
@ -31,16 +40,24 @@ public interface StreamDataStore extends DataStore {
} }
/** /**
* Opens an output stream. This output stream does not necessarily have to be a new instance. * Opens an output stream that can be used to write data.
*/ */
default OutputStream openOutput() throws Exception { default OutputStream openOutput() throws Exception {
throw new UnsupportedOperationException("Can't open store output"); throw new UnsupportedOperationException("Can't open store output");
} }
/**
* Checks whether this store can be opened.
* This can be not the case for example if the underlying store does not exist.
*/
default boolean canOpen() throws Exception { default boolean canOpen() throws Exception {
return true; return true;
} }
/**
* Indicates whether this store is persistent, i.e. whether the stored data can be read again or not.
* The caller has to adapt accordingly based on the persistence property.
*/
default boolean persistent() { default boolean persistent() {
return false; return false;
} }

View file

@ -37,7 +37,7 @@ public class CoreJacksonModule extends SimpleModule {
new NamedType(StdoutDataStore.class), new NamedType(StdoutDataStore.class),
new NamedType(LocalDirectoryDataStore.class), new NamedType(LocalDirectoryDataStore.class),
new NamedType(CollectionEntryDataStore.class), new NamedType(CollectionEntryDataStore.class),
new NamedType(StringStore.class), new NamedType(InMemoryStore.class),
new NamedType(LocalStore.class), new NamedType(LocalStore.class),
new NamedType(NamedStore.class), new NamedType(NamedStore.class),
@ -141,7 +141,7 @@ public class CoreJacksonModule extends SimpleModule {
@Override @Override
public Secret deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { public Secret deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return Secret.create(p.getValueAsString()); return new Secret(p.getValueAsString());
} }
} }

View file

@ -10,7 +10,7 @@ import java.util.Base64;
@EqualsAndHashCode @EqualsAndHashCode
public class Secret { public class Secret {
public static Secret create(String s) { public static Secret createForSecretValue(String s) {
if (s == null) { if (s == null) {
return null; return null;
} }

View file

@ -0,0 +1,23 @@
package io.xpipe.core.util;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Objects;
public class StreamHelper {
private static final int DEFAULT_BUFFER_SIZE = 8192;
public static long transferTo(InputStream in, OutputStream out) throws IOException {
Objects.requireNonNull(out, "out");
long transferred = 0;
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
int read;
while (in.available() > 0 && (read = in.read(buffer, 0, DEFAULT_BUFFER_SIZE)) >= 0) {
out.write(buffer, 0, read);
transferred += read;
}
return transferred;
}
}

View file

@ -38,6 +38,10 @@ public interface DataStoreProvider {
default void init() throws Exception { default void init() throws Exception {
} }
default boolean isHidden() {
return false;
}
default String i18n(String key) { default String i18n(String key) {
return I18n.get(getId() + "." + key); return I18n.get(getId() + "." + key);
} }

View file

@ -15,6 +15,10 @@ public interface I18n {
} }
public static ObservableValue<String> observable(String s, Object... vars) { public static ObservableValue<String> observable(String s, Object... vars) {
if (s == null) {
return null;
}
return Bindings.createStringBinding(() -> { return Bindings.createStringBinding(() -> {
return get(s, vars); return get(s, vars);
}); });

View file

@ -93,6 +93,13 @@ public class DynamicOptionsBuilder<T> {
return this; return this;
} }
public DynamicOptionsBuilder<T> addStringArea(String nameKey, Property<String> prop) {
var comp = new TextAreaComp(prop);
entries.add(new DynamicOptionsComp.Entry(I18n.observable(nameKey), comp));
props.add(prop);
return this;
}
public DynamicOptionsBuilder<T> addString(String nameKey, Property<String> prop) { public DynamicOptionsBuilder<T> addString(String nameKey, Property<String> prop) {
var comp = new TextFieldComp(prop); var comp = new TextFieldComp(prop);
entries.add(new DynamicOptionsComp.Entry(I18n.observable(nameKey), comp)); entries.add(new DynamicOptionsComp.Entry(I18n.observable(nameKey), comp));
@ -107,8 +114,9 @@ public class DynamicOptionsBuilder<T> {
return this; return this;
} }
public DynamicOptionsBuilder<T> addComp(ObservableValue<String> name, Comp<?> comp) { public DynamicOptionsBuilder<T> addComp(ObservableValue<String> name, Comp<?> comp, Property<?> prop) {
entries.add(new DynamicOptionsComp.Entry(name, comp)); entries.add(new DynamicOptionsComp.Entry(name, comp));
props.add(prop);
return this; return this;
} }

View file

@ -22,7 +22,7 @@ public class SecretFieldComp extends Comp<CompStructure<TextField>> {
var text = new PasswordField(); var text = new PasswordField();
text.setText(value.getValue() != null ? value.getValue().getSecretValue() : null); text.setText(value.getValue() != null ? value.getValue().getSecretValue() : null);
text.textProperty().addListener((c, o, n) -> { text.textProperty().addListener((c, o, n) -> {
value.setValue(n.length() > 0 ? Secret.create(n) : null); value.setValue(n.length() > 0 ? Secret.createForSecretValue(n) : null);
}); });
value.addListener((c, o, n) -> { value.addListener((c, o, n) -> {
PlatformThread.runLaterIfNeeded(() -> { PlatformThread.runLaterIfNeeded(() -> {

View file

@ -0,0 +1,31 @@
package io.xpipe.extension.comp;
import io.xpipe.fxcomps.Comp;
import io.xpipe.fxcomps.CompStructure;
import io.xpipe.fxcomps.SimpleCompStructure;
import io.xpipe.fxcomps.util.PlatformThread;
import javafx.beans.property.Property;
import javafx.scene.control.TextArea;
public class TextAreaComp extends Comp<CompStructure<TextArea>> {
private final Property<String> value;
public TextAreaComp(Property<String> value) {
this.value = value;
}
@Override
public CompStructure<TextArea> createBase() {
var text = new TextArea(value.getValue() != null ? value.getValue().toString() : null);
text.textProperty().addListener((c, o, n) -> {
value.setValue(n != null && n.length() > 0 ? n : null);
});
value.addListener((c, o, n) -> {
PlatformThread.runLaterIfNeeded(() -> {
text.setText(n);
});
});
return new SimpleCompStructure<>(text);
}
}

View file

@ -9,17 +9,21 @@ public class ExceptionConverter {
public static String convertMessage(Throwable ex) { public static String convertMessage(Throwable ex) {
var msg = ex.getLocalizedMessage(); var msg = ex.getLocalizedMessage();
if (ex instanceof FileNotFoundException) { if (ex instanceof FileNotFoundException) {
return I18n.get("fileNotFound", msg); return I18n.get("extension.fileNotFound", msg);
} }
if (ex instanceof ClassNotFoundException) { if (ex instanceof ClassNotFoundException) {
return I18n.get("classNotFound", msg); return I18n.get("extension.classNotFound", msg);
} }
if (ex instanceof NullPointerException) { if (ex instanceof NullPointerException) {
return I18n.get("extension.nullPointer", msg); return I18n.get("extension.nullPointer", msg);
} }
if (msg == null || msg.trim().length() == 0) {
return I18n.get("extension.noInformationAvailable");
}
return msg; return msg;
} }
} }