diff --git a/api/src/main/java/io/xpipe/api/impl/DataSourceImpl.java b/api/src/main/java/io/xpipe/api/impl/DataSourceImpl.java index 27cd9139b..6355738ea 100644 --- a/api/src/main/java/io/xpipe/api/impl/DataSourceImpl.java +++ b/api/src/main/java/io/xpipe/api/impl/DataSourceImpl.java @@ -60,7 +60,7 @@ public abstract class DataSourceImpl implements DataSource { }); var configInstance = startRes.getConfig(); - configInstance.getCurrentValues().putAll(config); + configInstance.getConfigInstance().getCurrentValues().putAll(config); var endReq = ReadExecuteExchange.Request.builder() .target(id).dataStore(store).config(configInstance).build(); XPipeConnection.execute(con -> { diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/ReadPreparationExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/ReadPreparationExchange.java index d749620a0..d95ae0dc1 100644 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/ReadPreparationExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/ReadPreparationExchange.java @@ -37,6 +37,8 @@ public class ReadPreparationExchange implements MessageExchange in, FailableBiConsumer con) throws Exception { + @Value + public static class Result { + Charset charset; + NewLine newLine; + } + + public static Result read(FailableSupplier in, FailableConsumer con) throws Exception { checkInit(); try (var is = in.get(); @@ -34,20 +45,53 @@ public class Charsetter { String charsetName = bom == null ? null : bom.getCharsetName(); var charset = charsetName != null ? Charset.forName(charsetName) : null; + bin.mark(MAX_BYTES); + var bytes = bin.readNBytes(MAX_BYTES); + bin.reset(); if (charset == null) { - bin.mark(MAX_BYTES); - var bytes = bin.readNBytes(MAX_BYTES); - bin.reset(); charset = inferCharset(bytes); } + var nl = inferNewLine(bytes); if (con != null) { - con.accept(bin, charset); + con.accept(new InputStreamReader(bin, charset)); } - return charset; + return new Result(charset, nl); } } + public static NewLine inferNewLine(byte[] content) { + Map count = new HashMap<>(); + for (var nl : NewLine.values()) { + var nlBytes = nl.getNewLine().getBytes(StandardCharsets.UTF_8); + count.put(nl, count(content, nlBytes)); + } + + if (count.values().stream().allMatch(v -> v == 0)) { + return null; + } + + return count.entrySet().stream().min(Comparator.comparingInt(Map.Entry::getValue)) + .orElseThrow().getKey(); + } + + public static int count(byte[] outerArray, byte[] smallerArray) { + int count = 0; + for(int i = 0; i < outerArray.length - smallerArray.length+1; ++i) { + boolean found = true; + for(int j = 0; j < smallerArray.length; ++j) { + if (outerArray[i+j] != smallerArray[j]) { + found = false; + break; + } + } + if (found) { + count++; + } + } + return count; + } + public static Charset inferCharset(byte[] content) { checkInit(); diff --git a/charsetter/src/main/java/io/xpipe/charsetter/CharsetterContext.java b/charsetter/src/main/java/io/xpipe/charsetter/CharsetterContext.java index 0d6f70688..a69689d2d 100644 --- a/charsetter/src/main/java/io/xpipe/charsetter/CharsetterContext.java +++ b/charsetter/src/main/java/io/xpipe/charsetter/CharsetterContext.java @@ -3,6 +3,7 @@ package io.xpipe.charsetter; import lombok.AllArgsConstructor; import lombok.Value; +import java.nio.charset.Charset; import java.util.List; import java.util.Locale; @@ -10,6 +11,10 @@ import java.util.Locale; @AllArgsConstructor public class CharsetterContext { + public static CharsetterContext empty() { + return new CharsetterContext(Charset.defaultCharset().name(), Locale.getDefault(), Locale.getDefault(), List.of()); + } + String systemCharsetName; Locale systemLocale; diff --git a/charsetter/src/main/java/io/xpipe/charsetter/NewLine.java b/charsetter/src/main/java/io/xpipe/charsetter/NewLine.java new file mode 100644 index 000000000..158552c3d --- /dev/null +++ b/charsetter/src/main/java/io/xpipe/charsetter/NewLine.java @@ -0,0 +1,41 @@ +package io.xpipe.charsetter; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Arrays; + +public enum NewLine { + + @JsonProperty("lf") + LF("\n", "lf"), + @JsonProperty("crlf") + CRLF("\r\n", "crlf"); + + public static NewLine platform() { + return Arrays.stream(values()) + .filter(n -> n.getNewLine().equals(System.getProperty("line.separator"))) + .findFirst().orElseThrow(); + } + + public static NewLine id(String id) { + return Arrays.stream(values()) + .filter(n -> n.getId().equalsIgnoreCase(id)) + .findFirst().orElseThrow(); + } + + private final String newLine; + private final String id; + + NewLine(String newLine, String id) { + this.newLine = newLine; + this.id = id; + } + + public String getNewLine() { + return newLine; + } + + public String getId() { + return id; + } +} diff --git a/charsetter/src/main/java/module-info.java b/charsetter/src/main/java/module-info.java index 38c83c3c9..db84c86b0 100644 --- a/charsetter/src/main/java/module-info.java +++ b/charsetter/src/main/java/module-info.java @@ -4,4 +4,5 @@ module io.xpipe.charsetter { requires org.apache.commons.io; requires org.apache.commons.lang3; requires static lombok; + requires com.fasterxml.jackson.databind; } \ No newline at end of file diff --git a/core/src/main/java/io/xpipe/core/config/ConfigConverter.java b/core/src/main/java/io/xpipe/core/config/ConfigConverter.java new file mode 100644 index 000000000..e6f67bdca --- /dev/null +++ b/core/src/main/java/io/xpipe/core/config/ConfigConverter.java @@ -0,0 +1,86 @@ +package io.xpipe.core.config; + +import java.nio.charset.Charset; + +public abstract class ConfigConverter { + + public static final ConfigConverter CHARSET = new ConfigConverter() { + @Override + protected Charset fromString(String s) { + return Charset.forName(s); + } + + @Override + protected String toString(Charset value) { + return value.name(); + } + }; + + public static final ConfigConverter STRING = new ConfigConverter() { + @Override + protected String fromString(String s) { + return s; + } + + @Override + protected String toString(String value) { + return value; + } + }; + + public static final ConfigConverter CHARACTER = new ConfigConverter() { + @Override + protected Character fromString(String s) { + if (s.length() != 1) { + throw new IllegalArgumentException("Invalid character: " + s); + } + + return s.toCharArray()[0]; + } + + @Override + protected String toString(Character value) { + return value.toString(); + } + }; + + public static final ConfigConverter BOOLEAN = new ConfigConverter() { + @Override + protected Boolean fromString(String s) { + if (s.equalsIgnoreCase("y") || s.equalsIgnoreCase("yes") || s.equalsIgnoreCase("true")) { + return true; + } + + if (s.equalsIgnoreCase("n") || s.equalsIgnoreCase("no") || s.equalsIgnoreCase("false")) { + return false; + } + + throw new IllegalArgumentException("Invalid boolean: " + s); + } + + @Override + protected String toString(Boolean value) { + return value ? "yes" : "no"; + } + }; + + public T convertFromString(String s) { + if (s == null) { + return null; + } + + return fromString(s); + } + + public String convertToString(T value) { + if (value == null) { + return null; + } + + return toString(value); + } + + protected abstract T fromString(String s); + + protected abstract String toString(T value); +} diff --git a/core/src/main/java/io/xpipe/core/config/ConfigOption.java b/core/src/main/java/io/xpipe/core/config/ConfigOption.java deleted file mode 100644 index 69e8d7c22..000000000 --- a/core/src/main/java/io/xpipe/core/config/ConfigOption.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.xpipe.core.config; - -import com.fasterxml.jackson.annotation.JsonCreator; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Value; - -@Value -@Builder -@AllArgsConstructor(onConstructor_={@JsonCreator}) -public class ConfigOption { - String name; - String key; -} diff --git a/core/src/main/java/io/xpipe/core/config/ConfigOptionSet.java b/core/src/main/java/io/xpipe/core/config/ConfigOptionSet.java deleted file mode 100644 index 522c2005b..000000000 --- a/core/src/main/java/io/xpipe/core/config/ConfigOptionSet.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.xpipe.core.config; - - -import com.fasterxml.jackson.annotation.JsonCreator; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Singular; -import lombok.Value; - -import java.util.List; - -@Value -@Builder -@AllArgsConstructor(onConstructor_={@JsonCreator}) -public class ConfigOptionSet { - - public static ConfigOptionSet empty() { - return new ConfigOptionSet(List.of()); - } - - @Singular - List options; -} diff --git a/core/src/main/java/io/xpipe/core/config/ConfigOptionSetInstance.java b/core/src/main/java/io/xpipe/core/config/ConfigOptionSetInstance.java deleted file mode 100644 index f49d51502..000000000 --- a/core/src/main/java/io/xpipe/core/config/ConfigOptionSetInstance.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.xpipe.core.config; - -import com.fasterxml.jackson.annotation.JsonCreator; -import lombok.AllArgsConstructor; -import lombok.Value; - -import java.util.Map; - -@Value -@AllArgsConstructor(onConstructor_={@JsonCreator}) -public class ConfigOptionSetInstance { - - /** - * The available configuration options. - */ - ConfigOptionSet configOptions; - - /** - * The current configuration options that are set. - */ - Map currentValues; - - public boolean isComplete() { - return currentValues.size() == configOptions.getOptions().size(); - } -} diff --git a/core/src/main/java/io/xpipe/core/config/ConfigParameter.java b/core/src/main/java/io/xpipe/core/config/ConfigParameter.java new file mode 100644 index 000000000..3190a8081 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/config/ConfigParameter.java @@ -0,0 +1,26 @@ +package io.xpipe.core.config; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.AllArgsConstructor; +import lombok.Value; + +@Value +@AllArgsConstructor +public class ConfigParameter { + String key; + + @JsonCreator + public ConfigParameter(String key) { + this.key = key; + this.converter = null; + } + + @JsonIgnore + ConfigConverter converter; + + @SuppressWarnings("unchecked") + public ConfigConverter getConverter() { + return (ConfigConverter) converter; + } +} diff --git a/core/src/main/java/io/xpipe/core/config/ConfigParameterSetInstance.java b/core/src/main/java/io/xpipe/core/config/ConfigParameterSetInstance.java new file mode 100644 index 000000000..481f26933 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/config/ConfigParameterSetInstance.java @@ -0,0 +1,54 @@ +package io.xpipe.core.config; + +import com.fasterxml.jackson.annotation.JsonCreator; +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Value +@AllArgsConstructor(onConstructor_={@JsonCreator}) +public class ConfigParameterSetInstance { + + /** + * The available configuration parameters. + */ + List configParameters; + + /** + * The current configuration options that are set. + */ + Map currentValues; + + public ConfigParameterSetInstance(Map map) { + configParameters = map.keySet().stream().toList(); + currentValues = map.entrySet().stream().collect(Collectors.toMap( + e -> e.getKey().getKey(), + e -> e.getKey().getConverter().convertToString(e.getValue()))); + } + + public > ConfigParameterSetInstance(Map map, Object v) { + configParameters = map.keySet().stream().toList(); + currentValues = map.entrySet().stream().collect(Collectors.toMap( + e -> e.getKey().getKey(), + e -> e.getKey().getConverter().convertToString(apply(e.getValue(), v)))); + } + + @SuppressWarnings("unchecked") + private static , V> Object apply(T func, Object v) { + return func.apply((X) v); + } + + public void update(ConfigParameter p, String val) { + currentValues.put(p.getKey(), val); + } + + public Map evaluate() { + return configParameters.stream().collect(Collectors.toMap( + p -> p, + p -> p.getConverter().convertFromString(currentValues.get(p.getKey())))); + } +} diff --git a/core/src/main/java/io/xpipe/core/connection/Connection.java b/core/src/main/java/io/xpipe/core/connection/Connection.java new file mode 100644 index 000000000..0aef2e809 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/connection/Connection.java @@ -0,0 +1,4 @@ +package io.xpipe.core.connection; + +public class Connection { +} diff --git a/core/src/main/java/io/xpipe/core/data/node/ArrayNode.java b/core/src/main/java/io/xpipe/core/data/node/ArrayNode.java index 748647fa6..d648323b6 100644 --- a/core/src/main/java/io/xpipe/core/data/node/ArrayNode.java +++ b/core/src/main/java/io/xpipe/core/data/node/ArrayNode.java @@ -8,6 +8,10 @@ import java.util.stream.Collectors; public abstract class ArrayNode extends DataStructureNode { + public static ArrayNode empty() { + return of(List.of()); + } + public static ArrayNode of(DataStructureNode... dsn) { return of(List.of(dsn)); } diff --git a/core/src/main/java/io/xpipe/core/source/DataSource.java b/core/src/main/java/io/xpipe/core/source/DataSource.java index 51db5b5d9..55a9e06c3 100644 --- a/core/src/main/java/io/xpipe/core/source/DataSource.java +++ b/core/src/main/java/io/xpipe/core/source/DataSource.java @@ -4,7 +4,8 @@ import com.fasterxml.jackson.databind.util.TokenBuffer; import io.xpipe.core.store.DataStore; import io.xpipe.core.util.JacksonHelper; import lombok.AllArgsConstructor; -import lombok.NonNull; +import lombok.Data; +import lombok.NoArgsConstructor; import lombok.SneakyThrows; import java.util.Optional; @@ -15,19 +16,20 @@ import java.util.Optional; * * This instance is only valid in combination with its associated data store instance. */ +@Data +@NoArgsConstructor @AllArgsConstructor public abstract class DataSource { - @NonNull protected DS store; @SneakyThrows @SuppressWarnings("unchecked") - public DataSource copy() { + public > T copy() { var mapper = JacksonHelper.newMapper(); TokenBuffer tb = new TokenBuffer(mapper, false); mapper.writeValue(tb, this); - return mapper.readValue(tb.asParser(), getClass()); + return (T) mapper.readValue(tb.asParser(), getClass()); } public DataSource withStore(DS store) { @@ -36,6 +38,10 @@ public abstract class DataSource { return c; } + public boolean isComplete() { + return true; + } + /** * Casts this instance to the required type without checking whether a cast is possible. */ @@ -63,6 +69,10 @@ public abstract class DataSource { public abstract DataSourceConnection openWriteConnection() throws Exception; + public DataSourceConnection openAppendingWriteConnection() throws Exception { + throw new UnsupportedOperationException("Appending write is not supported"); + } + public DS getStore() { return store; } diff --git a/core/src/main/java/io/xpipe/core/source/DataSourceConfigInstance.java b/core/src/main/java/io/xpipe/core/source/DataSourceConfigInstance.java index dbb86ee28..3c5923e90 100644 --- a/core/src/main/java/io/xpipe/core/source/DataSourceConfigInstance.java +++ b/core/src/main/java/io/xpipe/core/source/DataSourceConfigInstance.java @@ -1,12 +1,13 @@ package io.xpipe.core.source; import com.fasterxml.jackson.annotation.JsonCreator; -import io.xpipe.core.config.ConfigOptionSet; -import io.xpipe.core.config.ConfigOptionSetInstance; +import io.xpipe.core.config.ConfigParameter; +import io.xpipe.core.config.ConfigParameterSetInstance; import lombok.AllArgsConstructor; import lombok.Value; import java.util.Map; +import java.util.function.Function; /** * Represents the current configuration of a data source. @@ -17,7 +18,7 @@ import java.util.Map; public class DataSourceConfigInstance { public static DataSourceConfigInstance xpbt() { - return new DataSourceConfigInstance("xpbt", ConfigOptionSet.empty(), Map.of()); + return new DataSourceConfigInstance("xpbt", new ConfigParameterSetInstance(Map.of())); } /** @@ -26,22 +27,21 @@ public class DataSourceConfigInstance { String provider; /** - * The available configuration options. + * The available configuration parameters. */ - ConfigOptionSet configOptions; + ConfigParameterSetInstance configInstance; - /** - * The current configuration options that are set. - */ - Map currentValues; - - public boolean isComplete() { - return currentValues.size() == configOptions.getOptions().size(); + public DataSourceConfigInstance(String provider, Map map) { + this.provider = provider; + this.configInstance = new ConfigParameterSetInstance(map); } - public DataSourceConfigInstance(String provider, ConfigOptionSetInstance cInstance) { + public > DataSourceConfigInstance(String provider, Map map, Object val) { this.provider = provider; - this.configOptions = cInstance.getConfigOptions(); - this.currentValues = cInstance.getCurrentValues(); + this.configInstance = new ConfigParameterSetInstance(map, val); + } + + public Map evaluate() { + return configInstance.evaluate(); } } diff --git a/core/src/main/java/io/xpipe/core/source/TableDataSource.java b/core/src/main/java/io/xpipe/core/source/TableDataSource.java index de640f356..124f8d5fd 100644 --- a/core/src/main/java/io/xpipe/core/source/TableDataSource.java +++ b/core/src/main/java/io/xpipe/core/source/TableDataSource.java @@ -1,7 +1,13 @@ package io.xpipe.core.source; import io.xpipe.core.store.DataStore; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor public abstract class TableDataSource extends DataSource { public TableDataSource(DS store) { @@ -29,7 +35,15 @@ public abstract class TableDataSource extends DataSource lineAcceptor) throws Exception { + + } + + @Override + public ArrayNode readRows(int maxLines) throws Exception { + return ArrayNode.of(); + } + }; + } + /** * Returns the data type of the table data. */ @@ -53,9 +77,7 @@ public interface TableReadConnection extends DataSourceReadConnection { } default void forward(DataSourceConnection con) throws Exception { - try (var tCon = (TableWriteConnection) con) { - tCon.init(); - withRows(tCon.writeLinesAcceptor()); - } + var tCon = (TableWriteConnection) con; + withRows(tCon.writeLinesAcceptor()); } } diff --git a/core/src/main/java/io/xpipe/core/source/TextDataSource.java b/core/src/main/java/io/xpipe/core/source/TextDataSource.java index 9b1cbe18e..ab831c002 100644 --- a/core/src/main/java/io/xpipe/core/source/TextDataSource.java +++ b/core/src/main/java/io/xpipe/core/source/TextDataSource.java @@ -1,9 +1,11 @@ package io.xpipe.core.source; import io.xpipe.core.store.DataStore; +import lombok.NoArgsConstructor; import java.util.concurrent.atomic.AtomicInteger; +@NoArgsConstructor public abstract class TextDataSource extends DataSource { private static final int MAX_LINE_READ = 1000; diff --git a/core/src/main/java/io/xpipe/core/store/FileDataStore.java b/core/src/main/java/io/xpipe/core/store/FileDataStore.java index ae529ce95..5915b9289 100644 --- a/core/src/main/java/io/xpipe/core/store/FileDataStore.java +++ b/core/src/main/java/io/xpipe/core/store/FileDataStore.java @@ -10,5 +10,11 @@ public interface FileDataStore extends StreamDataStore { var i = n.lastIndexOf('.'); return Optional.of(i != -1 ? n.substring(0, i) : n); } + + @Override + default boolean persistent() { + return true; + } + String getFileName(); } 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 1df125ca1..c25b50c8b 100644 --- a/core/src/main/java/io/xpipe/core/store/InputStreamDataStore.java +++ b/core/src/main/java/io/xpipe/core/store/InputStreamDataStore.java @@ -1,6 +1,7 @@ package io.xpipe.core.store; import java.io.BufferedInputStream; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -15,7 +16,6 @@ public class InputStreamDataStore implements StreamDataStore { private final InputStream in; private BufferedInputStream bufferedInputStream; - private boolean opened = false; public InputStreamDataStore(InputStream in) { this.in = in; @@ -23,13 +23,84 @@ public class InputStreamDataStore implements StreamDataStore { @Override public InputStream openInput() throws Exception { - if (opened) { + if (bufferedInputStream != null) { + bufferedInputStream.reset(); return bufferedInputStream; } - opened = true; bufferedInputStream = new BufferedInputStream(in); - return bufferedInputStream; + bufferedInputStream.mark(Integer.MAX_VALUE); + return new InputStream() { + @Override + public int read() throws IOException { + return bufferedInputStream.read(); + } + + @Override + public int read(byte[] b) throws IOException { + return bufferedInputStream.read(b); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return bufferedInputStream.read(b, off, len); + } + + @Override + public byte[] readAllBytes() throws IOException { + return bufferedInputStream.readAllBytes(); + } + + @Override + public byte[] readNBytes(int len) throws IOException { + return bufferedInputStream.readNBytes(len); + } + + @Override + public int readNBytes(byte[] b, int off, int len) throws IOException { + return bufferedInputStream.readNBytes(b, off, len); + } + + @Override + public long skip(long n) throws IOException { + return bufferedInputStream.skip(n); + } + + @Override + public void skipNBytes(long n) throws IOException { + bufferedInputStream.skipNBytes(n); + } + + @Override + public int available() throws IOException { + return bufferedInputStream.available(); + } + + @Override + public void close() throws IOException { + reset(); + } + + @Override + public synchronized void mark(int readlimit) { + bufferedInputStream.mark(readlimit); + } + + @Override + public synchronized void reset() throws IOException { + bufferedInputStream.reset(); + } + + @Override + public boolean markSupported() { + return bufferedInputStream.markSupported(); + } + + @Override + public long transferTo(OutputStream out) throws IOException { + return bufferedInputStream.transferTo(out); + } + }; } @Override 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 8503cb40b..11e9a6e46 100644 --- a/core/src/main/java/io/xpipe/core/store/LocalFileDataStore.java +++ b/core/src/main/java/io/xpipe/core/store/LocalFileDataStore.java @@ -10,6 +10,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.time.Instant; import java.util.Optional; @@ -52,6 +53,11 @@ public class LocalFileDataStore implements FileDataStore { return Files.newOutputStream(file); } + @Override + public OutputStream openAppendingOutput() throws Exception { + return Files.newOutputStream(file, StandardOpenOption.APPEND); + } + @Override public boolean exists() { return Files.exists(file); diff --git a/core/src/main/java/io/xpipe/core/store/StreamDataStore.java b/core/src/main/java/io/xpipe/core/store/StreamDataStore.java index 852c4bb5a..e73c3b652 100644 --- a/core/src/main/java/io/xpipe/core/store/StreamDataStore.java +++ b/core/src/main/java/io/xpipe/core/store/StreamDataStore.java @@ -45,5 +45,13 @@ public interface StreamDataStore extends DataStore { throw new UnsupportedOperationException("Can't open store output"); } + default OutputStream openAppendingOutput() throws Exception { + throw new UnsupportedOperationException("Can't open store output"); + } + boolean exists(); + + default boolean persistent() { + return false; + } } diff --git a/core/src/main/java/io/xpipe/core/store/URLDataStore.java b/core/src/main/java/io/xpipe/core/store/URLDataStore.java new file mode 100644 index 000000000..2c47dad5d --- /dev/null +++ b/core/src/main/java/io/xpipe/core/store/URLDataStore.java @@ -0,0 +1,22 @@ +package io.xpipe.core.store; + +import lombok.Value; + +import java.io.InputStream; +import java.net.URL; + +@Value +public class URLDataStore implements StreamDataStore { + + URL url; + + @Override + public InputStream openInput() throws Exception { + return url.openStream(); + } + + @Override + public boolean exists() { + return true; + } +} diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java index 5f6825bd2..e1608501b 100644 --- a/core/src/main/java/module-info.java +++ b/core/src/main/java/module-info.java @@ -18,6 +18,7 @@ module io.xpipe.core { opens io.xpipe.core.data.typed; exports io.xpipe.core.config; opens io.xpipe.core.config; + exports io.xpipe.core.connection; requires com.fasterxml.jackson.core; requires com.fasterxml.jackson.databind; diff --git a/extension/build.gradle b/extension/build.gradle index bd93d5e18..1742727a0 100644 --- a/extension/build.gradle +++ b/extension/build.gradle @@ -30,7 +30,9 @@ repositories { } dependencies { + compileOnly 'org.junit.jupiter:junit-jupiter-api:5.8.2' implementation project(':core') + implementation project(':charsetter') implementation 'io.xpipe:fxcomps:0.1' implementation 'com.google.code.gson:gson:2.9.0' diff --git a/extension/src/main/java/io/xpipe/extension/DataSourceProvider.java b/extension/src/main/java/io/xpipe/extension/DataSourceProvider.java index e28aaa6bb..c92d4f48f 100644 --- a/extension/src/main/java/io/xpipe/extension/DataSourceProvider.java +++ b/extension/src/main/java/io/xpipe/extension/DataSourceProvider.java @@ -1,14 +1,16 @@ package io.xpipe.extension; -import io.xpipe.core.config.ConfigOption; -import io.xpipe.core.config.ConfigOptionSet; +import io.xpipe.charsetter.NewLine; +import io.xpipe.core.config.ConfigConverter; +import io.xpipe.core.config.ConfigParameter; import io.xpipe.core.source.DataSource; import io.xpipe.core.source.DataSourceType; import io.xpipe.core.store.DataStore; import javafx.beans.property.Property; import javafx.scene.layout.Region; +import lombok.SneakyThrows; +import lombok.Value; -import java.nio.charset.Charset; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -26,9 +28,19 @@ public interface DataSourceProvider> { return GeneralType.FILE; } + if (getDatabaseProvider() != null) { + return GeneralType.DATABASE; + } + throw new ExtensionException("Provider has no general type"); } + @SneakyThrows + @SuppressWarnings("unchecked") + default T createDefault() { + return (T) getSourceClass().getDeclaredConstructors()[0].newInstance(); + } + default boolean supportsConversion(T in, DataSourceType t) { return false; } @@ -37,7 +49,7 @@ public interface DataSourceProvider> { throw new ExtensionException(); } - default void init() { + default void init() throws Exception { } default String i18n(String key) { @@ -48,7 +60,7 @@ public interface DataSourceProvider> { return getId() + "." + key; } - default Region createConfigOptions(DataStore input, Property> source) { + default Region createConfigGui(Property source) { return null; } @@ -75,27 +87,32 @@ public interface DataSourceProvider> { Map> getFileExtensions(); } + interface DatabaseProvider { + + } + interface ConfigProvider> { + @Value + static class Parameter { + ConfigParameter parameter; + Object currentValue; + boolean required; + } + static > ConfigProvider empty(List names, Function func) { return new ConfigProvider<>() { @Override - public ConfigOptionSet getConfig() { - return ConfigOptionSet.empty(); + public void applyConfig(T source, Map values) { } @Override - public T toDescriptor(DataStore store, Map values) { - return func.apply(store); - } - - @Override - public Map toConfigOptions(T source) { + public Map> toCompleteConfig() { return Map.of(); } @Override - public Map> getConverters() { + public Map toRequiredReadConfig(T desc) { return Map.of(); } @@ -106,52 +123,29 @@ public interface DataSourceProvider> { }; } - ConfigOption - CHARSET_OPTION = new ConfigOption("Charset", "charset"); - Function - CHARSET_CONVERTER = ConfigProvider.charsetConverter(); - Function - CHARSET_STRING = Charset::name; + ConfigParameter CHARSET = new ConfigParameter( + "charset", ConfigConverter.CHARSET); - static String booleanName(String name) { - return name + " (y/n)"; - } + public static final ConfigConverter NEW_LINE_CONVERTER = new ConfigConverter() { + @Override + protected NewLine fromString(String s) { + return NewLine.id(s); + } - static Function booleanConverter() { - return s -> { - if (s.equalsIgnoreCase("y") || s.equalsIgnoreCase("yes")) { - return true; - } + @Override + protected String toString(NewLine value) { + return value.getId(); + } + }; - if (s.equalsIgnoreCase("n") || s.equalsIgnoreCase("no")) { - return false; - } + ConfigParameter NEWLINE = new ConfigParameter( + "newline", NEW_LINE_CONVERTER); - throw new IllegalArgumentException("Invalid boolean: " + s); - }; - } + void applyConfig(T source, Map values); - static Function characterConverter() { - return s -> { - if (s.length() != 1) { - throw new IllegalArgumentException("Invalid character: " + s); - } + Map> toCompleteConfig(); - return s.toCharArray()[0]; - }; - } - - static Function charsetConverter() { - return Charset::forName; - } - - ConfigOptionSet getConfig(); - - T toDescriptor(DataStore store, Map values); - - Map toConfigOptions(T desc); - - Map> getConverters(); + Map toRequiredReadConfig(T desc); List getPossibleNames(); } @@ -190,6 +184,10 @@ public interface DataSourceProvider> { return null; } + default DatabaseProvider getDatabaseProvider() { + return null; + } + default boolean hasDirectoryProvider() { return false; } @@ -206,16 +204,16 @@ public interface DataSourceProvider> { * Attempt to create a useful data source descriptor from a data store. * The result does not need to be always right, it should only reflect the best effort. */ - T createDefaultDescriptor(DataStore input) throws Exception; + T createDefaultSource(DataStore input) throws Exception; - default T createDefaultWriteDescriptor(DataStore input) throws Exception { - return createDefaultDescriptor(input); + default T createDefaultWriteSource(DataStore input) throws Exception { + return createDefaultSource(input); } @SuppressWarnings("unchecked") - default Class getDescriptorClass() { + default Class getSourceClass() { return (Class) Arrays.stream(getClass().getDeclaredClasses()) - .filter(c -> c.getName().endsWith("Descriptor")).findFirst() - .orElseThrow(() -> new AssertionError("Descriptor class not found")); + .filter(c -> c.getName().endsWith("Source")).findFirst() + .orElseThrow(() -> new ExtensionException("Descriptor class not found for " + getId())); } } diff --git a/extension/src/main/java/io/xpipe/extension/DataSourceProviders.java b/extension/src/main/java/io/xpipe/extension/DataSourceProviders.java index 0bdd90814..7357691a7 100644 --- a/extension/src/main/java/io/xpipe/extension/DataSourceProviders.java +++ b/extension/src/main/java/io/xpipe/extension/DataSourceProviders.java @@ -3,6 +3,7 @@ package io.xpipe.extension; import io.xpipe.core.source.*; import io.xpipe.core.store.DataStore; import io.xpipe.core.store.LocalFileDataStore; +import io.xpipe.extension.event.ErrorEvent; import lombok.SneakyThrows; import java.util.Optional; @@ -18,7 +19,13 @@ public class DataSourceProviders { if (ALL == null) { ALL = ServiceLoader.load(layer, DataSourceProvider.class).stream() .map(p -> (DataSourceProvider) p.get()).collect(Collectors.toSet()); - ALL.forEach(DataSourceProvider::init); + ALL.forEach(p -> { + try { + p.init(); + } catch (Exception e) { + ErrorEvent.fromThrowable(e).handle(); + } + }); } } @@ -41,7 +48,7 @@ public class DataSourceProviders { @SneakyThrows public static StructureDataSource createLocalStructureDescriptor(DataStore store) { return (StructureDataSource) - DataSourceProviders.byId("xpbs").getDescriptorClass() + DataSourceProviders.byId("xpbs").getSourceClass() .getDeclaredConstructors()[0].newInstance(store); } @@ -49,7 +56,7 @@ public class DataSourceProviders { @SneakyThrows public static RawDataSource createLocalRawDescriptor(DataStore store) { return (RawDataSource) - DataSourceProviders.byId("binary").getDescriptorClass() + DataSourceProviders.byId("binary").getSourceClass() .getDeclaredConstructors()[0].newInstance(store); } @@ -57,7 +64,7 @@ public class DataSourceProviders { @SneakyThrows public static RawDataSource createLocalCollectionDescriptor(DataStore store) { return (RawDataSource) - DataSourceProviders.byId("br").getDescriptorClass() + DataSourceProviders.byId("br").getSourceClass() .getDeclaredConstructors()[0].newInstance(store); } @@ -65,7 +72,7 @@ public class DataSourceProviders { @SneakyThrows public static TextDataSource createLocalTextDescriptor(DataStore store) { return (TextDataSource) - DataSourceProviders.byId("text").getDescriptorClass() + DataSourceProviders.byId("text").getSourceClass() .getDeclaredConstructors()[0].newInstance(store); } @@ -73,7 +80,7 @@ public class DataSourceProviders { @SneakyThrows public static TableDataSource createLocalTableDescriptor(DataStore store) { return (TableDataSource) - DataSourceProviders.byId("xpbt").getDescriptorClass() + DataSourceProviders.byId("xpbt").getSourceClass() .getDeclaredConstructors()[0].newInstance(store); } @@ -94,7 +101,7 @@ public class DataSourceProviders { throw new IllegalStateException("Not initialized"); } - return (T) ALL.stream().filter(d -> d.getDescriptorClass().equals(c)).findAny() + return (T) ALL.stream().filter(d -> d.getSourceClass().equals(c)).findAny() .orElseThrow(() -> new IllegalArgumentException("Provider for " + c.getSimpleName() + " not found")); } diff --git a/extension/src/main/java/io/xpipe/extension/ExtensionTest.java b/extension/src/main/java/io/xpipe/extension/ExtensionTest.java new file mode 100644 index 000000000..fcd292b88 --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/ExtensionTest.java @@ -0,0 +1,36 @@ +package io.xpipe.extension; + +import io.xpipe.charsetter.Charsetter; +import io.xpipe.charsetter.CharsetterContext; +import io.xpipe.core.util.JacksonHelper; +import io.xpipe.extension.util.ModuleHelper; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; + +public class ExtensionTest { + + @BeforeAll + public static void setup() throws Exception { + var mod = ModuleLayer.boot().modules().stream() + .filter(m -> m.getName().contains(".test")) + .findFirst().orElseThrow(); + var e = ModuleHelper.getEveryoneModule(); + for (var pkg : mod.getPackages()) { + ModuleHelper.exportAndOpen(pkg, e); + } + + var extMod = ModuleLayer.boot().modules().stream() + .filter(m -> m.getName().equals(mod.getName().replace(".test", ""))) + .findFirst().orElseThrow(); + for (var pkg : extMod.getPackages()) { + ModuleHelper.exportAndOpen(pkg, e); + } + + JacksonHelper.initClassBased(); + Charsetter.init(CharsetterContext.empty()); + } + + @AfterAll + public static void teardown() throws Exception { + } +} diff --git a/extension/src/main/java/io/xpipe/extension/UniformDataSourceProvider.java b/extension/src/main/java/io/xpipe/extension/UniformDataSourceProvider.java index f51e1e872..85806bc36 100644 --- a/extension/src/main/java/io/xpipe/extension/UniformDataSourceProvider.java +++ b/extension/src/main/java/io/xpipe/extension/UniformDataSourceProvider.java @@ -10,14 +10,14 @@ public interface UniformDataSourceProvider> extends Data @Override default ConfigProvider getConfigProvider() { - return ConfigProvider.empty(List.of(getId()), this::createDefaultDescriptor); + return ConfigProvider.empty(List.of(getId()), this::createDefaultSource); } @Override @SuppressWarnings("unchecked") - default T createDefaultDescriptor(DataStore input) { + default T createDefaultSource(DataStore input) { try { - return (T) getDescriptorClass().getDeclaredConstructors()[0].newInstance(input); + return (T) getSourceClass().getDeclaredConstructors()[0].newInstance(input); } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { throw new AssertionError(e); } diff --git a/extension/src/main/java/io/xpipe/extension/comp/CharChoiceComp.java b/extension/src/main/java/io/xpipe/extension/comp/CharChoiceComp.java index 47407d8aa..e72cf3d42 100644 --- a/extension/src/main/java/io/xpipe/extension/comp/CharChoiceComp.java +++ b/extension/src/main/java/io/xpipe/extension/comp/CharChoiceComp.java @@ -4,21 +4,20 @@ import io.xpipe.fxcomps.Comp; import io.xpipe.fxcomps.CompStructure; import javafx.beans.property.Property; import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; import javafx.geometry.Pos; import javafx.scene.layout.HBox; import org.apache.commons.collections4.BidiMap; import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap; -import java.util.function.Supplier; - public class CharChoiceComp extends Comp> { private final Property value; private final Property charChoiceValue; - private final BidiMap> range; - private final Supplier customName; + private final BidiMap> range; + private final ObservableValue customName; - public CharChoiceComp(Property value, BidiMap> range, Supplier customName) { + public CharChoiceComp(Property value, BidiMap> range, ObservableValue customName) { this.value = value; this.range = range; this.customName = customName; diff --git a/extension/src/main/java/io/xpipe/extension/comp/CharsetChoiceComp.java b/extension/src/main/java/io/xpipe/extension/comp/CharsetChoiceComp.java index 4f9417e4a..9e22d2dec 100644 --- a/extension/src/main/java/io/xpipe/extension/comp/CharsetChoiceComp.java +++ b/extension/src/main/java/io/xpipe/extension/comp/CharsetChoiceComp.java @@ -4,12 +4,13 @@ import io.xpipe.fxcomps.Comp; import io.xpipe.fxcomps.CompStructure; import io.xpipe.fxcomps.comp.ReplacementComp; import javafx.beans.property.Property; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.value.ObservableValue; import javafx.scene.control.ComboBox; import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap; import java.nio.charset.Charset; import java.util.LinkedHashMap; -import java.util.function.Supplier; public class CharsetChoiceComp extends ReplacementComp>> { @@ -21,9 +22,9 @@ public class CharsetChoiceComp extends ReplacementComp>> createComp() { - var map = new LinkedHashMap>(); + var map = new LinkedHashMap>(); for (var e : Charset.availableCharsets().entrySet()) { - map.put(e.getValue(), e::getKey); + map.put(e.getValue(), new SimpleStringProperty(e.getKey())); } return new ChoiceComp<>(charset, new DualLinkedHashBidiMap<>(map)); } diff --git a/extension/src/main/java/io/xpipe/extension/comp/ChoiceComp.java b/extension/src/main/java/io/xpipe/extension/comp/ChoiceComp.java index 60b4bfbf2..4f4842f5a 100644 --- a/extension/src/main/java/io/xpipe/extension/comp/ChoiceComp.java +++ b/extension/src/main/java/io/xpipe/extension/comp/ChoiceComp.java @@ -4,19 +4,18 @@ import io.xpipe.fxcomps.Comp; import io.xpipe.fxcomps.CompStructure; import io.xpipe.fxcomps.util.PlatformUtil; import javafx.beans.property.Property; +import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.scene.control.ComboBox; import javafx.util.StringConverter; import org.apache.commons.collections4.BidiMap; -import java.util.function.Supplier; - public class ChoiceComp extends Comp>> { private final Property value; - private final BidiMap> range; + private final BidiMap> range; - public ChoiceComp(Property value, BidiMap> range) { + public ChoiceComp(Property value, BidiMap> range) { this.value = value; this.range = range; } @@ -28,7 +27,11 @@ public class ChoiceComp extends Comp>> { cb.setConverter(new StringConverter<>() { @Override public String toString(T object) { - return range.get(object).get(); + if (object == null) { + return "null"; + } + + return range.get(object).getValue(); } @Override diff --git a/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsBuilder.java b/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsBuilder.java index 3028806d3..068ebed5e 100644 --- a/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsBuilder.java +++ b/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsBuilder.java @@ -1,17 +1,24 @@ package io.xpipe.extension.comp; +import io.xpipe.charsetter.NewLine; import io.xpipe.core.source.DataSource; +import io.xpipe.extension.I18n; import io.xpipe.fxcomps.Comp; import javafx.beans.Observable; import javafx.beans.binding.Bindings; import javafx.beans.property.Property; +import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; import javafx.scene.control.TextField; import javafx.scene.layout.Region; +import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap; +import java.nio.charset.Charset; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; -import java.util.function.Supplier; +import java.util.Map; +import java.util.function.Function; public class DynamicOptionsBuilder> { @@ -22,12 +29,54 @@ public class DynamicOptionsBuilder> { var comp = new TextField(); comp.textProperty().bindBidirectional(prop); entries.add(new DynamicOptionsComp.Entry(name, Comp.of(() -> comp))); + props.add(prop); return this; } - public Region build(Supplier creator, Property toBind) { - var bind = Bindings.createObjectBinding(() -> creator.get(), props.toArray(Observable[]::new)); - toBind.bind(bind); + public DynamicOptionsBuilder addNewLine(Property prop) { + var map = new LinkedHashMap>(); + for (var e : NewLine.values()) { + map.put(e, new SimpleStringProperty(e.getId())); + } + var comp = new ChoiceComp<>(prop, new DualLinkedHashBidiMap<>(map)); + entries.add(new DynamicOptionsComp.Entry(I18n.observable("newLine"), comp)); + props.add(prop); + return this; + } + + public DynamicOptionsBuilder addCharacter(Property prop, ObservableValue name, Map> names) { + var comp = new CharChoiceComp(prop, new DualLinkedHashBidiMap<>(names), null); + entries.add(new DynamicOptionsComp.Entry(name, comp)); + props.add(prop); + return this; + } + + public DynamicOptionsBuilder addCharacter(Property prop, ObservableValue name, Map> names, ObservableValue customName) { + var comp = new CharChoiceComp(prop, new DualLinkedHashBidiMap<>(names), customName); + entries.add(new DynamicOptionsComp.Entry(name, comp)); + props.add(prop); + return this; + } + + public DynamicOptionsBuilder addToggle(Property prop, ObservableValue name, Map> names) { + var comp = new ToggleGroupComp<>(prop, new DualLinkedHashBidiMap<>(names)); + entries.add(new DynamicOptionsComp.Entry(name, comp)); + props.add(prop); + return this; + } + + public DynamicOptionsBuilder addCharset(Property prop) { + var comp = new CharsetChoiceComp(prop); + entries.add(new DynamicOptionsComp.Entry(I18n.observable("charset"), comp)); + props.add(prop); + return this; + } + + public Region build(Function creator, Property toBind) { + var bind = Bindings.createObjectBinding(() -> creator.apply(toBind.getValue()), props.toArray(Observable[]::new)); + bind.addListener((c,o, n) -> { + toBind.setValue(n); + }); return new DynamicOptionsComp(entries).createRegion(); } } diff --git a/extension/src/main/java/io/xpipe/extension/comp/ToggleGroupComp.java b/extension/src/main/java/io/xpipe/extension/comp/ToggleGroupComp.java index d5a3a6bc1..0bc54bbe2 100644 --- a/extension/src/main/java/io/xpipe/extension/comp/ToggleGroupComp.java +++ b/extension/src/main/java/io/xpipe/extension/comp/ToggleGroupComp.java @@ -4,24 +4,23 @@ import io.xpipe.fxcomps.Comp; import io.xpipe.fxcomps.CompStructure; import io.xpipe.fxcomps.util.PlatformUtil; import javafx.beans.property.Property; +import javafx.beans.value.ObservableValue; import javafx.scene.control.ToggleButton; import javafx.scene.control.ToggleGroup; import javafx.scene.layout.HBox; import org.apache.commons.collections4.BidiMap; -import java.util.function.Supplier; - public class ToggleGroupComp extends Comp> { private final Property value; - private final BidiMap> range; + private final BidiMap> range; - public ToggleGroupComp(Property value, BidiMap> range) { + public ToggleGroupComp(Property value, BidiMap> range) { this.value = value; this.range = range; } - public BidiMap> getRange() { + public BidiMap> getRange() { return range; } @@ -31,7 +30,7 @@ public class ToggleGroupComp extends Comp> { box.getStyleClass().add("toggle-group-comp"); ToggleGroup group = new ToggleGroup(); for (var entry : range.entrySet()) { - var b = new ToggleButton(entry.getValue().get()); + var b = new ToggleButton(entry.getValue().getValue()); b.setOnAction(e -> { value.setValue(entry.getKey()); e.consume(); diff --git a/extension/src/main/java/io/xpipe/extension/event/EventHandler.java b/extension/src/main/java/io/xpipe/extension/event/EventHandler.java index 7dbe611cd..6bb7e02ea 100644 --- a/extension/src/main/java/io/xpipe/extension/event/EventHandler.java +++ b/extension/src/main/java/io/xpipe/extension/event/EventHandler.java @@ -15,7 +15,11 @@ public abstract class EventHandler { @Override public void handle(TrackEvent te) { - LoggerFactory.getLogger(te.getCategory()).info(te.getMessage()); + var cat = te.getCategory(); + if (cat == null) { + cat = "log"; + } + LoggerFactory.getLogger(cat).info(te.getMessage()); } @Override diff --git a/extension/src/main/java/io/xpipe/extension/util/ModuleHelper.java b/extension/src/main/java/io/xpipe/extension/util/ModuleHelper.java new file mode 100644 index 000000000..673ed7afc --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/util/ModuleHelper.java @@ -0,0 +1,58 @@ +package io.xpipe.extension.util; + +import lombok.SneakyThrows; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +public class ModuleHelper { + + public static void exportAndOpen(String modName) { + var mod = ModuleLayer.boot().modules().stream() + .filter(m -> m.getName().equalsIgnoreCase(modName)) + .findFirst().orElseThrow(); + var e = getEveryoneModule(); + for (var pkg : mod.getPackages()) { + exportAndOpen(pkg, e); + } + } + + @SneakyThrows + public static Module getEveryoneModule() { + Method getDeclaredFields0 = Class.class.getDeclaredMethod("getDeclaredFields0", boolean.class); + getDeclaredFields0.setAccessible(true); + Field[] fields = (Field[]) getDeclaredFields0.invoke(Module.class, false); + Field modifiers = null; + for (Field each : fields) { + if ("EVERYONE_MODULE".equals(each.getName())) { + modifiers = each; + break; + } + } + modifiers.setAccessible(true); + return (Module) modifiers.get(null); + } + + @SneakyThrows + public static void exportAndOpen(String pkg, Module mod) { + if (mod.isExported(pkg) && mod.isOpen(pkg)) { + return; + } + + Method getDeclaredFields0 = Class.class.getDeclaredMethod("getDeclaredMethods0", boolean.class); + getDeclaredFields0.setAccessible(true); + Method[] fields = (Method[]) getDeclaredFields0.invoke(Module.class, false); + Method modifiers = null; + for (Method each : fields) { + if ("implAddExportsOrOpens".equals(each.getName())) { + modifiers = each; + break; + } + } + modifiers.setAccessible(true); + + var e = getEveryoneModule(); + modifiers.invoke(mod, pkg, e, false, true); + modifiers.invoke(mod, pkg, e, true, true); + } +} diff --git a/extension/src/main/java/module-info.java b/extension/src/main/java/module-info.java index e41365388..5f068e640 100644 --- a/extension/src/main/java/module-info.java +++ b/extension/src/main/java/module-info.java @@ -27,6 +27,8 @@ module io.xpipe.extension { requires org.reactfx; requires org.kordamp.ikonli.javafx; requires com.fasterxml.jackson.databind; + requires static org.junit.jupiter.api; + requires io.xpipe.charsetter; uses DataSourceProvider; uses SupportedApplicationProvider;