mirror of
https://github.com/xpipe-io/xpipe.git
synced 2024-11-25 09:00:26 +00:00
Rework stores and add some documentation
This commit is contained in:
parent
4eb0c80d60
commit
0f37600c96
29 changed files with 725 additions and 187 deletions
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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";
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
||||||
|
|
79
core/src/main/java/io/xpipe/core/store/ProcessControl.java
Normal file
79
core/src/main/java/io/xpipe/core/store/ProcessControl.java
Normal 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();
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package io.xpipe.core.store;
|
||||||
|
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
public interface ProcessHandler {
|
||||||
|
|
||||||
|
void handle(OutputStream out, OutputStream err);
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
195
core/src/main/java/io/xpipe/core/store/ShellTypes.java
Normal file
195
core/src/main/java/io/xpipe/core/store/ShellTypes.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
23
core/src/main/java/io/xpipe/core/util/StreamHelper.java
Normal file
23
core/src/main/java/io/xpipe/core/util/StreamHelper.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(() -> {
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue