From 100438744e2ca5e54d5857f040478eb119d81473 Mon Sep 17 00:00:00 2001 From: Christopher Schnick Date: Tue, 3 May 2022 15:26:31 +0200 Subject: [PATCH] Add support for more data source types --- beacon/build.gradle | 4 + .../beacon/exchange/cli/ConvertExchange.java | 49 ++++++++ beacon/src/main/java/module-info.java | 2 + core/build.gradle | 4 + .../CollectionDataSourceDescriptor.java | 30 +++++ .../core/source/CollectionReadConnection.java | 24 ++++ .../source/CollectionWriteConnection.java | 6 + .../core/source/DataSourceDescriptor.java | 5 - .../io/xpipe/core/source/DataSourceInfo.java | 34 ++++-- .../io/xpipe/core/source/DataSourceType.java | 5 +- .../core/source/RawDataSourceDescriptor.java | 35 ++++++ .../xpipe/core/source/RawReadConnection.java | 18 +++ .../xpipe/core/source/RawWriteConnection.java | 6 + .../source/StructureDataSourceDescriptor.java | 38 +++++-- .../source/TableDataSourceDescriptor.java | 5 - .../core/source/TextDataSourceDescriptor.java | 10 +- .../xpipe/core/source/TextReadConnection.java | 28 +++-- .../io/xpipe/core/util/JacksonHelper.java | 2 + extension/build.gradle | 4 + .../xpipe/extension/DataSourceProvider.java | 105 +++++++++++++++--- .../xpipe/extension/DataSourceProviders.java | 76 ++++++++----- .../SimpleFileDataSourceProvider.java | 64 +++++++++++ .../extension/UniformDataSourceProvider.java | 35 ++++++ extension/src/main/java/module-info.java | 6 +- samples/sample_extension/build.gradle | 0 .../file_data_source_sample/build.gradle | 13 +++ .../MyRawFileDescriptor.java | 18 +++ .../MyRawFileProvider.java | 30 +++++ .../MyRawFileReadConnection.java | 43 +++++++ .../MyRawFileWriteConnection.java | 43 +++++++ .../src/main/java/module-info.java | 13 +++ .../resources/img/icon.png | Bin 0 -> 23849 bytes .../resources/lang/translations_de.properties | 3 + .../resources/lang/translations_en.properties | 3 + 34 files changed, 669 insertions(+), 92 deletions(-) create mode 100644 beacon/src/main/java/io/xpipe/beacon/exchange/cli/ConvertExchange.java create mode 100644 core/src/main/java/io/xpipe/core/source/CollectionDataSourceDescriptor.java create mode 100644 core/src/main/java/io/xpipe/core/source/CollectionReadConnection.java create mode 100644 core/src/main/java/io/xpipe/core/source/CollectionWriteConnection.java create mode 100644 core/src/main/java/io/xpipe/core/source/RawDataSourceDescriptor.java create mode 100644 core/src/main/java/io/xpipe/core/source/RawReadConnection.java create mode 100644 core/src/main/java/io/xpipe/core/source/RawWriteConnection.java create mode 100644 extension/src/main/java/io/xpipe/extension/SimpleFileDataSourceProvider.java create mode 100644 extension/src/main/java/io/xpipe/extension/UniformDataSourceProvider.java delete mode 100644 samples/sample_extension/build.gradle create mode 100644 samples/sample_extensions/file_data_source_sample/build.gradle create mode 100644 samples/sample_extensions/file_data_source_sample/src/main/java/io/xpipe/ext/file_data_source_sample/MyRawFileDescriptor.java create mode 100644 samples/sample_extensions/file_data_source_sample/src/main/java/io/xpipe/ext/file_data_source_sample/MyRawFileProvider.java create mode 100644 samples/sample_extensions/file_data_source_sample/src/main/java/io/xpipe/ext/file_data_source_sample/MyRawFileReadConnection.java create mode 100644 samples/sample_extensions/file_data_source_sample/src/main/java/io/xpipe/ext/file_data_source_sample/MyRawFileWriteConnection.java create mode 100644 samples/sample_extensions/file_data_source_sample/src/main/java/module-info.java create mode 100644 samples/sample_extensions/file_data_source_sample/src/main/resources/io/xpipe/ext/file_data_source_sample/resources/img/icon.png create mode 100644 samples/sample_extensions/file_data_source_sample/src/main/resources/io/xpipe/ext/file_data_source_sample/resources/lang/translations_de.properties create mode 100644 samples/sample_extensions/file_data_source_sample/src/main/resources/io/xpipe/ext/file_data_source_sample/resources/lang/translations_en.properties diff --git a/beacon/build.gradle b/beacon/build.gradle index 064021d0b..eea5327dd 100644 --- a/beacon/build.gradle +++ b/beacon/build.gradle @@ -11,6 +11,10 @@ apply from: "$rootDir/deps/lombok.gradle" apply from: 'publish.gradle' apply from: "$rootDir/deps/publish-base.gradle" +configurations { + compileOnly.extendsFrom(dep) +} + version = file('../version').text group = 'io.xpipe' archivesBaseName = 'beacon' diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/ConvertExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/cli/ConvertExchange.java new file mode 100644 index 000000000..301a53508 --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/cli/ConvertExchange.java @@ -0,0 +1,49 @@ +package io.xpipe.beacon.exchange.cli; + +import io.xpipe.beacon.exchange.MessageExchange; +import io.xpipe.beacon.message.RequestMessage; +import io.xpipe.beacon.message.ResponseMessage; +import io.xpipe.core.source.DataSourceConfigInstance; +import io.xpipe.core.source.DataSourceId; +import io.xpipe.core.source.DataSourceReference; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +public class ConvertExchange implements MessageExchange { + + @Override + public String getId() { + return "convert"; + } + + @Override + public Class getRequestClass() { + return ConvertExchange.Request.class; + } + + @Override + public Class getResponseClass() { + return ConvertExchange.Response.class; + } + + @Jacksonized + @Builder + @Value + public static class Request implements RequestMessage { + @NonNull + DataSourceReference ref; + + @NonNull DataSourceId copyId; + + @NonNull + DataSourceConfigInstance config; + } + + @Jacksonized + @Builder + @Value + public static class Response implements ResponseMessage { + } +} diff --git a/beacon/src/main/java/module-info.java b/beacon/src/main/java/module-info.java index fde7b72a4..27799a0a4 100644 --- a/beacon/src/main/java/module-info.java +++ b/beacon/src/main/java/module-info.java @@ -33,12 +33,14 @@ module io.xpipe.beacon { WriteExecuteExchange, SelectExchange, ReadPreparationExchange, + QueryTextDataExchange, ReadExecuteExchange, DialogExchange, QueryDataSourceExchange, StoreStreamExchange, EditPreparationExchange, EditExecuteExchange, + ConvertExchange, QueryTableDataExchange, VersionExchange; } \ No newline at end of file diff --git a/core/build.gradle b/core/build.gradle index 0d7423123..cd76f6065 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -12,6 +12,10 @@ apply from: "$rootDir/deps/junit.gradle" apply from: 'publish.gradle' apply from: "$rootDir/deps/publish-base.gradle" +configurations { + compileOnly.extendsFrom(dep) +} + version = file('../version').text group = 'io.xpipe' archivesBaseName = 'core' diff --git a/core/src/main/java/io/xpipe/core/source/CollectionDataSourceDescriptor.java b/core/src/main/java/io/xpipe/core/source/CollectionDataSourceDescriptor.java new file mode 100644 index 000000000..72c360cbb --- /dev/null +++ b/core/src/main/java/io/xpipe/core/source/CollectionDataSourceDescriptor.java @@ -0,0 +1,30 @@ +package io.xpipe.core.source; + +import io.xpipe.core.store.DataStore; + +public interface CollectionDataSourceDescriptor extends DataSourceDescriptor { + + @Override + default DataSourceInfo determineInfo(DS store) throws Exception { + try (var con = openReadConnection(store)) { + var c = (int) con.listEntries().count(); + return new DataSourceInfo.Structure(c); + } + } + + default CollectionReadConnection openReadConnection(DS store) throws Exception { + var con = newReadConnection(store); + con.init(); + return con; + } + + default CollectionWriteConnection openWriteConnection(DS store) throws Exception { + var con = newWriteConnection(store); + con.init(); + return con; + } + + CollectionWriteConnection newWriteConnection(DS store); + + CollectionReadConnection newReadConnection(DS store); +} diff --git a/core/src/main/java/io/xpipe/core/source/CollectionReadConnection.java b/core/src/main/java/io/xpipe/core/source/CollectionReadConnection.java new file mode 100644 index 000000000..c98f2219c --- /dev/null +++ b/core/src/main/java/io/xpipe/core/source/CollectionReadConnection.java @@ -0,0 +1,24 @@ +package io.xpipe.core.source; + +import lombok.SneakyThrows; + +import java.util.stream.Stream; + +public interface CollectionReadConnection extends DataSourceReadConnection { + + T open(String entry) throws Exception; + + Stream listEntries() throws Exception; + + @SneakyThrows + default void forward(DataSourceConnection con) throws Exception { + try (var tCon = (CollectionWriteConnection) con) { + tCon.init(); + listEntries().forEach(s -> { + try (var subCon = open(s)) { + ((CollectionWriteConnection) con).write(s, subCon); + } + }); + } + } +} diff --git a/core/src/main/java/io/xpipe/core/source/CollectionWriteConnection.java b/core/src/main/java/io/xpipe/core/source/CollectionWriteConnection.java new file mode 100644 index 000000000..a2242b364 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/source/CollectionWriteConnection.java @@ -0,0 +1,6 @@ +package io.xpipe.core.source; + +public interface CollectionWriteConnection extends DataSourceConnection { + + void write(String entry, DataSourceReadConnection con) throws Exception; +} diff --git a/core/src/main/java/io/xpipe/core/source/DataSourceDescriptor.java b/core/src/main/java/io/xpipe/core/source/DataSourceDescriptor.java index e5c3937d9..f18bfdf1e 100644 --- a/core/src/main/java/io/xpipe/core/source/DataSourceDescriptor.java +++ b/core/src/main/java/io/xpipe/core/source/DataSourceDescriptor.java @@ -27,11 +27,6 @@ public interface DataSourceDescriptor { */ DataSourceInfo determineInfo(DS store) throws Exception; - /** - * Returns the general data source type. - */ - DataSourceType getType(); - DataSourceReadConnection openReadConnection(DS store) throws Exception; DataSourceConnection openWriteConnection(DS store) throws Exception; diff --git a/core/src/main/java/io/xpipe/core/source/DataSourceInfo.java b/core/src/main/java/io/xpipe/core/source/DataSourceInfo.java index a75380fe2..b2c6c661c 100644 --- a/core/src/main/java/io/xpipe/core/source/DataSourceInfo.java +++ b/core/src/main/java/io/xpipe/core/source/DataSourceInfo.java @@ -7,8 +7,6 @@ import io.xpipe.core.data.type.TupleType; import lombok.EqualsAndHashCode; import lombok.Value; -import java.nio.ByteOrder; -import java.nio.charset.Charset; import java.util.OptionalInt; /** @@ -53,8 +51,11 @@ public abstract class DataSourceInfo { @JsonTypeName("structure") public static class Structure extends DataSourceInfo { + int entries; + @JsonCreator - public Structure() { + public Structure(int entries) { + this.entries = entries; } @Override @@ -63,17 +64,32 @@ public abstract class DataSourceInfo { } } + @EqualsAndHashCode(callSuper = false) + @Value + @JsonTypeName("collection") + public static class Collection extends DataSourceInfo { + + int entries; + + @JsonCreator + public Collection(int entries) { + this.entries = entries; + } + + @Override + public DataSourceType getType() { + return DataSourceType.COLLECTION; + } + } + @EqualsAndHashCode(callSuper = false) @Value @JsonTypeName("text") public static class Text extends DataSourceInfo { - Charset charset; - int lineCount; @JsonCreator - public Text(Charset charset, int lineCount) { - this.charset = charset; + public Text(int lineCount) { this.lineCount = lineCount; } @@ -89,12 +105,10 @@ public abstract class DataSourceInfo { @JsonTypeName("raw") public static class Raw extends DataSourceInfo { int byteCount; - ByteOrder byteOrder; @JsonCreator - public Raw(int byteCount, ByteOrder byteOrder) { + public Raw(int byteCount) { this.byteCount = byteCount; - this.byteOrder = byteOrder; } @Override diff --git a/core/src/main/java/io/xpipe/core/source/DataSourceType.java b/core/src/main/java/io/xpipe/core/source/DataSourceType.java index be4d78623..d6c617a47 100644 --- a/core/src/main/java/io/xpipe/core/source/DataSourceType.java +++ b/core/src/main/java/io/xpipe/core/source/DataSourceType.java @@ -18,5 +18,8 @@ public enum DataSourceType { TEXT, @JsonProperty("raw") - RAW + RAW, + + @JsonProperty("collection") + COLLECTION } diff --git a/core/src/main/java/io/xpipe/core/source/RawDataSourceDescriptor.java b/core/src/main/java/io/xpipe/core/source/RawDataSourceDescriptor.java new file mode 100644 index 000000000..fe79bd783 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/source/RawDataSourceDescriptor.java @@ -0,0 +1,35 @@ +package io.xpipe.core.source; + +import io.xpipe.core.store.DataStore; + +public abstract class RawDataSourceDescriptor implements DataSourceDescriptor { + + private static final int MAX_BYTES_READ = 100000; + + @Override + public DataSourceInfo determineInfo(DS store) throws Exception { + try (var con = openReadConnection(store)) { + var b = con.readBytes(MAX_BYTES_READ); + int usedCount = b.length == MAX_BYTES_READ ? -1 : b.length; + return new DataSourceInfo.Raw(usedCount); + } + } + + @Override + public RawReadConnection openReadConnection(DS store) throws Exception { + var con = newReadConnection(store); + con.init(); + return con; + } + + @Override + public RawWriteConnection openWriteConnection(DS store) throws Exception { + var con = newWriteConnection(store); + con.init(); + return con; + } + + protected abstract RawWriteConnection newWriteConnection(DS store); + + protected abstract RawReadConnection newReadConnection(DS store); +} diff --git a/core/src/main/java/io/xpipe/core/source/RawReadConnection.java b/core/src/main/java/io/xpipe/core/source/RawReadConnection.java new file mode 100644 index 000000000..725b537f4 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/source/RawReadConnection.java @@ -0,0 +1,18 @@ +package io.xpipe.core.source; + +public interface RawReadConnection extends DataSourceReadConnection { + + byte[] readBytes(int max) throws Exception; + + int BUFFER_SIZE = 8192; + + default void forward(DataSourceConnection con) throws Exception { + try (var tCon = (RawWriteConnection) con) { + tCon.init(); + byte[] b; + while ((b = readBytes(BUFFER_SIZE)).length > 0) { + tCon.write(b); + } + } + } +} diff --git a/core/src/main/java/io/xpipe/core/source/RawWriteConnection.java b/core/src/main/java/io/xpipe/core/source/RawWriteConnection.java new file mode 100644 index 000000000..3ce72c83f --- /dev/null +++ b/core/src/main/java/io/xpipe/core/source/RawWriteConnection.java @@ -0,0 +1,6 @@ +package io.xpipe.core.source; + +public interface RawWriteConnection extends DataSourceConnection { + + void write(byte[] bytes) throws Exception; +} diff --git a/core/src/main/java/io/xpipe/core/source/StructureDataSourceDescriptor.java b/core/src/main/java/io/xpipe/core/source/StructureDataSourceDescriptor.java index 47a2e9722..8a91d5e7d 100644 --- a/core/src/main/java/io/xpipe/core/source/StructureDataSourceDescriptor.java +++ b/core/src/main/java/io/xpipe/core/source/StructureDataSourceDescriptor.java @@ -1,27 +1,43 @@ package io.xpipe.core.source; +import io.xpipe.core.data.node.DataStructureNode; import io.xpipe.core.store.DataStore; -public abstract class StructureDataSourceDescriptor implements DataSourceDescriptor { +public interface StructureDataSourceDescriptor extends DataSourceDescriptor { - public final StructureReadConnection openReadConnection(DS store) throws Exception { + private int countEntries(DataStructureNode n) { + if (n.isValue()) { + return 1; + } + + int c = 0; + for (int i = 0; i < n.size(); i++) { + c += countEntries(n.at(i)); + } + return c; + } + + @Override + default DataSourceInfo determineInfo(DS store) throws Exception { + try (var con = openReadConnection(store)) { + var n = con.read(); + var c = countEntries(n); + return new DataSourceInfo.Structure(c); + } + } + + default StructureReadConnection openReadConnection(DS store) throws Exception { var con = newReadConnection(store); con.init(); return con; } - public final StructureWriteConnection openWriteConnection(DS store) throws Exception { + default StructureWriteConnection openWriteConnection(DS store) throws Exception { var con = newWriteConnection(store); con.init(); return con; } + StructureWriteConnection newWriteConnection(DS store); - protected abstract StructureWriteConnection newWriteConnection(DS store); - - protected abstract StructureReadConnection newReadConnection(DS store); - - @Override - public DataSourceType getType() { - return DataSourceType.STRUCTURE; - } + StructureReadConnection newReadConnection(DS store); } diff --git a/core/src/main/java/io/xpipe/core/source/TableDataSourceDescriptor.java b/core/src/main/java/io/xpipe/core/source/TableDataSourceDescriptor.java index 003903bbe..9f6089b26 100644 --- a/core/src/main/java/io/xpipe/core/source/TableDataSourceDescriptor.java +++ b/core/src/main/java/io/xpipe/core/source/TableDataSourceDescriptor.java @@ -19,9 +19,4 @@ public abstract class TableDataSourceDescriptor implements protected abstract TableWriteConnection newWriteConnection(DS store); protected abstract TableReadConnection newReadConnection(DS store); - - @Override - public DataSourceType getType() { - return DataSourceType.TABLE; - } } diff --git a/core/src/main/java/io/xpipe/core/source/TextDataSourceDescriptor.java b/core/src/main/java/io/xpipe/core/source/TextDataSourceDescriptor.java index b2a9c2734..4eca37246 100644 --- a/core/src/main/java/io/xpipe/core/source/TextDataSourceDescriptor.java +++ b/core/src/main/java/io/xpipe/core/source/TextDataSourceDescriptor.java @@ -4,9 +4,15 @@ import io.xpipe.core.store.DataStore; public abstract class TextDataSourceDescriptor implements DataSourceDescriptor { + private static final int MAX_LINE_READ = 1000; + @Override - public DataSourceType getType() { - return DataSourceType.TEXT; + public DataSourceInfo determineInfo(DS store) throws Exception { + try (var con = openReadConnection(store)) { + int count = (int) con.lines().limit(MAX_LINE_READ).count(); + int usedCount = count == MAX_LINE_READ ? -1 : count; + return new DataSourceInfo.Text(usedCount); + } } @Override diff --git a/core/src/main/java/io/xpipe/core/source/TextReadConnection.java b/core/src/main/java/io/xpipe/core/source/TextReadConnection.java index c69456e48..7081cca28 100644 --- a/core/src/main/java/io/xpipe/core/source/TextReadConnection.java +++ b/core/src/main/java/io/xpipe/core/source/TextReadConnection.java @@ -1,23 +1,31 @@ package io.xpipe.core.source; -import java.util.List; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.util.stream.Stream; public interface TextReadConnection extends DataSourceReadConnection { - /** - * Reads the complete contents. - */ - String readAll() throws Exception; - - List readAllLines() throws Exception; - - String readLine() throws Exception; - Stream lines() throws Exception; boolean isFinished() throws Exception; + default void forwardLines(OutputStream out, int maxLines) throws Exception { + if (maxLines == 0) { + return; + } + + int counter = 0; + for (var it = lines().iterator(); it.hasNext(); counter++) { + if (counter == maxLines) { + break; + } + + out.write(it.next().getBytes(StandardCharsets.UTF_8)); + out.write("\n".getBytes(StandardCharsets.UTF_8)); + } + } + default void forward(DataSourceConnection con) throws Exception { try (var tCon = (TextWriteConnection) con) { tCon.init(); diff --git a/core/src/main/java/io/xpipe/core/util/JacksonHelper.java b/core/src/main/java/io/xpipe/core/util/JacksonHelper.java index 7eb40c47e..684398682 100644 --- a/core/src/main/java/io/xpipe/core/util/JacksonHelper.java +++ b/core/src/main/java/io/xpipe/core/util/JacksonHelper.java @@ -1,6 +1,7 @@ package io.xpipe.core.util; import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; @@ -22,6 +23,7 @@ public class JacksonHelper { ObjectMapper objectMapper = INSTANCE; objectMapper.enable(SerializationFeature.INDENT_OUTPUT); objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); + objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); objectMapper.registerModules(findModules(layer)); objectMapper.setVisibility(objectMapper.getSerializationConfig().getDefaultVisibilityChecker() diff --git a/extension/build.gradle b/extension/build.gradle index 68e99dec1..bd93d5e18 100644 --- a/extension/build.gradle +++ b/extension/build.gradle @@ -17,6 +17,10 @@ apply from: "$rootDir/deps/slf4j.gradle" apply from: 'publish.gradle' apply from: "$rootDir/deps/publish-base.gradle" +configurations { + compileOnly.extendsFrom(dep) +} + version = file('../version').text group = 'io.xpipe' archivesBaseName = 'extension' diff --git a/extension/src/main/java/io/xpipe/extension/DataSourceProvider.java b/extension/src/main/java/io/xpipe/extension/DataSourceProvider.java index eb25189a2..ec195a458 100644 --- a/extension/src/main/java/io/xpipe/extension/DataSourceProvider.java +++ b/extension/src/main/java/io/xpipe/extension/DataSourceProvider.java @@ -12,31 +12,74 @@ import javafx.scene.layout.Region; import java.nio.charset.Charset; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.Function; import java.util.function.Supplier; public interface DataSourceProvider { + default String i18n(String key) { + return I18n.get(getId() + "." + key); + } + + default String i18nKey(String key) { + return getId() + "." + key; + } + + default Region createConfigOptions(DataStore input, Property> source) { + return null; + } + + default String getDisplayName() { + return i18n("displayName"); + } + + default String getDisplayImageFile() { + return "logo.png"; + } + + default String getDescription(DataSourceDescriptor source) { + return i18n("description"); + } + interface FileProvider { String getFileName(); - Map, String> getFileExtensions(); - } - - interface GuiProvider { - - Region createConfigOptions(DataStore input, Property> source); - - String getDisplayName(); - - String getDisplayImage(); - - Supplier getDescription(DataSourceDescriptor source); + Map> getFileExtensions(); } interface ConfigProvider { + static ConfigProvider empty(List names, Supplier> supplier) { + return new ConfigProvider() { + @Override + public ConfigOptionSet getConfig() { + return ConfigOptionSet.empty(); + } + + @Override + public DataSourceDescriptor toDescriptor(Map values) { + return supplier.get(); + } + + @Override + public Map toConfigOptions(DataSourceDescriptor desc) { + return Map.of(); + } + + @Override + public Map> getConverters() { + return Map.of(); + } + + @Override + public List getPossibleNames() { + return names; + } + }; + } + ConfigOption CHARSET_OPTION = new ConfigOption("Charset", "charset"); Function @@ -87,19 +130,45 @@ public interface DataSourceProvider { List getPossibleNames(); } - DataSourceType getType(); + default boolean isHidden() { + return false; + } + DataSourceType getPrimaryType(); + + /** + * Checks whether this provider prefers a certain kind of store. + * This is important for the correct autodetection of a store. + */ boolean prefersStore(DataStore store); - boolean supportsStore(DataStore store); + /** + * Checks whether this provider supports the store in principle. + * This method should not perform any further checks, + * just check whether it may be possible that the store is supported. + * + * This method will be called for validation purposes. + */ + boolean couldSupportStore(DataStore store); + + /** + * Performs a deep inspection to check whether this provider supports a given store. + * + * This functionality will be used in case no preferred provider has been found. + */ + default boolean supportsStore(DataStore store) { + return false; + } FileProvider getFileProvider(); - GuiProvider getGuiProvider(); - ConfigProvider getConfigProvider(); - String getId(); + default String getId() { + var n = getClass().getPackageName(); + var i = n.lastIndexOf('.'); + return i != -1 ? n.substring(i + 1) : n; + } DataSourceDescriptor createDefaultDescriptor(); @@ -112,4 +181,6 @@ public interface DataSourceProvider { DataSourceDescriptor createDefaultWriteDescriptor(DataStore input, DataSourceInfo info) throws Exception; Class> getDescriptorClass(); + + Optional determineDefaultName(DataStore store); } diff --git a/extension/src/main/java/io/xpipe/extension/DataSourceProviders.java b/extension/src/main/java/io/xpipe/extension/DataSourceProviders.java index fee2a2b77..f2c3960f3 100644 --- a/extension/src/main/java/io/xpipe/extension/DataSourceProviders.java +++ b/extension/src/main/java/io/xpipe/extension/DataSourceProviders.java @@ -1,12 +1,13 @@ package io.xpipe.extension; import io.xpipe.core.data.type.TupleType; -import io.xpipe.core.source.DataSourceType; -import io.xpipe.core.source.TableDataSourceDescriptor; +import io.xpipe.core.source.*; import io.xpipe.core.store.DataStore; import io.xpipe.core.store.LocalFileDataStore; -import io.xpipe.extension.event.ErrorEvent; +import io.xpipe.core.store.StreamDataStore; +import lombok.SneakyThrows; +import java.nio.charset.StandardCharsets; import java.util.Optional; import java.util.ServiceLoader; import java.util.Set; @@ -23,35 +24,54 @@ public class DataSourceProviders { } } - public static DataSourceProvider getNativeProviderForType(DataSourceType t) { - switch (t) { - case TABLE -> { - return DataSourceProviders.byId("xpbt").orElseThrow(); - } - case STRUCTURE -> { - return DataSourceProviders.byId("xpbs").orElseThrow(); - } - case TEXT -> { - return DataSourceProviders.byId("xpbx").orElseThrow(); - } - case RAW -> { - return DataSourceProviders.byId("xpbb").orElseThrow(); - } + @SuppressWarnings("unchecked") + public static DataSourceDescriptor getNativeDataSourceDescriptorForType(DataSourceType t) { + try { + return switch (t) { + case TABLE -> (DataSourceDescriptor) DataSourceProviders.byId("xpbt").orElseThrow() + .getDescriptorClass().getConstructors()[0].newInstance(); + case STRUCTURE -> (DataSourceDescriptor) DataSourceProviders.byId("xpbs").orElseThrow() + .getDescriptorClass().getConstructors()[0].newInstance(); + case TEXT -> (DataSourceDescriptor) DataSourceProviders.byId("text").orElseThrow() + .getDescriptorClass().getConstructors()[0].newInstance(StandardCharsets.UTF_8); + case RAW -> (DataSourceDescriptor) DataSourceProviders.byId("xpbr").orElseThrow() + .getDescriptorClass().getConstructors()[0].newInstance(); + }; + } catch (Exception ex) { + throw new AssertionError(ex); } - - throw new AssertionError(); } @SuppressWarnings("unchecked") + @SneakyThrows + public static StructureDataSourceDescriptor createLocalStructureDescriptor() { + return (StructureDataSourceDescriptor) + DataSourceProviders.byId("json").orElseThrow().getDescriptorClass() + .getDeclaredConstructors()[0].newInstance(); + } + + @SuppressWarnings("unchecked") + @SneakyThrows + public static RawDataSourceDescriptor createLocalRawDescriptor() { + return (RawDataSourceDescriptor) + DataSourceProviders.byId("binary").orElseThrow().getDescriptorClass() + .getDeclaredConstructors()[0].newInstance(); + } + + @SuppressWarnings("unchecked") + @SneakyThrows + public static TextDataSourceDescriptor createLocalTextDescriptor() { + return (TextDataSourceDescriptor) + DataSourceProviders.byId("text").orElseThrow().getDescriptorClass() + .getDeclaredConstructors()[0].newInstance(); + } + + @SuppressWarnings("unchecked") + @SneakyThrows public static TableDataSourceDescriptor createLocalTableDescriptor(TupleType type) { - try { - return (TableDataSourceDescriptor) - DataSourceProviders.byId("xpbt").orElseThrow().getDescriptorClass() - .getDeclaredConstructors()[0].newInstance(type); - } catch (Exception ex) { - ErrorEvent.fromThrowable(ex).terminal(true).build().handle(); - return null; - } + return (TableDataSourceDescriptor) + DataSourceProviders.byId("xpbt").orElseThrow().getDescriptorClass() + .getDeclaredConstructors()[0].newInstance(type); } public static Optional byDescriptorClass(Class clazz) { @@ -85,7 +105,7 @@ public class DataSourceProviders { } return ALL.stream().filter(d -> d.getFileProvider() != null) - .filter(d -> d.supportsStore(store)).findAny(); + .filter(d -> d.couldSupportStore(store)).findAny(); } public static Set getAll() { diff --git a/extension/src/main/java/io/xpipe/extension/SimpleFileDataSourceProvider.java b/extension/src/main/java/io/xpipe/extension/SimpleFileDataSourceProvider.java new file mode 100644 index 000000000..f3a359045 --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/SimpleFileDataSourceProvider.java @@ -0,0 +1,64 @@ +package io.xpipe.extension; + +import io.xpipe.core.store.DataStore; +import io.xpipe.core.store.FileDataStore; +import io.xpipe.core.store.StreamDataStore; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public interface SimpleFileDataSourceProvider extends DataSourceProvider { + + @Override + default Optional determineDefaultName(DataStore store) { + if (store instanceof FileDataStore l) { + var n = l.getFileName(); + var i = n.lastIndexOf('.'); + return Optional.of(i != -1 ? n.substring(0, i) : n); + } + + return Optional.empty(); + } + + @Override + default boolean prefersStore(DataStore store) { + for (var e : getSupportedExtensions().entrySet()) { + if (e.getValue() == null) { + continue; + } + + if (store instanceof FileDataStore l) { + return l.getFileName().matches("\\." + e.getValue() + "$"); + } + } + return false; + } + + @Override + default boolean couldSupportStore(DataStore store) { + return store instanceof StreamDataStore; + } + + default String getNameI18nKey() { + return i18nKey("displayName"); + } + Map> getSupportedExtensions(); + + @Override + default FileProvider getFileProvider() { + return new FileProvider() { + @Override + public String getFileName() { + return I18n.get(getNameI18nKey()); + } + + @Override + public Map> getFileExtensions() { + var map = new LinkedHashMap<>(getSupportedExtensions()); + return map; + } + }; + } +} diff --git a/extension/src/main/java/io/xpipe/extension/UniformDataSourceProvider.java b/extension/src/main/java/io/xpipe/extension/UniformDataSourceProvider.java new file mode 100644 index 000000000..aa7a41ca2 --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/UniformDataSourceProvider.java @@ -0,0 +1,35 @@ +package io.xpipe.extension; + +import io.xpipe.core.source.DataSourceDescriptor; +import io.xpipe.core.source.DataSourceInfo; +import io.xpipe.core.store.DataStore; + +import java.lang.reflect.InvocationTargetException; +import java.util.List; + +public interface UniformDataSourceProvider extends DataSourceProvider { + + @Override + default ConfigProvider getConfigProvider() { + return ConfigProvider.empty(List.of(getId()), this::createDefaultDescriptor); + } + + @Override + default DataSourceDescriptor createDefaultDescriptor() { + try { + return (DataSourceDescriptor) getDescriptorClass().getDeclaredConstructors()[0].newInstance(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new AssertionError(e); + } + } + + @Override + default DataSourceDescriptor createDefaultDescriptor(DataStore input) throws Exception { + return createDefaultDescriptor(); + } + + @Override + default DataSourceDescriptor createDefaultWriteDescriptor(DataStore input, DataSourceInfo info) throws Exception { + return createDefaultDescriptor(); + } +} diff --git a/extension/src/main/java/module-info.java b/extension/src/main/java/module-info.java index 60ec87930..e41365388 100644 --- a/extension/src/main/java/module-info.java +++ b/extension/src/main/java/module-info.java @@ -7,10 +7,10 @@ module io.xpipe.extension { exports io.xpipe.extension.event; exports io.xpipe.extension.prefs; - requires io.xpipe.core; - requires javafx.base; + requires transitive io.xpipe.core; + requires transitive javafx.base; requires javafx.graphics; - requires javafx.controls; + requires transitive javafx.controls; requires io.xpipe.fxcomps; requires org.apache.commons.collections4; requires static lombok; diff --git a/samples/sample_extension/build.gradle b/samples/sample_extension/build.gradle deleted file mode 100644 index e69de29bb..000000000 diff --git a/samples/sample_extensions/file_data_source_sample/build.gradle b/samples/sample_extensions/file_data_source_sample/build.gradle new file mode 100644 index 000000000..35438ca8d --- /dev/null +++ b/samples/sample_extensions/file_data_source_sample/build.gradle @@ -0,0 +1,13 @@ +plugins { + id 'java' + id "org.moditect.gradleplugin" version "1.0.0-rc3" +} + +apply from: "$rootDir/deps/java.gradle" +apply from: "$rootDir/deps/javafx.gradle" +apply from: "$rootDir/deps/lombok.gradle" +apply from: "$rootDir/deps/extension.gradle" + +configurations { + compileOnly.extendsFrom(dep) +} diff --git a/samples/sample_extensions/file_data_source_sample/src/main/java/io/xpipe/ext/file_data_source_sample/MyRawFileDescriptor.java b/samples/sample_extensions/file_data_source_sample/src/main/java/io/xpipe/ext/file_data_source_sample/MyRawFileDescriptor.java new file mode 100644 index 000000000..fbe46c1f3 --- /dev/null +++ b/samples/sample_extensions/file_data_source_sample/src/main/java/io/xpipe/ext/file_data_source_sample/MyRawFileDescriptor.java @@ -0,0 +1,18 @@ +package io.xpipe.ext.json; + +import io.xpipe.core.source.RawDataSourceDescriptor; +import io.xpipe.core.source.RawReadConnection; +import io.xpipe.core.source.RawWriteConnection; +import io.xpipe.core.store.StreamDataStore; + +public class MyRawFileDescriptor extends RawDataSourceDescriptor { + @Override + protected RawWriteConnection newWriteConnection(StreamDataStore store) { + return new MyRawFileWriteConnection(store); + } + + @Override + protected RawReadConnection newReadConnection(StreamDataStore store) { + return new MyRawFileReadConnection(store); + } +} diff --git a/samples/sample_extensions/file_data_source_sample/src/main/java/io/xpipe/ext/file_data_source_sample/MyRawFileProvider.java b/samples/sample_extensions/file_data_source_sample/src/main/java/io/xpipe/ext/file_data_source_sample/MyRawFileProvider.java new file mode 100644 index 000000000..2b078bfe9 --- /dev/null +++ b/samples/sample_extensions/file_data_source_sample/src/main/java/io/xpipe/ext/file_data_source_sample/MyRawFileProvider.java @@ -0,0 +1,30 @@ +package io.xpipe.ext.json; + +import io.xpipe.core.source.DataSourceDescriptor; +import io.xpipe.core.source.DataSourceType; +import io.xpipe.extension.DataSourceProvider; +import io.xpipe.extension.SimpleFileDataSourceProvider; +import io.xpipe.extension.UniformDataSourceProvider; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class MyRawFileProvider implements UniformDataSourceProvider, SimpleFileDataSourceProvider, DataSourceProvider { + + @Override + public DataSourceType getPrimaryType() { + return DataSourceType.RAW; + } + + @Override + public Map getSupportedExtensions() { + var map = new LinkedHashMap(); + map.put(i18nKey("fileName"), "myf"); + return map; + } + + @Override + public Class> getDescriptorClass() { + return MyRawFileDescriptor.class; + } +} diff --git a/samples/sample_extensions/file_data_source_sample/src/main/java/io/xpipe/ext/file_data_source_sample/MyRawFileReadConnection.java b/samples/sample_extensions/file_data_source_sample/src/main/java/io/xpipe/ext/file_data_source_sample/MyRawFileReadConnection.java new file mode 100644 index 000000000..bf91cc428 --- /dev/null +++ b/samples/sample_extensions/file_data_source_sample/src/main/java/io/xpipe/ext/file_data_source_sample/MyRawFileReadConnection.java @@ -0,0 +1,43 @@ +package io.xpipe.ext.json; + +import io.xpipe.core.source.RawReadConnection; +import io.xpipe.core.store.StreamDataStore; + +import java.io.InputStream; + +public class MyRawFileReadConnection implements RawReadConnection { + + private InputStream inputStream; + private final StreamDataStore store; + + public MyRawFileReadConnection(StreamDataStore store) { + this.store = store; + } + + @Override + public void init() throws Exception { + if (inputStream != null) { + throw new IllegalStateException("Already initialized"); + } + + inputStream = store.openInput(); + } + + @Override + public void close() throws Exception { + if (inputStream == null) { + throw new IllegalStateException("Not initialized"); + } + + inputStream.close(); + } + + @Override + public byte[] readBytes(int max) throws Exception { + if (inputStream == null) { + throw new IllegalStateException("Not initialized"); + } + + return inputStream.readNBytes(max); + } +} diff --git a/samples/sample_extensions/file_data_source_sample/src/main/java/io/xpipe/ext/file_data_source_sample/MyRawFileWriteConnection.java b/samples/sample_extensions/file_data_source_sample/src/main/java/io/xpipe/ext/file_data_source_sample/MyRawFileWriteConnection.java new file mode 100644 index 000000000..be1f21028 --- /dev/null +++ b/samples/sample_extensions/file_data_source_sample/src/main/java/io/xpipe/ext/file_data_source_sample/MyRawFileWriteConnection.java @@ -0,0 +1,43 @@ +package io.xpipe.ext.json; + +import io.xpipe.core.source.RawWriteConnection; +import io.xpipe.core.store.StreamDataStore; + +import java.io.OutputStream; + +public class MyRawFileWriteConnection implements RawWriteConnection { + + private final StreamDataStore store; + private OutputStream outputStream; + + public MyRawFileWriteConnection(StreamDataStore store) { + this.store = store; + } + + @Override + public void init() throws Exception { + if (outputStream != null) { + throw new IllegalStateException("Already initialized"); + } + + outputStream = store.openOutput(); + } + + @Override + public void close() throws Exception { + if (outputStream == null) { + throw new IllegalStateException("Not initialized"); + } + + outputStream.close(); + } + + @Override + public void write(byte[] bytes) throws Exception { + if (outputStream == null) { + throw new IllegalStateException("Not initialized"); + } + + outputStream.write(bytes); + } +} diff --git a/samples/sample_extensions/file_data_source_sample/src/main/java/module-info.java b/samples/sample_extensions/file_data_source_sample/src/main/java/module-info.java new file mode 100644 index 000000000..70aa944a9 --- /dev/null +++ b/samples/sample_extensions/file_data_source_sample/src/main/java/module-info.java @@ -0,0 +1,13 @@ +import io.xpipe.ext.json.MyRawFileProvider; +import io.xpipe.extension.DataSourceProvider; + +module io.xpipe.ext.file_data_source_sample { + exports io.xpipe.ext.json; + + opens io.xpipe.ext.json; + + requires io.xpipe.core; + requires io.xpipe.extension; + + provides DataSourceProvider with MyRawFileProvider; +} \ No newline at end of file diff --git a/samples/sample_extensions/file_data_source_sample/src/main/resources/io/xpipe/ext/file_data_source_sample/resources/img/icon.png b/samples/sample_extensions/file_data_source_sample/src/main/resources/io/xpipe/ext/file_data_source_sample/resources/img/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..135b5b83e45f10c01fc1b8bae802db3545f3dc15 GIT binary patch literal 23849 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 M**I9&S$Zb@56uXE@Bjb+ literal 0 HcmV?d00001 diff --git a/samples/sample_extensions/file_data_source_sample/src/main/resources/io/xpipe/ext/file_data_source_sample/resources/lang/translations_de.properties b/samples/sample_extensions/file_data_source_sample/src/main/resources/io/xpipe/ext/file_data_source_sample/resources/lang/translations_de.properties new file mode 100644 index 000000000..d86dcac53 --- /dev/null +++ b/samples/sample_extensions/file_data_source_sample/src/main/resources/io/xpipe/ext/file_data_source_sample/resources/lang/translations_de.properties @@ -0,0 +1,3 @@ +displayName=Mein Dateiformat +description=Meine Dateiformat-Beschreibung +fileName=Mein Dateiformat Datei \ No newline at end of file diff --git a/samples/sample_extensions/file_data_source_sample/src/main/resources/io/xpipe/ext/file_data_source_sample/resources/lang/translations_en.properties b/samples/sample_extensions/file_data_source_sample/src/main/resources/io/xpipe/ext/file_data_source_sample/resources/lang/translations_en.properties new file mode 100644 index 000000000..55e578f9e --- /dev/null +++ b/samples/sample_extensions/file_data_source_sample/src/main/resources/io/xpipe/ext/file_data_source_sample/resources/lang/translations_en.properties @@ -0,0 +1,3 @@ +displayName=My file format +description=My file format description +fileName=My file format file \ No newline at end of file