Rework beacon connection and implement various improvements

This commit is contained in:
Christopher Schnick 2022-03-07 22:59:48 +01:00
parent 46e83ae757
commit f5cccd5687
29 changed files with 386 additions and 117 deletions

View file

@ -31,12 +31,17 @@ test {
} }
workingDir = rootDir workingDir = rootDir
systemProperty "io.xpipe.storage.dir", "$projectDir/local/storage" // Daemon properties
systemProperty "io.xpipe.storage.persist", "false" systemProperty "io.xpipe.beacon.exec", "cmd.exe /c \"$rootDir\\gradlew.bat\" :app:run" +
systemProperty 'io.xpipe.app.writeSysOut', "true" " -Dio.xpipe.app.mode=tray" +
systemProperty 'io.xpipe.app.logLevel', "trace" " -Dio.xpipe.beacon.port=21722" +
" -Dio.xpipe.app.dataDir=$projectDir/local/" +
" -Dio.xpipe.storage.persist=false" +
" -Dio.xpipe.app.writeSysOut=true" +
" -Dio.xpipe.beacon.debugOutput=true" +
" -Dio.xpipe.app.logLevel=trace"
systemProperty "io.xpipe.beacon.exec", "cmd.exe /c \"$rootDir\\gradlew.bat\" :app:run -Dio.xpipe.daemon.mode=tray -Dio.xpipe.beacon.port=21722 -Dio.xpipe.app.dataDir=$projectDir/local/" // API properties
systemProperty 'io.xpipe.beacon.debugOutput', "true" // systemProperty 'io.xpipe.beacon.debugOutput', "true"
systemProperty "io.xpipe.beacon.port", "21722" systemProperty "io.xpipe.beacon.port", "21722"
} }

View file

