Rework exchanges, rework stores, add dialog API

This commit is contained in:
Christopher Schnick 2022-06-17 05:24:09 +02:00
parent b8164339d0
commit 9440037f03
45 changed files with 711 additions and 428 deletions

View file

@ -2,7 +2,7 @@ package io.xpipe.beacon.exchange;
import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage;
import io.xpipe.core.source.DataSourceConfigInstance;
import io.xpipe.core.dialog.DialogReference;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataSourceReference;
import lombok.Builder;
@ -11,13 +11,13 @@ import lombok.Value;
import lombok.extern.jackson.Jacksonized;
/**
* Performs an edit for a data source.
* Requests to edit a data source.
*/
public class EditExecuteExchange implements MessageExchange {
public class EditExchange implements MessageExchange {
@Override
public String getId() {
return "editExecute";
return "edit";
}
@Jacksonized
@ -26,15 +26,15 @@ public class EditExecuteExchange implements MessageExchange {
public static class Request implements RequestMessage {
@NonNull
DataSourceReference ref;
@NonNull
DataSourceConfigInstance config;
}
@Jacksonized
@Builder
@Value
public static class Response implements ResponseMessage {
@NonNull
DialogReference config;
DataSourceId id;
}
}

View file

@ -2,13 +2,17 @@ package io.xpipe.beacon.exchange;
import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage;
import io.xpipe.core.source.*;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataSourceInfo;
import io.xpipe.core.source.DataSourceReference;
import io.xpipe.core.store.DataStore;
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import java.util.Map;
/**
* Queries general information about a data source.
*/
@ -38,6 +42,8 @@ public class QueryDataSourceExchange implements MessageExchange {
@NonNull
DataStore store;
@NonNull
DataSourceConfigInstance config;
String provider;
@NonNull
Map<String, String> config;
}
}

View file

@ -2,7 +2,7 @@ package io.xpipe.beacon.exchange;
import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage;
import io.xpipe.core.source.DataSourceConfigInstance;
import io.xpipe.core.dialog.DialogReference;
import io.xpipe.core.store.DataStore;
import lombok.Builder;
import lombok.NonNull;
@ -38,6 +38,6 @@ public class ReadPreparationExchange implements MessageExchange {
String determinedType;
@NonNull
DataSourceConfigInstance config;
DialogReference config;
}
}

View file

@ -2,7 +2,7 @@ package io.xpipe.beacon.exchange;
import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage;
import io.xpipe.core.store.StreamDataStore;
import io.xpipe.core.store.FileStore;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
@ -27,6 +27,6 @@ public class StoreStreamExchange implements MessageExchange {
@Builder
@Value
public static class Response implements ResponseMessage {
StreamDataStore store;
FileStore store;
}
}

View file

@ -3,7 +3,7 @@ package io.xpipe.beacon.exchange.cli;
import io.xpipe.beacon.exchange.MessageExchange;
import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage;
import io.xpipe.core.source.DataSourceConfigInstance;
import io.xpipe.core.dialog.DialogReference;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataSourceReference;
import io.xpipe.core.source.DataSourceType;
@ -38,6 +38,6 @@ public class ConvertExchange implements MessageExchange {
@Value
public static class Response implements ResponseMessage {
@NonNull
DataSourceConfigInstance config;
DialogReference config;
}
}

View file

@ -0,0 +1,33 @@
package io.xpipe.beacon.exchange.cli;
import io.xpipe.beacon.exchange.MessageExchange;
import io.xpipe.beacon.exchange.data.StoreListEntry;
import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import java.util.List;
public class ListStoresExchange implements MessageExchange {
@Override
public String getId() {
return "listStores";
}
@Jacksonized
@Builder
@Value
public static class Request implements RequestMessage {
}
@Jacksonized
@Builder
@Value
public static class Response implements ResponseMessage {
List<StoreListEntry> entries;
}
}

View file

@ -1,5 +1,6 @@
package io.xpipe.beacon.exchange;
package io.xpipe.beacon.exchange.cli;
import io.xpipe.beacon.exchange.MessageExchange;
import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage;
import lombok.Builder;

View file

@ -1,5 +1,6 @@
package io.xpipe.beacon.exchange;
package io.xpipe.beacon.exchange.cli;
import io.xpipe.beacon.exchange.MessageExchange;
import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage;
import io.xpipe.core.source.DataSourceId;

View file

