diff --git a/api/src/main/java/io/xpipe/api/DataSource.java b/api/src/main/java/io/xpipe/api/DataSource.java new file mode 100644 index 000000000..97f882b86 --- /dev/null +++ b/api/src/main/java/io/xpipe/api/DataSource.java @@ -0,0 +1,70 @@ +package io.xpipe.api; + +import io.xpipe.api.impl.DataSourceImpl; +import io.xpipe.core.source.DataSourceId; +import io.xpipe.core.source.DataSourceType; + +import java.net.URL; +import java.util.Map; + +/** + * Represents a reference to an XPipe data source. + * + * The actual data is only queried when required and is not cached. + * Therefore, the queried data is always up-to-date at the point of calling a method that queries the data. + */ +public interface DataSource { + + /** + * Wrapper for {@link #get(DataSourceId)}. + * + * @throws IllegalArgumentException if {@code id} is not a valid data source id + */ + static DataSource get(String id) { + return get(DataSourceId.fromString(id)); + } + + /** + * Retrieves a reference to the given data source. + * + * @param id the data source id + * @return a reference to the data source that can be used to access the underlying data source + */ + static DataSource get(DataSourceId id) { + return DataSourceImpl.get(id); + } + + /** + * Retrieves a reference to the given local data source that is specified by a URL. + * + * This wrapped data source is only available temporarily and locally, + * i.e. it is not added to the XPipe data source storage. + * + * @param url the url that points to the data + * @param configOptions additional configuration options for the specific data source type + * @return a reference to the data source that can be used to access the underlying data source + */ + static DataSource wrap(URL url, Map configOptions) { + return null; + } + + /** + * Wrapper for {@link #wrap(URL, Map)} that passes no configuration options. + */ + static DataSource wrap(URL url) { + return wrap(url, Map.of()); + } + + DataSourceId getId(); + + DataSourceType getType(); + + /** + * Attemps to cast this object to a {@link DataTable}. + * + * @throws UnsupportedOperationException if the data source is not a table + */ + default DataTable asTable() { + throw new UnsupportedOperationException("Data source is not a table"); + } +} diff --git a/api/src/main/java/io/xpipe/api/DataTable.java b/api/src/main/java/io/xpipe/api/DataTable.java index 863029981..16de64a78 100644 --- a/api/src/main/java/io/xpipe/api/DataTable.java +++ b/api/src/main/java/io/xpipe/api/DataTable.java @@ -1,20 +1,15 @@ package io.xpipe.api; -import io.xpipe.api.impl.DataTableImpl; import io.xpipe.core.data.node.ArrayNode; import io.xpipe.core.data.node.TupleNode; import io.xpipe.core.data.type.DataType; -import io.xpipe.core.source.DataSourceId; import java.util.OptionalInt; +import java.util.stream.Stream; -public interface DataTable extends Iterable { +public interface DataTable extends Iterable, DataSource { - static DataTable get(String s) { - return DataTableImpl.get(s); - } - - DataSourceId getId(); + Stream stream(); int getRowCount(); diff --git a/api/src/main/java/io/xpipe/api/XPipeApiConnector.java b/api/src/main/java/io/xpipe/api/XPipeApiConnector.java index 5bcbbe0f2..bfe314dbe 100644 --- a/api/src/main/java/io/xpipe/api/XPipeApiConnector.java +++ b/api/src/main/java/io/xpipe/api/XPipeApiConnector.java @@ -1,7 +1,8 @@ package io.xpipe.api; import io.xpipe.beacon.*; -import io.xpipe.beacon.BeaconClient; + +import java.util.Optional; public abstract class XPipeApiConnector extends BeaconConnector { @@ -23,12 +24,48 @@ public abstract class XPipeApiConnector extends BeaconConnector { protected abstract void handle(BeaconClient sc) throws Exception; @Override - protected void waitForStartup() { - try { - Thread.sleep(5000); - } catch (InterruptedException e) { - e.printStackTrace(); + protected BeaconClient constructSocket() throws ConnectorException { + if (!BeaconServer.isRunning()) { + try { + start(); + } catch (Exception ex) { + throw new ConnectorException("Unable to start xpipe daemon", ex); + } + + var r = waitForStartup(); + if (r.isEmpty()) { + throw new ConnectorException("Wait for xpipe daemon timed out"); + } else { + return r.get(); + } } + + try { + return new BeaconClient(); + } catch (Exception ex) { + throw new ConnectorException("Unable to connect to running xpipe daemon", ex); + } + } + + private void start() throws Exception { + if (!BeaconServer.tryStart()) { + throw new UnsupportedOperationException("Unable to determine xpipe daemon launch command"); + }; + } + + private Optional waitForStartup() { + for (int i = 0; i < 40; i++) { + try { + Thread.sleep(500); + } catch (InterruptedException ignored) { + } + + var s = BeaconClient.tryConnect(); + if (s.isPresent()) { + return s; + } + } + return Optional.empty(); } @FunctionalInterface diff --git a/api/src/main/java/io/xpipe/api/impl/DataSourceImpl.java b/api/src/main/java/io/xpipe/api/impl/DataSourceImpl.java new file mode 100644 index 000000000..942e2b300 --- /dev/null +++ b/api/src/main/java/io/xpipe/api/impl/DataSourceImpl.java @@ -0,0 +1,47 @@ +package io.xpipe.api.impl; + +import io.xpipe.api.DataSource; +import io.xpipe.api.DataTable; +import io.xpipe.api.XPipeApiConnector; +import io.xpipe.beacon.BeaconClient; +import io.xpipe.beacon.ClientException; +import io.xpipe.beacon.ConnectorException; +import io.xpipe.beacon.ServerException; +import io.xpipe.beacon.exchange.ReadInfoExchange; +import io.xpipe.core.source.DataSourceId; + +public abstract class DataSourceImpl implements DataSource { + + public static DataSource get(DataSourceId ds) { + final DataSource[] source = new DataSource[1]; + new XPipeApiConnector() { + @Override + protected void handle(BeaconClient sc) throws ClientException, ServerException, ConnectorException { + var req = ReadInfoExchange.Request.builder().sourceId(ds).build(); + ReadInfoExchange.Response res = performSimpleExchange(sc, req); + switch (res.getType()) { + case TABLE -> { + var data = res.getTableData(); + source[0] = new DataTableImpl(res.getSourceId(), data.getRowCount(), data.getDataType()); + } + case STRUCTURE -> { + } + case RAW -> { + } + } + } + }.execute(); + return source[0]; + } + + private final DataSourceId sourceId; + + public DataSourceImpl(DataSourceId sourceId) { + this.sourceId = sourceId; + } + + @Override + public DataSourceId getId() { + return sourceId; + } +} 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 76354db8b..0e6802369 100644 --- a/api/src/main/java/io/xpipe/api/impl/DataTableImpl.java +++ b/api/src/main/java/io/xpipe/api/impl/DataTableImpl.java @@ -7,9 +7,8 @@ import io.xpipe.beacon.ClientException; import io.xpipe.beacon.ConnectorException; import io.xpipe.beacon.ServerException; import io.xpipe.beacon.exchange.ReadTableDataExchange; -import io.xpipe.beacon.exchange.ReadTableInfoExchange; -import io.xpipe.core.data.node.DataStructureNode; import io.xpipe.core.data.node.ArrayNode; +import io.xpipe.core.data.node.DataStructureNode; import io.xpipe.core.data.node.TupleNode; import io.xpipe.core.data.type.DataType; import io.xpipe.core.data.typed.TypedAbstractReader; @@ -17,6 +16,7 @@ import io.xpipe.core.data.typed.TypedDataStreamParser; import io.xpipe.core.data.typed.TypedDataStructureNodeReader; import io.xpipe.core.data.typed.TypedReusableDataStructureNodeReader; import io.xpipe.core.source.DataSourceId; +import io.xpipe.core.source.DataSourceType; import java.io.IOException; import java.io.InputStream; @@ -25,30 +25,14 @@ import java.util.*; import java.util.stream.Stream; import java.util.stream.StreamSupport; -public class DataTableImpl implements DataTable { - - public static DataTable get(String s) { - return get(DataSourceId.fromString(s)); - } - - public static DataTable get(DataSourceId ds) { - final DataTable[] table = {null}; - new XPipeApiConnector() { - @Override - protected void handle(BeaconClient sc) throws ClientException, ServerException, ConnectorException { - var req = new ReadTableInfoExchange.Request(ds); - ReadTableInfoExchange.Response res = performSimpleExchange(sc, req); - table[0] = new DataTableImpl(res.sourceId(), res.rowCount(), res.dataType()); - } - }.execute(); - return table[0]; - } +public class DataTableImpl extends DataSourceImpl implements DataTable { private final DataSourceId id; private final int size; private final DataType dataType; public DataTableImpl(DataSourceId id, int size, DataType dataType) { + super(id); this.id = id; this.size = size; this.dataType = dataType; @@ -64,6 +48,11 @@ public class DataTableImpl implements DataTable { return id; } + @Override + public DataSourceType getType() { + return DataSourceType.TABLE; + } + @Override public int getRowCount() { if (size == -1) { @@ -96,11 +85,12 @@ public class DataTableImpl implements DataTable { new XPipeApiConnector() { @Override protected void handle(BeaconClient sc) throws ClientException, ServerException, ConnectorException { - var req = new ReadTableDataExchange.Request(id, maxToRead); - performExchange(sc, req, (ReadTableDataExchange.Response res, InputStream in) -> { + var req = ReadTableDataExchange.Request.builder() + .sourceId(id).maxRows(maxToRead).build(); + performInputExchange(sc, req, (ReadTableDataExchange.Response res, InputStream in) -> { var r = new TypedDataStreamParser(dataType); r.parseStructures(in, TypedDataStructureNodeReader.immutable(dataType), nodes::add); - }, false); + }); } }.execute(); return ArrayNode.of(nodes); @@ -120,9 +110,10 @@ public class DataTableImpl implements DataTable { new XPipeApiConnector() { @Override protected void handle(BeaconClient sc) throws ClientException, ServerException, ConnectorException { - var req = new ReadTableDataExchange.Request(id, Integer.MAX_VALUE); - performExchange(sc, req, - (ReadTableDataExchange.Response res, InputStream in) -> input = in, false); + var req = ReadTableDataExchange.Request.builder() + .sourceId(id).maxRows(Integer.MAX_VALUE).build(); + performInputExchange(sc, req, + (ReadTableDataExchange.Response res, InputStream in) -> input = in); } }.execute(); diff --git a/api/src/main/java/module-info.java b/api/src/main/java/module-info.java index b90134273..c16f6d570 100644 --- a/api/src/main/java/module-info.java +++ b/api/src/main/java/module-info.java @@ -1,7 +1,6 @@ module io.xpipe.api { - requires io.xpipe.core; - requires io.xpipe.beacon; - requires org.apache.commons.lang; + requires transitive io.xpipe.core; + requires transitive io.xpipe.beacon; exports io.xpipe.api; } \ No newline at end of file diff --git a/beacon/build.gradle b/beacon/build.gradle index 2b9f94a1b..90201e303 100644 --- a/beacon/build.gradle +++ b/beacon/build.gradle @@ -14,12 +14,14 @@ repositories { } apply from: "$rootDir/deps/slf4j.gradle" -apply from: "$rootDir/deps/websocket.gradle" apply from: "$rootDir/deps/jackson.gradle" -apply from: "$rootDir/deps/commons.gradle" dependencies { implementation project(':core') + implementation project(':extension') + + compileOnly 'org.projectlombok:lombok:1.18.22' + annotationProcessor 'org.projectlombok:lombok:1.18.22' } test { diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java b/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java index 49d899e10..b2791252d 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java @@ -12,9 +12,6 @@ import io.xpipe.beacon.message.RequestMessage; import io.xpipe.beacon.message.ResponseMessage; import io.xpipe.beacon.message.ServerErrorMessage; import io.xpipe.core.util.JacksonHelper; -import org.apache.commons.lang3.function.FailableBiConsumer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; @@ -23,13 +20,34 @@ import java.net.InetAddress; import java.net.Socket; import java.util.Arrays; import java.util.Optional; -import java.util.function.Consumer; import static io.xpipe.beacon.BeaconConfig.BODY_SEPARATOR; public class BeaconClient { - private static final Logger logger = LoggerFactory.getLogger(BeaconClient.class); + @FunctionalInterface + public interface FailableBiConsumer { + + void accept(T var1, U var2) throws E; + } + + @FunctionalInterface + public interface FailableConsumer { + + void accept(T var1) throws E; + } + + public static Optional tryConnect() { + if (BeaconConfig.debugEnabled()) { + System.out.println("Attempting connection to server at port " + BeaconConfig.getUsedPort()); + } + + try { + return Optional.of(new BeaconClient()); + } catch (IOException ex) { + return Optional.empty(); + } + } private final Socket socket; private final InputStream in; @@ -51,29 +69,27 @@ public class BeaconClient { public void exchange( REQ req, - Consumer output, - FailableBiConsumer responseConsumer, - boolean keepOpen) throws ConnectorException, ClientException, ServerException { + FailableConsumer reqWriter, + FailableBiConsumer resReader) + throws ConnectorException, ClientException, ServerException { try { sendRequest(req); - if (output != null) { + if (reqWriter != null) { out.write(BODY_SEPARATOR); - output.accept(out); + reqWriter.accept(out); } var res = this.receiveResponse(); var sep = in.readNBytes(BODY_SEPARATOR.length); - if (!Arrays.equals(BODY_SEPARATOR, sep)) { + if (sep.length != 0 && !Arrays.equals(BODY_SEPARATOR, sep)) { throw new ConnectorException("Invalid body separator"); } - responseConsumer.accept(res, in); + resReader.accept(res, in); } catch (IOException ex) { throw new ConnectorException("Couldn't communicate with socket", ex); } finally { - if (!keepOpen) { - close(); - } + close(); } } diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconConnector.java b/beacon/src/main/java/io/xpipe/beacon/BeaconConnector.java index bc80a676e..a43fb2b81 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconConnector.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconConnector.java @@ -2,52 +2,42 @@ package io.xpipe.beacon; import io.xpipe.beacon.message.RequestMessage; import io.xpipe.beacon.message.ResponseMessage; -import org.apache.commons.lang3.function.FailableBiConsumer; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.function.Consumer; +import java.util.concurrent.atomic.AtomicReference; public abstract class BeaconConnector { - protected abstract void waitForStartup(); + protected abstract BeaconClient constructSocket() throws ConnectorException; - protected BeaconClient constructSocket() throws ConnectorException { - if (!BeaconServer.isRunning()) { - try { - BeaconServer.start(); - waitForStartup(); - if (!BeaconServer.isRunning()) { - throw new ConnectorException("Unable to start xpipe daemon"); - } - } catch (Exception ex) { - throw new ConnectorException("Unable to start xpipe daemon: " + ex.getMessage()); - } - } - - try { - return new BeaconClient(); - } catch (Exception ex) { - throw new ConnectorException("Unable to connect to running xpipe daemon: " + ex.getMessage()); - } - } - - protected void performExchange( + protected void performInputExchange( BeaconClient socket, REQ req, - FailableBiConsumer responseConsumer, - boolean keepOpen) throws ServerException, ConnectorException, ClientException { - performExchange(socket, req, null, responseConsumer, keepOpen); + BeaconClient.FailableBiConsumer responseConsumer) throws ServerException, ConnectorException, ClientException { + performInputOutputExchange(socket, req, null, responseConsumer); } - protected void performExchange( + protected void performInputOutputExchange( BeaconClient socket, REQ req, - Consumer output, - FailableBiConsumer responseConsumer, - boolean keepOpen) throws ServerException, ConnectorException, ClientException { - socket.exchange(req, output, responseConsumer, keepOpen); + BeaconClient.FailableConsumer reqWriter, + BeaconClient.FailableBiConsumer responseConsumer) + throws ServerException, ConnectorException, ClientException { + socket.exchange(req, reqWriter, responseConsumer); + } + + protected RES performOutputExchange( + BeaconClient socket, + REQ req, + BeaconClient.FailableConsumer reqWriter) + throws ServerException, ConnectorException, ClientException { + AtomicReference response = new AtomicReference<>(); + socket.exchange(req, reqWriter, (RES res, InputStream in) -> { + response.set(res); + }); + return response.get(); } protected RES performSimpleExchange( diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconHandler.java b/beacon/src/main/java/io/xpipe/beacon/BeaconHandler.java index 240b049e6..4a307736a 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconHandler.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconHandler.java @@ -10,6 +10,8 @@ public interface BeaconHandler { void prepareBody() throws IOException; + void startBodyRead() throws IOException; + public void sendResponse(T obj) throws Exception; public void sendClientErrorResponse(String message) throws Exception; diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java b/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java index fc2033454..e603b148c 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java @@ -20,19 +20,19 @@ public class BeaconServer { return !isPortAvailable(port); } - public static void start() throws Exception { + public static boolean tryStart() throws Exception { if (BeaconConfig.shouldStartInProcess()) { startInProcess(); - return; + return true; } var custom = BeaconConfig.getCustomExecCommand(); if (custom != null) { - Runtime.getRuntime().exec(System.getenv(custom)); - return; + var proc = new ProcessBuilder("cmd", "/c", "CALL", "C:\\Projects\\xpipe\\xpipe\\gradlew.bat", ":app:run").inheritIO().start(); + return true; } - throw new IllegalArgumentException("Unable to start xpipe daemon"); + return false; } private static void startInProcess() throws Exception { diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/CliOptionPageExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/CliOptionPageExchange.java new file mode 100644 index 000000000..0f425f8fd --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/CliOptionPageExchange.java @@ -0,0 +1,45 @@ +package io.xpipe.beacon.exchange; + +import io.xpipe.beacon.message.RequestMessage; +import io.xpipe.beacon.message.ResponseMessage; +import io.xpipe.core.data.type.DataType; +import io.xpipe.core.source.DataSourceId; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +public class CliOptionPageExchange implements MessageExchange { + + @Override + public String getId() { + return "cliOptionPage"; + } + + @Override + public Class getRequestClass() { + return CliOptionPageExchange.Request.class; + } + + @Override + public Class getResponseClass() { + return CliOptionPageExchange.Response.class; + } + + @Jacksonized + @Builder + @Value + public static class Request implements RequestMessage { + DataSourceId newSourceId; + String type; + boolean hasInputStream; + } + + @Jacksonized + @Builder + @Value + public static class Response implements ResponseMessage { + DataSourceId sourceId; + DataType dataType; + int rowCount; + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/ListCollectionsExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/ListCollectionsExchange.java index 4a92046b6..7a8070e85 100644 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/ListCollectionsExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/ListCollectionsExchange.java @@ -2,11 +2,14 @@ package io.xpipe.beacon.exchange; import io.xpipe.beacon.message.RequestMessage; import io.xpipe.beacon.message.ResponseMessage; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; import java.time.Instant; import java.util.List; -public abstract class ListCollectionsExchange implements MessageExchange { +public class ListCollectionsExchange implements MessageExchange { @Override public String getId() { @@ -23,7 +26,10 @@ public abstract class ListCollectionsExchange implements MessageExchange entries) implements ResponseMessage { - + @Jacksonized + @Builder + @Value + public static class Response implements ResponseMessage { + List entries; } } diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/ListEntriesExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/ListEntriesExchange.java index 8ecd5f4b8..cd102e46b 100644 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/ListEntriesExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/ListEntriesExchange.java @@ -2,11 +2,14 @@ package io.xpipe.beacon.exchange; import io.xpipe.beacon.message.RequestMessage; import io.xpipe.beacon.message.ResponseMessage; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; import java.time.Instant; import java.util.List; -public abstract class ListEntriesExchange implements MessageExchange { +public class ListEntriesExchange implements MessageExchange { @Override public String getId() { @@ -23,15 +26,21 @@ public abstract class ListEntriesExchange implements MessageExchange entries) implements ResponseMessage { - + @Jacksonized + @Builder + @Value + public static class Response implements ResponseMessage { + List entries; } } diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/MessageExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/MessageExchange.java index 6ee6c0b7d..dab1cee75 100644 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/MessageExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/MessageExchange.java @@ -2,9 +2,6 @@ package io.xpipe.beacon.exchange; import io.xpipe.beacon.message.RequestMessage; import io.xpipe.beacon.message.ResponseMessage; -import io.xpipe.beacon.BeaconHandler; - -import java.io.InputStream; public interface MessageExchange { @@ -13,6 +10,4 @@ public interface MessageExchange getRequestClass(); Class getResponseClass(); - - void handleRequest(BeaconHandler handler, RQ msg, InputStream body) throws Exception; } diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/ModeExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/ModeExchange.java index 85ce4c804..4803c5c5f 100644 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/ModeExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/ModeExchange.java @@ -2,8 +2,11 @@ package io.xpipe.beacon.exchange; import io.xpipe.beacon.message.RequestMessage; import io.xpipe.beacon.message.ResponseMessage; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; -public abstract class ModeExchange implements MessageExchange { +public class ModeExchange implements MessageExchange { @Override public String getId() { @@ -20,11 +23,17 @@ public abstract class ModeExchange implements MessageExchange { + + @Override + public String getId() { + return "readTableInfo"; + } + + @Override + public Class getRequestClass() { + return ReadInfoExchange.Request.class; + } + + @Override + public Class getResponseClass() { + return ReadInfoExchange.Response.class; + } + + @Jacksonized + @Builder + @Value + public static class Request implements RequestMessage { + DataSourceId sourceId; + } + + @Jacksonized + @Builder + @Value + public static class Response implements ResponseMessage { + DataSourceId sourceId; + DataSourceType type; + Object data; + + public TableData getTableData() { + return (TableData) data; + } + } + + @Jacksonized + @Builder + @Value + public static class TableData { + DataType dataType; + int rowCount; + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/ReadStructureExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/ReadStructureExchange.java index 6066e82db..fad9e29c2 100644 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/ReadStructureExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/ReadStructureExchange.java @@ -4,7 +4,7 @@ import io.xpipe.beacon.message.RequestMessage; import io.xpipe.beacon.message.ResponseMessage; import io.xpipe.core.source.DataSourceId; -public abstract class ReadStructureExchange implements MessageExchange { +public class ReadStructureExchange implements MessageExchange { @Override public String getId() { diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/ReadTableDataExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/ReadTableDataExchange.java index f4b56beee..e6cd747e7 100644 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/ReadTableDataExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/ReadTableDataExchange.java @@ -3,8 +3,11 @@ package io.xpipe.beacon.exchange; import io.xpipe.beacon.message.RequestMessage; import io.xpipe.beacon.message.ResponseMessage; import io.xpipe.core.source.DataSourceId; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; -public abstract class ReadTableDataExchange implements MessageExchange { +public class ReadTableDataExchange implements MessageExchange { @Override public String getId() { @@ -21,11 +24,18 @@ public abstract class ReadTableDataExchange implements MessageExchange { - - @Override - public String getId() { - return "readTableInfo"; - } - - @Override - public Class getRequestClass() { - return ReadTableInfoExchange.Request.class; - } - - @Override - public Class getResponseClass() { - return ReadTableInfoExchange.Response.class; - } - - public static record Request(DataSourceId sourceId) implements RequestMessage { - - } - - public static record Response(DataSourceId sourceId, DataType dataType, int rowCount) implements ResponseMessage { - - } -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/StatusExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/StatusExchange.java index f2c352512..b6484fd35 100644 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/StatusExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/StatusExchange.java @@ -2,8 +2,11 @@ package io.xpipe.beacon.exchange; import io.xpipe.beacon.message.RequestMessage; import io.xpipe.beacon.message.ResponseMessage; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; -public abstract class StatusExchange implements MessageExchange { +public class StatusExchange implements MessageExchange { @Override public String getId() { @@ -20,11 +23,16 @@ public abstract class StatusExchange implements MessageExchange { +public class StopExchange implements MessageExchange { @Override public String getId() { @@ -20,11 +23,17 @@ public abstract class StopExchange implements MessageExchange { + + @Override + public String getId() { + return "storeEnd"; + } + + @Override + public Class getRequestClass() { + return StoreEndExchange.Request.class; + } + + @Override + public Class getResponseClass() { + return StoreEndExchange.Response.class; + } + + @Jacksonized + @Builder + @Value + public static class Request implements RequestMessage { + UUID entryId; + Map values; + } + + @Jacksonized + @Builder + @Value + public static class Response implements ResponseMessage { + DataSourceId sourceId; + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/StoreStartExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/StoreStartExchange.java new file mode 100644 index 000000000..5ee1dd1b7 --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/StoreStartExchange.java @@ -0,0 +1,41 @@ +package io.xpipe.beacon.exchange; + +import io.xpipe.beacon.message.RequestMessage; +import io.xpipe.beacon.message.ResponseMessage; +import io.xpipe.extension.cli.CliOptionPage; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +public class StoreStartExchange implements MessageExchange { + + @Override + public String getId() { + return "storeStart"; + } + + @Override + public Class getRequestClass() { + return StoreStartExchange.Request.class; + } + + @Override + public Class getResponseClass() { + return StoreStartExchange.Response.class; + } + + @Jacksonized + @Builder + @Value + public static class Request implements RequestMessage { + String type; + boolean hasInputStream; + } + + @Jacksonized + @Builder + @Value + public static class Response implements ResponseMessage { + CliOptionPage page; + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/VersionExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/VersionExchange.java index 367466b51..c8dd7dc7c 100644 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/VersionExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/VersionExchange.java @@ -2,8 +2,11 @@ package io.xpipe.beacon.exchange; import io.xpipe.beacon.message.RequestMessage; import io.xpipe.beacon.message.ResponseMessage; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; -public abstract class VersionExchange implements MessageExchange { +public class VersionExchange implements MessageExchange { @Override public String getId() { @@ -20,18 +23,20 @@ public abstract class VersionExchange implements MessageExchange nodes) { + public static TupleNode of(List nodes) { if (nodes == null) { throw new IllegalArgumentException("Nodes must be not null"); } - return new NoKeyTupleNode(true, nodes); + return new NoKeyTupleNode(true, (List) nodes); } public static TupleNode of(List names, List nodes) { diff --git a/core/src/main/java/io/xpipe/core/source/DataSourceConnection.java b/core/src/main/java/io/xpipe/core/source/DataSourceConnection.java index 33e7c3842..65eab2ea5 100644 --- a/core/src/main/java/io/xpipe/core/source/DataSourceConnection.java +++ b/core/src/main/java/io/xpipe/core/source/DataSourceConnection.java @@ -3,6 +3,4 @@ package io.xpipe.core.source; public interface DataSourceConnection extends AutoCloseable { void init() throws Exception; - - void close() throws Exception; } diff --git a/core/src/main/java/io/xpipe/core/source/TableDataReadConnection.java b/core/src/main/java/io/xpipe/core/source/TableDataReadConnection.java index c905d99da..46954dea0 100644 --- a/core/src/main/java/io/xpipe/core/source/TableDataReadConnection.java +++ b/core/src/main/java/io/xpipe/core/source/TableDataReadConnection.java @@ -10,9 +10,9 @@ import java.io.OutputStream; public interface TableDataReadConnection extends DataSourceConnection { - TupleType determineDataType() throws Exception; + TupleType getDataType() throws Exception; - int determineRowCount() throws Exception; + int getRowCount() throws Exception; void withLines(DataStructureNodeAcceptor lineAcceptor) throws Exception; diff --git a/core/src/main/java/io/xpipe/core/source/TableDataSourceDescriptor.java b/core/src/main/java/io/xpipe/core/source/TableDataSourceDescriptor.java index 14bf982a8..c3d7b3cab 100644 --- a/core/src/main/java/io/xpipe/core/source/TableDataSourceDescriptor.java +++ b/core/src/main/java/io/xpipe/core/source/TableDataSourceDescriptor.java @@ -4,9 +4,21 @@ import io.xpipe.core.store.DataStore; public abstract class TableDataSourceDescriptor implements DataSourceDescriptor { - public abstract TableDataWriteConnection newWriteConnection(DS store); + public final TableDataReadConnection openReadConnection(DS store) throws Exception { + var con = newReadConnection(store); + con.init(); + return con; + } - public abstract TableDataReadConnection newReadConnection(DS store); + public final TableDataWriteConnection openWriteConnection(DS store) throws Exception { + var con = newWriteConnection(store); + con.init(); + return con; + } + + protected abstract TableDataWriteConnection newWriteConnection(DS store); + + protected abstract TableDataReadConnection newReadConnection(DS store); @Override public DataSourceType getType() { diff --git a/core/src/main/java/io/xpipe/core/store/InputStreamDataStore.java b/core/src/main/java/io/xpipe/core/store/InputStreamDataStore.java index f331cedc2..c7a23effe 100644 --- a/core/src/main/java/io/xpipe/core/store/InputStreamDataStore.java +++ b/core/src/main/java/io/xpipe/core/store/InputStreamDataStore.java @@ -1,11 +1,14 @@ package io.xpipe.core.store; +import java.io.BufferedInputStream; import java.io.InputStream; import java.io.OutputStream; -public abstract class InputStreamDataStore implements StreamDataStore { +public class InputStreamDataStore implements StreamDataStore { private final InputStream in; + private BufferedInputStream bufferedInputStream; + private boolean opened = false; public InputStreamDataStore(InputStream in) { this.in = in; @@ -13,11 +16,17 @@ public abstract class InputStreamDataStore implements StreamDataStore { @Override public InputStream openInput() throws Exception { - return in; + if (opened) { + return bufferedInputStream; + } + + opened = true; + bufferedInputStream = new BufferedInputStream(in); + return bufferedInputStream; } @Override public OutputStream openOutput() throws Exception { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException("No output available"); } } diff --git a/core/src/main/java/io/xpipe/core/store/LocalFileDataStore.java b/core/src/main/java/io/xpipe/core/store/LocalFileDataStore.java index 65dad077f..9e904c613 100644 --- a/core/src/main/java/io/xpipe/core/store/LocalFileDataStore.java +++ b/core/src/main/java/io/xpipe/core/store/LocalFileDataStore.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonTypeName; import org.apache.commons.io.FilenameUtils; +import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -65,7 +66,7 @@ public class LocalFileDataStore extends FileDataStore { @Override public InputStream openInput() throws Exception { - return Files.newInputStream(file); + return new BufferedInputStream(Files.newInputStream(file)); } @Override diff --git a/extension/src/main/java/io/xpipe/extension/DataSourceGuiProvider.java b/extension/src/main/java/io/xpipe/extension/DataSourceGuiProvider.java index d6794bad1..1065a3173 100644 --- a/extension/src/main/java/io/xpipe/extension/DataSourceGuiProvider.java +++ b/extension/src/main/java/io/xpipe/extension/DataSourceGuiProvider.java @@ -3,7 +3,6 @@ package io.xpipe.extension; import io.xpipe.core.source.DataSourceDescriptor; import io.xpipe.core.store.DataStore; import javafx.beans.property.Property; -import javafx.scene.image.Image; import javafx.scene.layout.Region; import java.nio.file.Path; @@ -24,13 +23,15 @@ public interface DataSourceGuiProvider { String getDisplayName(); - Image getImage(); + String getDisplayImage(); - Supplier getFileName(); + String getFileName(); Map, String> getFileExtensions(); - String getDataSourceDescription(DataSourceDescriptor source); + String getDataSourceShortDescription(DataSourceDescriptor source); + + String getDataSourceLongDescription(DataSourceDescriptor source); Class> getType(); } diff --git a/extension/src/main/java/io/xpipe/extension/DataSourceProvider.java b/extension/src/main/java/io/xpipe/extension/DataSourceProvider.java index 0d85c8626..e73bf4fac 100644 --- a/extension/src/main/java/io/xpipe/extension/DataSourceProvider.java +++ b/extension/src/main/java/io/xpipe/extension/DataSourceProvider.java @@ -1,10 +1,17 @@ package io.xpipe.extension; import io.xpipe.core.source.DataSourceDescriptor; +import io.xpipe.core.store.DataStore; + +import java.nio.file.Path; public interface DataSourceProvider { String getId(); + boolean supportsFile(Path file); + + DataSourceDescriptor createDefaultDataSource(DataStore input) throws Exception; + Class> getType(); } diff --git a/extension/src/main/java/io/xpipe/extension/DataSourceProviders.java b/extension/src/main/java/io/xpipe/extension/DataSourceProviders.java index 97b600e10..18ff138e9 100644 --- a/extension/src/main/java/io/xpipe/extension/DataSourceProviders.java +++ b/extension/src/main/java/io/xpipe/extension/DataSourceProviders.java @@ -1,5 +1,6 @@ package io.xpipe.extension; +import java.nio.file.Path; import java.util.Optional; import java.util.ServiceLoader; import java.util.Set; @@ -32,6 +33,14 @@ public class DataSourceProviders { return ALL.stream().filter(d -> d.getId().equals(name)).findAny(); } + public static Optional byFile(Path file) { + if (ALL == null) { + throw new IllegalStateException("Not initialized"); + } + + return ALL.stream().filter(d -> d.supportsFile(file)).findAny(); + } + public static Set getAll() { return ALL; } diff --git a/extension/src/main/java/io/xpipe/extension/I18n.java b/extension/src/main/java/io/xpipe/extension/I18n.java new file mode 100644 index 000000000..1f8d6e7d4 --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/I18n.java @@ -0,0 +1,19 @@ +package io.xpipe.extension; + +import java.util.ServiceLoader; +import java.util.function.Supplier; + +public interface I18n { + + I18n INSTANCE = ServiceLoader.load(I18n.class).findFirst().orElseThrow(); + + public static Supplier resolver(String s, Object... vars) { + return () -> get(s, vars); + } + + public static String get(String s, Object... vars) { + return INSTANCE.getLocalised(s, vars); + } + + String getLocalised(String s, Object... vars); +} diff --git a/extension/src/main/java/io/xpipe/extension/cli/CliOption.java b/extension/src/main/java/io/xpipe/extension/cli/CliOption.java new file mode 100644 index 000000000..4e03ca943 --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/cli/CliOption.java @@ -0,0 +1,27 @@ +package io.xpipe.extension.cli; + +public abstract class CliOption { + + private final String name; + protected T value; + + public CliOption(String name) { + this.name = name; + this.value = null; + } + + public CliOption(String name, T value) { + this.name = name; + this.value = value; + } + + protected abstract String enterValue(String val); + + public String getName() { + return name; + } + + public T getValue() { + return value; + } +} diff --git a/extension/src/main/java/io/xpipe/extension/cli/CliOptionPage.java b/extension/src/main/java/io/xpipe/extension/cli/CliOptionPage.java new file mode 100644 index 000000000..4f53b4717 --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/cli/CliOptionPage.java @@ -0,0 +1,22 @@ +package io.xpipe.extension.cli; + +import java.util.List; + +public class CliOptionPage { + + private String description; + private List> options; + + public CliOptionPage(String description, List> options) { + this.description = description; + this.options = options; + } + + public String getDescription() { + return description; + } + + public List> getOptions() { + return options; + } +} diff --git a/extension/src/main/java/module-info.java b/extension/src/main/java/module-info.java index 549dad6ac..909e74343 100644 --- a/extension/src/main/java/module-info.java +++ b/extension/src/main/java/module-info.java @@ -8,8 +8,10 @@ module io.xpipe.extension { requires javafx.graphics; exports io.xpipe.extension; + exports io.xpipe.extension.cli; uses DataSourceProvider; uses DataSourceGuiProvider; uses SupportedApplicationProvider; + uses io.xpipe.extension.I18n; } \ No newline at end of file diff --git a/samples/sample_extension/build.gradle b/samples/sample_extension/build.gradle new file mode 100644 index 000000000..e69de29bb diff --git a/samples/sample_program/build.gradle b/samples/sample_program/build.gradle new file mode 100644 index 000000000..9b3a01aa9 --- /dev/null +++ b/samples/sample_program/build.gradle @@ -0,0 +1,18 @@ +plugins { + id 'application' +} + +java { + modularity.inferModulePath = true + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +repositories { + mavenCentral() +} + +dependencies { + implementation project(':api') + implementation project(':core') +} \ No newline at end of file diff --git a/samples/sample_program/src/main/java/io/xpipe/sample/HomePricesSample.java b/samples/sample_program/src/main/java/io/xpipe/sample/HomePricesSample.java new file mode 100644 index 000000000..0ac6d034d --- /dev/null +++ b/samples/sample_program/src/main/java/io/xpipe/sample/HomePricesSample.java @@ -0,0 +1,30 @@ +package io.xpipe.sample; + +import io.xpipe.api.DataSource; +import io.xpipe.api.DataTable; +import io.xpipe.core.data.node.TupleNode; + +import java.util.Comparator; + +public class HomePricesSample { + + private static DataTable homePricesTable; + + public static void main(String[] args) { + var resource = HomePricesSample.class.getResource("homes.csv"); + + // Creates a wrapped data source using the url. + // Note that while this is possible, it is not recommended as + // all queries are routed through the XPipe client anyway. + // It allows us however to bundle the data with this sample program. + homePricesTable = DataSource.wrap(resource).asTable(); + + System.out.println("The highest selling house entry is: " + getHighestSellingHouse()); + } + + private static TupleNode getHighestSellingHouse() { + return homePricesTable.stream() + .min(Comparator.comparingInt(t -> t.forKey("Sell").asInt())) + .get(); + } +} diff --git a/samples/sample_program/src/main/java/module-info.java b/samples/sample_program/src/main/java/module-info.java new file mode 100644 index 000000000..f9254c169 --- /dev/null +++ b/samples/sample_program/src/main/java/module-info.java @@ -0,0 +1,3 @@ +module io.xpipe.sample { + requires io.xpipe.api; +} \ No newline at end of file diff --git a/samples/sample_program/src/main/resources/io/xpipe/sample/homes.csv b/samples/sample_program/src/main/resources/io/xpipe/sample/homes.csv new file mode 100644 index 000000000..17fc90473 --- /dev/null +++ b/samples/sample_program/src/main/resources/io/xpipe/sample/homes.csv @@ -0,0 +1,52 @@ +"Sell", "List", "Living", "Rooms", "Beds", "Baths", "Age", "Acres", "Taxes" +142, 160, 28, 10, 5, 3, 60, 0.28, 3167 +175, 180, 18, 8, 4, 1, 12, 0.43, 4033 +129, 132, 13, 6, 3, 1, 41, 0.33, 1471 +138, 140, 17, 7, 3, 1, 22, 0.46, 3204 +232, 240, 25, 8, 4, 3, 5, 2.05, 3613 +135, 140, 18, 7, 4, 3, 9, 0.57, 3028 +150, 160, 20, 8, 4, 3, 18, 4.00, 3131 +207, 225, 22, 8, 4, 2, 16, 2.22, 5158 +271, 285, 30, 10, 5, 2, 30, 0.53, 5702 + 89, 90, 10, 5, 3, 1, 43, 0.30, 2054 +153, 157, 22, 8, 3, 3, 18, 0.38, 4127 + 87, 90, 16, 7, 3, 1, 50, 0.65, 1445 +234, 238, 25, 8, 4, 2, 2, 1.61, 2087 +106, 116, 20, 8, 4, 1, 13, 0.22, 2818 +175, 180, 22, 8, 4, 2, 15, 2.06, 3917 +165, 170, 17, 8, 4, 2, 33, 0.46, 2220 +166, 170, 23, 9, 4, 2, 37, 0.27, 3498 +136, 140, 19, 7, 3, 1, 22, 0.63, 3607 +148, 160, 17, 7, 3, 2, 13, 0.36, 3648 +151, 153, 19, 8, 4, 2, 24, 0.34, 3561 +180, 190, 24, 9, 4, 2, 10, 1.55, 4681 +293, 305, 26, 8, 4, 3, 6, 0.46, 7088 +167, 170, 20, 9, 4, 2, 46, 0.46, 3482 +190, 193, 22, 9, 5, 2, 37, 0.48, 3920 +184, 190, 21, 9, 5, 2, 27, 1.30, 4162 +157, 165, 20, 8, 4, 2, 7, 0.30, 3785 +110, 115, 16, 8, 4, 1, 26, 0.29, 3103 +135, 145, 18, 7, 4, 1, 35, 0.43, 3363 +567, 625, 64, 11, 4, 4, 4, 0.85, 12192 +180, 185, 20, 8, 4, 2, 11, 1.00, 3831 +183, 188, 17, 7, 3, 2, 16, 3.00, 3564 +185, 193, 20, 9, 3, 2, 56, 6.49, 3765 +152, 155, 17, 8, 4, 1, 33, 0.70, 3361 +148, 153, 13, 6, 3, 2, 22, 0.39, 3950 +152, 159, 15, 7, 3, 1, 25, 0.59, 3055 +146, 150, 16, 7, 3, 1, 31, 0.36, 2950 +170, 190, 24, 10, 3, 2, 33, 0.57, 3346 +127, 130, 20, 8, 4, 1, 65, 0.40, 3334 +265, 270, 36, 10, 6, 3, 33, 1.20, 5853 +157, 163, 18, 8, 4, 2, 12, 1.13, 3982 +128, 135, 17, 9, 4, 1, 25, 0.52, 3374 +110, 120, 15, 8, 4, 2, 11, 0.59, 3119 +123, 130, 18, 8, 4, 2, 43, 0.39, 3268 +212, 230, 39, 12, 5, 3, 202, 4.29, 3648 +145, 145, 18, 8, 4, 2, 44, 0.22, 2783 +129, 135, 10, 6, 3, 1, 15, 1.00, 2438 +143, 145, 21, 7, 4, 2, 10, 1.20, 3529 +247, 252, 29, 9, 4, 2, 4, 1.25, 4626 +111, 120, 15, 8, 3, 1, 97, 1.11, 3205 +133, 145, 26, 7, 3, 1, 42, 0.36, 3059 + diff --git a/settings.gradle b/settings.gradle index 817556c03..9828e4e96 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,4 +5,10 @@ include 'beacon' include 'api' include 'extension' +include 'sample_extension' +project(":sample_extension").projectDir = file("$projectDir/samples/sample_extension") + +include 'sample_program' +project(":sample_program").projectDir = file("$projectDir/samples/sample_program") +