@ -1,7 +1,10 @@
package io.xpipe.api; package io.xpipe.api;
import io.xpipe.api.impl.DataTableAccumulatorImpl;
import io.xpipe.core.data.node.DataStructureNode;
import io.xpipe.core.data.node.DataStructureNodeAcceptor; import io.xpipe.core.data.node.DataStructureNodeAcceptor;
import io.xpipe.core.data.node.TupleNode; import io.xpipe.core.data.node.TupleNode;
import io.xpipe.core.data.type.TupleType;
import io.xpipe.core.source.DataSourceId; import io.xpipe.core.source.DataSourceId;
/** /**
@ -13,6 +16,17 @@ import io.xpipe.core.source.DataSourceId;
*/ */
public interface DataTableAccumulator { public interface DataTableAccumulator {
public static DataTableAccumulator create(TupleType type) {
return new DataTableAccumulatorImpl(type);
}
/**
* Wrapper for {@link #finish(DataSourceId)}.
*/
default DataTable finish(String id) {
return finish(DataSourceId.fromString(id));
}
/** /**
* Finishes the construction process and returns the data source reference. * Finishes the construction process and returns the data source reference.
* *
@ -25,12 +39,12 @@ public interface DataTableAccumulator {
* *
* @param row the row to add * @param row the row to add
*/ */
void add(TupleNode row); void add(DataStructureNode row);
/** /**
* Creates a tuple acceptor that adds all accepted tuples to the table. * Creates a tuple acceptor that adds all accepted tuples to the table.
*/ */
DataStructureNodeAcceptor<TupleNode> acceptor(); DataStructureNodeAcceptor<DataStructureNode> acceptor();
/** /**
* Returns the current amount of rows added to the table. * Returns the current amount of rows added to the table.

View file

@ -17,6 +17,8 @@ public final class XPipeConnection extends BeaconConnection {
try (var con = new XPipeConnection()) { try (var con = new XPipeConnection()) {
con.constructSocket(); con.constructSocket();
handler.handle(con); handler.handle(con);
} catch (BeaconException e) {
throw e;
} catch (Exception e) { } catch (Exception e) {
throw new BeaconException(e); throw new BeaconException(e);
} }
@ -26,6 +28,8 @@ public final class XPipeConnection extends BeaconConnection {
try (var con = new XPipeConnection()) { try (var con = new XPipeConnection()) {
con.constructSocket(); con.constructSocket();
return mapper.handle(con); return mapper.handle(con);
} catch (BeaconException e) {
throw e;
} catch (Exception e) { } catch (Exception e) {
throw new BeaconException(e); throw new BeaconException(e);
} }

View file

@ -44,7 +44,7 @@ public abstract class DataSourceImpl implements DataSource {
public static DataSource create(DataSourceId id, String type, Map<String,String> config, InputStream in) { public static DataSource create(DataSourceId id, String type, Map<String,String> config, InputStream in) {
var res = XPipeConnection.execute(con -> { var res = XPipeConnection.execute(con -> {
var req = PreStoreExchange.Request.builder().build(); var req = PreStoreExchange.Request.builder().build();
PreStoreExchange.Response r = con.performOutputExchange(req, in::transferTo); PreStoreExchange.Response r = con.performOutputExchange(req, out -> in.transferTo(out));
return r; return r;
}); });

View file

@ -4,8 +4,10 @@ import io.xpipe.api.DataSource;
import io.xpipe.api.DataTable; import io.xpipe.api.DataTable;
import io.xpipe.api.DataTableAccumulator; import io.xpipe.api.DataTableAccumulator;
import io.xpipe.api.connector.XPipeConnection; import io.xpipe.api.connector.XPipeConnection;
import io.xpipe.api.util.TypeDescriptor;
import io.xpipe.beacon.exchange.PreStoreExchange; import io.xpipe.beacon.exchange.PreStoreExchange;
import io.xpipe.beacon.exchange.ReadExecuteExchange; import io.xpipe.beacon.exchange.ReadExecuteExchange;
import io.xpipe.core.data.node.DataStructureNode;
import io.xpipe.core.data.node.DataStructureNodeAcceptor; import io.xpipe.core.data.node.DataStructureNodeAcceptor;
import io.xpipe.core.data.node.TupleNode; import io.xpipe.core.data.node.TupleNode;
import io.xpipe.core.data.type.TupleType; import io.xpipe.core.data.type.TupleType;
@ -14,21 +16,26 @@ import io.xpipe.core.source.DataSourceConfigInstance;
import io.xpipe.core.source.DataSourceId; import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataSourceReference; import io.xpipe.core.source.DataSourceReference;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
public class DataTableAccumulatorImpl implements DataTableAccumulator { public class DataTableAccumulatorImpl implements DataTableAccumulator {
private final XPipeConnection connection; private final XPipeConnection connection;
private final TupleType type; private final TupleType type;
private int rows; private int rows;
private TupleType writtenDescriptor;
public DataTableAccumulatorImpl(TupleType type) { public DataTableAccumulatorImpl(TupleType type) {
this.type = type; this.type = type;
connection = XPipeConnection.open(); connection = XPipeConnection.open();
connection.sendRequest(PreStoreExchange.Request.builder().build()); connection.sendRequest(PreStoreExchange.Request.builder().build());
connection.sendBodyStart(); connection.sendBody();
} }
@Override @Override
public synchronized DataTable finish(DataSourceId id) { public synchronized DataTable finish(DataSourceId id) {
connection.withOutputStream(OutputStream::close);
PreStoreExchange.Response res = connection.receiveResponse(); PreStoreExchange.Response res = connection.receiveResponse();
connection.close(); connection.close();
@ -40,16 +47,29 @@ public class DataTableAccumulatorImpl implements DataTableAccumulator {
return DataSource.get(DataSourceReference.id(id)).asTable(); return DataSource.get(DataSourceReference.id(id)).asTable();
} }
@Override private void writeDescriptor() {
public synchronized void add(TupleNode row) { if (writtenDescriptor != null) {
return;
}
writtenDescriptor = TupleType.tableType(type.getNames());
connection.withOutputStream(out -> { connection.withOutputStream(out -> {
TypedDataStreamWriter.writeStructure(connection.getOutputStream(), row, type); out.write((TypeDescriptor.create(type.getNames())).getBytes(StandardCharsets.UTF_8));
});
}
@Override
public synchronized void add(DataStructureNode row) {
TupleNode toUse = type.matches(row) ? row.asTuple() : type.convert(row).orElseThrow().asTuple();
connection.withOutputStream(out -> {
writeDescriptor();
TypedDataStreamWriter.writeStructure(out, toUse, writtenDescriptor);
rows++; rows++;
}); });
} }
@Override @Override
public synchronized DataStructureNodeAcceptor<TupleNode> acceptor() { public synchronized DataStructureNodeAcceptor<DataStructureNode> acceptor() {
return node -> { return node -> {
add(node); add(node);
return true; return true;

View file

@ -0,0 +1,13 @@
package io.xpipe.api.util;
import java.util.List;
import java.util.stream.Collectors;
public class TypeDescriptor {
public static String create(List<String> names) {
return "[" + names.stream()
.map(n -> n != null ? "\"" + n + "\"" : null)
.collect(Collectors.joining(",")) + "]\n";
}
}

View file

@ -6,7 +6,14 @@ import io.xpipe.beacon.BeaconServer;
public class ConnectionFactory { public class ConnectionFactory {
private static boolean alreadyStarted;
public static void start() throws Exception { public static void start() throws Exception {
if (BeaconServer.isRunning()) {
alreadyStarted = true;
return;
}
if (!BeaconServer.tryStart()) { if (!BeaconServer.tryStart()) {
throw new AssertionError(); throw new AssertionError();
} }
@ -18,6 +25,10 @@ public class ConnectionFactory {
} }
public static void stop() throws Exception { public static void stop() throws Exception {
if (alreadyStarted) {
return;
}
if (!BeaconServer.isRunning()) { if (!BeaconServer.isRunning()) {
return; return;
} }

View file

@ -6,12 +6,12 @@ import org.junit.jupiter.api.BeforeAll;
public class DaemonControl { public class DaemonControl {
@BeforeAll @BeforeAll
static void setup() throws Exception { public static void setup() throws Exception {
ConnectionFactory.start(); ConnectionFactory.start();
} }
@AfterAll @AfterAll
static void teardown() throws Exception { public static void teardown() throws Exception {
ConnectionFactory.stop(); ConnectionFactory.stop();
} }
} }

View file

@ -0,0 +1,33 @@
package io.xpipe.api.test;
import io.xpipe.api.DataTableAccumulator;
import io.xpipe.core.data.node.TupleNode;
import io.xpipe.core.data.node.ValueNode;
import io.xpipe.core.data.type.TupleType;
import io.xpipe.core.data.type.ValueType;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.OptionalInt;
public class DataTableAccumulatorTest extends DaemonControl {
@Test
public void test() {
var type = TupleType.of(
List.of("col1", "col2"),
List.of(ValueType.of(), ValueType.of()));
var acc = DataTableAccumulator.create(type);
var val = type.convert(
TupleNode.of(List.of(ValueNode.of("val1"), ValueNode.of("val2")))).orElseThrow();
acc.add(val);
var table = acc.finish(":test");
Assertions.assertEquals(table.getInfo().getDataType(), TupleType.tableType(List.of("col1", "col2")));
Assertions.assertEquals(table.getInfo().getRowCountIfPresent(), OptionalInt.empty());
var read = table.read(1).at(0);
Assertions.assertEquals(val, read);
}
}

View file

@ -10,7 +10,7 @@ import java.util.Map;
public class DataTableTest extends DaemonControl { public class DataTableTest extends DaemonControl {
@BeforeAll @BeforeAll
static void setup() throws Exception { public static void setupStorage() throws Exception {
DataSource.create(DataSourceId.fromString(":usernames"), "csv", Map.of(), DataTableTest.class.getResource("username.csv")); DataSource.create(DataSourceId.fromString(":usernames"), "csv", Map.of(), DataTableTest.class.getResource("username.csv"));
} }

View file

@ -104,31 +104,27 @@ public class BeaconClient implements AutoCloseable {
} }
} }
public void receiveBody() throws ConnectorException { public InputStream receiveBody() throws ConnectorException {
try { try {
var sep = in.readNBytes(BODY_SEPARATOR.length); var sep = in.readNBytes(BODY_SEPARATOR.length);
if (sep.length != 0 && !Arrays.equals(BODY_SEPARATOR, sep)) { if (sep.length != 0 && !Arrays.equals(BODY_SEPARATOR, sep)) {
throw new ConnectorException("Invalid body separator"); throw new ConnectorException("Invalid body separator");
} }
return BeaconFormat.readBlocks(socket);
} catch (IOException ex) { } catch (IOException ex) {
throw new ConnectorException(ex); throw new ConnectorException(ex);
} }
} }
public void startBody() throws ConnectorException { public OutputStream sendBody() throws ConnectorException {
try { try {
out.write(BODY_SEPARATOR); out.write(BODY_SEPARATOR);
return BeaconFormat.writeBlocks(socket);
} catch (IOException ex) { } catch (IOException ex) {
throw new ConnectorException(ex); throw new ConnectorException(ex);
} }
} }
public <REQ extends RequestMessage, RES extends ResponseMessage> RES simpleExchange(REQ req)
throws ServerException, ConnectorException, ClientException {
sendRequest(req);
return this.receiveResponse();
}
public <T extends RequestMessage> void sendRequest(T req) throws ClientException, ConnectorException { public <T extends RequestMessage> void sendRequest(T req) throws ClientException, ConnectorException {
ObjectNode json = JacksonHelper.newMapper().valueToTree(req); ObjectNode json = JacksonHelper.newMapper().valueToTree(req);
var prov = MessageExchanges.byRequest(req); var prov = MessageExchanges.byRequest(req);
@ -245,4 +241,8 @@ public class BeaconClient implements AutoCloseable {
public OutputStream getOutputStream() { public OutputStream getOutputStream() {
return out; return out;
} }
public Socket getSocket() {
return socket;
}
} }

View file

@ -11,6 +11,9 @@ public abstract class BeaconConnection implements AutoCloseable {
protected BeaconClient socket; protected BeaconClient socket;
private InputStream bodyInput;
private OutputStream bodyOutput;
protected abstract void constructSocket(); protected abstract void constructSocket();
@Override @Override
@ -26,14 +29,6 @@ public abstract class BeaconConnection implements AutoCloseable {
} }
} }
public void closeOutput() {
try {
socket.getOutputStream().close();
} catch (Exception e) {
throw new BeaconException("Could not close beacon output stream", e);
}
}
public void withOutputStream(BeaconClient.FailableConsumer<OutputStream, IOException> ex) { public void withOutputStream(BeaconClient.FailableConsumer<OutputStream, IOException> ex) {
try { try {
ex.accept(getOutputStream()); ex.accept(getOutputStream());
@ -59,13 +54,21 @@ public abstract class BeaconConnection implements AutoCloseable {
public OutputStream getOutputStream() { public OutputStream getOutputStream() {
checkClosed(); checkClosed();
return socket.getOutputStream(); if (bodyOutput == null) {
throw new IllegalStateException("Body output has not started yet");
}
return bodyOutput;
} }
public InputStream getInputStream() { public InputStream getInputStream() {
checkClosed(); checkClosed();
return socket.getInputStream(); if (bodyInput == null) {
throw new IllegalStateException("Body input has not started yet");
}
return bodyInput;
} }
public <REQ extends RequestMessage, RES extends ResponseMessage> void performInputExchange( public <REQ extends RequestMessage, RES extends ResponseMessage> void performInputExchange(
@ -83,7 +86,16 @@ public abstract class BeaconConnection implements AutoCloseable {
checkClosed(); checkClosed();
try { try {
socket.exchange(req, reqWriter, responseConsumer); socket.sendRequest(req);
if (reqWriter != null) {
try (var out = socket.sendBody()) {
reqWriter.accept(out);
}
}
RES res = socket.receiveResponse();
try (var in = socket.receiveBody()) {
responseConsumer.accept(res, in);
}
} catch (Exception e) { } catch (Exception e) {
throw new BeaconException("Could not communicate with beacon", e); throw new BeaconException("Could not communicate with beacon", e);
} }
@ -110,21 +122,23 @@ public abstract class BeaconConnection implements AutoCloseable {
} }
} }
public void sendBodyStart() { public OutputStream sendBody() {
checkClosed(); checkClosed();
try { try {
socket.startBody(); bodyOutput = socket.sendBody();
return bodyOutput;
} catch (Exception e) { } catch (Exception e) {
throw new BeaconException("Could not communicate with beacon", e); throw new BeaconException("Could not communicate with beacon", e);
} }
} }
public void receiveBody() { public InputStream receiveBody() {
checkClosed(); checkClosed();
try { try {
socket.receiveBody(); bodyInput = socket.receiveBody();
return bodyInput;
} catch (Exception e) { } catch (Exception e) {
throw new BeaconException("Could not communicate with beacon", e); throw new BeaconException("Could not communicate with beacon", e);
} }
@ -137,25 +151,22 @@ public abstract class BeaconConnection implements AutoCloseable {
try { try {
socket.sendRequest(req); socket.sendRequest(req);
socket.startBody(); try (var out = socket.sendBody()) {
reqWriter.accept(socket.getOutputStream()); reqWriter.accept(out);
}
return socket.receiveResponse(); return socket.receiveResponse();
} catch (Exception e) { } catch (Exception e) {
throw new BeaconException("Could not communicate with beacon", e); throw new BeaconException("Could not communicate with beacon", e);
} }
} }
// public void writeLength(int bytes) throws IOException {
// checkClosed();
// socket.getOutputStream().write(ByteBuffer.allocate(4).putInt(bytes).array());
// }
public <REQ extends RequestMessage, RES extends ResponseMessage> RES performSimpleExchange( public <REQ extends RequestMessage, RES extends ResponseMessage> RES performSimpleExchange(
REQ req) { REQ req) {
checkClosed(); checkClosed();
try { try {
return socket.simpleExchange(req); socket.sendRequest(req);
return socket.receiveResponse();
} catch (Exception e) { } catch (Exception e) {
throw new BeaconException("Could not communicate with beacon", e); throw new BeaconException("Could not communicate with beacon", e);
} }

View file

@ -0,0 +1,99 @@
package io.xpipe.beacon;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.ByteBuffer;
public class BeaconFormat {
public static OutputStream writeBlocks(Socket socket) throws IOException {
int size = 65536 - 4;
var out = socket.getOutputStream();
return new OutputStream() {
private final byte[] currentBytes = new byte[size];
private int index;
@Override
public void close() throws IOException {
finishBlock();
out.flush();
}
@Override
public void write(int b) throws IOException {
if (index == currentBytes.length) {
finishBlock();
}
currentBytes[index] = (byte) b;
index++;
}
private void finishBlock() throws IOException {
if (BeaconConfig.debugEnabled()) {
System.out.println("Sending data block of length " + index);
}
int length = index;
var lengthBuffer = ByteBuffer.allocate(4).putInt(length);
out.write(lengthBuffer.array());
out.write(currentBytes, 0, length);
index = 0;
}
};
// while (true) {
// var bytes = in.readNBytes(size);
// int length = bytes.length;
// var lengthBuffer = ByteBuffer.allocate(4).putInt(length);
// socket.getOutputStream().write(lengthBuffer.array());
// socket.getOutputStream().write(bytes);
//
// if (length == 0) {
// return;
// }
// }
}
public static InputStream readBlocks(Socket socket) throws IOException {
int size = 65536 - 4;
var in = socket.getInputStream();
return new InputStream() {
private byte[] currentBytes;
private int index;
private boolean finished;
@Override
public int read() throws IOException {
if ((currentBytes == null || index == currentBytes.length) && !finished) {
readBlock();
}
if (currentBytes != null && index == currentBytes.length && finished) {
return -1;
}
int out = currentBytes[index];
index++;
return out;
}
private void readBlock() throws IOException {
var length = in.readNBytes(4);
var lengthInt = ByteBuffer.wrap(length).getInt();
if (BeaconConfig.debugEnabled()) {
System.out.println("Receiving data block of length " + lengthInt);
}
currentBytes = in.readNBytes(lengthInt);
index = 0;
if (lengthInt < size) {
finished = true;
}
}
};
}
}

View file

@ -8,9 +8,7 @@ public interface BeaconHandler {
void postResponse(BeaconClient.FailableRunnable<Exception> r); void postResponse(BeaconClient.FailableRunnable<Exception> r);
void prepareBody() throws IOException; OutputStream sendBody() throws IOException;
InputStream startBodyRead() throws IOException; InputStream receiveBody() throws IOException;
OutputStream getOutputStream() throws Exception;
} }

View file

@ -2,7 +2,9 @@ package io.xpipe.beacon;
import io.xpipe.beacon.exchange.StopExchange; import io.xpipe.beacon.exchange.StopExchange;
import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader;
import java.net.DatagramSocket; import java.net.DatagramSocket;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.nio.file.Files; import java.nio.file.Files;
@ -24,10 +26,26 @@ public class BeaconServer {
return !isPortAvailable(port); return !isPortAvailable(port);
} }
private static void startFork(String custom) throws IOException {
boolean print = true;
var proc = Runtime.getRuntime().exec(custom);
new Thread(null, () -> {
try {
InputStreamReader isr = new InputStreamReader(proc.getInputStream());
BufferedReader br = new BufferedReader(isr);
String line = null;
while ((line = br.readLine()) != null)
System.out.println("[xpiped] " + line);
} catch (IOException ioe) {
ioe.printStackTrace();
}
}, "daemon fork").start();
}
public static boolean tryStart() throws Exception { public static boolean tryStart() throws Exception {
var custom = BeaconConfig.getCustomExecCommand(); var custom = BeaconConfig.getCustomExecCommand();
if (custom != null) { if (custom != null) {
Runtime.getRuntime().exec(custom); startFork(custom);
return true; return true;
} }
@ -45,7 +63,8 @@ public class BeaconServer {
} }
public static boolean tryStop(BeaconClient client) throws Exception { public static boolean tryStop(BeaconClient client) throws Exception {
StopExchange.Response res = client.simpleExchange(StopExchange.Request.builder().build()); client.sendRequest(StopExchange.Request.builder().build());
StopExchange.Response res =client.receiveResponse();
return res.isSuccess(); return res.isSuccess();
} }

View file

@ -1,47 +0,0 @@
package io.xpipe.beacon.exchange;
import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage;
import io.xpipe.core.source.DataSourceConfigOptions;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataSourceInfo;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import java.net.URL;
public class StoreResourceExchange implements MessageExchange<StoreResourceExchange.Request, StoreResourceExchange.Response> {
@Override
public String getId() {
return "storeResource";
}
@Override
public Class<StoreResourceExchange.Request> getRequestClass() {
return StoreResourceExchange.Request.class;
}
@Override
public Class<StoreResourceExchange.Response> getResponseClass() {
return StoreResourceExchange.Response.class;
}
@Jacksonized
@Builder
@Value
public static class Request implements RequestMessage {
URL url;
String providerId;
}
@Jacksonized
@Builder
@Value
public static class Response implements ResponseMessage {
DataSourceId sourceId;
DataSourceConfigOptions config;
DataSourceInfo info;
}
}

View file

@ -33,8 +33,7 @@ public class QueryTableDataExchange implements MessageExchange<QueryTableDataExc
@NonNull @NonNull
DataSourceId id; DataSourceId id;
@Builder.Default int maxRows;
int maxRows = -1;
} }
@Jacksonized @Jacksonized

View file

@ -26,7 +26,6 @@ module io.xpipe.beacon {
ModeExchange, ModeExchange,
StatusExchange, StatusExchange,
StopExchange, StopExchange,
StoreResourceExchange,
WritePreparationExchange, WritePreparationExchange,
WriteExecuteExchange, WriteExecuteExchange,
SelectExchange, SelectExchange,

View file

@ -1,13 +1,16 @@
package io.xpipe.core.data.type; package io.xpipe.core.data.type;
import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonTypeName;
import io.xpipe.core.data.node.ArrayNode;
import io.xpipe.core.data.node.DataStructureNode; import io.xpipe.core.data.node.DataStructureNode;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Value; import lombok.Value;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional;
/** /**
* An array type represents an array of {@link DataStructureNode} of a certain shared type. * An array type represents an array of {@link DataStructureNode} of a certain shared type.
@ -47,6 +50,29 @@ public class ArrayType extends DataType {
return "array"; return "array";
} }
@Override
public Optional<DataStructureNode> convert(DataStructureNode node) {
if (matches(node)) {
return Optional.of(node);
}
if (node.isValue()) {
return Optional.of(ArrayNode.of(node));
}
List<DataStructureNode> nodes = new ArrayList<>(node.size());
for (int i = 0; i < node.size(); i++) {
var converted = sharedType.convert(node.at(i));
if (converted.isEmpty()) {
return Optional.empty();
}
nodes.add(converted.get());
}
return Optional.of(ArrayNode.of(nodes));
}
@Override @Override
public boolean matches(DataStructureNode node) { public boolean matches(DataStructureNode node) {
if (!node.isArray()) { if (!node.isArray()) {

View file

@ -3,6 +3,8 @@ package io.xpipe.core.data.type;
import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.xpipe.core.data.node.DataStructureNode; import io.xpipe.core.data.node.DataStructureNode;
import java.util.Optional;
/** /**
* Represents the type of a {@link DataStructureNode} object. * Represents the type of a {@link DataStructureNode} object.
* To check whether a {@link DataStructureNode} instance conforms to the specified type, * To check whether a {@link DataStructureNode} instance conforms to the specified type,
@ -16,6 +18,11 @@ public abstract class DataType {
*/ */
public abstract String getName(); public abstract String getName();
/**
* Checks whether a node can be converted to this data type.
*/
public abstract Optional<DataStructureNode> convert(DataStructureNode node);
/** /**
* Checks whether a node conforms to this data type. * Checks whether a node conforms to this data type.
*/ */

View file

@ -3,14 +3,13 @@ package io.xpipe.core.data.type;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonTypeName;
import io.xpipe.core.data.node.DataStructureNode; import io.xpipe.core.data.node.DataStructureNode;
import io.xpipe.core.data.node.TupleNode;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Value; import lombok.Value;
import java.util.Collections; import java.util.*;
import java.util.List;
import java.util.Objects;
/** /**
* A tuple type in the context of XPipe is defined as an ordered, * A tuple type in the context of XPipe is defined as an ordered,
@ -26,6 +25,13 @@ public class TupleType extends DataType {
List<String> names; List<String> names;
List<DataType> types; List<DataType> types;
/**
* Creates a new tuple type that represents a table data type.
*/
public static TupleType tableType(List<String> names) {
return TupleType.of(names, Collections.nCopies(names.size(), WildcardType.of()));
}
/** /**
* Creates a new tuple type that contains no entries. * Creates a new tuple type that contains no entries.
*/ */
@ -59,6 +65,33 @@ public class TupleType extends DataType {
return "tuple"; return "tuple";
} }
@Override
public Optional<DataStructureNode> convert(DataStructureNode node) {
if (matches(node)) {
return Optional.of(node);
}
if (node.isValue() && types.size() == 1) {
return types.get(0).convert(node);
}
if (node.size() != types.size()) {
return Optional.empty();
}
List<DataStructureNode> nodes = new ArrayList<>(node.size());
for (int i = 0; i < node.size(); i++) {
var converted = types.get(i).convert(node.at(i));
if (converted.isEmpty()) {
return Optional.empty();
}
nodes.add(converted.get());
}
return Optional.of(TupleNode.of(names, nodes));
}
@Override @Override
public boolean matches(DataStructureNode node) { public boolean matches(DataStructureNode node) {
if (!node.isTuple()) { if (!node.isTuple()) {

View file

@ -7,6 +7,8 @@ import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Value; import lombok.Value;
import java.util.Optional;
/** /**
* A value type represents any node that holds some atomic value, i.e. it has no subtypes. * A value type represents any node that holds some atomic value, i.e. it has no subtypes.
*/ */
@ -28,6 +30,20 @@ public class ValueType extends DataType {
return "value"; return "value";
} }
@Override
public Optional<DataStructureNode> convert(DataStructureNode node) {
if (matches(node)) {
return Optional.of(node);
}
if (node.size() == 1) {
var n = node.at(0);
return convert(n);
}
return Optional.empty();
}
@Override @Override
public boolean matches(DataStructureNode node) { public boolean matches(DataStructureNode node) {
return node.isValue(); return node.isValue();

View file

@ -5,6 +5,8 @@ import io.xpipe.core.data.node.DataStructureNode;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Value; import lombok.Value;
import java.util.Optional;
/** /**
* A wildcard type matches any {@link DataStructureNode} instance. * A wildcard type matches any {@link DataStructureNode} instance.
* For simplicity reasons it is not possible to further specify a wildcard instance to only match a certain * For simplicity reasons it is not possible to further specify a wildcard instance to only match a certain
@ -29,6 +31,11 @@ public class WildcardType extends DataType {
return "wildcard"; return "wildcard";
} }
@Override
public Optional<DataStructureNode> convert(DataStructureNode node) {
return Optional.of(node);
}
@Override @Override
public boolean matches(DataStructureNode node) { public boolean matches(DataStructureNode node) {
return true; return true;

View file

@ -1,11 +1,7 @@
package io.xpipe.core.data.typed; package io.xpipe.core.data.typed;
import io.xpipe.core.data.node.DataStructureNode;
import io.xpipe.core.data.node.DataStructureNodeIO;
import io.xpipe.core.data.generic.GenericDataStreamWriter; import io.xpipe.core.data.generic.GenericDataStreamWriter;
import io.xpipe.core.data.node.ArrayNode; import io.xpipe.core.data.node.*;
import io.xpipe.core.data.node.SimpleTupleNode;
import io.xpipe.core.data.node.ValueNode;
import io.xpipe.core.data.type.ArrayType; import io.xpipe.core.data.type.ArrayType;
import io.xpipe.core.data.type.DataType; import io.xpipe.core.data.type.DataType;
import io.xpipe.core.data.type.TupleType; import io.xpipe.core.data.type.TupleType;
@ -22,7 +18,7 @@ public class TypedDataStreamWriter {
private static void write(OutputStream out, DataStructureNode node, DataType type) throws IOException { private static void write(OutputStream out, DataStructureNode node, DataType type) throws IOException {
if (type.isTuple() && node.isTuple()) { if (type.isTuple() && node.isTuple()) {
writeTuple(out, (SimpleTupleNode) node, (TupleType) type); writeTuple(out, (TupleNode) node, (TupleType) type);
} else if (node.isArray() && type.isArray()) { } else if (node.isArray() && type.isArray()) {
writeArray(out, (ArrayNode) node, (ArrayType) type); writeArray(out, (ArrayNode) node, (ArrayType) type);
} else if (node.isValue() && type.isValue()) { } else if (node.isValue() && type.isValue()) {
@ -40,7 +36,7 @@ public class TypedDataStreamWriter {
out.write(n.getRawData()); out.write(n.getRawData());
} }
private static void writeTuple(OutputStream out, SimpleTupleNode tuple, TupleType type) throws IOException { private static void writeTuple(OutputStream out, TupleNode tuple, TupleType type) throws IOException {
if (tuple.size() != type.getSize()) { if (tuple.size() != type.getSize()) {
throw new IllegalArgumentException("Tuple size mismatch"); throw new IllegalArgumentException("Tuple size mismatch");
} }

View file

@ -67,6 +67,9 @@ public class TypedDataStructureNodeReader implements TypedAbstractReader {
if (nodes.size() != 0 || children.size() != 0 || readNode == null) { if (nodes.size() != 0 || children.size() != 0 || readNode == null) {
throw new IllegalStateException("Reader is not finished yet"); throw new IllegalStateException("Reader is not finished yet");
} }
expectedType = flattened.get(0);
currentExpectedTypeIndex = 0;
} }
private void finishNode(DataStructureNode node) { private void finishNode(DataStructureNode node) {

View file

@ -12,6 +12,7 @@ import com.fasterxml.jackson.databind.module.SimpleModule;
import io.xpipe.core.data.type.ArrayType; import io.xpipe.core.data.type.ArrayType;
import io.xpipe.core.data.type.TupleType; import io.xpipe.core.data.type.TupleType;
import io.xpipe.core.data.type.ValueType; import io.xpipe.core.data.type.ValueType;
import io.xpipe.core.data.type.WildcardType;
import io.xpipe.core.source.DataSourceInfo; import io.xpipe.core.source.DataSourceInfo;
import io.xpipe.core.source.DataSourceReference; import io.xpipe.core.source.DataSourceReference;
import io.xpipe.core.store.LocalFileDataStore; import io.xpipe.core.store.LocalFileDataStore;
@ -29,6 +30,7 @@ public class CoreJacksonModule extends SimpleModule {
new NamedType(ValueType.class), new NamedType(ValueType.class),
new NamedType(TupleType.class), new NamedType(TupleType.class),
new NamedType(ArrayType.class), new NamedType(ArrayType.class),
new NamedType(WildcardType.class),
new NamedType(DataSourceInfo.Table.class) new NamedType(DataSourceInfo.Table.class)
); );

View file

@ -21,6 +21,7 @@ public class JacksonHelper {
public static synchronized void initModularized(ModuleLayer layer) { public static synchronized void initModularized(ModuleLayer layer) {
ObjectMapper objectMapper = INSTANCE; ObjectMapper objectMapper = INSTANCE;
objectMapper.enable(SerializationFeature.INDENT_OUTPUT); objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
objectMapper.registerModules(findModules(layer)); objectMapper.registerModules(findModules(layer));
objectMapper.setVisibility(objectMapper.getSerializationConfig().getDefaultVisibilityChecker() objectMapper.setVisibility(objectMapper.getSerializationConfig().getDefaultVisibilityChecker()

View file

@ -33,7 +33,7 @@ public interface DataSourceProvider {
Supplier<String> getDescription(DataSourceDescriptor<?> source); Supplier<String> getDescription(DataSourceDescriptor<?> source);
} }
interface CliProvider { interface ConfigProvider {
static String booleanName(String name) { static String booleanName(String name) {
return name + " (y/n)"; return name + " (y/n)";
@ -82,7 +82,7 @@ public interface DataSourceProvider {
GuiProvider getGuiProvider(); GuiProvider getGuiProvider();
CliProvider getCliProvider(); ConfigProvider getConfigProvider();
String getId(); String getId();

View file

@ -55,7 +55,7 @@ public class DataSourceProviders {
throw new IllegalStateException("Not initialized"); throw new IllegalStateException("Not initialized");
} }
return ALL.stream().filter(d -> d.getCliProvider() != null && d.getCliProvider().getPossibleNames().stream() return ALL.stream().filter(d -> d.getConfigProvider().getPossibleNames().stream()
.anyMatch(s -> s.equalsIgnoreCase(name))).findAny(); .anyMatch(s -> s.equalsIgnoreCase(name))).findAny();
} }