@ -1,22 +1,18 @@
package io.xpipe.beacon.exchange;
package io.xpipe.beacon.exchange.cli;
import io.xpipe.beacon.exchange.MessageExchange;
import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage;
import io.xpipe.core.source.DataSourceConfigInstance;
import io.xpipe.core.source.DataSourceReference;
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
/**
* Requests to edit a data source.
*/
public class EditPreparationExchange implements MessageExchange {
public class RemoveStoreExchange implements MessageExchange {
@Override
public String getId() {
return "editPreparation";
return "removeStore";
}
@Jacksonized
@ -24,13 +20,14 @@ public class EditPreparationExchange implements MessageExchange {
@Value
public static class Request implements RequestMessage {
@NonNull
DataSourceReference ref;
String storeName;
boolean removeUnderlying;
}
@Jacksonized
@Builder
@Value
public static class Response implements ResponseMessage {
DataSourceConfigInstance config;
}
}

View file

@ -1,5 +1,6 @@
package io.xpipe.beacon.exchange;
package io.xpipe.beacon.exchange.cli;
import io.xpipe.beacon.exchange.MessageExchange;
import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage;
import lombok.Builder;

View file

@ -1,5 +1,6 @@
package io.xpipe.beacon.exchange;
package io.xpipe.beacon.exchange.cli;
import io.xpipe.beacon.exchange.MessageExchange;
import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage;
import io.xpipe.core.source.DataSourceId;

View file

@ -0,0 +1,33 @@
package io.xpipe.beacon.exchange.cli;
import io.xpipe.beacon.exchange.MessageExchange;
import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage;
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
public class RenameStoreExchange implements MessageExchange {
@Override
public String getId() {
return "renameStore";
}
@Jacksonized
@Builder
@Value
public static class Request implements RequestMessage {
@NonNull
String storeName;
@NonNull
String newName;
}
@Jacksonized
@Builder
@Value
public static class Response implements ResponseMessage {
}
}

View file

@ -3,14 +3,12 @@ package io.xpipe.beacon.exchange.cli;
import io.xpipe.beacon.exchange.MessageExchange;
import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage;
import io.xpipe.core.dialog.DialogElement;
import io.xpipe.core.dialog.DialogReference;
import io.xpipe.core.store.DataStore;
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import java.util.UUID;
public class StoreAddExchange implements MessageExchange {
@Override
@ -22,15 +20,17 @@ public class StoreAddExchange implements MessageExchange {
@Builder
@Value
public static class Request implements RequestMessage {
@NonNull
String input;
DataStore storeInput;
String type;
String name;
}
@Jacksonized
@Builder
@Value
public static class Response implements ResponseMessage {
UUID dialogKey;
DialogElement dialogElement;
DialogReference config;
}
}

View file

@ -3,7 +3,7 @@ package io.xpipe.beacon.exchange.cli;
import io.xpipe.beacon.exchange.MessageExchange;
import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage;
import io.xpipe.core.source.DataSourceConfigInstance;
import io.xpipe.core.dialog.DialogReference;
import io.xpipe.core.source.DataSourceReference;
import io.xpipe.core.store.StreamDataStore;
import lombok.Builder;
@ -37,6 +37,6 @@ public class WritePreparationExchange implements MessageExchange {
@Value
public static class Response implements ResponseMessage {
@NonNull
DataSourceConfigInstance config;
DialogReference config;
}
}

View file

@ -0,0 +1,14 @@
package io.xpipe.beacon.exchange.data;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
@Value
@Jacksonized
@Builder
public class StoreListEntry {
String name;
String type;
}

View file

@ -29,6 +29,8 @@ module io.xpipe.beacon {
ModeExchange,
StatusExchange,
StopExchange,
RenameStoreExchange,
RemoveStoreExchange,
StoreAddExchange,
WritePreparationExchange,
WriteExecuteExchange,
@ -36,11 +38,11 @@ module io.xpipe.beacon {
ReadPreparationExchange,
QueryTextDataExchange,
ReadExecuteExchange,
ListStoresExchange,
DialogExchange,
QueryDataSourceExchange,
StoreStreamExchange,
EditPreparationExchange,
EditExecuteExchange,
EditExchange,
RemoveEntryExchange,
RemoveCollectionExchange,
RenameCollectionExchange,

View file

@ -1,4 +0,0 @@
package io.xpipe.core.connection;
public class Connection {
}

View file

@ -9,13 +9,21 @@ import lombok.Getter;
public class BaseQueryElement extends DialogElement {
private final String description;
private final boolean newLine;
private final boolean required;
private final boolean hidden;
protected String value;
@JsonCreator
public BaseQueryElement(String description, boolean required, String value) {
public BaseQueryElement(String description, boolean newLine, boolean required, boolean hidden, String value) {
this.description = description;
this.newLine = newLine;
this.required = required;
this.hidden = hidden;
this.value = value;
}
public boolean isNewLine() {
return newLine;
}
}

View file

@ -0,0 +1,12 @@
package io.xpipe.core.dialog;
import com.fasterxml.jackson.annotation.JsonTypeName;
@JsonTypeName("busy")
public class BusyElement extends DialogElement {
@Override
public boolean apply(String value) {
return true;
}
}

View file

@ -0,0 +1,15 @@
package io.xpipe.core.dialog;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
@Value
@Builder
@Jacksonized
@AllArgsConstructor
public class Choice {
Character character;
String description;
}

View file

@ -2,17 +2,14 @@ package io.xpipe.core.dialog;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonTypeName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import java.util.List;
@JsonTypeName("choice")
public class ChoiceElement extends DialogElement {
private final List<Element> elements;
private final String description;
private final List<Choice> elements;
private int selected;
@ -42,26 +39,22 @@ public class ChoiceElement extends DialogElement {
return false;
}
@Value
@Builder
@Jacksonized
@AllArgsConstructor
public static class Element {
Character character;
String description;
}
@JsonCreator
public ChoiceElement(List<Element> elements, int selected) {
public ChoiceElement(String description, List<Choice> elements, int selected) {
this.description = description;
this.elements = elements;
this.selected = selected;
}
public List<Element> getElements() {
public List<Choice> getElements() {
return elements;
}
public int getSelected() {
return selected;
}
public String getDescription() {
return description;
}
}

View file

@ -1,42 +1,112 @@
package io.xpipe.core.dialog;
import io.xpipe.core.util.Secret;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
public abstract class Dialog {
private static class Sequence extends Dialog {
private int index = 0;
private final DialogElement[] es;
public Sequence(DialogElement... es) {
this.es = es;
}
@Override
public DialogElement start() {
index = 0;
return es[0];
}
@Override
public DialogElement receive(String answer) {
if (es[index].apply(answer)) {
if (index == es.length - 1) {
complete();
return null;
} else {
return es[++index];
}
public static Dialog empty() {
return new Dialog() {
@Override
public DialogElement start() throws Exception {
return null;
}
return es[index];
@Override
protected DialogElement next(String answer) throws Exception {
return null;
}
};
}
public static class Choice extends Dialog {
private final ChoiceElement element;
private Choice(String description, List<io.xpipe.core.dialog.Choice> elements, int selected) {
this.element = new ChoiceElement(description, elements, selected);
}
@Override
public DialogElement start() throws Exception {
return element;
}
@Override
protected DialogElement next(String answer) throws Exception {
if (element.apply(answer)) {
return null;
}
return element;
}
private int getSelected() {
return element.getSelected();
}
}
public static Dialog chain(DialogElement... es) {
return new Dialog.Sequence(es);
public static Dialog.Choice choice(String description, List<io.xpipe.core.dialog.Choice> elements, int selected) {
Dialog.Choice c = new Dialog.Choice(description, elements, selected);
c.evaluateTo(c::getSelected);
return c;
}
@SafeVarargs
public static <T> Dialog.Choice choice(String description, Function<T, String> toString, T def, T... vals) {
var elements = Arrays.stream(vals).map(v -> new io.xpipe.core.dialog.Choice(null, toString.apply(v))).toList();
var index = Arrays.asList(vals).indexOf(def);
var c = choice(description, elements, index);
c.evaluateTo(() -> vals[c.getSelected()]);
return c;
}
public static class Query extends Dialog {
private final QueryElement element;
private Query(String description, boolean newLine, boolean required, Object value, QueryConverter<?> converter, boolean hidden) {
this.element = new QueryElement(description, newLine, required,value, converter, hidden);
}
@Override
public Optional<Map.Entry<String, String>> toValue() {
return Optional.of(new AbstractMap.SimpleEntry<>(element.getDescription(), element.getValue()));
}
@Override
public DialogElement start() throws Exception {
return element;
}
@Override
protected DialogElement next(String answer) throws Exception {
if (element.apply(answer)) {
return null;
}
return element;
}
private <T> T getConvertedValue() {
return element.getConvertedValue();
}
}
public static Dialog.Query query(String description, boolean newLine, boolean required, Object value, QueryConverter<?> converter) {
var q = new Dialog.Query(description, newLine, required, value, converter, false);
q.evaluateTo(q::getConvertedValue);
return q;
}
public static Dialog.Query querySecret(String description, boolean newLine, boolean required, Secret value) {
var q = new Dialog.Query(description, newLine, required, value, QueryConverter.SECRET, true);
q.evaluateTo(q::getConvertedValue);
return q;
}
public static Dialog chain(Dialog... ds) {
@ -45,50 +115,100 @@ public abstract class Dialog {
private int current = 0;
@Override
public DialogElement start() {
public DialogElement start() throws Exception {
current = 0;
eval = null;
return ds[0].start();
}
@Override
public DialogElement receive(String answer) {
protected DialogElement next(String answer) throws Exception {
DialogElement currentElement = ds[current].receive(answer);
if (currentElement == null) {
ds[current].complete();
if (current == ds.length - 1) {
complete();
return null;
} else {
return ds[++current].start();
}
DialogElement next = null;
while (current < ds.length - 1 && (next = ds[++current].start()) == null) {
};
return next;
}
return currentElement;
}
};
}.evaluateTo(ds[ds.length - 1]);
}
public static Dialog repeatIf(Dialog d, Supplier<Boolean> shouldRepeat) {
public static <T> Dialog repeatIf(Dialog d, Predicate<T> shouldRepeat) {
return new Dialog() {
@Override
public DialogElement start() {
public DialogElement start() throws Exception {
eval = null;
return d.start();
}
@Override
public DialogElement receive(String answer) {
protected DialogElement next(String answer) throws Exception {
var next = d.receive(answer);
if (next == null) {
if (shouldRepeat.get()) {
if (shouldRepeat.test(d.getResult())) {
return d.start();
}
}
return next;
}
}.evaluateTo(d.onCompletion);
}.evaluateTo(d).onCompletion(d.completion);
}
public static Dialog header(String msg) {
return of(new HeaderElement(msg)).evaluateTo(() -> msg);
}
public static Dialog header(Supplier<String> msg) {
final String[] msgEval = {null};
return new Dialog() {
@Override
public DialogElement start() throws Exception {
msgEval[0] = msg.get();
return new HeaderElement(msgEval[0]);
}
@Override
protected DialogElement next(String answer) throws Exception {
return null;
}
}.evaluateTo(() -> msgEval[0]);
}
public static Dialog busy() {
return of(new BusyElement());
}
public static interface FailableSupplier<T> {
T get() throws Exception;
}
public static Dialog lazy(FailableSupplier<Dialog> d) {
return new Dialog() {
Dialog dialog;
@Override
public DialogElement start() throws Exception {
eval = null;
dialog = d.get();
evaluateTo(dialog);
return dialog.start();
}
@Override
protected DialogElement next(String answer) throws Exception {
return dialog.receive(answer);
}
};
}
public static Dialog of(DialogElement e) {
@ -96,14 +216,14 @@ public abstract class Dialog {
@Override
public DialogElement start() {
public DialogElement start() throws Exception {
eval = null;
return e;
}
@Override
public DialogElement receive(String answer) {
protected DialogElement next(String answer) throws Exception {
if (e.apply(answer)) {
complete();
return null;
}
@ -112,18 +232,38 @@ public abstract class Dialog {
};
}
public static Dialog retryIf(Dialog d, Supplier<String> msg) {
public static Dialog skipIf(Dialog d, Supplier<Boolean> check) {
return new Dialog() {
private Dialog active;
@Override
public DialogElement start() throws Exception {
active = check.get() ? null : d;
return active != null ? active.start() : null;
}
@Override
protected DialogElement next(String answer) throws Exception {
return active != null ? active.receive(answer) : null;
}
}.evaluateTo(d).onCompletion(d.completion);
}
public static <T> Dialog retryIf(Dialog d, Function<T, String> msg) {
return new Dialog() {
private boolean retry;
@Override
public DialogElement start() {
public DialogElement start() throws Exception {
eval = null;
return d.start();
}
@Override
public DialogElement receive(String answer) {
protected DialogElement next(String answer) throws Exception {
if (retry) {
retry = false;
return d.start();
@ -131,7 +271,7 @@ public abstract class Dialog {
var next = d.receive(answer);
if (next == null) {
var s = msg.get();
var s = msg.apply(d.getResult());
if (s != null) {
retry = true;
return new HeaderElement(s);
@ -140,47 +280,78 @@ public abstract class Dialog {
return next;
}
}.evaluateTo(d.onCompletion);
}.evaluateTo(d.evaluation).onCompletion(d.completion);
}
public static Dialog choice(ChoiceElement choice, Function<Integer, Dialog> c) {
public static Dialog fork(String description, List<io.xpipe.core.dialog.Choice> elements, int selected, Function<Integer, Dialog> c) {
var choice = new ChoiceElement(description, elements, selected);
return new Dialog() {
private Dialog choiceMade;
@Override
public DialogElement start() {
public DialogElement start() throws Exception {
choiceMade = null;
eval = null;
return choice;
}
@Override
public DialogElement receive(String answer) {
protected DialogElement next(String answer) throws Exception {
if (choiceMade != null) {
var r = choiceMade.receive(answer);
if (r == null) {
complete();
}
return r;
}
if (choice.apply(answer)) {
choiceMade = c.apply(choice.getSelected());
return choiceMade.start();
return choiceMade != null ? choiceMade.start() : null;
}
return choice;
}
};
}.evaluateTo(() -> choice.getSelected());
}
private Object eval;
private Supplier<?> onCompletion;
protected Object eval;
private Supplier<?> evaluation;
private final List<Consumer<?>> completion = new ArrayList<>();
public abstract DialogElement start();
public abstract DialogElement start() throws Exception;
public Optional<Map.Entry<String, String>> toValue() {
return Optional.empty();
}
public Dialog evaluateTo(Dialog d) {
evaluation = d.evaluation;
return this;
}
public Dialog evaluateTo(Supplier<?> s) {
onCompletion = s;
evaluation = s;
return this;
}
@SuppressWarnings("unchecked")
public <T> Dialog map(Function<T, ?> s) {
var oldEval = evaluation;
evaluation = () -> s.apply((T) oldEval.get());
return this;
}
public Dialog onCompletion(Consumer<?> s) {
completion.add(s);
return this;
}
public Dialog onCompletion(Runnable r) {
completion.add(v -> r.run());
return this;
}
public Dialog onCompletion(List<Consumer<?>> s) {
completion.addAll(s);
return this;
}
@ -189,11 +360,24 @@ public abstract class Dialog {
return (T) eval;
}
public void complete() {
if (onCompletion != null) {
eval = onCompletion.get();
@SuppressWarnings("unchecked")
public <T> void complete() {
if (evaluation != null) {
eval = evaluation.get();
completion.forEach(c -> {
Consumer<T> ct = (Consumer<T>) c;
ct.accept((T) eval);
});
}
}
public abstract DialogElement receive(String answer);
public final DialogElement receive(String answer) throws Exception {
var next = next(answer);
if (next == null) {
complete();
}
return next;
}
protected abstract DialogElement next(String answer) throws Exception;
}

View file

@ -0,0 +1,18 @@
package io.xpipe.core.dialog;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import java.util.UUID;
@Value
@Builder
@Jacksonized
@AllArgsConstructor
public class DialogReference {
UUID dialogId;
DialogElement start;
}

View file

@ -1,8 +1,12 @@
package io.xpipe.core.dialog;
import java.net.MalformedURLException;
import java.net.URL;
import io.xpipe.core.util.Secret;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.util.AbstractMap;
import java.util.Map;
public abstract class QueryConverter<T> {
@ -30,18 +34,51 @@ public abstract class QueryConverter<T> {
}
};
public static final QueryConverter<URL> URL = new QueryConverter<URL>() {
public static final QueryConverter<Secret> SECRET = new QueryConverter<Secret>() {
@Override
protected URL fromString(String s) {
protected Secret fromString(String s) {
return Secret.parse(s);
}
@Override
protected String toString(Secret value) {
return value.getValue();
}
};
public static final QueryConverter<Map.Entry<String, String>> HTTP_HEADER = new QueryConverter<Map.Entry<String, String>>() {
@Override
protected Map.Entry<String, String> fromString(String s) {
if (!s.contains(":")) {
throw new IllegalArgumentException("Missing colon");
}
var split = s.split(":");
if (split.length != 2) {
throw new IllegalArgumentException("Too many colons");
}
return new AbstractMap.SimpleEntry<>(split[0].trim(), split[1].trim());
}
@Override
protected String toString(Map.Entry<String, String> value) {
return value.getKey() + ": " + value.getValue();
}
};
public static final QueryConverter<URI> URI = new QueryConverter<URI>() {
@Override
protected URI fromString(String s) {
try {
return new URL(s);
} catch (MalformedURLException e) {
throw new IllegalArgumentException(e);
return new URI(s);
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e.getMessage());
}
}
@Override
protected String toString(URL value) {
protected String toString(URI value) {
return value.toString();
}
};

View file

@ -1,21 +1,14 @@
package io.xpipe.core.dialog;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
@JsonSerialize(as = BaseQueryElement.class)
public class QueryElement extends BaseQueryElement {
@JsonIgnore
private final QueryConverter<?> converter;
public QueryElement(String description, boolean required, String value, QueryConverter<?> converter) {
super(description, required, value);
this.converter = converter;
}
public QueryElement(String description, boolean required, Object value, QueryConverter<?> converter) {
super(description, required, value != null ? value.toString() : null);
public QueryElement(String description, boolean newLine, boolean required, Object value, QueryConverter<?> converter, boolean hidden) {
super(description, newLine, required, hidden, value != null ? value.toString() : null);
this.converter = converter;
}

View file

@ -1,6 +1,6 @@
package io.xpipe.core.store;
public abstract class CollectionEntryDataStore implements FileDataStore {
public abstract class CollectionEntryDataStore implements StreamDataStore, FilenameStore {
private final boolean directory;
private final String name;

View file

@ -1,19 +0,0 @@
package io.xpipe.core.store;
import com.fasterxml.jackson.annotation.JsonTypeName;
import lombok.Value;
import java.io.InputStream;
@Value
@JsonTypeName("commandInput")
public class CommandInputStore implements StreamDataStore {
String cmd;
@Override
public InputStream openInput() throws Exception {
var proc = Runtime.getRuntime().exec(cmd);
return proc.getInputStream();
}
}

View file

@ -16,6 +16,17 @@ import java.util.Optional;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
public interface DataStore {
default void validate() throws Exception {
}
default boolean delete() throws Exception {
return false;
}
default String toDisplay() {
return null;
}
/**
* Casts this instance to the required type without checking whether a cast is possible.
*/

View file

@ -0,0 +1,44 @@
package io.xpipe.core.store;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonTypeName;
import lombok.Value;
import java.nio.file.Path;
@Value
@JsonTypeName("file")
public class FileStore implements StreamDataStore, FilenameStore {
public static FileStore local(Path p) {
return new FileStore(MachineStore.local(), p.toString());
}
public static FileStore local(String p) {
return new FileStore(MachineStore.local(), p);
}
MachineStore machine;
String file;
@JsonCreator
public FileStore(MachineStore machine, String file) {
this.machine = machine;
this.file = file;
}
@Override
public String toDisplay() {
return file + "@" + machine.toDisplay();
}
@Override
public boolean persistent() {
return true;
}
@Override
public String getFileName() {
return file;
}
}

View file

@ -2,7 +2,7 @@ package io.xpipe.core.store;
import java.util.Optional;
public interface FileDataStore extends StreamDataStore {
public interface FilenameStore extends DataStore {
@Override
default Optional<String> determineDefaultName() {
@ -11,10 +11,5 @@ public interface FileDataStore extends StreamDataStore {
return Optional.of(i != -1 ? n.substring(0, i) : n);
}
@Override
default boolean persistent() {
return true;
}
String getFileName();
}

View file

@ -1,52 +0,0 @@
package io.xpipe.core.store;
import com.fasterxml.jackson.annotation.JsonTypeName;
import lombok.Value;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Map;
import java.util.Optional;
@Value
@JsonTypeName("httpRequest")
public class HttpRequestStore implements StreamDataStore {
public static boolean isHttpRequest(String s) {
return s.startsWith("http:") || s.startsWith("https:");
}
public static Optional<HttpRequestStore> fromString(String s) {
try {
var uri = new URI(s);
return Optional.of(new HttpRequestStore(uri, Map.of()));
} catch (URISyntaxException e) {
return Optional.empty();
}
}
URI uri;
Map<String, String> headers;
@Override
public InputStream openInput() throws Exception {
var b = HttpRequest.newBuilder().uri(uri);
headers.forEach(b::setHeader);
var req = b.GET().build();
var client = HttpClient.newHttpClient();
var res = client.send(req, HttpResponse.BodyHandlers.ofByteArray());
return new ByteArrayInputStream(res.body());
}
@Override
public boolean exists() {
return false;
}
}

View file

@ -1,70 +0,0 @@
package io.xpipe.core.store;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonTypeName;
import lombok.EqualsAndHashCode;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.Instant;
import java.util.Optional;
@JsonTypeName("local")
@EqualsAndHashCode
public class LocalFileDataStore implements FileDataStore {
private final Path file;
@JsonCreator
public LocalFileDataStore(Path file) {
this.file = file;
}
public String toString() {
return getFileName();
}
@Override
public Optional<Instant> determineLastModified() {
try {
var l = Files.getLastModifiedTime(file);
return Optional.of(l.toInstant());
} catch (IOException e) {
return Optional.empty();
}
}
public Path getFile() {
return file;
}
@Override
public InputStream openInput() throws Exception {
return new BufferedInputStream(Files.newInputStream(file));
}
@Override
public OutputStream openOutput() throws Exception {
return Files.newOutputStream(file);
}
@Override
public OutputStream openAppendingOutput() throws Exception {
return Files.newOutputStream(file, StandardOpenOption.APPEND);
}
@Override
public boolean exists() {
return Files.exists(file);
}
@Override
public String getFileName() {
return file.getFileName().toString();
}
}

View file

@ -0,0 +1,29 @@
package io.xpipe.core.store;
import com.fasterxml.jackson.annotation.JsonTypeName;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
@JsonTypeName("local")
public class LocalMachineStore implements MachineStore {
@Override
public String toDisplay() {
return "local";
}
@Override
public InputStream openInput(String file) throws Exception {
var p = Path.of(file);
return Files.newInputStream(p);
}
@Override
public OutputStream openOutput(String file) throws Exception {
var p = Path.of(file);
return Files.newOutputStream(p);
}
}

View file

@ -0,0 +1,16 @@
package io.xpipe.core.store;
import java.io.InputStream;
import java.io.OutputStream;
public interface MachineStore extends DataStore {
static MachineStore local() {
return new LocalMachineStore();
}
InputStream openInput(String file) throws Exception;
OutputStream openOutput(String file) throws Exception;
}

View file

@ -1,34 +0,0 @@
package io.xpipe.core.store;
import java.io.InputStream;
import java.io.OutputStream;
import java.time.Instant;
import java.util.Optional;
public class RemoteFileDataStore implements StreamDataStore {
@Override
public Optional<String> determineDefaultName() {
return Optional.empty();
}
@Override
public Optional<Instant> determineLastModified() {
return Optional.empty();
}
@Override
public InputStream openInput() throws Exception {
return null;
}
@Override
public OutputStream openOutput() throws Exception {
return null;
}
@Override
public boolean exists() {
return false;
}
}

View file

@ -1,14 +1,7 @@
package io.xpipe.core.store;
import lombok.NonNull;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.Optional;
/**
* A data store that can be accessed using InputStreams and/or OutputStreams.
@ -16,21 +9,6 @@ import java.util.Optional;
*/
public interface StreamDataStore extends DataStore {
static Optional<StreamDataStore> fromString(@NonNull String s) {
try {
var path = Path.of(s);
return Optional.of(new LocalFileDataStore(path));
} catch (InvalidPathException ignored) {
}
try {
var path = new URL(s);
} catch (MalformedURLException ignored) {
}
return Optional.empty();
}
/**
* Opens an input stream. This input stream does not necessarily have to be a new instance.
*/

View file

@ -11,19 +11,20 @@ import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.jsontype.NamedType;
import com.fasterxml.jackson.databind.module.SimpleModule;
import io.xpipe.core.dialog.BaseQueryElement;
import io.xpipe.core.dialog.ChoiceElement;
import io.xpipe.core.dialog.HeaderElement;
import io.xpipe.core.data.type.ArrayType;
import io.xpipe.core.data.type.TupleType;
import io.xpipe.core.data.type.ValueType;
import io.xpipe.core.data.type.WildcardType;
import io.xpipe.core.dialog.BaseQueryElement;
import io.xpipe.core.dialog.BusyElement;
import io.xpipe.core.dialog.ChoiceElement;
import io.xpipe.core.dialog.HeaderElement;
import io.xpipe.core.source.DataSourceInfo;
import io.xpipe.core.source.DataSourceReference;
import io.xpipe.core.store.CollectionEntryDataStore;
import io.xpipe.core.store.HttpRequestStore;
import io.xpipe.core.store.FileStore;
import io.xpipe.core.store.LocalDirectoryDataStore;
import io.xpipe.core.store.LocalFileDataStore;
import io.xpipe.core.store.LocalMachineStore;
import java.io.IOException;
import java.nio.charset.Charset;
@ -34,10 +35,9 @@ public class CoreJacksonModule extends SimpleModule {
@Override
public void setupModule(SetupContext context) {
context.registerSubtypes(
new NamedType(LocalFileDataStore.class),
new NamedType(FileStore.class),
new NamedType(LocalDirectoryDataStore.class),
new NamedType(CollectionEntryDataStore.class),
new NamedType(HttpRequestStore.class),
new NamedType(ValueType.class),
new NamedType(TupleType.class),
new NamedType(ArrayType.class),
@ -49,6 +49,8 @@ public class CoreJacksonModule extends SimpleModule {
new NamedType(DataSourceInfo.Raw.class),
new NamedType(BaseQueryElement.class),
new NamedType(ChoiceElement.class),
new NamedType(BusyElement.class),
new NamedType(LocalMachineStore.class),
new NamedType(HeaderElement.class)
);
@ -58,6 +60,9 @@ public class CoreJacksonModule extends SimpleModule {
addSerializer(Path.class, new LocalPathSerializer());
addDeserializer(Path.class, new LocalPathDeserializer());
addSerializer(Secret.class, new SecretSerializer());
addDeserializer(Secret.class, new SecretDeserializer());
addSerializer(DataSourceReference.class, new DataSourceReferenceSerializer());
addDeserializer(DataSourceReference.class, new DataSourceReferenceDeserializer());
@ -119,6 +124,23 @@ public class CoreJacksonModule extends SimpleModule {
}
}
public static class SecretSerializer extends JsonSerializer<Secret> {
@Override
public void serialize(Secret value, JsonGenerator jgen, SerializerProvider provider)
throws IOException {
jgen.writeString(value.getValue());
}
}
public static class SecretDeserializer extends JsonDeserializer<Secret> {
@Override
public Secret deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return Secret.parse(p.getValueAsString());
}
}
@JsonSerialize(as = Throwable.class)
public abstract static class ThrowableTypeMixIn {

View file

@ -0,0 +1,30 @@
package io.xpipe.core.util;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
@AllArgsConstructor
@EqualsAndHashCode
public class Secret {
public static Secret parse(String s) {
return new Secret(Base64.getEncoder().encodeToString(s.getBytes(StandardCharsets.UTF_8)));
}
String value;
public String getDisplay() {
return "*".repeat(value.length());
}
public String getValue() {
return value;
}
public String getSecret() {
return new String(Base64.getDecoder().decode(value), StandardCharsets.UTF_8);
}
}

View file

@ -1,8 +1,8 @@
package io.xpipe.extension;
import io.xpipe.charsetter.NewLine;
import io.xpipe.core.dialog.Dialog;
import io.xpipe.core.dialog.QueryConverter;
import io.xpipe.core.dialog.ConfigParameter;
import io.xpipe.core.source.DataSource;
import io.xpipe.core.source.DataSourceType;
import io.xpipe.core.store.DataStore;
@ -10,10 +10,10 @@ import javafx.beans.property.Property;
import javafx.scene.layout.Region;
import lombok.SneakyThrows;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
public interface DataSourceProvider<T extends DataSource<?>> {
@ -90,57 +90,27 @@ public interface DataSourceProvider<T extends DataSource<?>> {
}
interface ConfigProvider<T extends DataSource<?>> {
public static Dialog charset(Charset c) {
return Dialog.query("charset", false, false, c, QueryConverter.CHARSET);
}
static <T extends DataSource<?>> ConfigProvider<T> empty(List<String> names, Function<DataStore, T> func) {
return new ConfigProvider<>() {
@Override
public void applyConfig(T source, Map<ConfigParameter, Object> values) {
}
public static Dialog newLine(NewLine l) {
return Dialog.query("newline", false, false, l, NEW_LINE_CONVERTER);
}
@Override
public Map<ConfigParameter, Function<T, Object>> toCompleteConfig() {
return Map.of();
}
@Override
public Map<ConfigParameter, Object> toRequiredReadConfig(T desc) {
return Map.of();
}
@Override
public List<String> getPossibleNames() {
return names;
}
};
public static final QueryConverter<NewLine> NEW_LINE_CONVERTER = new QueryConverter<NewLine>() {
@Override
protected NewLine fromString(String s) {
return NewLine.id(s);
}
ConfigParameter CHARSET = new ConfigParameter(
"charset", QueryConverter.CHARSET);
@Override
protected String toString(NewLine value) {
return value.getId();
}
};
public static final QueryConverter<NewLine> NEW_LINE_CONVERTER = new QueryConverter<NewLine>() {
@Override
protected NewLine fromString(String s) {
return NewLine.id(s);
}
@Override
protected String toString(NewLine value) {
return value.getId();
}
};
ConfigParameter NEWLINE = new ConfigParameter(
"newline", NEW_LINE_CONVERTER);
void applyConfig(T source, Map<ConfigParameter, Object> values);
Map<ConfigParameter, Function<T, Object>> toCompleteConfig();
Map<ConfigParameter, Object> toRequiredReadConfig(T desc);
List<String> getPossibleNames();
}
Dialog configDialog(T source, boolean all);
default boolean isHidden() {
return false;
@ -184,12 +154,8 @@ public interface DataSourceProvider<T extends DataSource<?>> {
return false;
}
ConfigProvider<T> getConfigProvider();
default String getId() {
var n = getClass().getPackageName();
var i = n.lastIndexOf('.');
return i != -1 ? n.substring(i + 1) : n;
return getPossibleNames().get(0);
}
/**
@ -208,4 +174,7 @@ public interface DataSourceProvider<T extends DataSource<?>> {
.filter(c -> c.getName().endsWith("Source")).findFirst()
.orElseThrow(() -> new ExtensionException("Descriptor class not found for " + getId()));
}
List<String> getPossibleNames();
}

View file

@ -2,7 +2,7 @@ package io.xpipe.extension;
import io.xpipe.core.source.*;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.LocalFileDataStore;
import io.xpipe.core.store.FileStore;
import io.xpipe.extension.event.ErrorEvent;
import lombok.SneakyThrows;
@ -46,40 +46,40 @@ public class DataSourceProviders {
@SuppressWarnings("unchecked")
@SneakyThrows
public static StructureDataSource<LocalFileDataStore> createLocalStructureDescriptor(DataStore store) {
return (StructureDataSource<LocalFileDataStore>)
public static StructureDataSource<FileStore> createLocalStructureDescriptor(DataStore store) {
return (StructureDataSource<FileStore>)
DataSourceProviders.byId("xpbs").getSourceClass()
.getDeclaredConstructors()[0].newInstance(store);
}
@SuppressWarnings("unchecked")
@SneakyThrows
public static RawDataSource<LocalFileDataStore> createLocalRawDescriptor(DataStore store) {
return (RawDataSource<LocalFileDataStore>)
public static RawDataSource<FileStore> createLocalRawDescriptor(DataStore store) {
return (RawDataSource<FileStore>)
DataSourceProviders.byId("binary").getSourceClass()
.getDeclaredConstructors()[0].newInstance(store);
}
@SuppressWarnings("unchecked")
@SneakyThrows
public static RawDataSource<LocalFileDataStore> createLocalCollectionDescriptor(DataStore store) {
return (RawDataSource<LocalFileDataStore>)
public static RawDataSource<FileStore> createLocalCollectionDescriptor(DataStore store) {
return (RawDataSource<FileStore>)
DataSourceProviders.byId("br").getSourceClass()
.getDeclaredConstructors()[0].newInstance(store);
}
@SuppressWarnings("unchecked")
@SneakyThrows
public static TextDataSource<LocalFileDataStore> createLocalTextDescriptor(DataStore store) {
return (TextDataSource<LocalFileDataStore>)
public static TextDataSource<FileStore> createLocalTextDescriptor(DataStore store) {
return (TextDataSource<FileStore>)
DataSourceProviders.byId("text").getSourceClass()
.getDeclaredConstructors()[0].newInstance(store);
}
@SuppressWarnings("unchecked")
@SneakyThrows
public static TableDataSource<LocalFileDataStore> createLocalTableDescriptor(DataStore store) {
return (TableDataSource<LocalFileDataStore>)
public static TableDataSource<FileStore> createLocalTableDescriptor(DataStore store) {
return (TableDataSource<FileStore>)
DataSourceProviders.byId("xpbt").getSourceClass()
.getDeclaredConstructors()[0].newInstance(store);
}
@ -110,7 +110,7 @@ public class DataSourceProviders {
throw new IllegalStateException("Not initialized");
}
return ALL.stream().filter(d -> d.getConfigProvider().getPossibleNames().stream()
return ALL.stream().filter(d -> d.getPossibleNames().stream()
.anyMatch(s -> s.equalsIgnoreCase(name))).findAny();
}

View file

@ -1,6 +1,7 @@
package io.xpipe.extension;
import io.xpipe.core.dialog.Dialog;
import io.xpipe.core.store.DataStore;
import java.net.URL;
import java.util.List;
@ -34,13 +35,19 @@ public interface DataStoreProvider {
return null;
}
default Dialog dialogForString(String s) {
return null;
}
Dialog defaultDialog();
default String display(DataStore store) {
return store.toDisplay();
}
List<String> getPossibleNames();
default String getId() {
var n = getClass().getPackageName();
var i = n.lastIndexOf('.');
return i != -1 ? n.substring(i + 1) : n;
return getPossibleNames().get(0);
}
}

View file

@ -45,6 +45,14 @@ public class DataStoreProviders {
return ALL.stream().map(d -> d.dialogForURL(url)).filter(Objects::nonNull).findAny();
}
public static Optional<Dialog> byString(String s) {
if (ALL == null) {
throw new IllegalStateException("Not initialized");
}
return ALL.stream().map(d -> d.dialogForString(s)).filter(Objects::nonNull).findAny();
}
public static Set<DataStoreProvider> getAll() {
return ALL;
}

View file

@ -2,7 +2,7 @@ package io.xpipe.extension;
import io.xpipe.core.source.DataSource;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.FileDataStore;
import io.xpipe.core.store.FileStore;
import io.xpipe.core.store.StreamDataStore;
import java.util.LinkedHashMap;
@ -23,7 +23,7 @@ public interface SimpleFileDataSourceProvider<T extends DataSource<?>> extends D
continue;
}
if (store instanceof FileDataStore l) {
if (store instanceof FileStore l) {
return l.getFileName().endsWith("." + ext);
}
}

View file

@ -1,16 +1,16 @@
package io.xpipe.extension;
import io.xpipe.core.dialog.Dialog;
import io.xpipe.core.source.DataSource;
import io.xpipe.core.store.DataStore;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
public interface UniformDataSourceProvider<T extends DataSource<?>> extends DataSourceProvider<T> {
@Override
default ConfigProvider<T> getConfigProvider() {
return ConfigProvider.empty(List.of(getId()), this::createDefaultSource);
default Dialog configDialog(T source, boolean all) {
return Dialog.empty();
}
@Override

View file

@ -10,5 +10,9 @@ public interface PrefsChoiceValue extends Translatable {
return I18n.get(getId());
}
default String toDefaultString() {
return I18n.get(getId());
}
String getId();
}