Refactor various components

This commit is contained in:
Christopher Schnick 2022-01-01 20:11:13 +01:00
parent b54d8ad362
commit d63882c5ff
45 changed files with 771 additions and 190 deletions

View file

@ -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<String, String> 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");
}
}

View file

@ -1,20 +1,15 @@
package io.xpipe.api; package io.xpipe.api;
import io.xpipe.api.impl.DataTableImpl;
import io.xpipe.core.data.node.ArrayNode; import io.xpipe.core.data.node.ArrayNode;
import io.xpipe.core.data.node.TupleNode; import io.xpipe.core.data.node.TupleNode;
import io.xpipe.core.data.type.DataType; import io.xpipe.core.data.type.DataType;
import io.xpipe.core.source.DataSourceId;
import java.util.OptionalInt; import java.util.OptionalInt;
import java.util.stream.Stream;
public interface DataTable extends Iterable<TupleNode> { public interface DataTable extends Iterable<TupleNode>, DataSource {
static DataTable get(String s) { Stream<TupleNode> stream();
return DataTableImpl.get(s);
}
DataSourceId getId();
int getRowCount(); int getRowCount();

View file

@ -1,7 +1,8 @@
package io.xpipe.api; package io.xpipe.api;
import io.xpipe.beacon.*; import io.xpipe.beacon.*;
import io.xpipe.beacon.BeaconClient;
import java.util.Optional;
public abstract class XPipeApiConnector extends BeaconConnector { public abstract class XPipeApiConnector extends BeaconConnector {
@ -23,12 +24,48 @@ public abstract class XPipeApiConnector extends BeaconConnector {
protected abstract void handle(BeaconClient sc) throws Exception; protected abstract void handle(BeaconClient sc) throws Exception;
@Override @Override
protected void waitForStartup() { protected BeaconClient constructSocket() throws ConnectorException {
try { if (!BeaconServer.isRunning()) {
Thread.sleep(5000); try {
} catch (InterruptedException e) { start();
e.printStackTrace(); } 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<BeaconClient> 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 @FunctionalInterface

View file

@ -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;
}
}

View file

@ -7,9 +7,8 @@ import io.xpipe.beacon.ClientException;
import io.xpipe.beacon.ConnectorException; import io.xpipe.beacon.ConnectorException;
import io.xpipe.beacon.ServerException; import io.xpipe.beacon.ServerException;
import io.xpipe.beacon.exchange.ReadTableDataExchange; 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.ArrayNode;
import io.xpipe.core.data.node.DataStructureNode;
import io.xpipe.core.data.node.TupleNode; import io.xpipe.core.data.node.TupleNode;
import io.xpipe.core.data.type.DataType; import io.xpipe.core.data.type.DataType;
import io.xpipe.core.data.typed.TypedAbstractReader; 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.TypedDataStructureNodeReader;
import io.xpipe.core.data.typed.TypedReusableDataStructureNodeReader; import io.xpipe.core.data.typed.TypedReusableDataStructureNodeReader;
import io.xpipe.core.source.DataSourceId; import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataSourceType;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -25,30 +25,14 @@ import java.util.*;
import java.util.stream.Stream; import java.util.stream.Stream;
import java.util.stream.StreamSupport; import java.util.stream.StreamSupport;
public class DataTableImpl implements DataTable { public class DataTableImpl extends DataSourceImpl 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];
}
private final DataSourceId id; private final DataSourceId id;
private final int size; private final int size;
private final DataType dataType; private final DataType dataType;
public DataTableImpl(DataSourceId id, int size, DataType dataType) { public DataTableImpl(DataSourceId id, int size, DataType dataType) {
super(id);
this.id = id; this.id = id;
this.size = size; this.size = size;
this.dataType = dataType; this.dataType = dataType;
@ -64,6 +48,11 @@ public class DataTableImpl implements DataTable {
return id; return id;
} }
@Override
public DataSourceType getType() {
return DataSourceType.TABLE;
}
@Override @Override
public int getRowCount() { public int getRowCount() {
if (size == -1) { if (size == -1) {
@ -96,11 +85,12 @@ public class DataTableImpl implements DataTable {
new XPipeApiConnector() { new XPipeApiConnector() {
@Override @Override
protected void handle(BeaconClient sc) throws ClientException, ServerException, ConnectorException { protected void handle(BeaconClient sc) throws ClientException, ServerException, ConnectorException {
var req = new ReadTableDataExchange.Request(id, maxToRead); var req = ReadTableDataExchange.Request.builder()
performExchange(sc, req, (ReadTableDataExchange.Response res, InputStream in) -> { .sourceId(id).maxRows(maxToRead).build();
performInputExchange(sc, req, (ReadTableDataExchange.Response res, InputStream in) -> {
var r = new TypedDataStreamParser(dataType); var r = new TypedDataStreamParser(dataType);
r.parseStructures(in, TypedDataStructureNodeReader.immutable(dataType), nodes::add); r.parseStructures(in, TypedDataStructureNodeReader.immutable(dataType), nodes::add);
}, false); });
} }
}.execute(); }.execute();
return ArrayNode.of(nodes); return ArrayNode.of(nodes);
@ -120,9 +110,10 @@ public class DataTableImpl implements DataTable {
new XPipeApiConnector() { new XPipeApiConnector() {
@Override @Override
protected void handle(BeaconClient sc) throws ClientException, ServerException, ConnectorException { protected void handle(BeaconClient sc) throws ClientException, ServerException, ConnectorException {
var req = new ReadTableDataExchange.Request(id, Integer.MAX_VALUE); var req = ReadTableDataExchange.Request.builder()
performExchange(sc, req, .sourceId(id).maxRows(Integer.MAX_VALUE).build();
(ReadTableDataExchange.Response res, InputStream in) -> input = in, false); performInputExchange(sc, req,
(ReadTableDataExchange.Response res, InputStream in) -> input = in);
} }
}.execute(); }.execute();

View file

@ -1,7 +1,6 @@
module io.xpipe.api { module io.xpipe.api {
requires io.xpipe.core; requires transitive io.xpipe.core;
requires io.xpipe.beacon; requires transitive io.xpipe.beacon;
requires org.apache.commons.lang;
exports io.xpipe.api; exports io.xpipe.api;
} }

View file

@ -14,12 +14,14 @@ repositories {
} }
apply from: "$rootDir/deps/slf4j.gradle" apply from: "$rootDir/deps/slf4j.gradle"
apply from: "$rootDir/deps/websocket.gradle"
apply from: "$rootDir/deps/jackson.gradle" apply from: "$rootDir/deps/jackson.gradle"
apply from: "$rootDir/deps/commons.gradle"
dependencies { dependencies {
implementation project(':core') implementation project(':core')
implementation project(':extension')
compileOnly 'org.projectlombok:lombok:1.18.22'
annotationProcessor 'org.projectlombok:lombok:1.18.22'
} }
test { test {

View file

@ -12,9 +12,6 @@ import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage; import io.xpipe.beacon.message.ResponseMessage;
import io.xpipe.beacon.message.ServerErrorMessage; import io.xpipe.beacon.message.ServerErrorMessage;
import io.xpipe.core.util.JacksonHelper; 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.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -23,13 +20,34 @@ import java.net.InetAddress;
import java.net.Socket; import java.net.Socket;
import java.util.Arrays; import java.util.Arrays;
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer;
import static io.xpipe.beacon.BeaconConfig.BODY_SEPARATOR; import static io.xpipe.beacon.BeaconConfig.BODY_SEPARATOR;
public class BeaconClient { public class BeaconClient {
private static final Logger logger = LoggerFactory.getLogger(BeaconClient.class); @FunctionalInterface
public interface FailableBiConsumer<T, U, E extends Throwable> {
void accept(T var1, U var2) throws E;
}
@FunctionalInterface
public interface FailableConsumer<T, E extends Throwable> {
void accept(T var1) throws E;
}
public static Optional<BeaconClient> 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 Socket socket;
private final InputStream in; private final InputStream in;
@ -51,29 +69,27 @@ public class BeaconClient {
public <REQ extends RequestMessage, RES extends ResponseMessage> void exchange( public <REQ extends RequestMessage, RES extends ResponseMessage> void exchange(
REQ req, REQ req,
Consumer<OutputStream> output, FailableConsumer<OutputStream, IOException> reqWriter,
FailableBiConsumer<RES, InputStream, IOException> responseConsumer, FailableBiConsumer<RES, InputStream, IOException> resReader)
boolean keepOpen) throws ConnectorException, ClientException, ServerException { throws ConnectorException, ClientException, ServerException {
try { try {
sendRequest(req); sendRequest(req);
if (output != null) { if (reqWriter != null) {
out.write(BODY_SEPARATOR); out.write(BODY_SEPARATOR);
output.accept(out); reqWriter.accept(out);
} }
var res = this.<RES>receiveResponse(); var res = this.<RES>receiveResponse();
var sep = in.readNBytes(BODY_SEPARATOR.length); 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"); throw new ConnectorException("Invalid body separator");
} }
responseConsumer.accept(res, in); resReader.accept(res, in);
} catch (IOException ex) { } catch (IOException ex) {
throw new ConnectorException("Couldn't communicate with socket", ex); throw new ConnectorException("Couldn't communicate with socket", ex);
} finally { } finally {
if (!keepOpen) { close();
close();
}
} }
} }

View file

@ -2,52 +2,42 @@ package io.xpipe.beacon;
import io.xpipe.beacon.message.RequestMessage; import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage; import io.xpipe.beacon.message.ResponseMessage;
import org.apache.commons.lang3.function.FailableBiConsumer;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.function.Consumer; import java.util.concurrent.atomic.AtomicReference;
public abstract class BeaconConnector { public abstract class BeaconConnector {
protected abstract void waitForStartup(); protected abstract BeaconClient constructSocket() throws ConnectorException;
protected BeaconClient constructSocket() throws ConnectorException { protected <REQ extends RequestMessage, RES extends ResponseMessage> void performInputExchange(
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 <REQ extends RequestMessage, RES extends ResponseMessage> void performExchange(
BeaconClient socket, BeaconClient socket,
REQ req, REQ req,
FailableBiConsumer<RES, InputStream, IOException> responseConsumer, BeaconClient.FailableBiConsumer<RES, InputStream, IOException> responseConsumer) throws ServerException, ConnectorException, ClientException {
boolean keepOpen) throws ServerException, ConnectorException, ClientException { performInputOutputExchange(socket, req, null, responseConsumer);
performExchange(socket, req, null, responseConsumer, keepOpen);
} }
protected <REQ extends RequestMessage, RES extends ResponseMessage> void performExchange( protected <REQ extends RequestMessage, RES extends ResponseMessage> void performInputOutputExchange(
BeaconClient socket, BeaconClient socket,
REQ req, REQ req,
Consumer<OutputStream> output, BeaconClient.FailableConsumer<OutputStream, IOException> reqWriter,
FailableBiConsumer<RES, InputStream, IOException> responseConsumer, BeaconClient.FailableBiConsumer<RES, InputStream, IOException> responseConsumer)
boolean keepOpen) throws ServerException, ConnectorException, ClientException { throws ServerException, ConnectorException, ClientException {
socket.exchange(req, output, responseConsumer, keepOpen); socket.exchange(req, reqWriter, responseConsumer);
}
protected <REQ extends RequestMessage, RES extends ResponseMessage> RES performOutputExchange(
BeaconClient socket,
REQ req,
BeaconClient.FailableConsumer<OutputStream, IOException> reqWriter)
throws ServerException, ConnectorException, ClientException {
AtomicReference<RES> response = new AtomicReference<>();
socket.exchange(req, reqWriter, (RES res, InputStream in) -> {
response.set(res);
});
return response.get();
} }
protected <REQ extends RequestMessage, RES extends ResponseMessage> RES performSimpleExchange( protected <REQ extends RequestMessage, RES extends ResponseMessage> RES performSimpleExchange(

View file

@ -10,6 +10,8 @@ public interface BeaconHandler {
void prepareBody() throws IOException; void prepareBody() throws IOException;
void startBodyRead() throws IOException;
public <T extends ResponseMessage> void sendResponse(T obj) throws Exception; public <T extends ResponseMessage> void sendResponse(T obj) throws Exception;
public void sendClientErrorResponse(String message) throws Exception; public void sendClientErrorResponse(String message) throws Exception;

View file

@ -20,19 +20,19 @@ public class BeaconServer {
return !isPortAvailable(port); return !isPortAvailable(port);
} }
public static void start() throws Exception { public static boolean tryStart() throws Exception {
if (BeaconConfig.shouldStartInProcess()) { if (BeaconConfig.shouldStartInProcess()) {
startInProcess(); startInProcess();
return; return true;
} }
var custom = BeaconConfig.getCustomExecCommand(); var custom = BeaconConfig.getCustomExecCommand();
if (custom != null) { if (custom != null) {
Runtime.getRuntime().exec(System.getenv(custom)); var proc = new ProcessBuilder("cmd", "/c", "CALL", "C:\\Projects\\xpipe\\xpipe\\gradlew.bat", ":app:run").inheritIO().start();
return; return true;
} }
throw new IllegalArgumentException("Unable to start xpipe daemon"); return false;
} }
private static void startInProcess() throws Exception { private static void startInProcess() throws Exception {

View file

@ -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<CliOptionPageExchange.Request, CliOptionPageExchange.Response> {
@Override
public String getId() {
return "cliOptionPage";
}
@Override
public Class<CliOptionPageExchange.Request> getRequestClass() {
return CliOptionPageExchange.Request.class;
}
@Override
public Class<CliOptionPageExchange.Response> 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;
}
}

View file

@ -2,11 +2,14 @@ package io.xpipe.beacon.exchange;
import io.xpipe.beacon.message.RequestMessage; import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage; import io.xpipe.beacon.message.ResponseMessage;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import java.time.Instant; import java.time.Instant;
import java.util.List; import java.util.List;
public abstract class ListCollectionsExchange implements MessageExchange<ListCollectionsExchange.Request, ListCollectionsExchange.Response> { public class ListCollectionsExchange implements MessageExchange<ListCollectionsExchange.Request, ListCollectionsExchange.Response> {
@Override @Override
public String getId() { public String getId() {
@ -23,7 +26,10 @@ public abstract class ListCollectionsExchange implements MessageExchange<ListCol
return Response.class; return Response.class;
} }
public static record Request() implements RequestMessage { @Jacksonized
@Builder
@Value
public static class Request implements RequestMessage {
} }
@ -31,7 +37,10 @@ public abstract class ListCollectionsExchange implements MessageExchange<ListCol
} }
public static record Response(List<Entry> entries) implements ResponseMessage { @Jacksonized
@Builder
@Value
public static class Response implements ResponseMessage {
List<Entry> entries;
} }
} }

View file

@ -2,11 +2,14 @@ package io.xpipe.beacon.exchange;
import io.xpipe.beacon.message.RequestMessage; import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage; import io.xpipe.beacon.message.ResponseMessage;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import java.time.Instant; import java.time.Instant;
import java.util.List; import java.util.List;
public abstract class ListEntriesExchange implements MessageExchange<ListEntriesExchange.Request, ListEntriesExchange.Response> { public class ListEntriesExchange implements MessageExchange<ListEntriesExchange.Request, ListEntriesExchange.Response> {
@Override @Override
public String getId() { public String getId() {
@ -23,15 +26,21 @@ public abstract class ListEntriesExchange implements MessageExchange<ListEntries
return Response.class; return Response.class;
} }
public static record Request(String collection) implements RequestMessage { @Jacksonized
@Builder
@Value
public static class Request implements RequestMessage {
String collection;
} }
public static record Entry(String name, String type, String description, Instant lastUsed) { public static record Entry(String name, String type, String description, Instant lastUsed) {
} }
public static record Response(List<Entry> entries) implements ResponseMessage { @Jacksonized
@Builder
@Value
public static class Response implements ResponseMessage {
List<Entry> entries;
} }
} }

View file

@ -2,9 +2,6 @@ package io.xpipe.beacon.exchange;
import io.xpipe.beacon.message.RequestMessage; import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage; import io.xpipe.beacon.message.ResponseMessage;
import io.xpipe.beacon.BeaconHandler;
import java.io.InputStream;
public interface MessageExchange<RQ extends RequestMessage, RP extends ResponseMessage> { public interface MessageExchange<RQ extends RequestMessage, RP extends ResponseMessage> {
@ -13,6 +10,4 @@ public interface MessageExchange<RQ extends RequestMessage, RP extends ResponseM
Class<RQ> getRequestClass(); Class<RQ> getRequestClass();
Class<RP> getResponseClass(); Class<RP> getResponseClass();
void handleRequest(BeaconHandler handler, RQ msg, InputStream body) throws Exception;
} }

View file

@ -2,8 +2,11 @@ package io.xpipe.beacon.exchange;
import io.xpipe.beacon.message.RequestMessage; import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage; import io.xpipe.beacon.message.ResponseMessage;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
public abstract class ModeExchange implements MessageExchange<ModeExchange.Request, ModeExchange.Response> { public class ModeExchange implements MessageExchange<ModeExchange.Request, ModeExchange.Response> {
@Override @Override
public String getId() { public String getId() {
@ -20,11 +23,17 @@ public abstract class ModeExchange implements MessageExchange<ModeExchange.Reque
return ModeExchange.Response.class; return ModeExchange.Response.class;
} }
public static record Request(String modeId) implements RequestMessage { @Jacksonized
@Builder
@Value
public static class Request implements RequestMessage {
String modeId;
} }
public static record Response() implements ResponseMessage { @Jacksonized
@Builder
@Value
public static class Response implements ResponseMessage {
} }
} }

View file

@ -0,0 +1,56 @@
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 io.xpipe.core.source.DataSourceType;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
public class ReadInfoExchange implements MessageExchange<ReadInfoExchange.Request, ReadInfoExchange.Response> {
@Override
public String getId() {
return "readTableInfo";
}
@Override
public Class<ReadInfoExchange.Request> getRequestClass() {
return ReadInfoExchange.Request.class;
}
@Override
public Class<ReadInfoExchange.Response> 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;
}
}

View file

@ -4,7 +4,7 @@ import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage; import io.xpipe.beacon.message.ResponseMessage;
import io.xpipe.core.source.DataSourceId; import io.xpipe.core.source.DataSourceId;
public abstract class ReadStructureExchange implements MessageExchange<ReadStructureExchange.Request, ReadStructureExchange.Response> { public class ReadStructureExchange implements MessageExchange<ReadStructureExchange.Request, ReadStructureExchange.Response> {
@Override @Override
public String getId() { public String getId() {

View file

@ -3,8 +3,11 @@ package io.xpipe.beacon.exchange;
import io.xpipe.beacon.message.RequestMessage; import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage; import io.xpipe.beacon.message.ResponseMessage;
import io.xpipe.core.source.DataSourceId; import io.xpipe.core.source.DataSourceId;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
public abstract class ReadTableDataExchange implements MessageExchange<ReadTableDataExchange.Request, ReadTableDataExchange.Response> { public class ReadTableDataExchange implements MessageExchange<ReadTableDataExchange.Request, ReadTableDataExchange.Response> {
@Override @Override
public String getId() { public String getId() {
@ -21,11 +24,18 @@ public abstract class ReadTableDataExchange implements MessageExchange<ReadTable
return ReadTableDataExchange.Response.class; return ReadTableDataExchange.Response.class;
} }
public static record Request(DataSourceId sourceId, int maxRows) implements RequestMessage { @Jacksonized
@Builder
@Value
public static class Request implements RequestMessage {
DataSourceId sourceId;
int maxRows;
} }
public static record Response() implements ResponseMessage { @Jacksonized
@Builder
@Value
public static class Response implements ResponseMessage {
} }
} }

View file

@ -1,32 +0,0 @@
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;
public abstract class ReadTableInfoExchange implements MessageExchange<ReadTableInfoExchange.Request, ReadTableInfoExchange.Response> {
@Override
public String getId() {
return "readTableInfo";
}
@Override
public Class<ReadTableInfoExchange.Request> getRequestClass() {
return ReadTableInfoExchange.Request.class;
}
@Override
public Class<ReadTableInfoExchange.Response> 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 {
}
}

View file

@ -2,8 +2,11 @@ package io.xpipe.beacon.exchange;
import io.xpipe.beacon.message.RequestMessage; import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage; import io.xpipe.beacon.message.ResponseMessage;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
public abstract class StatusExchange implements MessageExchange<StatusExchange.Request, StatusExchange.Response> { public class StatusExchange implements MessageExchange<StatusExchange.Request, StatusExchange.Response> {
@Override @Override
public String getId() { public String getId() {
@ -20,11 +23,16 @@ public abstract class StatusExchange implements MessageExchange<StatusExchange.R
return Response.class; return Response.class;
} }
public static record Request() implements RequestMessage { @Jacksonized
@Builder
@Value
public static class Request implements RequestMessage {
} }
public static record Response(String mode) implements ResponseMessage { @Jacksonized
@Builder
@Value
public static class Response implements ResponseMessage {
String mode;
} }
} }

View file

@ -2,8 +2,11 @@ package io.xpipe.beacon.exchange;
import io.xpipe.beacon.message.RequestMessage; import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage; import io.xpipe.beacon.message.ResponseMessage;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
public abstract class StopExchange implements MessageExchange<StopExchange.Request, StopExchange.Response> { public class StopExchange implements MessageExchange<StopExchange.Request, StopExchange.Response> {
@Override @Override
public String getId() { public String getId() {
@ -20,11 +23,17 @@ public abstract class StopExchange implements MessageExchange<StopExchange.Reque
return StopExchange.Response.class; return StopExchange.Response.class;
} }
public static record Request(boolean force) implements RequestMessage { @Jacksonized
@Builder
@Value
public static class Request implements RequestMessage {
boolean force;
} }
public static record Response(boolean success) implements ResponseMessage { @Jacksonized
@Builder
@Value
public static class Response implements ResponseMessage {
boolean success;
} }
} }

View file

@ -0,0 +1,44 @@
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;
import java.util.Map;
import java.util.UUID;
public class StoreEndExchange implements MessageExchange<StoreEndExchange.Request, StoreEndExchange.Response> {
@Override
public String getId() {
return "storeEnd";
}
@Override
public Class<StoreEndExchange.Request> getRequestClass() {
return StoreEndExchange.Request.class;
}
@Override
public Class<StoreEndExchange.Response> getResponseClass() {
return StoreEndExchange.Response.class;
}
@Jacksonized
@Builder
@Value
public static class Request implements RequestMessage {
UUID entryId;
Map<String, String> values;
}
@Jacksonized
@Builder
@Value
public static class Response implements ResponseMessage {
DataSourceId sourceId;
}
}

View file

@ -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<StoreStartExchange.Request, StoreStartExchange.Response> {
@Override
public String getId() {
return "storeStart";
}
@Override
public Class<StoreStartExchange.Request> getRequestClass() {
return StoreStartExchange.Request.class;
}
@Override
public Class<StoreStartExchange.Response> 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;
}
}

View file

@ -2,8 +2,11 @@ package io.xpipe.beacon.exchange;
import io.xpipe.beacon.message.RequestMessage; import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage; import io.xpipe.beacon.message.ResponseMessage;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
public abstract class VersionExchange implements MessageExchange<VersionExchange.Request, VersionExchange.Response> { public class VersionExchange implements MessageExchange<VersionExchange.Request, VersionExchange.Response> {
@Override @Override
public String getId() { public String getId() {
@ -20,18 +23,20 @@ public abstract class VersionExchange implements MessageExchange<VersionExchange
return VersionExchange.Response.class; return VersionExchange.Response.class;
} }
public static record Request() implements RequestMessage { @lombok.extern.jackson.Jacksonized
@lombok.Builder
@lombok.Value
public static class Request implements RequestMessage {
} }
@Jacksonized
@Builder
@Value
public static class Response implements ResponseMessage { public static class Response implements ResponseMessage {
public final String version; String version;
public final String jvmVersion; String buildVersion;
String jvmVersion;
public Response(String version, String jvmVersion) {
this.version = version;
this.jvmVersion = jvmVersion;
}
} }
} }

View file

@ -1,22 +1,30 @@
import io.xpipe.beacon.exchange.MessageExchange; import io.xpipe.beacon.exchange.*;
module io.xpipe.beacon { module io.xpipe.beacon {
exports io.xpipe.beacon; exports io.xpipe.beacon;
exports io.xpipe.beacon.exchange; exports io.xpipe.beacon.exchange;
exports io.xpipe.beacon.message; exports io.xpipe.beacon.message;
requires org.slf4j; requires org.slf4j;
requires org.slf4j.simple;
requires com.fasterxml.jackson.core; requires com.fasterxml.jackson.core;
requires com.fasterxml.jackson.databind; requires com.fasterxml.jackson.databind;
requires com.fasterxml.jackson.module.paramnames; requires com.fasterxml.jackson.module.paramnames;
requires io.xpipe.core; requires io.xpipe.core;
requires io.xpipe.extension;
opens io.xpipe.beacon; opens io.xpipe.beacon;
opens io.xpipe.beacon.exchange; opens io.xpipe.beacon.exchange;
opens io.xpipe.beacon.message; opens io.xpipe.beacon.message;
requires org.apache.commons.lang; requires static lombok;
uses MessageExchange; uses MessageExchange;
provides io.xpipe.beacon.exchange.MessageExchange with
ListCollectionsExchange,
ListEntriesExchange,
ReadTableDataExchange,
ReadInfoExchange,
StatusExchange,
StopExchange,
VersionExchange;
} }

View file

@ -11,12 +11,12 @@ public abstract class TupleNode extends DataStructureNode {
return new Builder(); return new Builder();
} }
public static TupleNode of(List<DataStructureNode> nodes) { public static TupleNode of(List<? extends DataStructureNode> nodes) {
if (nodes == null) { if (nodes == null) {
throw new IllegalArgumentException("Nodes must be not null"); throw new IllegalArgumentException("Nodes must be not null");
} }
return new NoKeyTupleNode(true, nodes); return new NoKeyTupleNode(true, (List<DataStructureNode>) nodes);
} }
public static TupleNode of(List<String> names, List<DataStructureNode> nodes) { public static TupleNode of(List<String> names, List<DataStructureNode> nodes) {

View file

@ -3,6 +3,4 @@ package io.xpipe.core.source;
public interface DataSourceConnection extends AutoCloseable { public interface DataSourceConnection extends AutoCloseable {
void init() throws Exception; void init() throws Exception;
void close() throws Exception;
} }

View file

@ -10,9 +10,9 @@ import java.io.OutputStream;
public interface TableDataReadConnection extends DataSourceConnection { public interface TableDataReadConnection extends DataSourceConnection {
TupleType determineDataType() throws Exception; TupleType getDataType() throws Exception;
int determineRowCount() throws Exception; int getRowCount() throws Exception;
void withLines(DataStructureNodeAcceptor<TupleNode> lineAcceptor) throws Exception; void withLines(DataStructureNodeAcceptor<TupleNode> lineAcceptor) throws Exception;

View file

@ -4,9 +4,21 @@ import io.xpipe.core.store.DataStore;
public abstract class TableDataSourceDescriptor<DS extends DataStore> implements DataSourceDescriptor<DS> { public abstract class TableDataSourceDescriptor<DS extends DataStore> implements DataSourceDescriptor<DS> {
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 @Override
public DataSourceType getType() { public DataSourceType getType() {

View file

@ -1,11 +1,14 @@
package io.xpipe.core.store; package io.xpipe.core.store;
import java.io.BufferedInputStream;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
public abstract class InputStreamDataStore implements StreamDataStore { public class InputStreamDataStore implements StreamDataStore {
private final InputStream in; private final InputStream in;
private BufferedInputStream bufferedInputStream;
private boolean opened = false;
public InputStreamDataStore(InputStream in) { public InputStreamDataStore(InputStream in) {
this.in = in; this.in = in;
@ -13,11 +16,17 @@ public abstract class InputStreamDataStore implements StreamDataStore {
@Override @Override
public InputStream openInput() throws Exception { public InputStream openInput() throws Exception {
return in; if (opened) {
return bufferedInputStream;
}
opened = true;
bufferedInputStream = new BufferedInputStream(in);
return bufferedInputStream;
} }
@Override @Override
public OutputStream openOutput() throws Exception { public OutputStream openOutput() throws Exception {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException("No output available");
} }
} }

View file

@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonTypeName;
import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.FilenameUtils;
import java.io.BufferedInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
@ -65,7 +66,7 @@ public class LocalFileDataStore extends FileDataStore {
@Override @Override
public InputStream openInput() throws Exception { public InputStream openInput() throws Exception {
return Files.newInputStream(file); return new BufferedInputStream(Files.newInputStream(file));
} }
@Override @Override

View file

@ -3,7 +3,6 @@ package io.xpipe.extension;
import io.xpipe.core.source.DataSourceDescriptor; import io.xpipe.core.source.DataSourceDescriptor;
import io.xpipe.core.store.DataStore; import io.xpipe.core.store.DataStore;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.scene.image.Image;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import java.nio.file.Path; import java.nio.file.Path;
@ -24,13 +23,15 @@ public interface DataSourceGuiProvider {
String getDisplayName(); String getDisplayName();
Image getImage(); String getDisplayImage();
Supplier<String> getFileName(); String getFileName();
Map<Supplier<String>, String> getFileExtensions(); Map<Supplier<String>, String> getFileExtensions();
String getDataSourceDescription(DataSourceDescriptor<?> source); String getDataSourceShortDescription(DataSourceDescriptor<?> source);
String getDataSourceLongDescription(DataSourceDescriptor<?> source);
Class<? extends DataSourceDescriptor<?>> getType(); Class<? extends DataSourceDescriptor<?>> getType();
} }

View file

@ -1,10 +1,17 @@
package io.xpipe.extension; package io.xpipe.extension;
import io.xpipe.core.source.DataSourceDescriptor; import io.xpipe.core.source.DataSourceDescriptor;
import io.xpipe.core.store.DataStore;
import java.nio.file.Path;
public interface DataSourceProvider { public interface DataSourceProvider {
String getId(); String getId();
boolean supportsFile(Path file);
DataSourceDescriptor<?> createDefaultDataSource(DataStore input) throws Exception;
Class<? extends DataSourceDescriptor<?>> getType(); Class<? extends DataSourceDescriptor<?>> getType();
} }

View file

@ -1,5 +1,6 @@
package io.xpipe.extension; package io.xpipe.extension;
import java.nio.file.Path;
import java.util.Optional; import java.util.Optional;
import java.util.ServiceLoader; import java.util.ServiceLoader;
import java.util.Set; import java.util.Set;
@ -32,6 +33,14 @@ public class DataSourceProviders {
return ALL.stream().filter(d -> d.getId().equals(name)).findAny(); return ALL.stream().filter(d -> d.getId().equals(name)).findAny();
} }
public static Optional<DataSourceProvider> byFile(Path file) {
if (ALL == null) {
throw new IllegalStateException("Not initialized");
}
return ALL.stream().filter(d -> d.supportsFile(file)).findAny();
}
public static Set<DataSourceProvider> getAll() { public static Set<DataSourceProvider> getAll() {
return ALL; return ALL;
} }

View file

@ -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<String> 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);
}

View file

@ -0,0 +1,27 @@
package io.xpipe.extension.cli;
public abstract class CliOption<T> {
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;
}
}

View file

@ -0,0 +1,22 @@
package io.xpipe.extension.cli;
import java.util.List;
public class CliOptionPage {
private String description;
private List<CliOption<?>> options;
public CliOptionPage(String description, List<CliOption<?>> options) {
this.description = description;
this.options = options;
}
public String getDescription() {
return description;
}
public List<CliOption<?>> getOptions() {
return options;
}
}

View file

@ -8,8 +8,10 @@ module io.xpipe.extension {
requires javafx.graphics; requires javafx.graphics;
exports io.xpipe.extension; exports io.xpipe.extension;
exports io.xpipe.extension.cli;
uses DataSourceProvider; uses DataSourceProvider;
uses DataSourceGuiProvider; uses DataSourceGuiProvider;
uses SupportedApplicationProvider; uses SupportedApplicationProvider;
uses io.xpipe.extension.I18n;
} }

View file

View file

@ -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')
}

View file

@ -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();
}
}

View file

@ -0,0 +1,3 @@
module io.xpipe.sample {
requires io.xpipe.api;
}

View file

@ -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
Can't render this file because it has a wrong number of fields in line 52.

View file

@ -5,4 +5,10 @@ include 'beacon'
include 'api' include 'api'
include 'extension' 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")