diff --git a/api/build.gradle b/api/build.gradle index 1859c112f..3287446d2 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -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 { diff --git a/api/publish.gradle b/api/publish.gradle index e26cc82d8..0806964fb 100644 --- a/api/publish.gradle +++ b/api/publish.gradle @@ -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.' diff --git a/api/src/main/java/io/xpipe/api/DataSource.java b/api/src/main/java/io/xpipe/api/DataSource.java index 0130110f7..18c2fa0ce 100644 --- a/api/src/main/java/io/xpipe/api/DataSource.java +++ b/api/src/main/java/io/xpipe/api/DataSource.java @@ -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. */ diff --git a/api/src/main/java/io/xpipe/api/DataSourceConfig.java b/api/src/main/java/io/xpipe/api/DataSourceConfig.java index f259d7232..921488039 100644 --- a/api/src/main/java/io/xpipe/api/DataSourceConfig.java +++ b/api/src/main/java/io/xpipe/api/DataSourceConfig.java @@ -26,7 +26,7 @@ public final class DataSourceConfig { return provider; } - public Map getConfigInstance() { + public Map getConfig() { return configInstance; } } diff --git a/api/src/main/java/io/xpipe/api/DataText.java b/api/src/main/java/io/xpipe/api/DataText.java index 386a4b672..a97e104a2 100644 --- a/api/src/main/java/io/xpipe/api/DataText.java +++ b/api/src/main/java/io/xpipe/api/DataText.java @@ -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 { +public interface DataText extends DataSource { DataSourceInfo.Text getInfo(); @@ -12,6 +13,8 @@ public interface DataText extends DataSource, Iterable { List readLines(int maxLines); + Stream lines(); + String readAll(); String read(int maxCharacters); diff --git a/api/src/main/java/io/xpipe/api/connector/XPipeConnection.java b/api/src/main/java/io/xpipe/api/connector/XPipeConnection.java index 428828268..5e5cebf3b 100644 --- a/api/src/main/java/io/xpipe/api/connector/XPipeConnection.java +++ b/api/src/main/java/io/xpipe/api/connector/XPipeConnection.java @@ -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 waitForStartup() { - for (int i = 0; i < 80; i++) { + public static Optional 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) { diff --git a/api/src/main/java/io/xpipe/api/impl/DataRawImpl.java b/api/src/main/java/io/xpipe/api/impl/DataRawImpl.java index b740f6d0d..23ce61ec0 100644 --- a/api/src/main/java/io/xpipe/api/impl/DataRawImpl.java +++ b/api/src/main/java/io/xpipe/api/impl/DataRawImpl.java @@ -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; } diff --git a/api/src/main/java/io/xpipe/api/impl/DataSourceImpl.java b/api/src/main/java/io/xpipe/api/impl/DataSourceImpl.java index 825006504..9c1eda10f 100644 --- a/api/src/main/java/io/xpipe/api/impl/DataSourceImpl.java +++ b/api/src/main/java/io/xpipe/api/impl/DataSourceImpl.java @@ -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 diff --git a/api/src/main/java/io/xpipe/api/impl/DataStructureImpl.java b/api/src/main/java/io/xpipe/api/impl/DataStructureImpl.java index cfe9a8ca3..0f8cee40d 100644 --- a/api/src/main/java/io/xpipe/api/impl/DataStructureImpl.java +++ b/api/src/main/java/io/xpipe/api/impl/DataStructureImpl.java @@ -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; } diff --git a/api/src/main/java/io/xpipe/api/impl/DataTableImpl.java b/api/src/main/java/io/xpipe/api/impl/DataTableImpl.java index 7194b0a75..3a1dfe067 100644 --- a/api/src/main/java/io/xpipe/api/impl/DataTableImpl.java +++ b/api/src/main/java/io/xpipe/api/impl/DataTableImpl.java @@ -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; } diff --git a/api/src/main/java/io/xpipe/api/impl/DataTextImpl.java b/api/src/main/java/io/xpipe/api/impl/DataTextImpl.java index 217241cb4..af19f8d6b 100644 --- a/api/src/main/java/io/xpipe/api/impl/DataTextImpl.java +++ b/api/src/main/java/io/xpipe/api/impl/DataTextImpl.java @@ -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 readAllLines() { - return null; + return readLines(Integer.MAX_VALUE); } @Override public List readLines(int maxLines) { - return null; + try (Stream lines = lines()) { + return lines.limit(maxLines).toList(); + } + } + + @Override + public Stream lines() { + var iterator = new Iterator() { + + 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 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 iterator() { - return null; - } + } diff --git a/beacon/build.gradle b/beacon/build.gradle index fbba9b83d..aa72a4f13 100644 --- a/beacon/build.gradle +++ b/beacon/build.gradle @@ -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' diff --git a/beacon/publish.gradle b/beacon/publish.gradle index fe31c4d9d..264bf0730 100644 --- a/beacon/publish.gradle +++ b/beacon/publish.gradle @@ -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.' diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java b/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java index 169fb0cd1..a72965da7 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java @@ -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()); } diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconConfig.java b/beacon/src/main/java/io/xpipe/beacon/BeaconConfig.java index d71907ac2..d37dd3591 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconConfig.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconConfig.java @@ -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; + } } + + diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconConnection.java b/beacon/src/main/java/io/xpipe/beacon/BeaconConnection.java index 340d4c635..21b564d4d 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconConnection.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconConnection.java @@ -143,7 +143,7 @@ public abstract class BeaconConnection implements AutoCloseable { public RES performOutputExchange( REQ req, - BeaconClient.FailableConsumer reqWriter) { + BeaconClient.FailableConsumer reqWriter) { checkClosed(); try { diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconFormat.java b/beacon/src/main/java/io/xpipe/beacon/BeaconFormat.java index 78b335b8b..6292d5d74 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconFormat.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconFormat.java @@ -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); } diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java b/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java index 1391f59a7..fbba038b2 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java @@ -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 getDaemonExecutableFromHome() { - var env = System.getenv("XPIPE_HOME"); - if (env == null) { - return Optional.empty(); - } - - Path file; - + private static Optional 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 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 { diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/AddSourceExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/AddSourceExchange.java new file mode 100644 index 000000000..2397f70da --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/AddSourceExchange.java @@ -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; + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/ForwardExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/ForwardExchange.java new file mode 100644 index 000000000..971d92ac7 --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/ForwardExchange.java @@ -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 { + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/QueryDataSourceExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/QueryDataSourceExchange.java index bc1697350..34166eaa5 100644 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/QueryDataSourceExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/QueryDataSourceExchange.java @@ -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 config; + + DataSource internalSource; } } diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/api/QueryTextDataExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/api/QueryTextDataExchange.java index a47036300..578327633 100644 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/api/QueryTextDataExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/api/QueryTextDataExchange.java @@ -23,7 +23,8 @@ public class QueryTextDataExchange implements MessageExchange { @NonNull DataSourceReference ref; - int maxLines; + @Builder.Default + int maxLines = -1; } @Jacksonized diff --git a/beacon/src/main/java/module-info.java b/beacon/src/main/java/module-info.java index 2f9caae27..75cf65084 100644 --- a/beacon/src/main/java/module-info.java +++ b/beacon/src/main/java/module-info.java @@ -22,7 +22,9 @@ module io.xpipe.beacon { uses MessageExchange; provides io.xpipe.beacon.exchange.MessageExchange with + ForwardExchange, EditStoreExchange, + AddSourceExchange, StoreProviderListExchange, ListCollectionsExchange, ListEntriesExchange, diff --git a/core/build.gradle b/core/build.gradle index 6a2c44d80..4a9089d55 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -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" \ No newline at end of file diff --git a/core/publish.gradle b/core/publish.gradle index 9b3881927..9a259c5ed 100644 --- a/core/publish.gradle +++ b/core/publish.gradle @@ -1,6 +1,8 @@ publishing { publications { mavenJava(MavenPublication) { + artifactId = project.archivesBaseName + from components.java pom { diff --git a/core/src/main/java/io/xpipe/core/charsetter/Charsettable.java b/core/src/main/java/io/xpipe/core/charsetter/Charsettable.java index 3a78f0a3a..fc76765ee 100644 --- a/core/src/main/java/io/xpipe/core/charsetter/Charsettable.java +++ b/core/src/main/java/io/xpipe/core/charsetter/Charsettable.java @@ -1,8 +1,6 @@ package io.xpipe.core.charsetter; -import java.nio.charset.Charset; - public interface Charsettable { - Charset getCharset(); + StreamCharset getCharset(); } diff --git a/core/src/main/java/io/xpipe/core/charsetter/Charsetter.java b/core/src/main/java/io/xpipe/core/charsetter/Charsetter.java index 13ccbf759..092a0beca 100644 --- a/core/src/main/java/io/xpipe/core/charsetter/Charsetter.java +++ b/core/src/main/java/io/xpipe/core/charsetter/Charsetter.java @@ -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 in, FailableConsumer 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 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; } diff --git a/core/src/main/java/io/xpipe/core/charsetter/StreamCharset.java b/core/src/main/java/io/xpipe/core/charsetter/StreamCharset.java new file mode 100644 index 000000000..0f2cec39f --- /dev/null +++ b/core/src/main/java/io/xpipe/core/charsetter/StreamCharset.java @@ -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 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 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; + } +} diff --git a/core/src/main/java/io/xpipe/core/data/node/DataStructureNode.java b/core/src/main/java/io/xpipe/core/data/node/DataStructureNode.java index 6fd475486..faafc2a11 100644 --- a/core/src/main/java/io/xpipe/core/data/node/DataStructureNode.java +++ b/core/src/main/java/io/xpipe/core/data/node/DataStructureNode.java @@ -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 { + 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) { diff --git a/core/src/main/java/io/xpipe/core/data/node/SimpleImmutableValueNode.java b/core/src/main/java/io/xpipe/core/data/node/SimpleImmutableValueNode.java index 1b8179a93..69ac842b7 100644 --- a/core/src/main/java/io/xpipe/core/data/node/SimpleImmutableValueNode.java +++ b/core/src/main/java/io/xpipe/core/data/node/SimpleImmutableValueNode.java @@ -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); } } diff --git a/core/src/main/java/io/xpipe/core/data/typed/TypedDataStreamParser.java b/core/src/main/java/io/xpipe/core/data/typed/TypedDataStreamParser.java index 4c9e4df48..7d4a01d18 100644 --- a/core/src/main/java/io/xpipe/core/data/typed/TypedDataStreamParser.java +++ b/core/src/main/java/io/xpipe/core/data/typed/TypedDataStreamParser.java @@ -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(); diff --git a/core/src/main/java/io/xpipe/core/dialog/BaseQueryElement.java b/core/src/main/java/io/xpipe/core/dialog/BaseQueryElement.java index 419899ef8..35cbbe9e8 100644 --- a/core/src/main/java/io/xpipe/core/dialog/BaseQueryElement.java +++ b/core/src/main/java/io/xpipe/core/dialog/BaseQueryElement.java @@ -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; } diff --git a/core/src/main/java/io/xpipe/core/dialog/ChoiceElement.java b/core/src/main/java/io/xpipe/core/dialog/ChoiceElement.java index 9819a1b26..a587fa419 100644 --- a/core/src/main/java/io/xpipe/core/dialog/ChoiceElement.java +++ b/core/src/main/java/io/xpipe/core/dialog/ChoiceElement.java @@ -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; diff --git a/core/src/main/java/io/xpipe/core/dialog/Dialog.java b/core/src/main/java/io/xpipe/core/dialog/Dialog.java index 7535db57c..8af3b381a 100644 --- a/core/src/main/java/io/xpipe/core/dialog/Dialog.java +++ b/core/src/main/java/io/xpipe/core/dialog/Dialog.java @@ -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; } diff --git a/core/src/main/java/io/xpipe/core/dialog/DialogElement.java b/core/src/main/java/io/xpipe/core/dialog/DialogElement.java index 103d9f701..736025d10 100644 --- a/core/src/main/java/io/xpipe/core/dialog/DialogElement.java +++ b/core/src/main/java/io/xpipe/core/dialog/DialogElement.java @@ -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(); } diff --git a/core/src/main/java/io/xpipe/core/dialog/QueryConverter.java b/core/src/main/java/io/xpipe/core/dialog/QueryConverter.java index 42f2ce393..4ca67a01d 100644 --- a/core/src/main/java/io/xpipe/core/dialog/QueryConverter.java +++ b/core/src/main/java/io/xpipe/core/dialog/QueryConverter.java @@ -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 { - public static final QueryConverter CHARSET = new QueryConverter() { + public static final QueryConverter NEW_LINE = new QueryConverter() { @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 CHARSET = new QueryConverter() { + @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 { @Override protected String toString(Boolean value) { - return value ? "yes" : "no"; + return value ? + "yes" : + "no"; } }; diff --git a/core/src/main/java/io/xpipe/core/dialog/QueryElement.java b/core/src/main/java/io/xpipe/core/dialog/QueryElement.java index d1620af77..1d4fdbd96 100644 --- a/core/src/main/java/io/xpipe/core/dialog/QueryElement.java +++ b/core/src/main/java/io/xpipe/core/dialog/QueryElement.java @@ -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; } diff --git a/core/src/main/java/io/xpipe/core/source/DataSource.java b/core/src/main/java/io/xpipe/core/source/DataSource.java index 01c686a66..3cb492a0d 100644 --- a/core/src/main/java/io/xpipe/core/source/DataSource.java +++ b/core/src/main/java/io/xpipe/core/source/DataSource.java @@ -38,12 +38,12 @@ public abstract class DataSource { return c; } - protected boolean supportsRead() { - return false; + public boolean supportsRead() { + return true; } - protected boolean supportsWrite() { - return false; + public boolean supportsWrite() { + return true; } /** diff --git a/core/src/main/java/io/xpipe/core/source/StreamReadConnection.java b/core/src/main/java/io/xpipe/core/source/StreamReadConnection.java new file mode 100644 index 000000000..9e60b3551 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/source/StreamReadConnection.java @@ -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(); + } +} diff --git a/core/src/main/java/io/xpipe/core/source/StreamWriteConnection.java b/core/src/main/java/io/xpipe/core/source/StreamWriteConnection.java new file mode 100644 index 000000000..92d49e436 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/source/StreamWriteConnection.java @@ -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(); + } +} diff --git a/core/src/main/java/io/xpipe/core/source/TableReadConnection.java b/core/src/main/java/io/xpipe/core/source/TableReadConnection.java index ed39a73c3..670b4f9ab 100644 --- a/core/src/main/java/io/xpipe/core/source/TableReadConnection.java +++ b/core/src/main/java/io/xpipe/core/source/TableReadConnection.java @@ -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(); + + 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. diff --git a/core/src/main/java/io/xpipe/core/source/TableWriteConnection.java b/core/src/main/java/io/xpipe/core/source/TableWriteConnection.java index 9b7a038ff..b19bfcfdc 100644 --- a/core/src/main/java/io/xpipe/core/source/TableWriteConnection.java +++ b/core/src/main/java/io/xpipe/core/source/TableWriteConnection.java @@ -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 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()); + } + } } diff --git a/core/src/main/java/io/xpipe/core/source/TextDataSource.java b/core/src/main/java/io/xpipe/core/source/TextDataSource.java index ab831c002..868890dd6 100644 --- a/core/src/main/java/io/xpipe/core/source/TextDataSource.java +++ b/core/src/main/java/io/xpipe/core/source/TextDataSource.java @@ -16,6 +16,10 @@ public abstract class TextDataSource extends DataSource extends DataSource 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 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()); } - } } } diff --git a/core/src/main/java/io/xpipe/core/store/DataStore.java b/core/src/main/java/io/xpipe/core/store/DataStore.java index a408b4c9b..6416c4f44 100644 --- a/core/src/main/java/io/xpipe/core/store/DataStore.java +++ b/core/src/main/java/io/xpipe/core/store/DataStore.java @@ -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. * diff --git a/core/src/main/java/io/xpipe/core/store/FileStore.java b/core/src/main/java/io/xpipe/core/store/FileStore.java index 3acbe51c2..996254f8b 100644 --- a/core/src/main/java/io/xpipe/core/store/FileStore.java +++ b/core/src/main/java/io/xpipe/core/store/FileStore.java @@ -75,4 +75,5 @@ public class FileStore implements StreamDataStore, FilenameStore { } return split[split.length - 1]; } + } diff --git a/core/src/main/java/io/xpipe/core/store/FilenameStore.java b/core/src/main/java/io/xpipe/core/store/FilenameStore.java index c14375953..224ae56e1 100644 --- a/core/src/main/java/io/xpipe/core/store/FilenameStore.java +++ b/core/src/main/java/io/xpipe/core/store/FilenameStore.java @@ -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(); } diff --git a/core/src/main/java/io/xpipe/core/store/InMemoryStore.java b/core/src/main/java/io/xpipe/core/store/InMemoryStore.java index 85e4cb36f..21d86f52e 100644 --- a/core/src/main/java/io/xpipe/core/store/InMemoryStore.java +++ b/core/src/main/java/io/xpipe/core/store/InMemoryStore.java @@ -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"; diff --git a/core/src/main/java/io/xpipe/core/store/LocalStore.java b/core/src/main/java/io/xpipe/core/store/LocalStore.java index c11b59c80..bbdb25d4e 100644 --- a/core/src/main/java/io/xpipe/core/store/LocalStore.java +++ b/core/src/main/java/io/xpipe/core/store/LocalStore.java @@ -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); } diff --git a/core/src/main/java/io/xpipe/core/store/MachineFileStore.java b/core/src/main/java/io/xpipe/core/store/MachineFileStore.java index a1e88073b..907962bfd 100644 --- a/core/src/main/java/io/xpipe/core/store/MachineFileStore.java +++ b/core/src/main/java/io/xpipe/core/store/MachineFileStore.java @@ -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; } diff --git a/core/src/main/java/io/xpipe/core/store/ShellTypes.java b/core/src/main/java/io/xpipe/core/store/ShellTypes.java index 4686e1d2f..da38f7631 100644 --- a/core/src/main/java/io/xpipe/core/store/ShellTypes.java +++ b/core/src/main/java/io/xpipe/core/store/ShellTypes.java @@ -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 switchTo(List 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"; diff --git a/core/src/main/java/io/xpipe/core/store/StandardShellStore.java b/core/src/main/java/io/xpipe/core/store/StandardShellStore.java index 74e590797..491d0e228 100644 --- a/core/src/main/java/io/xpipe/core/store/StandardShellStore.java +++ b/core/src/main/java/io/xpipe/core/store/StandardShellStore.java @@ -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 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 { + + } } diff --git a/core/src/main/java/io/xpipe/core/store/StdinDataStore.java b/core/src/main/java/io/xpipe/core/store/StdinDataStore.java index d92971fee..f30481618 100644 --- a/core/src/main/java/io/xpipe/core/store/StdinDataStore.java +++ b/core/src/main/java/io/xpipe/core/store/StdinDataStore.java @@ -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; diff --git a/core/src/main/java/io/xpipe/core/store/StdoutDataStore.java b/core/src/main/java/io/xpipe/core/store/StdoutDataStore.java index 3c12eb99a..b7cc67284 100644 --- a/core/src/main/java/io/xpipe/core/store/StdoutDataStore.java +++ b/core/src/main/java/io/xpipe/core/store/StdoutDataStore.java @@ -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 diff --git a/core/src/main/java/io/xpipe/core/store/StreamDataStore.java b/core/src/main/java/io/xpipe/core/store/StreamDataStore.java index cd91b3c2a..2ef11eca6 100644 --- a/core/src/main/java/io/xpipe/core/store/StreamDataStore.java +++ b/core/src/main/java/io/xpipe/core/store/StreamDataStore.java @@ -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. diff --git a/core/src/main/java/io/xpipe/core/util/JacksonHelper.java b/core/src/main/java/io/xpipe/core/util/JacksonHelper.java index 684398682..b572b434e 100644 --- a/core/src/main/java/io/xpipe/core/util/JacksonHelper.java +++ b/core/src/main/java/io/xpipe/core/util/JacksonHelper.java @@ -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 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; } diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java index 6b1e90485..90aa85aff 100644 --- a/core/src/main/java/module-info.java +++ b/core/src/main/java/module-info.java @@ -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; diff --git a/extension/build.gradle b/extension/build.gradle index 5e9c646e7..c2d836a02 100644 --- a/extension/build.gradle +++ b/extension/build.gradle @@ -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' } diff --git a/extension/publish.gradle b/extension/publish.gradle index b977bfd1e..077b1b2b2 100644 --- a/extension/publish.gradle +++ b/extension/publish.gradle @@ -1,6 +1,9 @@ + publishing { publications { mavenJava(MavenPublication) { + artifactId = project.archivesBaseName + from components.java pom { diff --git a/extension/src/main/java/io/xpipe/extension/DataSourceProvider.java b/extension/src/main/java/io/xpipe/extension/DataSourceProvider.java index 6a78be5ce..37a3d8d18 100644 --- a/extension/src/main/java/io/xpipe/extension/DataSourceProvider.java +++ b/extension/src/main/java/io/xpipe/extension/DataSourceProvider.java @@ -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> { } 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> { } 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 source, Property appliedSource) { + default Region configGui(Property source, Property appliedSource, boolean all) { return null; } @@ -83,33 +82,7 @@ public interface DataSourceProvider> { Map> 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 Dialog query(String desc, T value, boolean required, QueryConverter c, boolean all) { - return Dialog.query(desc, false, required, value != null && !all, value, c); - } - - public static final QueryConverter NEW_LINE_CONVERTER = new QueryConverter() { - @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> { * 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> { 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 createDefaultSource(DataStore input) throws Exception; - default T createDefaultWriteSource(DataStore input) throws Exception { - return createDefaultSource(input); - } - Class getSourceClass(); diff --git a/extension/src/main/java/io/xpipe/extension/DataSourceProviders.java b/extension/src/main/java/io/xpipe/extension/DataSourceProviders.java index 04f2a9141..0e0764e6a 100644 --- a/extension/src/main/java/io/xpipe/extension/DataSourceProviders.java +++ b/extension/src/main/java/io/xpipe/extension/DataSourceProviders.java @@ -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 createLocalStructureDescriptor(DataStore store) { return (StructureDataSource) - DataSourceProviders.byId("xpbs").getSourceClass() + DataSourceProviders.byId("xpbs") + .getSourceClass() .getDeclaredConstructors()[0].newInstance(store); } @@ -59,7 +64,8 @@ public class DataSourceProviders { @SneakyThrows public static RawDataSource createLocalRawDescriptor(DataStore store) { return (RawDataSource) - DataSourceProviders.byId("binary").getSourceClass() + DataSourceProviders.byId("binary") + .getSourceClass() .getDeclaredConstructors()[0].newInstance(store); } @@ -67,7 +73,8 @@ public class DataSourceProviders { @SneakyThrows public static RawDataSource createLocalCollectionDescriptor(DataStore store) { return (RawDataSource) - DataSourceProviders.byId("br").getSourceClass() + DataSourceProviders.byId("br") + .getSourceClass() .getDeclaredConstructors()[0].newInstance(store); } @@ -75,7 +82,8 @@ public class DataSourceProviders { @SneakyThrows public static TextDataSource createLocalTextDescriptor(DataStore store) { return (TextDataSource) - DataSourceProviders.byId("text").getSourceClass() + DataSourceProviders.byId("text") + .getSourceClass() .getDeclaredConstructors()[0].newInstance(store); } @@ -83,7 +91,8 @@ public class DataSourceProviders { @SneakyThrows public static TableDataSource createLocalTableDescriptor(DataStore store) { return (TableDataSource) - 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> byPreferredStore(DataStore store) { + private static List 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> 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> getAll() { diff --git a/extension/src/main/java/io/xpipe/extension/DataStoreProviders.java b/extension/src/main/java/io/xpipe/extension/DataStoreProviders.java index be7183d05..8e3c712c8 100644 --- a/extension/src/main/java/io/xpipe/extension/DataStoreProviders.java +++ b/extension/src/main/java/io/xpipe/extension/DataStoreProviders.java @@ -57,16 +57,17 @@ public class DataStoreProviders { public static 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 Optional byStoreClass(Class c) { + public static T byStoreClass(Class c) { if (ALL == null) { throw new IllegalStateException("Not initialized"); } - return (Optional) ALL.stream().filter(d -> d.getStoreClasses().contains(c)).findAny(); + + return (T) ALL.stream().filter(d -> d.getStoreClasses().contains(c)).findAny().orElseThrow(); } public static Set getAll() { diff --git a/extension/src/main/java/io/xpipe/extension/DialogHelper.java b/extension/src/main/java/io/xpipe/extension/DialogHelper.java index 7c79c1aec..5582f8336 100644 --- a/extension/src/main/java/io/xpipe/extension/DialogHelper.java +++ b/extension/src/main/java/io/xpipe/extension/DialogHelper.java @@ -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 Dialog query(String desc, T value, boolean required, QueryConverter 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 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); diff --git a/extension/src/main/java/io/xpipe/extension/SimpleFileDataSourceProvider.java b/extension/src/main/java/io/xpipe/extension/SimpleFileDataSourceProvider.java index 7ef4d103e..859099134 100644 --- a/extension/src/main/java/io/xpipe/extension/SimpleFileDataSourceProvider.java +++ b/extension/src/main/java/io/xpipe/extension/SimpleFileDataSourceProvider.java @@ -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> 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> 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> extends D default String getNameI18nKey() { return i18nKey("displayName"); } + Map> getSupportedExtensions(); @Override diff --git a/extension/src/main/java/io/xpipe/extension/UniformDataSourceProvider.java b/extension/src/main/java/io/xpipe/extension/UniformDataSourceProvider.java index 316eb7ae9..bb96e8c93 100644 --- a/extension/src/main/java/io/xpipe/extension/UniformDataSourceProvider.java +++ b/extension/src/main/java/io/xpipe/extension/UniformDataSourceProvider.java @@ -15,7 +15,7 @@ public interface UniformDataSourceProvider> 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) { diff --git a/extension/src/main/java/io/xpipe/extension/XPipeDaemon.java b/extension/src/main/java/io/xpipe/extension/XPipeDaemon.java index 7cb37308b..b8ef8e578 100644 --- a/extension/src/main/java/io/xpipe/extension/XPipeDaemon.java +++ b/extension/src/main/java/io/xpipe/extension/XPipeDaemon.java @@ -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 getNamedStores(); + + public Image image(String file); + + Comp streamStoreChooser(Property storeProperty, Property> provider); + + Comp namedStoreChooser(ObservableValue> filter, Property selected, DataStoreProvider.Category category); + + Comp sourceProviderChooser(Property> provider, DataSourceProvider.Category category); + Optional getNamedStore(String name); Optional getStoreName(DataStore store); diff --git a/extension/src/main/java/io/xpipe/extension/comp/CharsetChoiceComp.java b/extension/src/main/java/io/xpipe/extension/comp/CharsetChoiceComp.java index 3c2658c89..6a50e3f5e 100644 --- a/extension/src/main/java/io/xpipe/extension/comp/CharsetChoiceComp.java +++ b/extension/src/main/java/io/xpipe/extension/comp/CharsetChoiceComp.java @@ -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>> { + private final Property charset; - private final Property charset; - - public CharsetChoiceComp(Property charset) { + public CharsetChoiceComp(Property charset) { this.charset = charset; } @Override - protected Comp>> createComp() { - var map = new LinkedHashMap>(); - 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(); } } diff --git a/extension/src/main/java/io/xpipe/extension/comp/CustomComboBoxBuilder.java b/extension/src/main/java/io/xpipe/extension/comp/CustomComboBoxBuilder.java new file mode 100644 index 000000000..031577fbd --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/comp/CustomComboBoxBuilder.java @@ -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 { + + private final Property selected; + private final Function nodeFunction; + private final Map nodeMap = new HashMap<>(); + private final Map actionsMap = new HashMap<>(); + private final List nodes = new ArrayList<>(); + private final Set disabledNodes = new HashSet<>(); + private final Node emptyNode; + private final Predicate veto; + private BiPredicate filterPredicate; + private final Property filterString = new SimpleStringProperty(); + private final List 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 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 selected, Function nodeFunction, Node emptyNode, Predicate veto) { + this.selected = selected; + this.nodeFunction = nodeFunction; + this.emptyNode = emptyNode; + this.veto = veto; + } + + public ComboBox build() { + var cb = new ComboBox(); + 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 { + + @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 { + + @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); + } + } + } +} diff --git a/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsBuilder.java b/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsBuilder.java index f7a4bd176..b96211a98 100644 --- a/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsBuilder.java +++ b/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsBuilder.java @@ -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 { this.title = title; } + public DynamicOptionsBuilder addTitle(ObservableValue title) { + entries.add(new DynamicOptionsComp.Entry(null, Comp.of(() -> new Label(title.getValue())).styleClass("title"))); + return this; + } + public DynamicOptionsBuilder makeLazy() { var p = props.get(props.size() - 1); props.remove(p); @@ -108,7 +113,7 @@ public class DynamicOptionsBuilder { return this; } - public DynamicOptionsBuilder addCharset(Property prop) { + public DynamicOptionsBuilder addCharset(Property prop) { var comp = new CharsetChoiceComp(prop); entries.add(new DynamicOptionsComp.Entry(I18n.observable("extension.charset"), comp)); props.add(prop); diff --git a/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsComp.java b/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsComp.java index 356172a68..23a6d1cbd 100644 --- a/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsComp.java +++ b/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsComp.java @@ -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> { flow.setAlignment(Pos.CENTER); flow.setHgap(7); flow.setVgap(7); + flow.setPadding(new Insets(8, 0, 0, 0)); var nameRegions = new ArrayList(); var compRegions = new ArrayList(); diff --git a/extension/src/main/java/io/xpipe/extension/comp/FilterComp.java b/extension/src/main/java/io/xpipe/extension/comp/FilterComp.java new file mode 100644 index 000000000..67f9b0a4d --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/comp/FilterComp.java @@ -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 { + + public FilterComp(Property filterText) { + this.filterText = filterText; + } + + @Value + @Builder + public static class Structure implements CompStructure { + StackPane pane; + Node inactiveIcon; + Label inactiveText; + TextField text; + + @Override + public StackPane get() { + return pane; + } + } + + private final Property 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(); + } +} diff --git a/extension/src/main/java/io/xpipe/extension/event/EventHandler.java b/extension/src/main/java/io/xpipe/extension/event/EventHandler.java index 6bb7e02ea..4c8f09cc2 100644 --- a/extension/src/main/java/io/xpipe/extension/event/EventHandler.java +++ b/extension/src/main/java/io/xpipe/extension/event/EventHandler.java @@ -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(); } }; diff --git a/extension/src/main/java/io/xpipe/extension/prefs/PrefsHandler.java b/extension/src/main/java/io/xpipe/extension/prefs/PrefsHandler.java index 22d8e9551..c45d533f6 100644 --- a/extension/src/main/java/io/xpipe/extension/prefs/PrefsHandler.java +++ b/extension/src/main/java/io/xpipe/extension/prefs/PrefsHandler.java @@ -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 category, String group, Setting setting); +// void addSetting(List category, String group, Setting setting); } diff --git a/api/src/test/java/io/xpipe/api/test/DaemonControl.java b/extension/src/main/java/io/xpipe/extension/test/ExtensionTest.java similarity index 61% rename from api/src/test/java/io/xpipe/api/test/DaemonControl.java rename to extension/src/main/java/io/xpipe/extension/test/ExtensionTest.java index 37e20a133..c067bd5a1 100644 --- a/api/src/test/java/io/xpipe/api/test/DaemonControl.java +++ b/extension/src/main/java/io/xpipe/extension/test/ExtensionTest.java @@ -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(); } } diff --git a/api/src/test/java/io/xpipe/api/test/ConnectionFactory.java b/extension/src/main/java/io/xpipe/extension/test/ExtensionTestConnector.java similarity index 53% rename from api/src/test/java/io/xpipe/api/test/ConnectionFactory.java rename to extension/src/main/java/io/xpipe/extension/test/ExtensionTestConnector.java index 4a6f24191..65eae53b1 100644 --- a/api/src/test/java/io/xpipe/api/test/ConnectionFactory.java +++ b/extension/src/main/java/io/xpipe/extension/test/ExtensionTestConnector.java @@ -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(); } diff --git a/extension/src/main/java/io/xpipe/extension/util/AppendingTableWriteConnection.java b/extension/src/main/java/io/xpipe/extension/util/AppendingTableWriteConnection.java new file mode 100644 index 000000000..0ecee468c --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/util/AppendingTableWriteConnection.java @@ -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 writeLinesAcceptor() { + return ((TableWriteConnection)connection).writeLinesAcceptor(); + } +} diff --git a/extension/src/main/java/io/xpipe/extension/util/AppendingTextWriteConnection.java b/extension/src/main/java/io/xpipe/extension/util/AppendingTextWriteConnection.java new file mode 100644 index 000000000..20a36a2cd --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/util/AppendingTextWriteConnection.java @@ -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); + } +} diff --git a/extension/src/main/java/io/xpipe/extension/util/AppendingWriteConnection.java b/extension/src/main/java/io/xpipe/extension/util/AppendingWriteConnection.java new file mode 100644 index 000000000..5fa6b8c87 --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/util/AppendingWriteConnection.java @@ -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(); + } + +} diff --git a/extension/src/main/java/io/xpipe/extension/util/ExtensionJacksonModule.java b/extension/src/main/java/io/xpipe/extension/util/ExtensionJacksonModule.java new file mode 100644 index 000000000..236af3269 --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/util/ExtensionJacksonModule.java @@ -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 { + + @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 { + + @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()); + } + } +} diff --git a/extension/src/main/java/module-info.java b/extension/src/main/java/module-info.java index 77f1c7138..e46d57d84 100644 --- a/extension/src/main/java/module-info.java +++ b/extension/src/main/java/module-info.java @@ -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; } \ No newline at end of file diff --git a/jreleaser.gradle b/jreleaser.gradle index 80d84554f..c5e0de7b6 100644 --- a/jreleaser.gradle +++ b/jreleaser.gradle @@ -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' } } } diff --git a/misc/version b/misc/version index a5fb8d5df..dc6168404 100644 --- a/misc/version +++ b/misc/version @@ -1 +1 @@ -0.0.1.4-SNAPSHOT \ No newline at end of file +0.0.1.6-SNAPSHOT \ No newline at end of file