Rework and refactoring

This commit is contained in:
Christopher Schnick 2022-09-02 01:41:51 +02:00
parent 6efad1fe42
commit c7606f9f8e
82 changed files with 1618 additions and 396 deletions

View file

@ -7,11 +7,10 @@ plugins {
apply from: "$rootDir/deps/java.gradle"
apply from: "$rootDir/deps/junit.gradle"
apply from: "$rootDir/deps/jackson.gradle"
version = file('../misc/version').text
group = 'io.xpipe'
archivesBaseName = 'api'
archivesBaseName = 'xpipe-api'
repositories {
mavenCentral()
@ -22,8 +21,9 @@ test {
}
dependencies {
implementation project(':core')
api project(':core')
implementation project(':beacon')
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.13.0"
}
configurations {

View file

@ -1,8 +1,17 @@
publishing {
publications {
mavenJava(MavenPublication) {
artifactId = project.archivesBaseName
from components.java
pom.withXml {
def pomNode = asNode()
pomNode.dependencies.'*'.findAll().each() {
it.scope*.value = 'compile'
}
}
pom {
name = 'X-Pipe Java API'
description = 'Contains everything necessary to interact with X-Pipe from Java applications.'

View file

@ -4,6 +4,7 @@ import io.xpipe.api.impl.DataSourceImpl;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataSourceReference;
import io.xpipe.core.source.DataSourceType;
import io.xpipe.core.store.DataStore;
import java.io.IOException;
import java.io.InputStream;
@ -26,6 +27,10 @@ import java.nio.file.Path;
*/
public interface DataSource {
void forwardTo(DataSource target);
void appendTo(DataSource target);
/**
* NOT YET IMPLEMENTED!
*
@ -141,6 +146,30 @@ public interface DataSource {
return DataSourceImpl.create(id, type, in);
}
/**
* Creates a new data source from an input stream.
*
* @param id the data source id
* @return a {@link DataSource} instances that can be used to access the underlying data
*/
public static DataSource create(DataSourceId id, io.xpipe.core.source.DataSource<?> source) {
return DataSourceImpl.create(id, source);
}
/**
* Creates a new data source from an input stream.
*1
* @param id the data source id
* @param type the data source type
* @param in the data store to add
* @return a {@link DataSource} instances that can be used to access the underlying data
*/
public static DataSource create(DataSourceId id, String type, DataStore in) {
return DataSourceImpl.create(id, type, in);
}
public io.xpipe.core.source.DataSource<?> getInternalSource();
/**
* Returns the id of this data source.
*/

View file

@ -26,7 +26,7 @@ public final class DataSourceConfig {
return provider;
}
public Map<String, String> getConfigInstance() {
public Map<String, String> getConfig() {
return configInstance;
}
}

View file

@ -3,8 +3,9 @@ package io.xpipe.api;
import io.xpipe.core.source.DataSourceInfo;
import java.util.List;
import java.util.stream.Stream;
public interface DataText extends DataSource, Iterable<String> {
public interface DataText extends DataSource {
DataSourceInfo.Text getInfo();
@ -12,6 +13,8 @@ public interface DataText extends DataSource, Iterable<String> {
List<String> readLines(int maxLines);
Stream<String> lines();
String readAll();
String read(int maxCharacters);

View file

@ -18,8 +18,14 @@ public final class XPipeConnection extends BeaconConnection {
public static void finishDialog(DialogReference reference) {
try (var con = new XPipeConnection()) {
con.constructSocket();
var element = reference.getStart();
while (true) {
if (element.requiresExplicitUserInput()) {
throw new IllegalStateException();
}
DialogExchange.Response response = con.performSimpleExchange(DialogExchange.Request.builder().dialogKey(reference.getDialogId()).build());
element = response.getElement();
if (response.getElement() == null) {
break;
}
@ -69,7 +75,7 @@ public final class XPipeConnection extends BeaconConnection {
throw new BeaconException("Unable to start xpipe daemon", ex);
}
var r = waitForStartup();
var r = waitForStartup(null);
if (r.isEmpty()) {
throw new BeaconException("Wait for xpipe daemon timed out");
} else {
@ -86,13 +92,17 @@ public final class XPipeConnection extends BeaconConnection {
}
private void start() throws Exception {
if (!BeaconServer.tryStart()) {
if (BeaconServer.tryStart() == null) {
throw new UnsupportedOperationException("Unable to determine xpipe daemon launch command");
};
}
public static Optional<BeaconClient> waitForStartup() {
for (int i = 0; i < 80; i++) {
public static Optional<BeaconClient> waitForStartup(Process process) {
for (int i = 0; i < 160; i++) {
if (process != null && !process.isAlive()) {
return Optional.empty();
}
try {
Thread.sleep(500);
} catch (InterruptedException ignored) {

View file

@ -10,8 +10,8 @@ public class DataRawImpl extends DataSourceImpl implements DataRaw {
private final DataSourceInfo.Raw info;
public DataRawImpl(DataSourceId sourceId, DataSourceConfig sourceConfig, DataSourceInfo.Raw info) {
super(sourceId, sourceConfig);
public DataRawImpl(DataSourceId sourceId, DataSourceConfig sourceConfig, DataSourceInfo.Raw info, io.xpipe.core.source.DataSource<?> internalSource) {
super(sourceId, sourceConfig, internalSource);
this.info = info;
}

View file

@ -3,16 +3,40 @@ package io.xpipe.api.impl;
import io.xpipe.api.DataSource;
import io.xpipe.api.DataSourceConfig;
import io.xpipe.api.connector.XPipeConnection;
import io.xpipe.beacon.exchange.QueryDataSourceExchange;
import io.xpipe.beacon.exchange.ReadExchange;
import io.xpipe.beacon.exchange.StoreStreamExchange;
import io.xpipe.beacon.exchange.*;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataSourceReference;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.StreamDataStore;
import java.io.InputStream;
public abstract class DataSourceImpl implements DataSource {
@Override
public void forwardTo(DataSource target) {
XPipeConnection.execute(con -> {
var req = ForwardExchange.Request.builder()
.source(DataSourceReference.id(sourceId))
.target(DataSourceReference.id(target.getId()))
.build();
ForwardExchange.Response res = con.performSimpleExchange(req);
});
}
@Override
public void appendTo(DataSource target) {
XPipeConnection.execute(con -> {
var req = ForwardExchange.Request.builder()
.source(DataSourceReference.id(sourceId))
.target(DataSourceReference.id(target.getId()))
.append(true)
.build();
ForwardExchange.Response res = con.performSimpleExchange(req);
});
}
public static DataSource get(DataSourceReference ds) {
return XPipeConnection.execute(con -> {
var req = QueryDataSourceExchange.Request.builder().ref(ds).build();
@ -21,19 +45,19 @@ public abstract class DataSourceImpl implements DataSource {
return switch (res.getInfo().getType()) {
case TABLE -> {
var data = res.getInfo().asTable();
yield new DataTableImpl(res.getId(), config, data);
yield new DataTableImpl(res.getId(), config, data, res.getInternalSource());
}
case STRUCTURE -> {
var info = res.getInfo().asStructure();
yield new DataStructureImpl(res.getId(), config, info);
yield new DataStructureImpl(res.getId(), config, info, res.getInternalSource());
}
case TEXT -> {
var info = res.getInfo().asText();
yield new DataTextImpl(res.getId(), config, info);
yield new DataTextImpl(res.getId(), config, info, res.getInternalSource());
}
case RAW -> {
var info = res.getInfo().asRaw();
yield new DataRawImpl(res.getId(), config, info);
yield new DataRawImpl(res.getId(), config, info, res.getInternalSource());
}
case COLLECTION -> throw new UnsupportedOperationException("Unimplemented case: " + res.getInfo().getType());
default -> throw new IllegalArgumentException("Unexpected value: " + res.getInfo().getType());
@ -41,6 +65,55 @@ public abstract class DataSourceImpl implements DataSource {
});
}
public static DataSource create(DataSourceId id, io.xpipe.core.source.DataSource<?> source) {
var startReq = AddSourceExchange.Request.builder()
.source(source)
.target(id)
.build();
var returnedId = XPipeConnection.execute(con -> {
AddSourceExchange.Response r = con.performSimpleExchange(startReq);
return r.getId();
});
var ref = DataSourceReference.id(returnedId);
return get(ref);
}
public static DataSource create(DataSourceId id, String type, DataStore store) {
if (store instanceof StreamDataStore s && s.isLocalToApplication()) {
var res = XPipeConnection.execute(con -> {
var req = StoreStreamExchange.Request.builder().build();
StoreStreamExchange.Response r = con.performOutputExchange(req, out -> {
try (InputStream inputStream = s.openInput()) {
inputStream.transferTo(out);
}
});
return r;
});
store = res.getStore();
}
var startReq = ReadExchange.Request.builder()
.provider(type)
.store(store)
.target(id)
.configureAll(false)
.build();
var startRes = XPipeConnection.execute(con -> {
ReadExchange.Response r = con.performSimpleExchange(startReq);
return r;
});
var configInstance = startRes.getConfig();
XPipeConnection.finishDialog(configInstance);
var ref = id != null ?
DataSourceReference.id(id) :
DataSourceReference.latest();
return get(ref);
}
public static DataSource create(DataSourceId id, String type, InputStream in) {
var res = XPipeConnection.execute(con -> {
var req = StoreStreamExchange.Request.builder().build();
@ -64,16 +137,24 @@ public abstract class DataSourceImpl implements DataSource {
var configInstance = startRes.getConfig();
XPipeConnection.finishDialog(configInstance);
var ref = id != null ? DataSourceReference.id(id) : DataSourceReference.latest();
var ref = id != null ?
DataSourceReference.id(id) :
DataSourceReference.latest();
return get(ref);
}
private final DataSourceId sourceId;
private final DataSourceConfig config;
private final io.xpipe.core.source.DataSource<?> internalSource;
public DataSourceImpl(DataSourceId sourceId, DataSourceConfig config) {
public DataSourceImpl(DataSourceId sourceId, DataSourceConfig config, io.xpipe.core.source.DataSource<?> internalSource) {
this.sourceId = sourceId;
this.config = config;
this.internalSource = internalSource;
}
public io.xpipe.core.source.DataSource<?> getInternalSource() {
return internalSource;
}
@Override

View file

@ -9,8 +9,8 @@ public class DataStructureImpl extends DataSourceImpl implements DataStructure {
private final DataSourceInfo.Structure info;
public DataStructureImpl(DataSourceId sourceId, DataSourceConfig sourceConfig, DataSourceInfo.Structure info) {
super(sourceId, sourceConfig);
public DataStructureImpl(DataSourceId sourceId, DataSourceConfig sourceConfig, DataSourceInfo.Structure info, io.xpipe.core.source.DataSource<?> internalSource) {
super(sourceId, sourceConfig, internalSource);
this.info = info;
}

View file

@ -25,8 +25,8 @@ public class DataTableImpl extends DataSourceImpl implements DataTable {
private final DataSourceInfo.Table info;
DataTableImpl(DataSourceId id, DataSourceConfig sourceConfig, DataSourceInfo.Table info) {
super(id, sourceConfig);
DataTableImpl(DataSourceId id, DataSourceConfig sourceConfig, DataSourceInfo.Table info, io.xpipe.core.source.DataSource<?> internalSource) {
super(id, sourceConfig, internalSource);
this.info = info;
}

View file

@ -2,17 +2,33 @@ package io.xpipe.api.impl;
import io.xpipe.api.DataSourceConfig;
import io.xpipe.api.DataText;
import io.xpipe.core.source.*;
import io.xpipe.api.connector.XPipeConnection;
import io.xpipe.beacon.BeaconConnection;
import io.xpipe.beacon.BeaconException;
import io.xpipe.beacon.exchange.api.QueryTextDataExchange;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataSourceInfo;
import io.xpipe.core.source.DataSourceReference;
import io.xpipe.core.source.DataSourceType;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.List;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
public class DataTextImpl extends DataSourceImpl implements DataText {
private final DataSourceInfo.Text info;
public DataTextImpl(DataSourceId sourceId, DataSourceConfig sourceConfig, DataSourceInfo.Text info) {
super(sourceId, sourceConfig);
public DataTextImpl(DataSourceId sourceId, DataSourceConfig sourceConfig, DataSourceInfo.Text info, io.xpipe.core.source.DataSource<?> internalSource) {
super(sourceId, sourceConfig, internalSource);
this.info = info;
}
@ -33,26 +49,80 @@ public class DataTextImpl extends DataSourceImpl implements DataText {
@Override
public List<String> readAllLines() {
return null;
return readLines(Integer.MAX_VALUE);
}
@Override
public List<String> readLines(int maxLines) {
return null;
try (Stream<String> lines = lines()) {
return lines.limit(maxLines).toList();
}
}
@Override
public Stream<String> lines() {
var iterator = new Iterator<String>() {
private final BeaconConnection connection;
private final BufferedReader reader;
private String nextValue;
{
connection = XPipeConnection.open();
var req = QueryTextDataExchange.Request.builder()
.ref(DataSourceReference.id(getId())).maxLines(-1).build();
connection.sendRequest(req);
connection.receiveResponse();
reader = new BufferedReader(new InputStreamReader(connection.receiveBody(), StandardCharsets.UTF_8));
}
private void close() {
connection.close();
}
@Override
public boolean hasNext() {
connection.checkClosed();
try {
nextValue = reader.readLine();
} catch (IOException e) {
throw new BeaconException(e);
}
return nextValue != null;
}
@Override
public String next() {
return nextValue;
}
};
return StreamSupport
.stream(Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED), false)
.onClose(iterator::close);
}
@Override
public String readAll() {
return null;
try (Stream<String> lines = lines()) {
return lines.collect(Collectors.joining("\n"));
}
}
@Override
public String read(int maxCharacters) {
return null;
StringBuilder builder = new StringBuilder();
lines().takeWhile(s -> {
if (builder.length() > maxCharacters) {
return false;
}
builder.append(s);
return true;
});
return builder.toString();
}
@Override
public Iterator<String> iterator() {
return null;
}
}

View file

@ -6,23 +6,24 @@ plugins {
}
apply from: "$rootDir/deps/java.gradle"
apply from: "$rootDir/deps/jackson.gradle"
apply from: "$rootDir/deps/lombok.gradle"
configurations {
compileOnly.extendsFrom(dep)
dependencies {
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.13.0"
implementation group: 'com.fasterxml.jackson.module', name: 'jackson-module-parameter-names', version: "2.13.0"
implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.13.0"
}
version = file('../misc/version').text
group = 'io.xpipe'
archivesBaseName = 'beacon'
archivesBaseName = 'xpipe-beacon'
repositories {
mavenCentral()
}
dependencies {
implementation project(':core')
api project(':core')
}
apply from: 'publish.gradle'

View file

@ -1,8 +1,18 @@
publishing {
publications {
mavenJava(MavenPublication) {
artifactId = project.archivesBaseName
from components.java
pom.withXml {
def pomNode = asNode()
pomNode.dependencies.'*'.findAll().each() {
it.scope*.value = 'compile'
}
}
pom {
name = 'X-Pipe Beacon'
description = 'The socket-based implementation used for the communication with the X-Pipe daemon.'

View file

@ -102,11 +102,11 @@ public class BeaconClient implements AutoCloseable {
var msg = JsonNodeFactory.instance.objectNode();
msg.set("xPipeMessage", json);
if (BeaconConfig.debugEnabled()) {
if (BeaconConfig.printMessages()) {
System.out.println("Sending request to server of type " + req.getClass().getName());
}
if (BeaconConfig.debugEnabled()) {
if (BeaconConfig.printMessages()) {
System.out.println("Sending raw request:");
System.out.println(msg.toPrettyString());
}
@ -131,7 +131,7 @@ public class BeaconClient implements AutoCloseable {
throw new ConnectorException("Couldn't read from socket", ex);
}
if (BeaconConfig.debugEnabled()) {
if (BeaconConfig.printMessages()) {
System.out.println("Received response:");
System.out.println(read.toPrettyString());
}

View file

@ -8,19 +8,39 @@ import java.nio.charset.StandardCharsets;
public class BeaconConfig {
public static final byte[] BODY_SEPARATOR = "\n\n".getBytes(StandardCharsets.UTF_8);
private static final String DEBUG_PROP = "io.xpipe.beacon.debugOutput";
public static boolean debugEnabled() {
if (System.getProperty(DEBUG_PROP) != null) {
return Boolean.parseBoolean(System.getProperty(DEBUG_PROP));
private static final String PRINT_MESSAGES_PROPERTY = "io.xpipe.beacon.printMessages";
public static boolean printMessages() {
if (System.getProperty(PRINT_MESSAGES_PROPERTY) != null) {
return Boolean.parseBoolean(System.getProperty(PRINT_MESSAGES_PROPERTY));
}
return false;
}
private static final String LAUNCH_DAEMON_IN_DEBUG_PROP = "io.xpipe.beacon.launchDebugDaemon";
public static boolean launchDaemonInDebugMode() {
if (System.getProperty(LAUNCH_DAEMON_IN_DEBUG_PROP) != null) {
return Boolean.parseBoolean(System.getProperty(LAUNCH_DAEMON_IN_DEBUG_PROP));
}
return false;
}
private static final String ATTACH_DEBUGGER_PROP = "io.xpipe.beacon.attachDebuggerToDaemon";
public static boolean attachDebuggerToDaemon() {
if (System.getProperty(ATTACH_DEBUGGER_PROP) != null) {
return Boolean.parseBoolean(System.getProperty(ATTACH_DEBUGGER_PROP));
}
return false;
}
private static final String EXEC_DEBUG_PROP = "io.xpipe.beacon.debugExecOutput";
public static boolean execDebugEnabled() {
private static final String EXEC_DEBUG_PROP = "io.xpipe.beacon.printDaemonOutput";
public static boolean printDaemonOutput() {
if (System.getProperty(EXEC_DEBUG_PROP) != null) {
return Boolean.parseBoolean(System.getProperty(EXEC_DEBUG_PROP));
}
@ -42,13 +62,25 @@ public class BeaconConfig {
private static final String EXEC_PROCESS_PROP = "io.xpipe.beacon.exec";
private static final String EXEC_PROCESS_PROP = "io.xpipe.beacon.customDaemonCommand";
public static String getCustomExecCommand() {
public static String getCustomDaemonCommand() {
if (System.getProperty(EXEC_PROCESS_PROP) != null) {
return System.getProperty(EXEC_PROCESS_PROP);
}
return null;
}
private static final String DAEMON_ARGUMENTS_PROP = "io.xpipe.beacon.daemonArgs";
public static String getDaemonArguments() {
if (System.getProperty(DAEMON_ARGUMENTS_PROP) != null) {
return System.getProperty(DAEMON_ARGUMENTS_PROP);
}
return null;
}
}

View file

@ -143,7 +143,7 @@ public abstract class BeaconConnection implements AutoCloseable {
public <REQ extends RequestMessage, RES extends ResponseMessage> RES performOutputExchange(
REQ req,
BeaconClient.FailableConsumer<OutputStream, IOException> reqWriter) {
BeaconClient.FailableConsumer<OutputStream, Exception> reqWriter) {
checkClosed();
try {

View file

@ -36,7 +36,7 @@ public class BeaconFormat {
}
private void finishBlock() throws IOException {
if (BeaconConfig.debugEnabled()) {
if (BeaconConfig.printMessages()) {
System.out.println("Sending data block of length " + index);
}
@ -76,7 +76,7 @@ public class BeaconFormat {
var length = in.readNBytes(4);
var lengthInt = ByteBuffer.wrap(length).getInt();
if (BeaconConfig.debugEnabled()) {
if (BeaconConfig.printMessages()) {
System.out.println("Receiving data block of length " + lengthInt);
}

View file

@ -4,7 +4,6 @@ import io.xpipe.beacon.exchange.StopExchange;
import lombok.experimental.UtilityClass;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
@ -16,6 +15,14 @@ import java.util.Optional;
@UtilityClass
public class BeaconServer {
public static void main(String[] args) throws Exception {
if (tryStartCustom() == null) {
if (tryStart() == null) {
System.exit(1);
}
}
}
public static boolean isRunning() {
try (var socket = new BeaconClient()) {
return true;
@ -24,26 +31,56 @@ public class BeaconServer {
}
}
private static void startFork(String custom) throws IOException {
boolean print = BeaconConfig.execDebugEnabled();
if (print) {
System.out.println("Executing custom daemon launch command: " + custom);
public static Process tryStartCustom() throws Exception {
var custom = BeaconConfig.getCustomDaemonCommand();
if (custom != null) {
var command = custom + " " + (BeaconConfig.getDaemonArguments() != null ?
BeaconConfig.getDaemonArguments() :
"");
Process process = Runtime.getRuntime().exec(command);
printDaemonOutput(process, command);
return process;
}
var proc = Runtime.getRuntime().exec(custom);
return null;
}
public static Process tryStart() throws Exception {
var daemonExecutable = getDaemonExecutable();
if (daemonExecutable.isPresent()) {
var command = "\"" + daemonExecutable.get() + "\" --external " + (BeaconConfig.getDaemonArguments() != null ?
BeaconConfig.getDaemonArguments() :
"");
// Tell daemon that we launched from an external tool
Process process = Runtime.getRuntime().exec(command);
printDaemonOutput(process, command);
return process;
}
return null;
}
private static void printDaemonOutput(Process proc, String command) {
boolean print = BeaconConfig.printDaemonOutput();
if (print) {
System.out.println("Starting daemon: " + command);
}
if (!print) {
return;
}
new Thread(null, () -> {
try {
InputStreamReader isr = new InputStreamReader(proc.getInputStream());
BufferedReader br = new BufferedReader(isr);
String line;
while ((line = br.readLine()) != null) {
if (print) {
System.out.println("[xpiped] " + line);
}
System.out.println("[xpiped] " + line);
}
} catch (Exception ioe) {
ioe.printStackTrace();
}
}, "daemon fork sysout").start();
}, "daemon sysout").start();
new Thread(null, () -> {
try {
@ -51,46 +88,12 @@ public class BeaconServer {
BufferedReader br = new BufferedReader(isr);
String line;
while ((line = br.readLine()) != null) {
if (print) {
System.err.println("[xpiped] " + line);
}
}
int exit = proc.waitFor();
if (exit != 0) {
System.err.println("Daemon launch command failed");
System.err.println("[xpiped] " + line);
}
} catch (Exception ioe) {
ioe.printStackTrace();
}
}, "daemon fork syserr").start();
}
public static boolean tryStartFork() throws Exception {
var custom = BeaconConfig.getCustomExecCommand();
if (custom != null) {
System.out.println("Starting fork: " + custom);
startFork(custom);
return true;
}
return false;
}
public static boolean tryStart() throws Exception {
var daemonExecutable = getDaemonExecutable();
if (daemonExecutable.isPresent()) {
if (BeaconConfig.debugEnabled()) {
System.out.println("Starting daemon executable: " + daemonExecutable.get());
}
// Tell daemon that we launched from an external tool
new ProcessBuilder(daemonExecutable.get().toString(), "--external")
.redirectErrorStream(true)
.redirectOutput(ProcessBuilder.Redirect.DISCARD)
.start();
return true;
}
return false;
}, "daemon syserr").start();
}
public static boolean tryStop(BeaconClient client) throws Exception {
@ -99,40 +102,59 @@ public class BeaconServer {
return res.isSuccess();
}
private static Optional<Path> getDaemonExecutableFromHome() {
var env = System.getenv("XPIPE_HOME");
if (env == null) {
return Optional.empty();
}
Path file;
private static Optional<Path> getDaemonBasePath() {
Path base = null;
// Prepare for invalid XPIPE_HOME path value
try {
if (System.getProperty("os.name").startsWith("Windows")) {
file = Path.of(env, "app", "xpiped.exe");
} else {
file = Path.of(env, "app", "bin", "xpiped");
}
return Files.exists(file) ? Optional.of(file) : Optional.empty();
var environmentVariable = System.getenv("XPIPE_HOME");
base = environmentVariable != null ? Path.of(environmentVariable) : null;
} catch (Exception ex) {
return Optional.empty();
}
if (base == null) {
if (System.getProperty("os.name").startsWith("Windows")) {
base = Path.of(System.getenv("LOCALAPPDATA"), "X-Pipe");
} else {
base = Path.of("/opt/xpipe/");
}
if (!Files.exists(base)) {
base = null;
}
}
return Optional.ofNullable(base);
}
public static Optional<Path> getDaemonExecutable() {
var home = getDaemonExecutableFromHome();
if (home.isPresent()) {
return home;
}
var base = getDaemonBasePath().orElseThrow();
var debug = BeaconConfig.launchDaemonInDebugMode();
Path executable = null;
if (!debug) {
if (System.getProperty("os.name").startsWith("Windows")) {
executable = Path.of("app", "xpiped.exe");
} else {
executable = Path.of("app/bin/xpiped");
}
Path file;
if (System.getProperty("os.name").startsWith("Windows")) {
file = Path.of(System.getenv("LOCALAPPDATA"), "X-Pipe", "app", "xpiped.exe");
} else {
file = Path.of("/opt/xpipe/app/bin/xpiped");
String scriptName = null;
if (BeaconConfig.attachDebuggerToDaemon()) {
scriptName = "xpiped_debug_attach";
} else {
scriptName = "xpiped_debug";
}
if (System.getProperty("os.name").startsWith("Windows")) {
scriptName = scriptName + ".bat";
} else {
scriptName = scriptName + ".sh";
}
executable = Path.of("app", "scripts", scriptName);
}
Path file = base.resolve(executable);
if (Files.exists(file)) {
return Optional.of(file);
} else {

View file

@ -0,0 +1,34 @@
package io.xpipe.beacon.exchange;
import io.xpipe.beacon.RequestMessage;
import io.xpipe.beacon.ResponseMessage;
import io.xpipe.core.source.DataSource;
import io.xpipe.core.source.DataSourceId;
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
public class AddSourceExchange implements MessageExchange {
@Override
public String getId() {
return "addSource";
}
@Jacksonized
@Builder
@Value
public static class Request implements RequestMessage {
DataSourceId target;
@NonNull DataSource<?> source;
}
@Jacksonized
@Builder
@Value
public static class Response implements ResponseMessage {
@NonNull
DataSourceId id;
}
}

View file

@ -0,0 +1,36 @@
package io.xpipe.beacon.exchange;
import io.xpipe.beacon.RequestMessage;
import io.xpipe.beacon.ResponseMessage;
import io.xpipe.core.source.DataSourceReference;
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
public class ForwardExchange implements MessageExchange {
@Override
public String getId() {
return "forward";
}
@Jacksonized
@Builder
@Value
public static class Request implements RequestMessage {
@NonNull
DataSourceReference source;
@NonNull
DataSourceReference target;
boolean append;
}
@Jacksonized
@Builder
@Value
public static class Response implements ResponseMessage {
}
}

View file

@ -2,6 +2,7 @@ package io.xpipe.beacon.exchange;
import io.xpipe.beacon.RequestMessage;
import io.xpipe.beacon.ResponseMessage;
import io.xpipe.core.source.DataSource;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataSourceInfo;
import io.xpipe.core.source.DataSourceReference;
@ -34,8 +35,10 @@ public class QueryDataSourceExchange implements MessageExchange {
@Builder
@Value
public static class Response implements ResponseMessage {
@NonNull
DataSourceId id;
boolean disabled;
boolean hidden;
@NonNull
DataSourceInfo info;
@NonNull
@ -44,5 +47,7 @@ public class QueryDataSourceExchange implements MessageExchange {
String provider;
@NonNull
Map<String, String> config;
DataSource<?> internalSource;
}
}

View file

@ -23,7 +23,8 @@ public class QueryTextDataExchange implements MessageExchange {
@NonNull
DataSourceReference ref;
int maxLines;
@Builder.Default
int maxLines = -1;
}
@Jacksonized

View file

@ -22,7 +22,9 @@ module io.xpipe.beacon {
uses MessageExchange;
provides io.xpipe.beacon.exchange.MessageExchange with
ForwardExchange,
EditStoreExchange,
AddSourceExchange,
StoreProviderListExchange,
ListCollectionsExchange,
ListEntriesExchange,

View file

@ -17,15 +17,11 @@ configurations {
version = file('../misc/version').text
group = 'io.xpipe'
archivesBaseName = 'core'
archivesBaseName = 'xpipe-core'
repositories {
mavenCentral()
}
dependencies{
compileOnly 'org.apache.commons:commons-exec:1.3'
}
apply from: 'publish.gradle'
apply from: "$rootDir/deps/publish-base.gradle"

View file

@ -1,6 +1,8 @@
publishing {
publications {
mavenJava(MavenPublication) {
artifactId = project.archivesBaseName
from components.java
pom {

View file

@ -1,8 +1,6 @@
package io.xpipe.core.charsetter;
import java.nio.charset.Charset;
public interface Charsettable {
Charset getCharset();
StreamCharset getCharset();
}

View file

@ -1,12 +1,14 @@
package io.xpipe.core.charsetter;
import io.xpipe.core.store.FileStore;
import io.xpipe.core.store.StreamDataStore;
import lombok.Value;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.*;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
@ -27,11 +29,12 @@ public abstract class Charsetter {
@Value
public static class Result {
Charset charset;
StreamCharset charset;
NewLine newLine;
}
protected Charsetter() {}
protected Charsetter() {
}
public static Charsetter INSTANCE;
@ -50,8 +53,77 @@ public abstract class Charsetter {
void accept(T var1) throws E;
}
public BufferedReader reader(StreamDataStore store, StreamCharset charset) throws Exception {
return reader(store.openBufferedInput(), charset);
}
public OutputStreamWriter writer(StreamDataStore store, StreamCharset charset) throws Exception {
var out = new OutputStreamWriter(store.openOutput(), charset.getCharset());
return out;
}
public BufferedReader reader(InputStream stream, StreamCharset charset) throws Exception {
if (charset.hasByteOrderMark()) {
var bom = stream.readNBytes(charset.getByteOrderMark().length);
if (bom.length != 0 && !Arrays.equals(bom, charset.getByteOrderMark())) {
throw new IllegalStateException("Invalid charset: " + toString());
}
}
return new BufferedReader(new InputStreamReader(stream, charset.getCharset()));
}
public abstract Result read(FailableSupplier<InputStream, Exception> in, FailableConsumer<InputStreamReader, Exception> con) throws Exception;
private static final int MAX_BYTES = 8192;
public Result detect(StreamDataStore store) throws Exception {
Result result = new Result(null, null);
if (store.canOpen()) {
try (InputStream inputStream = store.openBufferedInput()) {
StreamCharset detected = null;
for (var charset : StreamCharset.COMMON) {
if (charset.hasByteOrderMark()) {
inputStream.mark(charset.getByteOrderMark().length);
var bom = inputStream.readNBytes(charset.getByteOrderMark().length);
inputStream.reset();
if (Arrays.equals(bom, charset.getByteOrderMark())) {
detected = charset;
break;
}
}
}
var bytes = inputStream.readNBytes(MAX_BYTES);
if (detected == null) {
detected = StreamCharset.get(inferCharset(bytes), false);
}
var nl = inferNewLine(bytes);
result = new Result(detected, nl);
}
}
if (store instanceof FileStore fileStore) {
var newline = fileStore.getMachine().getNewLine();
if (result.getNewLine() == null) {
result = new Result(result.getCharset(), newline);
}
}
if (result.getCharset() == null) {
result = new Result(StreamCharset.UTF8, result.getNewLine());
}
if (result.getNewLine() == null) {
result = new Result(result.getCharset(), NewLine.platform());
}
return result;
}
public NewLine inferNewLine(byte[] content) {
Map<NewLine, Integer> count = new HashMap<>();
for (var nl : NewLine.values()) {
@ -69,10 +141,10 @@ public abstract class Charsetter {
private static int count(byte[] outerArray, byte[] smallerArray) {
int count = 0;
for(int i = 0; i < outerArray.length - smallerArray.length+1; ++i) {
for (int i = 0; i < outerArray.length - smallerArray.length + 1; ++i) {
boolean found = true;
for(int j = 0; j < smallerArray.length; ++j) {
if (outerArray[i+j] != smallerArray[j]) {
for (int j = 0; j < smallerArray.length; ++j) {
if (outerArray[i + j] != smallerArray[j]) {
found = false;
break;
}

View file

@ -0,0 +1,90 @@
package io.xpipe.core.charsetter;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Value;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Locale;
import java.util.stream.Stream;
@Value
public class StreamCharset {
public static StreamCharset get(Charset charset, boolean byteOrderMark) {
return Stream.concat(COMMON.stream(), RARE.stream())
.filter(streamCharset -> streamCharset.getCharset()
.equals(charset) && streamCharset.hasByteOrderMark() == byteOrderMark)
.findFirst()
.orElseThrow();
}
@JsonCreator
public static StreamCharset get(String s) {
var byteOrderMark = s.endsWith("-bom");
var charset = Charset.forName(s.substring(
0, s.length() - (byteOrderMark ?
4 :
0)));
return StreamCharset.get(charset, byteOrderMark);
}
Charset charset;
byte[] byteOrderMark;
@JsonValue
public String toString() {
return getCharset()
.name().toLowerCase(Locale.ROOT) + (hasByteOrderMark() ?
"-bom" :
"");
}
public static final StreamCharset UTF8 = new StreamCharset(StandardCharsets.UTF_8, null);
public static final StreamCharset UTF8_BOM = new StreamCharset(StandardCharsets.UTF_8, new byte[]{
(byte) 0xEF,
(byte) 0xBB,
(byte) 0xBF
});
public static final StreamCharset UTF16 = new StreamCharset(StandardCharsets.UTF_16, null);
public static final StreamCharset UTF16_BOM = new StreamCharset(StandardCharsets.UTF_16, new byte[]{
(byte) 0xFE,
(byte) 0xFF
});
public static final StreamCharset UTF16_LE = new StreamCharset(StandardCharsets.UTF_16LE, null);
public static final StreamCharset UTF16_LE_BOM = new StreamCharset(StandardCharsets.UTF_16LE, new byte[]{
(byte) 0xFF,
(byte) 0xFE
});
public static final StreamCharset UTF32 = new StreamCharset(Charset.forName("utf-32"), null);
public static final StreamCharset UTF32_BOM = new StreamCharset(Charset.forName("utf-32"), new byte[]{
0x00,
0x00,
(byte) 0xFE,
(byte) 0xFF
});
public static final List<StreamCharset> COMMON = List.of(
UTF8, UTF8_BOM, UTF16, UTF16_BOM, UTF16_LE, UTF16_LE_BOM, UTF32, UTF32_BOM, new StreamCharset(StandardCharsets.US_ASCII, null),
new StreamCharset(StandardCharsets.ISO_8859_1, null),
new StreamCharset(Charset.forName("Windows-1251"), null), new StreamCharset(Charset.forName("Windows-1252"), null)
);
public static final List<StreamCharset> RARE = Charset.availableCharsets()
.values()
.stream()
.filter(charset -> COMMON.stream()
.noneMatch(c -> c.getCharset()
.equals(charset)))
.map(charset -> new StreamCharset(charset, null))
.toList();
public boolean hasByteOrderMark() {
return byteOrderMark != null;
}
}

View file

@ -2,15 +2,25 @@ package io.xpipe.core.data.node;
import io.xpipe.core.data.type.DataType;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.Spliterator;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Stream;
public abstract class DataStructureNode implements Iterable<DataStructureNode> {
public static final String KEY_TABLE_NAME = "tableName";
public static final String KEY_ROW_NAME = "rowName";
private Properties properties = new Properties();
public String getMetaString(String key) {
if (properties == null) {
return null;
}
return properties.getProperty(key);
}
public abstract DataStructureNode mutableCopy();
public String keyNameAt(int index) {

View file

@ -1,5 +1,7 @@
package io.xpipe.core.data.node;
import java.nio.charset.StandardCharsets;
public class SimpleImmutableValueNode extends ImmutableValueNode {
private final byte[] data;
@ -30,6 +32,6 @@ public class SimpleImmutableValueNode extends ImmutableValueNode {
return "null";
}
return new String(getRawData());
return new String(getRawData(), StandardCharsets.UTF_8);
}
}

View file

@ -45,7 +45,7 @@ public class TypedDataStreamParser {
public DataStructureNode parseStructure(InputStream in, TypedAbstractReader cb) throws IOException {
if (!hasNext(in)) {
throw new IllegalStateException("No structure to read");
return null;
}
cb.onNodeBegin();

View file

@ -29,6 +29,11 @@ public class BaseQueryElement extends DialogElement {
this.value = value;
}
@Override
public boolean requiresExplicitUserInput() {
return required && value == null;
}
public boolean isNewLine() {
return newLine;
}

View file

@ -18,6 +18,11 @@ public class ChoiceElement extends DialogElement {
private int selected;
@Override
public boolean requiresExplicitUserInput() {
return required && selected == -1;
}
@Override
public String toDisplayString() {
return description;

View file

@ -126,6 +126,11 @@ public abstract class Dialog {
@Override
protected DialogElement next(String answer) throws Exception {
if (element.requiresExplicitUserInput() && (answer == null || answer.trim()
.length() == 0)) {
return element;
}
if (element.apply(answer)) {
return null;
}

View file

@ -19,6 +19,10 @@ public abstract class DialogElement {
public abstract String toDisplayString();
public boolean requiresExplicitUserInput() {
return false;
}
public boolean apply(String value) {
throw new UnsupportedOperationException();
}

View file

@ -1,24 +1,38 @@
package io.xpipe.core.dialog;
import io.xpipe.core.charsetter.NewLine;
import io.xpipe.core.charsetter.StreamCharset;
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> {
public static final QueryConverter<Charset> CHARSET = new QueryConverter<Charset>() {
public static final QueryConverter<NewLine> NEW_LINE = new QueryConverter<NewLine>() {
@Override
protected Charset fromString(String s) {
return Charset.forName(s);
protected NewLine fromString(String s) {
return NewLine.id(s);
}
@Override
protected String toString(Charset value) {
return value.name();
protected String toString(NewLine value) {
return value.getId();
}
};
public static final QueryConverter<StreamCharset> CHARSET = new QueryConverter<StreamCharset>() {
@Override
protected StreamCharset fromString(String s) {
return StreamCharset.get(s);
}
@Override
protected String toString(StreamCharset value) {
return value.toString();
}
};
@ -127,7 +141,9 @@ public abstract class QueryConverter<T> {
@Override
protected String toString(Boolean value) {
return value ? "yes" : "no";
return value ?
"yes" :
"no";
}
};

View file

@ -14,12 +14,11 @@ public class QueryElement extends BaseQueryElement {
@Override
public boolean apply(String value) {
if (value == null && this.value != null) {
if (value == null) {
if (isRequired() && this.value == null) {
return false;
}
this.value = null;
return true;
}

View file

@ -38,12 +38,12 @@ public abstract class DataSource<DS extends DataStore> {
return c;
}
protected boolean supportsRead() {
return false;
public boolean supportsRead() {
return true;
}
protected boolean supportsWrite() {
return false;
public boolean supportsWrite() {
return true;
}
/**

View file

@ -0,0 +1,42 @@
package io.xpipe.core.source;
import io.xpipe.core.charsetter.Charsetter;
import io.xpipe.core.charsetter.StreamCharset;
import io.xpipe.core.store.StreamDataStore;
import java.io.InputStream;
import java.io.Reader;
public abstract class StreamReadConnection implements DataSourceReadConnection {
protected InputStream inputStream;
protected Reader reader;
private final StreamCharset charset;
protected final StreamDataStore store;
public StreamReadConnection(StreamDataStore store, StreamCharset charset) {
this.store = store;
this.charset = charset;
}
@Override
public void init() throws Exception {
if (inputStream != null) {
throw new IllegalStateException("Already initialized");
}
inputStream = store.openInput();
if (charset != null) {
reader = Charsetter.get().reader(inputStream, charset);
}
}
@Override
public void close() throws Exception {
if (inputStream == null) {
throw new IllegalStateException("Not initialized");
}
inputStream.close();
}
}

View file

@ -0,0 +1,49 @@
package io.xpipe.core.source;
import io.xpipe.core.charsetter.StreamCharset;
import io.xpipe.core.store.StreamDataStore;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
public class StreamWriteConnection implements DataSourceConnection {
protected final StreamDataStore store;
protected OutputStream outputStream;
private final StreamCharset charset;
protected OutputStreamWriter writer;
public StreamWriteConnection(StreamDataStore store, StreamCharset charset) {
this.store = store;
this.charset = charset;
}
@Override
public void init() throws Exception {
if (outputStream != null) {
throw new IllegalStateException("Already initialized");
}
outputStream = store.openOutput();
if (charset != null) {
if (charset.hasByteOrderMark()) {
outputStream
.write(charset.getByteOrderMark());
}
writer = new OutputStreamWriter(outputStream, charset.getCharset());
}
}
@Override
public void close() throws Exception {
if (outputStream == null) {
throw new IllegalStateException("Not initialized");
}
if (writer != null) {
writer.close();
}
outputStream.close();
}
}

View file

@ -1,6 +1,7 @@
package io.xpipe.core.source;
import io.xpipe.core.data.node.DataStructureNode;
import io.xpipe.core.data.node.DataStructureNodeAcceptor;
import io.xpipe.core.data.node.ArrayNode;
import io.xpipe.core.data.node.TupleNode;
@ -8,6 +9,7 @@ import io.xpipe.core.data.type.TupleType;
import io.xpipe.core.data.typed.TypedDataStreamWriter;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicInteger;
/**
@ -57,7 +59,17 @@ public interface TableReadConnection extends DataSourceReadConnection {
/**
* Reads multiple rows in bulk.
*/
ArrayNode readRows(int maxLines) throws Exception;
default ArrayNode readRows(int maxLines) throws Exception{
var list = new ArrayList<DataStructureNode>();
AtomicInteger rowCounter = new AtomicInteger();
withRows(t -> {
list.add(t);
rowCounter.getAndIncrement();
return rowCounter.get() != maxLines;
});
return ArrayNode.of(list);
}
/**
* Writes the rows to an OutputStream in the X-Pipe binary format.

View file

@ -1,5 +1,6 @@
package io.xpipe.core.source;
import io.xpipe.core.data.node.DataStructureNode;
import io.xpipe.core.data.node.DataStructureNodeAcceptor;
import io.xpipe.core.data.node.ArrayNode;
import io.xpipe.core.data.node.TupleNode;
@ -11,5 +12,10 @@ public interface TableWriteConnection extends DataSourceConnection {
DataStructureNodeAcceptor<TupleNode> writeLinesAcceptor();
void writeLines(ArrayNode lines) throws Exception;
default void writeLines(ArrayNode lines) throws Exception{
var consumer = writeLinesAcceptor();
for (DataStructureNode dataStructureNode : lines.getNodes()) {
consumer.accept(dataStructureNode.asTuple());
}
}
}

View file

@ -16,6 +16,10 @@ public abstract class TextDataSource<DS extends DataStore> extends DataSource<DS
@Override
public final DataSourceInfo determineInfo() throws Exception {
if (!getStore().canOpen()) {
return new DataSourceInfo.Text(-1, -1);
}
try (var con = openReadConnection()) {
AtomicInteger lineCount = new AtomicInteger();
AtomicInteger charCount = new AtomicInteger();
@ -42,7 +46,16 @@ public abstract class TextDataSource<DS extends DataStore> extends DataSource<DS
return con;
}
@Override
public final TextWriteConnection openAppendingWriteConnection() throws Exception {
var con = newAppendingWriteConnection();
con.init();
return con;
}
protected abstract TextWriteConnection newWriteConnection();
protected abstract TextWriteConnection newAppendingWriteConnection();
protected abstract TextReadConnection newReadConnection();
}

View file

@ -1,36 +1,22 @@
package io.xpipe.core.source;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public interface TextReadConnection extends DataSourceReadConnection {
Stream<String> lines() throws Exception;
boolean isFinished() throws Exception;
default void forwardLines(OutputStream out, int maxLines) throws Exception {
if (maxLines == 0) {
return;
}
int counter = 0;
for (var it = lines().iterator(); it.hasNext(); counter++) {
if (counter == maxLines) {
break;
}
out.write(it.next().getBytes(StandardCharsets.UTF_8));
out.write("\n".getBytes(StandardCharsets.UTF_8));
default String readAll() throws Exception {
try (Stream<String> lines = lines()) {
return lines.collect(Collectors.joining("\n"));
}
}
default void forward(DataSourceConnection con) throws Exception {
try (var tCon = (TextWriteConnection) con) {
var tCon = (TextWriteConnection) con;
for (var it = lines().iterator(); it.hasNext(); ) {
tCon.writeLine(it.next());
}
}
}
}

View file

@ -16,6 +16,30 @@ import java.util.Optional;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
public interface DataStore {
/**
* 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 {
return true;
}
default boolean canWrite() throws Exception {
return true;
}
/**
* 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 false;
}
/**
* Performs a validation of this data store.
*

View file

@ -75,4 +75,5 @@ public class FileStore implements StreamDataStore, FilenameStore {
}
return split[split.length - 1];
}
}

View file

@ -15,5 +15,14 @@ public interface FilenameStore extends DataStore {
return Optional.of(i != -1 ? n.substring(0, i) : n);
}
default String getFileExtension() {
var split = getFileName().split("[\\\\.]");
if (split.length == 0) {
return "";
}
return split[split.length - 1];
}
String getFileName();
}

View file

@ -3,9 +3,9 @@ package io.xpipe.core.store;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonTypeName;
import lombok.Value;
import lombok.experimental.NonFinal;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.*;
/**
* A store whose contents are stored in memory.
@ -14,8 +14,13 @@ import java.io.InputStream;
@JsonTypeName("inMemory")
public class InMemoryStore implements StreamDataStore {
@NonFinal
byte[] value;
public InMemoryStore() {
this.value = new byte[0];
}
@JsonCreator
public InMemoryStore(byte[] value) {
this.value = value;
@ -31,6 +36,17 @@ public class InMemoryStore implements StreamDataStore {
return new ByteArrayInputStream(value);
}
@Override
public OutputStream openOutput() throws Exception {
return new ByteArrayOutputStream(){
@Override
public void close() throws IOException {
super.close();
InMemoryStore.this.value = this.toByteArray();
}
};
}
@Override
public String toSummaryString() {
return "inMemory";

View file

@ -1,6 +1,7 @@
package io.xpipe.core.store;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.xpipe.core.charsetter.NewLine;
import io.xpipe.core.util.Secret;
import lombok.Value;
@ -97,6 +98,17 @@ public class LocalStore extends StandardShellStore implements MachineFileStore {
return Files.exists(Path.of(file));
}
@Override
public void mkdirs(String file) throws Exception {
Files.createDirectories(Path.of(file).getParent());
}
@Override
public NewLine getNewLine() {
return ShellTypes.getDefault().getNewLine();
}
@Override
public String toSummaryString() {
return "localhost";
@ -110,6 +122,7 @@ public class LocalStore extends StandardShellStore implements MachineFileStore {
@Override
public OutputStream openOutput(String file) throws Exception {
mkdirs(file);
var p = Path.of(file);
return Files.newOutputStream(p);
}

View file

@ -1,5 +1,7 @@
package io.xpipe.core.store;
import io.xpipe.core.charsetter.NewLine;
import java.io.InputStream;
import java.io.OutputStream;
@ -15,4 +17,8 @@ public interface MachineFileStore extends DataStore {
OutputStream openOutput(String file) throws Exception;
public boolean exists(String file) throws Exception;
void mkdirs(String file) throws Exception;
NewLine getNewLine() throws Exception;
}

View file

@ -1,6 +1,7 @@
package io.xpipe.core.store;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.xpipe.core.charsetter.NewLine;
import io.xpipe.core.util.Secret;
import java.io.ByteArrayInputStream;
@ -63,6 +64,11 @@ public class ShellTypes {
return StandardCharsets.UTF_16LE;
}
@Override
public NewLine getNewLine() {
return NewLine.CRLF;
}
@Override
public String getName() {
return "powershell";
@ -82,6 +88,11 @@ public class ShellTypes {
@JsonProperty("cmd")
public static final StandardShellStore.ShellType CMD = new StandardShellStore.ShellType() {
@Override
public NewLine getNewLine() {
return NewLine.CRLF;
}
@Override
public List<String> switchTo(List<String> cmd) {
var l = new ArrayList<>(cmd);
@ -169,6 +180,11 @@ public class ShellTypes {
return StandardCharsets.UTF_8;
}
@Override
public NewLine getNewLine() {
return NewLine.LF;
}
@Override
public String getName() {
return "sh";

View file

@ -1,5 +1,6 @@
package io.xpipe.core.store;
import io.xpipe.core.charsetter.NewLine;
import io.xpipe.core.util.Secret;
import java.io.InputStream;
@ -26,6 +27,8 @@ public abstract class StandardShellStore extends ShellStore implements MachineFi
Charset getCharset();
NewLine getNewLine();
String getName();
String getDisplayName();
@ -33,6 +36,11 @@ public abstract class StandardShellStore extends ShellStore implements MachineFi
List<String> getOperatingSystemNameCommand();
}
public NewLine getNewLine() throws Exception {
return determineType().getNewLine();
}
public abstract ShellType determineType() throws Exception;
public final String querySystemName() throws Exception {
@ -67,4 +75,9 @@ public abstract class StandardShellStore extends ShellStore implements MachineFi
p.start();
return p.waitFor() == 0;
}
@Override
public void mkdirs(String file) throws Exception {
}
}

View file

@ -13,6 +13,11 @@ import java.io.OutputStream;
@JsonTypeName("stdin")
public class StdinDataStore implements StreamDataStore {
@Override
public boolean isLocalToApplication() {
return true;
}
@Override
public InputStream openInput() throws Exception {
var in = System.in;

View file

@ -12,6 +12,11 @@ import java.io.OutputStream;
@JsonTypeName("stdout")
public class StdoutDataStore implements StreamDataStore {
@Override
public boolean isLocalToApplication() {
return true;
}
@Override
public OutputStream openOutput() throws Exception {
// Create an output stream that will write to standard out but will not close it

View file

@ -9,16 +9,6 @@ import java.io.OutputStream;
*/
public interface StreamDataStore extends DataStore {
/**
* 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;
}
/**
* Opens an input stream that can be used to read its data.
@ -46,13 +36,6 @@ public interface StreamDataStore extends DataStore {
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 {
return true;
}
/**
* Indicates whether this store is persistent, i.e. whether the stored data can be read again or not.

View file

@ -12,26 +12,33 @@ import java.util.ServiceLoader;
public class JacksonHelper {
private static final ObjectMapper INSTANCE = new ObjectMapper();
private static final ObjectMapper BASE = new ObjectMapper();
private static ObjectMapper INSTANCE = new ObjectMapper();
private static boolean init = false;
private static List<Module> MODULES;
public static synchronized void initClassBased() {
initModularized(null);
}
public static synchronized void initModularized(ModuleLayer layer) {
ObjectMapper objectMapper = INSTANCE;
MODULES = findModules(layer);
ObjectMapper objectMapper = BASE;
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
objectMapper.registerModules(findModules(layer));
objectMapper.disable(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE);
objectMapper.setVisibility(objectMapper.getSerializationConfig().getDefaultVisibilityChecker()
.withFieldVisibility(JsonAutoDetect.Visibility.ANY)
.withGetterVisibility(JsonAutoDetect.Visibility.NONE)
.withSetterVisibility(JsonAutoDetect.Visibility.NONE)
.withCreatorVisibility(JsonAutoDetect.Visibility.NONE)
.withIsGetterVisibility(JsonAutoDetect.Visibility.NONE));
INSTANCE = BASE.copy();
INSTANCE.registerModules(MODULES);
init = true;
}
@ -56,6 +63,16 @@ public class JacksonHelper {
return INSTANCE.copy();
}
public static ObjectMapper newMapper(Class<?> excludedModule) {
if (!init) {
throw new IllegalStateException("Not initialized");
}
var mapper = BASE.copy();
mapper.registerModules(MODULES.stream().filter(module -> !module.getClass().equals(excludedModule)).toList());
return mapper;
}
public static boolean isInit() {
return init;
}

View file

@ -1,6 +1,6 @@
import io.xpipe.core.util.CoreJacksonModule;
module io.xpipe.core {
open module io.xpipe.core {
exports io.xpipe.core.store;
exports io.xpipe.core.source;
exports io.xpipe.core.data.generic;
@ -11,15 +11,6 @@ module io.xpipe.core {
exports io.xpipe.core.dialog;
exports io.xpipe.core.charsetter;
opens io.xpipe.core.store;
opens io.xpipe.core.source;
opens io.xpipe.core.data.type;
opens io.xpipe.core.data.generic;
opens io.xpipe.core.util;
opens io.xpipe.core.data.node;
opens io.xpipe.core.data.typed;
opens io.xpipe.core.dialog;
requires static com.fasterxml.jackson.core;
requires static com.fasterxml.jackson.databind;
requires java.net.http;

View file

@ -8,12 +8,7 @@ plugins {
apply from: "$rootDir/deps/java.gradle"
apply from: "$rootDir/deps/javafx.gradle"
apply from: "$rootDir/deps/richtextfx.gradle"
apply from: "$rootDir/deps/preferencesfx.gradle"
apply from: "$rootDir/deps/jackson.gradle"
apply from: "$rootDir/deps/commons.gradle"
apply from: "$rootDir/deps/lombok.gradle"
apply from: "$rootDir/deps/ikonli.gradle"
apply from: "$rootDir/deps/slf4j.gradle"
configurations {
compileOnly.extendsFrom(dep)
@ -21,16 +16,20 @@ configurations {
version = file('../misc/version').text
group = 'io.xpipe'
archivesBaseName = 'extension'
archivesBaseName = 'xpipe-extension'
dependencies {
compileOnly 'net.synedra:validatorfx:0.3.1'
compileOnly 'org.junit.jupiter:junit-jupiter-api:5.8.2'
compileOnly 'com.jfoenix:jfoenix:9.0.10'
implementation project(':core')
api project(':core')
api project(':beacon')
api project(':api')
api group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: "2.13.0"
// implementation project(':fxcomps')
implementation 'io.xpipe:fxcomps:0.2.1'
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.13.0"
implementation group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.2.0"
implementation 'net.synedra:validatorfx:0.3.1'
implementation 'org.junit.jupiter:junit-jupiter-api:5.9.0'
implementation 'com.jfoenix:jfoenix:9.0.10'
implementation 'io.xpipe:fxcomps:0.2.2'
implementation 'org.controlsfx:controlsfx:11.1.1'
}

View file

@ -1,6 +1,9 @@
publishing {
publications {
mavenJava(MavenPublication) {
artifactId = project.archivesBaseName
from components.java
pom {

View file

@ -1,15 +1,13 @@
package io.xpipe.extension;
import io.xpipe.core.charsetter.NewLine;
import io.xpipe.core.dialog.Dialog;
import io.xpipe.core.dialog.QueryConverter;
import io.xpipe.core.source.DataSource;
import io.xpipe.core.source.DataSourceType;
import io.xpipe.core.store.DataStore;
import javafx.beans.property.Property;
import javafx.scene.layout.Region;
import lombok.SneakyThrows;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
@ -21,20 +19,21 @@ public interface DataSourceProvider<T extends DataSource<?>> {
}
default void validate() throws Exception {
getGeneralType();
getCategory();
getSourceClass();
}
default Category getGeneralType() {
@SneakyThrows
default T create(Object... arguments) {
return (T) getSourceClass().getDeclaredConstructors()[0].newInstance(arguments);
}
default Category getCategory() {
if (getFileProvider() != null) {
return Category.FILE;
}
if (getDatabaseProvider() != null) {
return Category.DATABASE;
}
throw new ExtensionException("Provider has no general type");
throw new ExtensionException("Provider has no set general type");
}
default boolean supportsConversion(T in, DataSourceType t) {
@ -49,14 +48,14 @@ public interface DataSourceProvider<T extends DataSource<?>> {
}
default String i18n(String key) {
return I18n.get(getId() + "." + key);
return I18n.get(i18nKey(key));
}
default String i18nKey(String key) {
return getId() + "." + key;
}
default Region createConfigGui(Property<T> source, Property<T> appliedSource) {
default Region configGui(Property<T> source, Property<T> appliedSource, boolean all) {
return null;
}
@ -83,33 +82,7 @@ public interface DataSourceProvider<T extends DataSource<?>> {
Map<String, List<String>> getFileExtensions();
}
interface DatabaseProvider {
}
public static Dialog charset(Charset c, boolean all) {
return Dialog.query("charset", false, false, c != null &&!all, c, QueryConverter.CHARSET);
}
public static Dialog newLine(NewLine l, boolean all) {
return Dialog.query("newline", false, false, l != null &&!all, l, NEW_LINE_CONVERTER);
}
static <T> Dialog query(String desc, T value, boolean required, QueryConverter<T> c, boolean all) {
return Dialog.query(desc, false, required, value != null && !all, value, c);
}
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();
}
};
Dialog configDialog(T source, boolean all);
@ -123,7 +96,7 @@ public interface DataSourceProvider<T extends DataSource<?>> {
* Checks whether this provider prefers a certain kind of store.
* This is important for the correct autodetection of a store.
*/
boolean prefersStore(DataStore store);
boolean prefersStore(DataStore store, DataSourceType type);
/**
* Checks whether this provider supports the store in principle.
@ -147,23 +120,10 @@ public interface DataSourceProvider<T extends DataSource<?>> {
return null;
}
default DatabaseProvider getDatabaseProvider() {
return null;
}
default boolean hasDirectoryProvider() {
return false;
}
default String getId() {
return getPossibleNames().get(0);
}
default String getModuleName() {
var n = getClass().getPackageName();
var i = n.lastIndexOf('.');
return i != -1 ? n.substring(i + 1) : n;
}
/**
* Attempt to create a useful data source descriptor from a data store.
@ -171,10 +131,6 @@ public interface DataSourceProvider<T extends DataSource<?>> {
*/
T createDefaultSource(DataStore input) throws Exception;
default T createDefaultWriteSource(DataStore input) throws Exception {
return createDefaultSource(input);
}
Class<T> getSourceClass();

View file

@ -6,6 +6,7 @@ import io.xpipe.core.store.FileStore;
import io.xpipe.extension.event.ErrorEvent;
import lombok.SneakyThrows;
import java.util.List;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.Set;
@ -17,15 +18,18 @@ public class DataSourceProviders {
public static void init(ModuleLayer layer) {
if (ALL == null) {
ALL = ServiceLoader.load(layer, DataSourceProvider.class).stream()
.map(p -> (DataSourceProvider<?>) p.get()).collect(Collectors.toSet());
ALL = ServiceLoader.load(layer, DataSourceProvider.class)
.stream()
.map(p -> (DataSourceProvider<?>) p.get())
.collect(Collectors.toSet());
ALL.removeIf(p -> {
try {
p.init();
p.validate();
return false;
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
ErrorEvent.fromThrowable(e)
.handle();
return true;
}
});
@ -51,7 +55,8 @@ public class DataSourceProviders {
@SneakyThrows
public static StructureDataSource<FileStore> createLocalStructureDescriptor(DataStore store) {
return (StructureDataSource<FileStore>)
DataSourceProviders.byId("xpbs").getSourceClass()
DataSourceProviders.byId("xpbs")
.getSourceClass()
.getDeclaredConstructors()[0].newInstance(store);
}
@ -59,7 +64,8 @@ public class DataSourceProviders {
@SneakyThrows
public static RawDataSource<FileStore> createLocalRawDescriptor(DataStore store) {
return (RawDataSource<FileStore>)
DataSourceProviders.byId("binary").getSourceClass()
DataSourceProviders.byId("binary")
.getSourceClass()
.getDeclaredConstructors()[0].newInstance(store);
}
@ -67,7 +73,8 @@ public class DataSourceProviders {
@SneakyThrows
public static RawDataSource<FileStore> createLocalCollectionDescriptor(DataStore store) {
return (RawDataSource<FileStore>)
DataSourceProviders.byId("br").getSourceClass()
DataSourceProviders.byId("br")
.getSourceClass()
.getDeclaredConstructors()[0].newInstance(store);
}
@ -75,7 +82,8 @@ public class DataSourceProviders {
@SneakyThrows
public static TextDataSource<FileStore> createLocalTextDescriptor(DataStore store) {
return (TextDataSource<FileStore>)
DataSourceProviders.byId("text").getSourceClass()
DataSourceProviders.byId("text")
.getSourceClass()
.getDeclaredConstructors()[0].newInstance(store);
}
@ -83,7 +91,8 @@ public class DataSourceProviders {
@SneakyThrows
public static TableDataSource<FileStore> createLocalTableDescriptor(DataStore store) {
return (TableDataSource<FileStore>)
DataSourceProviders.byId("xpbt").getSourceClass()
DataSourceProviders.byId("xpbt")
.getSourceClass()
.getDeclaredConstructors()[0].newInstance(store);
}
@ -93,7 +102,10 @@ public class DataSourceProviders {
throw new IllegalStateException("Not initialized");
}
return (T) ALL.stream().filter(d -> d.getId().equals(name)).findAny()
return (T) ALL.stream()
.filter(d -> d.getId()
.equals(name))
.findAny()
.orElseThrow(() -> new IllegalArgumentException("Provider " + name + " not found"));
}
@ -104,7 +116,10 @@ public class DataSourceProviders {
throw new IllegalStateException("Not initialized");
}
return (T) ALL.stream().filter(d -> d.getSourceClass().equals(c)).findAny()
return (T) ALL.stream()
.filter(d -> d.getSourceClass()
.equals(c))
.findAny()
.orElseThrow(() -> new IllegalArgumentException("Provider for " + c.getSimpleName() + " not found"));
}
@ -113,17 +128,37 @@ public class DataSourceProviders {
throw new IllegalStateException("Not initialized");
}
return ALL.stream().filter(d -> d.getPossibleNames().stream()
.anyMatch(s -> s.equalsIgnoreCase(name))).findAny();
return ALL.stream()
.filter(d -> d.getPossibleNames()
.stream()
.anyMatch(s -> nameAlternatives(s).stream().anyMatch(s1 -> s1.equalsIgnoreCase(name))) || d.getId().equalsIgnoreCase(name))
.findAny();
}
public static Optional<DataSourceProvider<?>> byPreferredStore(DataStore store) {
private static List<String> nameAlternatives(String name) {
var split = List.of(name.split("_"));
return List.of(String.join(" ", split), String.join(
"_", split
), String.join(
"-", split
), split.stream()
.map(s -> s.equals(split.get(0)) ?
s :
s.substring(0, 1)
.toUpperCase() + s.substring(1))
.collect(Collectors.joining()));
}
public static Optional<DataSourceProvider<?>> byPreferredStore(DataStore store, DataSourceType type) {
if (ALL == null) {
throw new IllegalStateException("Not initialized");
}
return ALL.stream().filter(d -> d.getFileProvider() != null)
.filter(d -> d.prefersStore(store)).findAny();
return ALL.stream()
.filter(d -> type == null || d.getPrimaryType() == type)
.filter(d -> d.getFileProvider() != null)
.filter(d -> d.prefersStore(store, type))
.findAny();
}
public static Set<DataSourceProvider<?>> getAll() {

View file

@ -57,16 +57,17 @@ public class DataStoreProviders {
public static <T extends DataStoreProvider> T byStore(DataStore store) {
return (T) byStoreClass(store.getClass()).orElseThrow(() -> new IllegalArgumentException("Provider for " + store.getClass().getSimpleName() + " not found"));
return (T) byStoreClass(store.getClass());
}
@SuppressWarnings("unchecked")
public static <T extends DataStoreProvider> Optional<T> byStoreClass(Class<?> c) {
public static <T extends DataStoreProvider> T byStoreClass(Class<?> c) {
if (ALL == null) {
throw new IllegalStateException("Not initialized");
}
return (Optional<T>) ALL.stream().filter(d -> d.getStoreClasses().contains(c)).findAny();
return (T) ALL.stream().filter(d -> d.getStoreClasses().contains(c)).findAny().orElseThrow();
}
public static Set<DataStoreProvider> getAll() {

View file

@ -1,5 +1,7 @@
package io.xpipe.extension;
import io.xpipe.core.charsetter.NewLine;
import io.xpipe.core.charsetter.StreamCharset;
import io.xpipe.core.dialog.Dialog;
import io.xpipe.core.dialog.QueryConverter;
import io.xpipe.core.store.DataStore;
@ -20,47 +22,69 @@ public class DialogHelper {
public static Dialog addressQuery(Address address) {
var hostNameQuery = Dialog.query("Hostname", false, true, false, address.getHostname(), QueryConverter.STRING);
var portQuery = Dialog.query("Port", false, true, false, address.getPort(), QueryConverter.INTEGER);
return Dialog.chain(hostNameQuery, portQuery).evaluateTo(() -> new Address(hostNameQuery.getResult(), portQuery.getResult()));
return Dialog.chain(hostNameQuery, portQuery)
.evaluateTo(() -> new Address(hostNameQuery.getResult(), portQuery.getResult()));
}
public static Dialog machineQuery(DataStore store) {
var storeName = XPipeDaemon.getInstance().getStoreName(store).orElse("local");
return Dialog.query("Machine", false, true, false, storeName, QueryConverter.STRING).map((String name) -> {
if (name.equals("local")) {
return new LocalStore();
}
var storeName = XPipeDaemon.getInstance()
.getStoreName(store)
.orElse("local");
return Dialog.query("Machine", false, true, false, storeName, QueryConverter.STRING)
.map((String name) -> {
if (name.equals("local")) {
return new LocalStore();
}
var stored = XPipeDaemon.getInstance().getNamedStore(name);
if (stored.isEmpty()) {
throw new IllegalArgumentException(String.format("Store not found: %s", name));
}
var stored = XPipeDaemon.getInstance()
.getNamedStore(name);
if (stored.isEmpty()) {
throw new IllegalArgumentException(String.format("Store not found: %s", name));
}
if (!(stored.get() instanceof MachineFileStore)) {
throw new IllegalArgumentException(String.format("Store not a machine store: %s", name));
}
if (!(stored.get() instanceof MachineFileStore)) {
throw new IllegalArgumentException(String.format("Store not a machine store: %s", name));
}
return stored.get();
});
return stored.get();
});
}
public static Dialog shellQuery(DataStore store) {
var storeName = XPipeDaemon.getInstance().getStoreName(store).orElse("local");
return Dialog.query("Shell", false, true, false, storeName, QueryConverter.STRING).map((String name) -> {
if (name.equals("local")) {
return new LocalStore();
}
var storeName = XPipeDaemon.getInstance()
.getStoreName(store)
.orElse("local");
return Dialog.query("Shell", false, true, false, storeName, QueryConverter.STRING)
.map((String name) -> {
if (name.equals("local")) {
return new LocalStore();
}
var stored = XPipeDaemon.getInstance().getNamedStore(name);
if (stored.isEmpty()) {
throw new IllegalArgumentException(String.format("Store not found: %s", name));
}
var stored = XPipeDaemon.getInstance()
.getNamedStore(name);
if (stored.isEmpty()) {
throw new IllegalArgumentException(String.format("Store not found: %s", name));
}
if (!(stored.get() instanceof ShellStore)) {
throw new IllegalArgumentException(String.format("Store not a shell store: %s", name));
}
if (!(stored.get() instanceof ShellStore)) {
throw new IllegalArgumentException(String.format("Store not a shell store: %s", name));
}
return stored.get();
});
return stored.get();
});
}
public static Dialog charsetQuery(StreamCharset c, boolean all) {
return Dialog.query("Charset", false, true, c != null &&!all, c, QueryConverter.CHARSET);
}
public static Dialog newLineQuery(NewLine n, boolean all) {
return Dialog.query("Newline", false, true, n != null &&!all, n, QueryConverter.NEW_LINE);
}
public static <T> Dialog query(String desc, T value, boolean required, QueryConverter<T> c, boolean all) {
return Dialog.query(desc, false, required, value != null && !all, value, c);
}
public static Dialog fileQuery(String name) {
@ -71,6 +95,21 @@ public class DialogHelper {
return Dialog.query("User", false, true, false, name, QueryConverter.STRING);
}
public static Dialog namedStoreQuery(DataStore store, Class<? extends DataStore> filter) {
var name = XPipeDaemon.getInstance()
.getStoreName(store)
.orElse(null);
return Dialog.query("Store", false, true, false, name, QueryConverter.STRING)
.map((String newName) -> {
var found = XPipeDaemon.getInstance()
.getNamedStore(newName)
.orElseThrow();
if (!filter.isAssignableFrom(found.getClass())) {
throw new IllegalArgumentException("Incompatible store type");
}
return found;
});
}
public static Dialog passwordQuery(Secret password) {
return Dialog.querySecret("Password", false, true, password);

View file

@ -3,7 +3,7 @@ package io.xpipe.extension;
import io.xpipe.core.source.DataSource;
import io.xpipe.core.source.DataSourceType;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.FileStore;
import io.xpipe.core.store.FilenameStore;
import io.xpipe.core.store.StreamDataStore;
import java.util.LinkedHashMap;
@ -19,11 +19,16 @@ public interface SimpleFileDataSourceProvider<T extends DataSource<?>> extends D
@Override
default DataSource<?> convert(T in, DataSourceType t) throws Exception {
return DataSourceProviders.byId("binary").createDefaultSource(in.getStore());
return DataSourceProviders.byId("binary")
.createDefaultSource(in.getStore());
}
@Override
default boolean prefersStore(DataStore store) {
default boolean prefersStore(DataStore store, DataSourceType type) {
if (type != null && type != getPrimaryType()) {
return false;
}
for (var e : getSupportedExtensions().entrySet()) {
if (e.getValue() == null) {
continue;
@ -34,8 +39,9 @@ public interface SimpleFileDataSourceProvider<T extends DataSource<?>> extends D
continue;
}
if (store instanceof FileStore l) {
return l.getFileName().endsWith("." + ext);
if (store instanceof FilenameStore l) {
return l.getFileExtension()
.equalsIgnoreCase(ext);
}
}
}
@ -50,6 +56,7 @@ public interface SimpleFileDataSourceProvider<T extends DataSource<?>> extends D
default String getNameI18nKey() {
return i18nKey("displayName");
}
Map<String, List<String>> getSupportedExtensions();
@Override

View file

@ -15,7 +15,7 @@ public interface UniformDataSourceProvider<T extends DataSource<?>> extends Data
@Override
@SuppressWarnings("unchecked")
default T createDefaultSource(DataStore input) {
default T createDefaultSource(DataStore input) throws Exception {
try {
return (T) getSourceClass().getDeclaredConstructors()[0].newInstance(input);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {

View file

@ -1,9 +1,15 @@
package io.xpipe.extension;
import io.xpipe.core.store.DataStore;
import io.xpipe.fxcomps.Comp;
import javafx.beans.property.Property;
import javafx.beans.value.ObservableValue;
import javafx.scene.image.Image;
import java.util.List;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.function.Predicate;
public interface XPipeDaemon {
@ -11,6 +17,16 @@ public interface XPipeDaemon {
return ServiceLoader.load(XPipeDaemon.class).findFirst().orElseThrow();
}
List<DataStore> getNamedStores();
public Image image(String file);
Comp<?> streamStoreChooser(Property<DataStore> storeProperty, Property<DataSourceProvider<?>> provider);
Comp<?> namedStoreChooser(ObservableValue<Predicate<DataStore>> filter, Property<? extends DataStore> selected, DataStoreProvider.Category category);
Comp<?> sourceProviderChooser(Property<DataSourceProvider<?>> provider, DataSourceProvider.Category category);
Optional<DataStore> getNamedStore(String name);
Optional<String> getStoreName(DataStore store);

View file

@ -1,33 +1,39 @@
package io.xpipe.extension.comp;
import io.xpipe.fxcomps.Comp;
import io.xpipe.fxcomps.CompStructure;
import io.xpipe.fxcomps.comp.ReplacementComp;
import io.xpipe.core.charsetter.StreamCharset;
import io.xpipe.extension.I18n;
import io.xpipe.fxcomps.SimpleComp;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.List;
public class CharsetChoiceComp extends SimpleComp {
public class CharsetChoiceComp extends ReplacementComp<CompStructure<ComboBox<Charset>>> {
private final Property<StreamCharset> charset;
private final Property<Charset> charset;
public CharsetChoiceComp(Property<Charset> charset) {
public CharsetChoiceComp(Property<StreamCharset> charset) {
this.charset = charset;
}
@Override
protected Comp<CompStructure<ComboBox<Charset>>> createComp() {
var map = new LinkedHashMap<Charset, ObservableValue<String>>();
for (var e : List.of(StandardCharsets.UTF_8, StandardCharsets.UTF_16,
StandardCharsets.UTF_16BE, StandardCharsets.ISO_8859_1, Charset.forName("Windows-1251"), Charset.forName("Windows-1252"), StandardCharsets.US_ASCII)) {
map.put(e, new SimpleStringProperty(e.displayName()));
protected Region createSimple() {
var builder = new CustomComboBoxBuilder<>(charset, streamCharset -> {
return new Label(streamCharset.getCharset().displayName() + (streamCharset.hasByteOrderMark() ?
" (BOM)" :
""));
}, new Label(I18n.get("extension.none")), null);
builder.addHeader(I18n.get("extension.common"));
for (var e : StreamCharset.COMMON) {
builder.add(e);
}
return new ChoiceComp<>(charset, map);
builder.addHeader(I18n.get("extension.other"));
builder.addFilter((charset, filter) -> {
return charset.getCharset().displayName().contains(filter);
});
for (var e : StreamCharset.RARE) {
builder.add(e);
}
return builder.build();
}
}

View file

@ -0,0 +1,192 @@
package io.xpipe.extension.comp;
import io.xpipe.fxcomps.util.SimpleChangeListener;
import javafx.application.Platform;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.Separator;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import java.util.*;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.Predicate;
public class CustomComboBoxBuilder<T> {
private final Property<T> selected;
private final Function<T, Node> nodeFunction;
private final Map<Node, T> nodeMap = new HashMap<>();
private final Map<Node, Runnable> actionsMap = new HashMap<>();
private final List<Node> nodes = new ArrayList<>();
private final Set<Node> disabledNodes = new HashSet<>();
private final Node emptyNode;
private final Predicate<T> veto;
private BiPredicate<T, String> filterPredicate;
private final Property<String> filterString = new SimpleStringProperty();
private final List<T> filterable = new ArrayList<>();
private Node filterNode;
public void addAction(Node node, Runnable run) {
nodes.add(node);
actionsMap.put(node, run);
}
public Node add(T val) {
var node = nodeFunction.apply(val);
nodeMap.put(node, val);
nodes.add(node);
if (filterPredicate != null) {
filterable.add(val);
}
return node;
}
public void addSeparator() {
var sep = new Separator(Orientation.HORIZONTAL);
nodes.add(sep);
disabledNodes.add(sep);
}
public void addHeader(String name) {
var spacer = new Region();
spacer.setPrefHeight(10);
var header = new Label(name);
header.setAlignment(Pos.CENTER);
var v = new VBox(spacer, header, new Separator(Orientation.HORIZONTAL));
v.setAlignment(Pos.CENTER);
nodes.add(v);
disabledNodes.add(v);
}
public void addFilter(BiPredicate<T, String> filterPredicate) {
this.filterPredicate = filterPredicate;
var spacer = new Region();
spacer.setPrefHeight(10);
var header = new FilterComp(filterString).createStructure();
var v = new VBox(header.get());
v.setAlignment(Pos.CENTER);
nodes.add(v);
filterNode = header.getText();
}
public CustomComboBoxBuilder(Property<T> selected, Function<T, Node> nodeFunction, Node emptyNode, Predicate<T> veto) {
this.selected = selected;
this.nodeFunction = nodeFunction;
this.emptyNode = emptyNode;
this.veto = veto;
}
public ComboBox<Node> build() {
var cb = new ComboBox<Node>();
cb.getItems().addAll(nodes);
cb.setCellFactory((lv) -> {
return new Cell();
});
cb.setButtonCell(new SelectedCell());
SimpleChangeListener.apply(selected, c -> {
var item = nodeMap.entrySet().stream().filter(e -> e.getValue() != null && e.getValue().equals(c)).map(e -> e.getKey()).findAny().orElse(null);
cb.setValue(Optional.ofNullable(item).orElse(emptyNode));
});
cb.valueProperty().addListener((c, o, n) -> {
if (nodeMap.containsKey(n)) {
if (veto != null && !veto.test(nodeMap.get(n))) {
cb.setValue(o);
;
return;
}
selected.setValue(nodeMap.get(n));
}
if (actionsMap.containsKey(n)) {
cb.setValue(o);
actionsMap.get(n).run();
}
});
if (filterPredicate != null) {
SimpleChangeListener.apply(filterString, c -> {
var filteredNodes = nodes.stream().filter(e -> e.equals(cb.getValue()) || !(nodeMap.get(e) != null && (
filterable.contains(nodeMap.get(e)) && filterString.getValue() != null && !filterPredicate.test(nodeMap.get(e), c)))).toList();
cb.setItems(FXCollections.observableList(filteredNodes));
});
filterNode.sceneProperty().addListener((c, o, n) -> {
if (n != null) {
n.getWindow().focusedProperty().addListener((c2, o2, n2) -> {
Platform.runLater(() -> {
filterNode.requestFocus();
});
});
}
Platform.runLater(() -> {
filterNode.requestFocus();
});
});
}
return cb;
}
private class SelectedCell extends ListCell<Node> {
@Override
protected void updateItem(Node item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
return;
}
if (item.equals(emptyNode)) {
setGraphic(item);
return;
}
if (!nodeMap.containsKey(item)) {
return;
}
var val = nodeMap.get(item);
var newNode = nodeFunction.apply(val);
setGraphic(newNode);
}
}
private class Cell extends ListCell<Node> {
@Override
protected void updateItem(Node item, boolean empty) {
setGraphic(item);
if (getItem() == item) {
return;
}
super.updateItem(item, empty);
if (item == null) {
return;
}
setGraphic(item);
if (disabledNodes.contains(item)) {
this.setDisable(true);
// this.setPadding(Insets.EMPTY);
}else {
this.setDisable(false);
}
}
}
}

View file

@ -1,6 +1,7 @@
package io.xpipe.extension.comp;
import io.xpipe.core.charsetter.NewLine;
import io.xpipe.core.charsetter.StreamCharset;
import io.xpipe.core.util.Secret;
import io.xpipe.extension.I18n;
import io.xpipe.extension.Validator;
@ -12,7 +13,6 @@ import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import net.synedra.validatorfx.Check;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
@ -43,6 +43,11 @@ public class DynamicOptionsBuilder<T> {
this.title = title;
}
public DynamicOptionsBuilder<T> addTitle(ObservableValue<String> title) {
entries.add(new DynamicOptionsComp.Entry(null, Comp.of(() -> new Label(title.getValue())).styleClass("title")));
return this;
}
public DynamicOptionsBuilder<T> makeLazy() {
var p = props.get(props.size() - 1);
props.remove(p);
@ -108,7 +113,7 @@ public class DynamicOptionsBuilder<T> {
return this;
}
public DynamicOptionsBuilder<T> addCharset(Property<Charset> prop) {
public DynamicOptionsBuilder<T> addCharset(Property<StreamCharset> prop) {
var comp = new CharsetChoiceComp(prop);
entries.add(new DynamicOptionsComp.Entry(I18n.observable("extension.charset"), comp));
props.add(prop);

View file

@ -6,6 +6,7 @@ import io.xpipe.fxcomps.SimpleCompStructure;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
@ -33,6 +34,7 @@ public class DynamicOptionsComp extends Comp<CompStructure<FlowPane>> {
flow.setAlignment(Pos.CENTER);
flow.setHgap(7);
flow.setVgap(7);
flow.setPadding(new Insets(8, 0, 0, 0));
var nameRegions = new ArrayList<Region>();
var compRegions = new ArrayList<Region>();

View file

@ -0,0 +1,55 @@
package io.xpipe.extension.comp;
import io.xpipe.fxcomps.Comp;
import io.xpipe.fxcomps.CompStructure;
import io.xpipe.fxcomps.util.PlatformThread;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.StackPane;
import lombok.Builder;
import lombok.Value;
import org.kordamp.ikonli.javafx.FontIcon;
public class FilterComp extends Comp<FilterComp.Structure> {
public FilterComp(Property<String> filterText) {
this.filterText = filterText;
}
@Value
@Builder
public static class Structure implements CompStructure<StackPane> {
StackPane pane;
Node inactiveIcon;
Label inactiveText;
TextField text;
@Override
public StackPane get() {
return pane;
}
}
private final Property<String> filterText;
@Override
public Structure createBase() {
var fi = new FontIcon("mdi2m-magnify");
var bgLabel = new Label("Search ...", fi);
bgLabel.getStyleClass().add("background");
var filter = new TextField();
PlatformThread.connect(filterText, filter.textProperty());
bgLabel.visibleProperty().bind(Bindings.createBooleanBinding(() -> (filter.getText() == null || filter.getText().isEmpty()),
filter.textProperty(), filter.focusedProperty()));
var stack = new StackPane(bgLabel, filter);
stack.getStyleClass().add("filter-comp");
bgLabel.prefHeightProperty().bind(stack.heightProperty());
filter.prefHeightProperty().bind(stack.heightProperty());
return Structure.builder().inactiveIcon(fi).inactiveText(bgLabel).text(filter).pane(stack).build();
}
}

View file

@ -1,7 +1,5 @@
package io.xpipe.extension.event;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.ServiceLoader;
@ -19,12 +17,13 @@ public abstract class EventHandler {
if (cat == null) {
cat = "log";
}
LoggerFactory.getLogger(cat).info(te.getMessage());
System.out.println("[" + cat + "] " + te.getMessage());
}
@Override
public void handle(ErrorEvent ee) {
LoggerFactory.getLogger(EventHandler.class).error(ee.getDescription(), ee.getThrowable());
if (ee.getDescription() != null) System.err.println(ee.getDescription());
if (ee.getThrowable() != null) ee.getThrowable().printStackTrace();
}
};

View file

@ -1,10 +1,6 @@
package io.xpipe.extension.prefs;
import com.dlsc.preferencesfx.model.Setting;
import java.util.List;
public interface PrefsHandler {
void addSetting(List<String> category, String group, Setting<?,?> setting);
// void addSetting(List<String> category, String group, Setting<?,?> setting);
}

View file

@ -1,17 +1,17 @@
package io.xpipe.api.test;
package io.xpipe.extension.test;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
public class DaemonControl {
public class ExtensionTest {
@BeforeAll
public static void setup() throws Exception {
ConnectionFactory.start();
ExtensionTestConnector.start();
}
@AfterAll
public static void teardown() throws Exception {
ConnectionFactory.stop();
ExtensionTestConnector.stop();
}
}

View file

@ -1,24 +1,36 @@
package io.xpipe.api.test;
package io.xpipe.extension.test;
import io.xpipe.api.connector.XPipeConnection;
import io.xpipe.beacon.BeaconClient;
import io.xpipe.beacon.BeaconServer;
import io.xpipe.core.charsetter.Charsetter;
import io.xpipe.core.charsetter.CharsetterContext;
import io.xpipe.core.util.JacksonHelper;
import io.xpipe.extension.DataSourceProviders;
public class ConnectionFactory {
public class ExtensionTestConnector {
private static boolean alreadyStarted;
public static void start() throws Exception {
DataSourceProviders.init(ModuleLayer.boot());
JacksonHelper.initClassBased();
Charsetter.init(CharsetterContext.empty());
if (BeaconServer.isRunning()) {
alreadyStarted = true;
return;
}
if (!BeaconServer.tryStart()) {
throw new AssertionError();
Process process = null;
if ((process = BeaconServer.tryStartCustom()) != null) {
} else {
if ((process = BeaconServer.tryStart()) == null) {
throw new AssertionError();
}
}
XPipeConnection.waitForStartup().orElseThrow();
XPipeConnection.waitForStartup(process).orElseThrow();
if (!BeaconServer.isRunning()) {
throw new AssertionError();
}

View file

@ -0,0 +1,21 @@
package io.xpipe.extension.util;
import io.xpipe.core.data.node.DataStructureNodeAcceptor;
import io.xpipe.core.data.node.TupleNode;
import io.xpipe.core.source.DataSource;
import io.xpipe.core.source.DataSourceConnection;
import io.xpipe.core.source.DataSourceType;
import io.xpipe.core.source.TableWriteConnection;
public class AppendingTableWriteConnection extends AppendingWriteConnection implements TableWriteConnection {
public AppendingTableWriteConnection(DataSource<?> source, DataSourceConnection connection
) {
super(DataSourceType.TABLE, source, connection);
}
@Override
public DataStructureNodeAcceptor<TupleNode> writeLinesAcceptor() {
return ((TableWriteConnection)connection).writeLinesAcceptor();
}
}

View file

@ -0,0 +1,20 @@
package io.xpipe.extension.util;
import io.xpipe.core.source.DataSource;
import io.xpipe.core.source.DataSourceConnection;
import io.xpipe.core.source.DataSourceType;
import io.xpipe.core.source.TextWriteConnection;
public class AppendingTextWriteConnection extends AppendingWriteConnection implements TextWriteConnection {
public AppendingTextWriteConnection(
DataSource<?> source, DataSourceConnection connection
) {
super(DataSourceType.TEXT, source, connection);
}
@Override
public void writeLine(String line) throws Exception {
((TextWriteConnection) connection).writeLine(line);
}
}

View file

@ -0,0 +1,48 @@
package io.xpipe.extension.util;
import io.xpipe.core.source.DataSource;
import io.xpipe.core.source.DataSourceConnection;
import io.xpipe.core.source.DataSourceType;
import io.xpipe.core.store.FileStore;
import io.xpipe.extension.DataSourceProviders;
import java.nio.file.Files;
public class AppendingWriteConnection implements DataSourceConnection {
private final DataSourceType type;
private final DataSource<?> source;
protected final DataSourceConnection connection;
public AppendingWriteConnection(DataSourceType type, DataSource<?> source, DataSourceConnection connection) {
this.type = type;
this.source = source;
this.connection = connection;
}
public void init() throws Exception {
var temp = Files.createTempFile(null, null);
var nativeStore = FileStore.local(temp);
var nativeSource = DataSourceProviders.getNativeDataSourceDescriptorForType(type).createDefaultSource(nativeStore);
if (source.getStore().canOpen()) {
try (var in = source.openReadConnection(); var out = nativeSource.openWriteConnection()) {
in.forward(out);
}
;
}
connection.init();
if (source.getStore().canOpen()) {
try (var in = nativeSource.openReadConnection()) {
in.forward(connection);
}
}
}
public void close() throws Exception {
connection.close();
}
}

View file

@ -0,0 +1,50 @@
package io.xpipe.extension.util;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.xpipe.core.source.DataSource;
import io.xpipe.core.util.JacksonHelper;
import io.xpipe.extension.DataSourceProviders;
import java.io.IOException;
public class ExtensionJacksonModule extends SimpleModule {
@Override
public void setupModule(SetupContext context) {
addSerializer(DataSource.class, new DataSourceSerializer());
addDeserializer(DataSource.class, new DataSourceDeserializer());
context.addSerializers(_serializers);
context.addDeserializers(_deserializers);
}
public static class DataSourceSerializer extends JsonSerializer<DataSource> {
@Override
public void serialize(DataSource value, JsonGenerator jgen, SerializerProvider provider)
throws IOException {
ObjectMapper mapper = JacksonHelper.newMapper(ExtensionJacksonModule.class);
var prov = DataSourceProviders.byDataSourceClass(value.getClass());
ObjectNode objectNode = mapper.valueToTree(value);
objectNode.put("type", prov.getId());
jgen.writeTree(objectNode);
}
}
public static class DataSourceDeserializer extends JsonDeserializer<DataSource> {
@Override
public DataSource deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
var mapper = JacksonHelper.newMapper(ExtensionJacksonModule.class);
var tree = (ObjectNode) mapper.readTree(p);
var type = tree.get("type").textValue();
var prov = DataSourceProviders.byId(type);
return mapper.treeToValue(tree, prov.getSourceClass());
}
}
}

View file

@ -1,33 +1,35 @@
import com.fasterxml.jackson.databind.Module;
import io.xpipe.extension.DataSourceProvider;
import io.xpipe.extension.SupportedApplicationProvider;
import io.xpipe.extension.util.ExtensionJacksonModule;
module io.xpipe.extension {
open module io.xpipe.extension {
exports io.xpipe.extension;
exports io.xpipe.extension.comp;
exports io.xpipe.extension.event;
exports io.xpipe.extension.prefs;
exports io.xpipe.extension.util;
exports io.xpipe.extension.test;
requires transitive io.xpipe.core;
requires transitive javafx.base;
requires javafx.graphics;
requires transitive javafx.controls;
requires io.xpipe.fxcomps;
requires static lombok;
requires static com.dlsc.preferencesfx;
requires static com.dlsc.formsfx;
requires static org.slf4j;
requires static org.controlsfx.controls;
requires java.desktop;
requires org.fxmisc.richtext;
requires static net.synedra.validatorfx;
requires org.fxmisc.flowless;
requires org.fxmisc.undofx;
requires org.fxmisc.wellbehavedfx;
requires org.reactfx;
requires org.kordamp.ikonli.javafx;
requires io.xpipe.beacon;
requires io.xpipe.api;
requires com.fasterxml.jackson.databind;
requires static org.junit.jupiter.api;
requires transitive javafx.base;
requires static javafx.graphics;
requires static javafx.controls;
requires static io.xpipe.fxcomps;
requires static lombok;
requires static org.controlsfx.controls;
requires static java.desktop;
requires static org.fxmisc.richtext;
requires static net.synedra.validatorfx;
requires static org.fxmisc.flowless;
requires static org.fxmisc.undofx;
requires static org.fxmisc.wellbehavedfx;
requires static org.reactfx;
requires static org.kordamp.ikonli.javafx;
requires static com.jfoenix;
uses DataSourceProvider;
@ -37,4 +39,6 @@ module io.xpipe.extension {
uses io.xpipe.extension.prefs.PrefsProvider;
uses io.xpipe.extension.DataStoreProvider;
uses io.xpipe.extension.XPipeDaemon;
provides Module with ExtensionJacksonModule;
}

View file

@ -49,28 +49,28 @@ jreleaser {
api {
artifact {
distributionType = 'SINGLE_JAR'
path = 'api/build/libs/api-{{version}}.jar'
path = 'api/build/libs/xpipe-api-{{version}}.jar'
}
}
core {
artifact {
distributionType = 'SINGLE_JAR'
path = 'core/build/libs/core-{{version}}.jar'
path = 'core/build/libs/xpipe-core-{{version}}.jar'
}
}
beacon {
artifact {
distributionType = 'SINGLE_JAR'
path = 'beacon/build/libs/beacon-{{version}}.jar'
path = 'beacon/build/libs/xpipe-beacon-{{version}}.jar'
}
}
extension {
artifact {
distributionType = 'SINGLE_JAR'
path = 'extension/build/libs/extension-{{version}}.jar'
path = 'extension/build/libs/xpipe-extension-{{version}}.jar'
}
}
}

View file

@ -1 +1 @@
0.0.1.4-SNAPSHOT
0.0.1.6-SNAPSHOT