diff --git a/core/src/main/java/io/xpipe/core/data/node/ImmutableValueNode.java b/core/src/main/java/io/xpipe/core/data/node/ImmutableValueNode.java index 577915fa1..b0c77ebc2 100644 --- a/core/src/main/java/io/xpipe/core/data/node/ImmutableValueNode.java +++ b/core/src/main/java/io/xpipe/core/data/node/ImmutableValueNode.java @@ -1,18 +1,10 @@ package io.xpipe.core.data.node; -public class ImmutableValueNode extends ValueNode { - - private final byte[] data; - private final boolean textual; - - ImmutableValueNode(byte[] data, boolean textual) { - this.data = data; - this.textual = textual; - } +public abstract class ImmutableValueNode extends ValueNode { @Override public String toString(int indent) { - return (textual ? "\"" : "") + new String(data) + (textual ? "\"" : "") + " (I)"; + return (isTextual() ? "\"" : "") + asString() + (isTextual() ? "\"" : "") + " (I)"; } @Override @@ -25,16 +17,6 @@ public class ImmutableValueNode extends ValueNode { return this; } - @Override - public ValueNode mutableCopy() { - return ValueNode.mutable(data, textual); - } - - @Override - public boolean isTextual() { - return textual; - } - @Override public DataStructureNode setRaw(byte[] data) { throw new UnsupportedOperationException("Value node is immutable"); @@ -49,8 +31,4 @@ public class ImmutableValueNode extends ValueNode { public DataStructureNode set(Object newValue, boolean textual) { throw new UnsupportedOperationException("Value node is immutable"); } - - public byte[] getRawData() { - return data; - } } diff --git a/core/src/main/java/io/xpipe/core/data/node/LinkedTupleNode.java b/core/src/main/java/io/xpipe/core/data/node/LinkedTupleNode.java new file mode 100644 index 000000000..fc7bac69f --- /dev/null +++ b/core/src/main/java/io/xpipe/core/data/node/LinkedTupleNode.java @@ -0,0 +1,189 @@ +package io.xpipe.core.data.node; + +import io.xpipe.core.data.type.DataType; +import io.xpipe.core.data.type.TupleType; + +import java.util.*; +import java.util.function.Consumer; +import java.util.stream.Stream; + +public class LinkedTupleNode extends TupleNode { + + private final List tupleNodes; + private List joined; + + public LinkedTupleNode(List tupleNodes) { + this.tupleNodes = new ArrayList<>(tupleNodes); + } + + @Override + public String keyNameAt(int index) { + int list = getTupleNodeForIndex(index); + return tupleNodes.get(list).keyNameAt(getLocalIndex(list, index)); + } + + @Override + public List getKeyValuePairs() { + // Lazy initialize joined list + if (joined == null) { + this.joined = new ArrayList<>(); + for (var n : tupleNodes) { + joined.addAll(n.getKeyValuePairs()); + } + this.joined = Collections.unmodifiableList(joined); + } + + return joined; + } + + @Override + public List getKeyNames() { + return getKeyValuePairs().stream().map(KeyValue::key).toList(); + } + + @Override + public List getNodes() { + return getKeyValuePairs().stream().map(KeyValue::value).toList(); + } + + @Override + protected String getName() { + return "linked tuple node"; + } + + @Override + public boolean isMutable() { + return tupleNodes.stream().allMatch(DataStructureNode::isMutable); + } + + @Override + public DataStructureNode clear() { + tupleNodes.forEach(DataStructureNode::clear); + return this; + } + + @Override + public DataStructureNode set(int index, DataStructureNode node) { + return super.set(index, node); + } + + @Override + public DataStructureNode put(String keyName, DataStructureNode node) { + return super.put(keyName, node); + } + + @Override + public DataStructureNode remove(int index) { + return super.remove(index); + } + + @Override + public DataStructureNode remove(String keyName) { + return super.remove(keyName); + } + + @Override + public DataType determineDataType() { + return TupleType.of(getKeyNames(), getNodes().stream().map(DataStructureNode::determineDataType).toList()); + } + + @Override + public DataStructureNode at(int index) { + int list = getTupleNodeForIndex(index); + return tupleNodes.get(list).at(getLocalIndex(list, index)); + } + + @Override + public DataStructureNode forKey(String name) { + for (var ar : tupleNodes) { + var r = ar.forKeyIfPresent(name); + if (r.isPresent()) { + return r.get(); + } + } + throw new IllegalArgumentException("Invalid key " + name); + } + + @Override + public Optional forKeyIfPresent(String name) { + for (var ar : tupleNodes) { + var r = ar.forKeyIfPresent(name); + if (r.isPresent()) { + return r; + } + } + return Optional.empty(); + } + + @Override + public Stream stream() { + return getNodes().stream(); + } + + @Override + public void forEach(Consumer action) { + for (var ar : tupleNodes) { + ar.forEach(action); + } + } + + @Override + public Spliterator spliterator() { + return stream().spliterator(); + } + + @Override + public Iterator iterator() { + return stream().iterator(); + } + + @Override + public String toString() { + return "LinkedTupleNode(" + size() + ")"; + } + + + @Override + public int size() { + return tupleNodes.stream().mapToInt(TupleNode::size).sum(); + } + + private int getLocalIndex(int listIndex, int absIndex) { + int current = 0; + for (int i = 0; i < listIndex; i++) { + current += tupleNodes.get(i).size(); + } + return absIndex - current; + } + + private int getTupleNodeForIndex(int index) { + int current = 0; + for (var a : tupleNodes) { + if (index < current + a.size()) { + return tupleNodes.indexOf(a); + } else { + current += a.size(); + } + } + throw new IllegalArgumentException(); + } + + @Override + protected String getIdentifier() { + return "linked tuple node"; + } + + @Override + public TupleNode mutableCopy() { + if (isMutable()) { + return this; + } + + return new LinkedTupleNode(tupleNodes.stream().map(n -> n.isMutable() ? n : n.mutableCopy()).toList()); + } + + @Override + public TupleNode immutableView() { + return this; + } +} diff --git a/core/src/main/java/io/xpipe/core/data/node/MutableValueNode.java b/core/src/main/java/io/xpipe/core/data/node/MutableValueNode.java index ef5a39dd2..a422141ab 100644 --- a/core/src/main/java/io/xpipe/core/data/node/MutableValueNode.java +++ b/core/src/main/java/io/xpipe/core/data/node/MutableValueNode.java @@ -31,7 +31,7 @@ public class MutableValueNode extends ValueNode { @Override public ValueNode immutableView() { - return new ImmutableValueNode(data, textual); + return new SimpleImmutableValueNode(data, textual); } @Override diff --git a/core/src/main/java/io/xpipe/core/data/node/SimpleImmutableValueNode.java b/core/src/main/java/io/xpipe/core/data/node/SimpleImmutableValueNode.java new file mode 100644 index 000000000..e921aa7cd --- /dev/null +++ b/core/src/main/java/io/xpipe/core/data/node/SimpleImmutableValueNode.java @@ -0,0 +1,31 @@ +package io.xpipe.core.data.node; + +public class SimpleImmutableValueNode extends ImmutableValueNode { + + private final byte[] data; + private final boolean textual; + + SimpleImmutableValueNode(byte[] data, boolean textual) { + this.data = data; + this.textual = textual; + } + + @Override + public ValueNode mutableCopy() { + return ValueNode.mutable(data, textual); + } + + @Override + public boolean isTextual() { + return textual; + } + + public byte[] getRawData() { + return data; + } + + @Override + public final String asString() { + return new String(getRawData()); + } +} diff --git a/core/src/main/java/io/xpipe/core/data/node/ValueNode.java b/core/src/main/java/io/xpipe/core/data/node/ValueNode.java index 5ceb1b39a..85b79ef6f 100644 --- a/core/src/main/java/io/xpipe/core/data/node/ValueNode.java +++ b/core/src/main/java/io/xpipe/core/data/node/ValueNode.java @@ -30,7 +30,7 @@ public abstract class ValueNode extends DataStructureNode { public abstract ValueNode mutableCopy(); public static ValueNode immutable(byte[] data, boolean textual) { - return new ImmutableValueNode(data, textual); + return new SimpleImmutableValueNode(data, textual); } public static ValueNode immutable(Object o, boolean textual) { @@ -86,11 +86,6 @@ public abstract class ValueNode extends DataStructureNode { return Integer.parseInt(asString()); } - @Override - public final String asString() { - return new String(getRawData()); - } - @Override public final boolean isValue() { return true; diff --git a/core/src/main/java/io/xpipe/core/source/CollectionDataSourceDescriptor.java b/core/src/main/java/io/xpipe/core/source/CollectionDataSourceDescriptor.java index 72c360cbb..c212d9206 100644 --- a/core/src/main/java/io/xpipe/core/source/CollectionDataSourceDescriptor.java +++ b/core/src/main/java/io/xpipe/core/source/CollectionDataSourceDescriptor.java @@ -2,29 +2,49 @@ package io.xpipe.core.source; import io.xpipe.core.store.DataStore; -public interface CollectionDataSourceDescriptor extends DataSourceDescriptor { +import java.util.HashMap; +import java.util.Map; + +public abstract class CollectionDataSourceDescriptor extends DataSourceDescriptor { + + private final Map preferredProviders; + + public CollectionDataSourceDescriptor(DS store) { + super(store); + this.preferredProviders = new HashMap<>(); + } + + public CollectionDataSourceDescriptor annotate(String file, String provider) { + preferredProviders.put(file, provider); + return this; + } + + public CollectionDataSourceDescriptor annotate(Map preferredProviders) { + this.preferredProviders.putAll(preferredProviders); + return this; + } @Override - default DataSourceInfo determineInfo(DS store) throws Exception { - try (var con = openReadConnection(store)) { + public final DataSourceInfo determineInfo() throws Exception { + try (var con = openReadConnection()) { var c = (int) con.listEntries().count(); - return new DataSourceInfo.Structure(c); + return new DataSourceInfo.Collection(c); } } - default CollectionReadConnection openReadConnection(DS store) throws Exception { - var con = newReadConnection(store); + public final CollectionReadConnection openReadConnection() throws Exception { + var con = newReadConnection(); con.init(); return con; } - default CollectionWriteConnection openWriteConnection(DS store) throws Exception { - var con = newWriteConnection(store); + public final CollectionWriteConnection openWriteConnection() throws Exception { + var con = newWriteConnection(); con.init(); return con; } - CollectionWriteConnection newWriteConnection(DS store); + protected abstract CollectionWriteConnection newWriteConnection(); - CollectionReadConnection newReadConnection(DS store); + protected abstract CollectionReadConnection newReadConnection(); } diff --git a/core/src/main/java/io/xpipe/core/source/CollectionReadConnection.java b/core/src/main/java/io/xpipe/core/source/CollectionReadConnection.java index c98f2219c..2d368ad83 100644 --- a/core/src/main/java/io/xpipe/core/source/CollectionReadConnection.java +++ b/core/src/main/java/io/xpipe/core/source/CollectionReadConnection.java @@ -1,23 +1,22 @@ package io.xpipe.core.source; +import io.xpipe.core.store.CollectionEntryDataStore; import lombok.SneakyThrows; import java.util.stream.Stream; public interface CollectionReadConnection extends DataSourceReadConnection { - T open(String entry) throws Exception; - - Stream listEntries() 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); - } +// 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 index a2242b364..2597f5c36 100644 --- a/core/src/main/java/io/xpipe/core/source/CollectionWriteConnection.java +++ b/core/src/main/java/io/xpipe/core/source/CollectionWriteConnection.java @@ -1,6 +1,8 @@ package io.xpipe.core.source; +import java.io.InputStream; + public interface CollectionWriteConnection extends DataSourceConnection { - void write(String entry, DataSourceReadConnection con) throws Exception; + void write(String entry, InputStream content) throws Exception; } diff --git a/core/src/main/java/io/xpipe/core/source/DataSourceConnection.java b/core/src/main/java/io/xpipe/core/source/DataSourceConnection.java index 6acf3f493..c44e3dec1 100644 --- a/core/src/main/java/io/xpipe/core/source/DataSourceConnection.java +++ b/core/src/main/java/io/xpipe/core/source/DataSourceConnection.java @@ -9,5 +9,7 @@ public interface DataSourceConnection extends AutoCloseable { * Initializes this connection. Required to be called * exactly once prior to attempting to use this connection. */ - void init() throws Exception; + default void init() throws Exception {} + + default void close() 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 f18bfdf1e..1de157e02 100644 --- a/core/src/main/java/io/xpipe/core/source/DataSourceDescriptor.java +++ b/core/src/main/java/io/xpipe/core/source/DataSourceDescriptor.java @@ -10,13 +10,31 @@ import java.util.Optional; * * This instance is only valid in combination with its associated data store instance. */ -public interface DataSourceDescriptor { +public abstract class DataSourceDescriptor { + + protected DS store; + + public DataSourceDescriptor(DS store) { + this.store = store; + } + + public DataSourceDescriptor withStore(DS newStore) { + return null; + } + + /** + * Casts this instance to the required type without checking whether a cast is possible. + */ + @SuppressWarnings("unchecked") + public final > DSD asNeeded() { + return (DSD) this; + } /** * Determines on optional default name for this data store that is * used when determining a suitable default name for a data source. */ - default Optional determineDefaultName(DS store) { + public Optional determineDefaultName() { return Optional.empty(); } @@ -25,9 +43,13 @@ public interface DataSourceDescriptor { * This is usually called only once on data source * creation as this process might be expensive. */ - DataSourceInfo determineInfo(DS store) throws Exception; + public abstract DataSourceInfo determineInfo() throws Exception; - DataSourceReadConnection openReadConnection(DS store) throws Exception; + public abstract DataSourceReadConnection openReadConnection() throws Exception; - DataSourceConnection openWriteConnection(DS store) throws Exception; + public abstract DataSourceConnection openWriteConnection() throws Exception; + + public DS getStore() { + return store; + } } 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 b2c6c661c..918ee9a82 100644 --- a/core/src/main/java/io/xpipe/core/source/DataSourceInfo.java +++ b/core/src/main/java/io/xpipe/core/source/DataSourceInfo.java @@ -117,24 +117,6 @@ public abstract class DataSourceInfo { } } - - @EqualsAndHashCode(callSuper = false) - @Value - @JsonTypeName("archive") - public static class Archive extends DataSourceInfo { - int contentCount; - - @JsonCreator - public Archive(int contentCount) { - this.contentCount = contentCount; - } - - @Override - public DataSourceType getType() { - return null; - } - } - /** * Casts this instance to a table info. */ @@ -178,4 +160,15 @@ public abstract class DataSourceInfo { return (Raw) this; } + + /** + * Casts this instance to a collection info. + */ + public Collection asCollection() { + if (!getType().equals(DataSourceType.COLLECTION)) { + throw new IllegalStateException("Not a collection"); + } + + return (Collection) this; + } } diff --git a/core/src/main/java/io/xpipe/core/source/RawDataSourceDescriptor.java b/core/src/main/java/io/xpipe/core/source/RawDataSourceDescriptor.java index fe79bd783..1a5294f11 100644 --- a/core/src/main/java/io/xpipe/core/source/RawDataSourceDescriptor.java +++ b/core/src/main/java/io/xpipe/core/source/RawDataSourceDescriptor.java @@ -2,13 +2,17 @@ package io.xpipe.core.source; import io.xpipe.core.store.DataStore; -public abstract class RawDataSourceDescriptor implements DataSourceDescriptor { +public abstract class RawDataSourceDescriptor extends DataSourceDescriptor { private static final int MAX_BYTES_READ = 100000; + public RawDataSourceDescriptor(DS store) { + super(store); + } + @Override - public DataSourceInfo determineInfo(DS store) throws Exception { - try (var con = openReadConnection(store)) { + public final DataSourceInfo determineInfo() throws Exception { + try (var con = openReadConnection()) { var b = con.readBytes(MAX_BYTES_READ); int usedCount = b.length == MAX_BYTES_READ ? -1 : b.length; return new DataSourceInfo.Raw(usedCount); @@ -16,20 +20,20 @@ public abstract class RawDataSourceDescriptor implements } @Override - public RawReadConnection openReadConnection(DS store) throws Exception { - var con = newReadConnection(store); + public final RawReadConnection openReadConnection() throws Exception { + var con = newReadConnection(); con.init(); return con; } @Override - public RawWriteConnection openWriteConnection(DS store) throws Exception { - var con = newWriteConnection(store); + public final RawWriteConnection openWriteConnection() throws Exception { + var con = newWriteConnection(); con.init(); return con; } - protected abstract RawWriteConnection newWriteConnection(DS store); + protected abstract RawWriteConnection newWriteConnection(); - protected abstract RawReadConnection newReadConnection(DS store); + protected abstract RawReadConnection newReadConnection(); } 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 8a91d5e7d..c6f6bf92c 100644 --- a/core/src/main/java/io/xpipe/core/source/StructureDataSourceDescriptor.java +++ b/core/src/main/java/io/xpipe/core/source/StructureDataSourceDescriptor.java @@ -3,7 +3,11 @@ package io.xpipe.core.source; import io.xpipe.core.data.node.DataStructureNode; import io.xpipe.core.store.DataStore; -public interface StructureDataSourceDescriptor extends DataSourceDescriptor { +public abstract class StructureDataSourceDescriptor extends DataSourceDescriptor { + + public StructureDataSourceDescriptor(DS store) { + super(store); + } private int countEntries(DataStructureNode n) { if (n.isValue()) { @@ -18,26 +22,27 @@ public interface StructureDataSourceDescriptor extends Dat } @Override - default DataSourceInfo determineInfo(DS store) throws Exception { - try (var con = openReadConnection(store)) { + public final DataSourceInfo determineInfo() throws Exception { + try (var con = openReadConnection()) { var n = con.read(); var c = countEntries(n); return new DataSourceInfo.Structure(c); } } - default StructureReadConnection openReadConnection(DS store) throws Exception { - var con = newReadConnection(store); + public final StructureReadConnection openReadConnection() throws Exception { + var con = newReadConnection(); con.init(); return con; } - default StructureWriteConnection openWriteConnection(DS store) throws Exception { - var con = newWriteConnection(store); + public final StructureWriteConnection openWriteConnection() throws Exception { + var con = newWriteConnection(); con.init(); return con; } - StructureWriteConnection newWriteConnection(DS store); - StructureReadConnection newReadConnection(DS store); + protected abstract StructureWriteConnection newWriteConnection(); + + protected abstract StructureReadConnection newReadConnection(); } 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 9f6089b26..1a77b9d88 100644 --- a/core/src/main/java/io/xpipe/core/source/TableDataSourceDescriptor.java +++ b/core/src/main/java/io/xpipe/core/source/TableDataSourceDescriptor.java @@ -2,21 +2,34 @@ package io.xpipe.core.source; import io.xpipe.core.store.DataStore; -public abstract class TableDataSourceDescriptor implements DataSourceDescriptor { +public abstract class TableDataSourceDescriptor extends DataSourceDescriptor { - public final TableReadConnection openReadConnection(DS store) throws Exception { - var con = newReadConnection(store); + public TableDataSourceDescriptor(DS store) { + super(store); + } + + @Override + public final DataSourceInfo determineInfo() throws Exception { + try (var con = openReadConnection()) { + var dataType = con.getDataType(); + var rowCount = con.getRowCount(); + return new DataSourceInfo.Table(dataType, rowCount); + } + } + + public final TableReadConnection openReadConnection() throws Exception { + var con = newReadConnection(); con.init(); return con; } - public final TableWriteConnection openWriteConnection(DS store) throws Exception { - var con = newWriteConnection(store); + public final TableWriteConnection openWriteConnection() throws Exception { + var con = newWriteConnection(); con.init(); return con; } - protected abstract TableWriteConnection newWriteConnection(DS store); + protected abstract TableWriteConnection newWriteConnection(); - protected abstract TableReadConnection newReadConnection(DS store); + protected abstract TableReadConnection newReadConnection(); } 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 4eca37246..7f3c1f3c0 100644 --- a/core/src/main/java/io/xpipe/core/source/TextDataSourceDescriptor.java +++ b/core/src/main/java/io/xpipe/core/source/TextDataSourceDescriptor.java @@ -2,13 +2,17 @@ package io.xpipe.core.source; import io.xpipe.core.store.DataStore; -public abstract class TextDataSourceDescriptor implements DataSourceDescriptor { +public abstract class TextDataSourceDescriptor extends DataSourceDescriptor { private static final int MAX_LINE_READ = 1000; + public TextDataSourceDescriptor(DS store) { + super(store); + } + @Override - public DataSourceInfo determineInfo(DS store) throws Exception { - try (var con = openReadConnection(store)) { + public final DataSourceInfo determineInfo() throws Exception { + try (var con = openReadConnection()) { int count = (int) con.lines().limit(MAX_LINE_READ).count(); int usedCount = count == MAX_LINE_READ ? -1 : count; return new DataSourceInfo.Text(usedCount); @@ -16,20 +20,20 @@ public abstract class TextDataSourceDescriptor implements } @Override - public TextReadConnection openReadConnection(DS store) throws Exception { - var con = newReadConnection(store); + public final TextReadConnection openReadConnection() throws Exception { + var con = newReadConnection(); con.init(); return con; } @Override - public TextWriteConnection openWriteConnection(DS store) throws Exception { - var con = newWriteConnection(store); + public final TextWriteConnection openWriteConnection() throws Exception { + var con = newWriteConnection(); con.init(); return con; } - protected abstract TextWriteConnection newWriteConnection(DS store); + protected abstract TextWriteConnection newWriteConnection(); - protected abstract TextReadConnection newReadConnection(DS store); + protected abstract TextReadConnection newReadConnection(); } diff --git a/core/src/main/java/io/xpipe/core/store/CollectionEntryDataStore.java b/core/src/main/java/io/xpipe/core/store/CollectionEntryDataStore.java new file mode 100644 index 000000000..45f36252a --- /dev/null +++ b/core/src/main/java/io/xpipe/core/store/CollectionEntryDataStore.java @@ -0,0 +1,36 @@ +package io.xpipe.core.store; + +public abstract class CollectionEntryDataStore implements FileDataStore { + + private final boolean directory; + private final String name; + private final DataStore collectionStore; + + + public CollectionEntryDataStore(boolean directory, String name, DataStore collectionStore) { + this.directory = directory; + this.name = name; + this.collectionStore = collectionStore; + } + + @Override + public String getFileName() { + return name; + } + + public boolean isDirectory() { + return directory; + } + + public DataStore getCollectionStore() { + return collectionStore; + } + + public String getName() { + return name; + } + + public String getPreferredProvider() { + return null; + } +} 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 ae70f1fb3..ae529ce95 100644 --- a/core/src/main/java/io/xpipe/core/store/FileDataStore.java +++ b/core/src/main/java/io/xpipe/core/store/FileDataStore.java @@ -1,6 +1,14 @@ package io.xpipe.core.store; +import java.util.Optional; + public interface FileDataStore extends StreamDataStore { + @Override + default Optional determineDefaultName() { + var n = getFileName(); + var i = n.lastIndexOf('.'); + return Optional.of(i != -1 ? n.substring(0, i) : n); + } String getFileName(); } diff --git a/core/src/main/java/io/xpipe/core/store/LocalDirectoryDataStore.java b/core/src/main/java/io/xpipe/core/store/LocalDirectoryDataStore.java new file mode 100644 index 000000000..c0f075b67 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/store/LocalDirectoryDataStore.java @@ -0,0 +1,43 @@ +package io.xpipe.core.store; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonTypeName; +import lombok.EqualsAndHashCode; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import java.util.Optional; + +@JsonTypeName("localDir") +@EqualsAndHashCode +public class LocalDirectoryDataStore implements DataStore { + + private final Path file; + + @JsonCreator + public LocalDirectoryDataStore(Path file) { + this.file = file; + } + + @Override + public Optional determineDefaultName() { + return Optional.of(file.getFileName().toString()); + } + + @Override + public Optional determineLastModified() { + try { + var l = Files.getLastModifiedTime(file); + return Optional.of(l.toInstant()); + } catch (IOException e) { + return Optional.empty(); + } + } + + public Path getPath() { + return file; + } + +} 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 43365b454..8503cb40b 100644 --- a/core/src/main/java/io/xpipe/core/store/LocalFileDataStore.java +++ b/core/src/main/java/io/xpipe/core/store/LocalFileDataStore.java @@ -24,9 +24,8 @@ public class LocalFileDataStore implements FileDataStore { this.file = file; } - @Override - public Optional determineDefaultName() { - return Optional.of(file.getFileName().toString()); + public String toString() { + return getFileName(); } @Override 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 99d242c42..852c4bb5a 100644 --- a/core/src/main/java/io/xpipe/core/store/StreamDataStore.java +++ b/core/src/main/java/io/xpipe/core/store/StreamDataStore.java @@ -34,12 +34,16 @@ public interface StreamDataStore extends DataStore { /** * Opens an input stream. This input stream does not necessarily have to be a new instance. */ - InputStream openInput() throws Exception; + default InputStream openInput() throws Exception { + throw new UnsupportedOperationException("Can't open store input"); + } /** * Opens an output stream. This output stream does not necessarily have to be a new instance. */ - OutputStream openOutput() throws Exception; + default OutputStream openOutput() throws Exception { + throw new UnsupportedOperationException("Can't open store output"); + } boolean exists(); } diff --git a/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java b/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java index e056f49a2..cd3f87caa 100644 --- a/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java +++ b/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java @@ -17,6 +17,8 @@ import io.xpipe.core.data.type.ValueType; import io.xpipe.core.data.type.WildcardType; import io.xpipe.core.source.DataSourceInfo; import io.xpipe.core.source.DataSourceReference; +import io.xpipe.core.store.CollectionEntryDataStore; +import io.xpipe.core.store.LocalDirectoryDataStore; import io.xpipe.core.store.LocalFileDataStore; import java.io.IOException; @@ -29,6 +31,8 @@ public class CoreJacksonModule extends SimpleModule { public void setupModule(SetupContext context) { context.registerSubtypes( new NamedType(LocalFileDataStore.class), + new NamedType(LocalDirectoryDataStore.class), + new NamedType(CollectionEntryDataStore.class), new NamedType(ValueType.class), new NamedType(TupleType.class), new NamedType(ArrayType.class), @@ -36,6 +40,7 @@ public class CoreJacksonModule extends SimpleModule { new NamedType(DataSourceInfo.Table.class), new NamedType(DataSourceInfo.Structure.class), new NamedType(DataSourceInfo.Text.class), + new NamedType(DataSourceInfo.Collection.class), new NamedType(DataSourceInfo.Raw.class) ); diff --git a/extension/src/main/java/io/xpipe/extension/DataSourceProvider.java b/extension/src/main/java/io/xpipe/extension/DataSourceProvider.java index ec195a458..227797706 100644 --- a/extension/src/main/java/io/xpipe/extension/DataSourceProvider.java +++ b/extension/src/main/java/io/xpipe/extension/DataSourceProvider.java @@ -3,20 +3,42 @@ package io.xpipe.extension; import io.xpipe.core.config.ConfigOption; import io.xpipe.core.config.ConfigOptionSet; import io.xpipe.core.source.DataSourceDescriptor; -import io.xpipe.core.source.DataSourceInfo; import io.xpipe.core.source.DataSourceType; import io.xpipe.core.store.DataStore; import javafx.beans.property.Property; import javafx.scene.layout.Region; import java.nio.charset.Charset; +import java.util.Arrays; 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 { +public interface DataSourceProvider> { + + static enum GeneralType { + FILE, + DATABASE; + } + + default GeneralType getGeneralType() { + if (getFileProvider() != null) { + return GeneralType.FILE; + } + + throw new ExtensionException("Provider has no general type"); + } + + default boolean supportsConversion(T in, DataSourceType t) { + return false; + } + + default DataSourceDescriptor convert(T in, DataSourceType t) throws Exception { + throw new ExtensionException(); + } + + default void init() { + } default String i18n(String key) { return I18n.get(getId() + "." + key); @@ -34,12 +56,16 @@ public interface DataSourceProvider { return i18n("displayName"); } - default String getDisplayImageFile() { - return "logo.png"; + default String getDisplayDescription() { + return i18n("displayDescription"); } - default String getDescription(DataSourceDescriptor source) { - return i18n("description"); + default String getDisplayIconFileName() { + return getId() + ":icon.png"; + } + + default String getSourceDescription(T source) { + return getDisplayName(); } interface FileProvider { @@ -49,22 +75,22 @@ public interface DataSourceProvider { Map> getFileExtensions(); } - interface ConfigProvider { + interface ConfigProvider> { - static ConfigProvider empty(List names, Supplier> supplier) { - return new ConfigProvider() { + static > ConfigProvider empty(List names, Function func) { + return new ConfigProvider<>() { @Override public ConfigOptionSet getConfig() { return ConfigOptionSet.empty(); } @Override - public DataSourceDescriptor toDescriptor(Map values) { - return supplier.get(); + public T toDescriptor(DataStore store, Map values) { + return func.apply(store); } @Override - public Map toConfigOptions(DataSourceDescriptor desc) { + public Map toConfigOptions(T source) { return Map.of(); } @@ -121,9 +147,9 @@ public interface DataSourceProvider { ConfigOptionSet getConfig(); - DataSourceDescriptor toDescriptor(Map values); + T toDescriptor(DataStore store, Map values); - Map toConfigOptions(DataSourceDescriptor desc); + Map toConfigOptions(T desc); Map> getConverters(); @@ -160,9 +186,15 @@ public interface DataSourceProvider { return false; } - FileProvider getFileProvider(); + default FileProvider getFileProvider() { + return null; + } - ConfigProvider getConfigProvider(); + default boolean hasDirectoryProvider() { + return false; + } + + ConfigProvider getConfigProvider(); default String getId() { var n = getClass().getPackageName(); @@ -170,17 +202,20 @@ public interface DataSourceProvider { return i != -1 ? n.substring(i + 1) : n; } - DataSourceDescriptor createDefaultDescriptor(); - /** * 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. */ - DataSourceDescriptor createDefaultDescriptor(DataStore input) throws Exception; + T createDefaultDescriptor(DataStore input) throws Exception; - DataSourceDescriptor createDefaultWriteDescriptor(DataStore input, DataSourceInfo info) throws Exception; + default T createDefaultWriteDescriptor(DataStore input) throws Exception { + return createDefaultDescriptor(input); + } - Class> getDescriptorClass(); - - Optional determineDefaultName(DataStore store); + @SuppressWarnings("unchecked") + default Class getDescriptorClass() { + return (Class) Arrays.stream(getClass().getDeclaredClasses()) + .filter(c -> c.getName().endsWith("Descriptor")).findFirst() + .orElseThrow(() -> new AssertionError("Descriptor class not found")); + } } diff --git a/extension/src/main/java/io/xpipe/extension/DataSourceProviders.java b/extension/src/main/java/io/xpipe/extension/DataSourceProviders.java index f2c3960f3..bbdff7047 100644 --- a/extension/src/main/java/io/xpipe/extension/DataSourceProviders.java +++ b/extension/src/main/java/io/xpipe/extension/DataSourceProviders.java @@ -1,13 +1,10 @@ package io.xpipe.extension; -import io.xpipe.core.data.type.TupleType; import io.xpipe.core.source.*; import io.xpipe.core.store.DataStore; import io.xpipe.core.store.LocalFileDataStore; -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; @@ -15,27 +12,25 @@ import java.util.stream.Collectors; public class DataSourceProviders { - private static Set ALL; + private static Set> ALL; public static void init(ModuleLayer layer) { if (ALL == null) { ALL = ServiceLoader.load(layer, DataSourceProvider.class).stream() - .map(ServiceLoader.Provider::get).collect(Collectors.toSet()); + .map(p -> (DataSourceProvider) p.get()).collect(Collectors.toSet()); + ALL.forEach(DataSourceProvider::init); } } - @SuppressWarnings("unchecked") - public static DataSourceDescriptor getNativeDataSourceDescriptorForType(DataSourceType t) { + public static DataSourceProvider 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(); + case TABLE -> DataSourceProviders.byId("xpbt"); + case STRUCTURE -> DataSourceProviders.byId("xpbs"); + case TEXT -> DataSourceProviders.byId("text"); + case RAW -> DataSourceProviders.byId("binary"); + //TODO + case COLLECTION -> null; }; } catch (Exception ex) { throw new AssertionError(ex); @@ -44,53 +39,66 @@ public class DataSourceProviders { @SuppressWarnings("unchecked") @SneakyThrows - public static StructureDataSourceDescriptor createLocalStructureDescriptor() { + public static StructureDataSourceDescriptor createLocalStructureDescriptor(DataStore store) { return (StructureDataSourceDescriptor) - DataSourceProviders.byId("json").orElseThrow().getDescriptorClass() - .getDeclaredConstructors()[0].newInstance(); + DataSourceProviders.byId("xpbs").getDescriptorClass() + .getDeclaredConstructors()[0].newInstance(store); } @SuppressWarnings("unchecked") @SneakyThrows - public static RawDataSourceDescriptor createLocalRawDescriptor() { + public static RawDataSourceDescriptor createLocalRawDescriptor(DataStore store) { return (RawDataSourceDescriptor) - DataSourceProviders.byId("binary").orElseThrow().getDescriptorClass() - .getDeclaredConstructors()[0].newInstance(); + DataSourceProviders.byId("binary").getDescriptorClass() + .getDeclaredConstructors()[0].newInstance(store); } @SuppressWarnings("unchecked") @SneakyThrows - public static TextDataSourceDescriptor createLocalTextDescriptor() { + public static RawDataSourceDescriptor createLocalCollectionDescriptor(DataStore store) { + return (RawDataSourceDescriptor) + DataSourceProviders.byId("br").getDescriptorClass() + .getDeclaredConstructors()[0].newInstance(store); + } + + @SuppressWarnings("unchecked") + @SneakyThrows + public static TextDataSourceDescriptor createLocalTextDescriptor(DataStore store) { return (TextDataSourceDescriptor) - DataSourceProviders.byId("text").orElseThrow().getDescriptorClass() - .getDeclaredConstructors()[0].newInstance(); + DataSourceProviders.byId("text").getDescriptorClass() + .getDeclaredConstructors()[0].newInstance(store); } @SuppressWarnings("unchecked") @SneakyThrows - public static TableDataSourceDescriptor createLocalTableDescriptor(TupleType type) { + public static TableDataSourceDescriptor createLocalTableDescriptor(DataStore store) { return (TableDataSourceDescriptor) - DataSourceProviders.byId("xpbt").orElseThrow().getDescriptorClass() - .getDeclaredConstructors()[0].newInstance(type); + DataSourceProviders.byId("xpbt").getDescriptorClass() + .getDeclaredConstructors()[0].newInstance(store); } - public static Optional byDescriptorClass(Class clazz) { + @SuppressWarnings("unchecked") + public static > T byId(String name) { if (ALL == null) { throw new IllegalStateException("Not initialized"); } - return ALL.stream().filter(d -> d.getDescriptorClass().equals(clazz)).findAny(); + return (T) ALL.stream().filter(d -> d.getId().equals(name)).findAny() + .orElseThrow(() -> new IllegalArgumentException("Provider " + name + " not found")); } - public static Optional byId(String name) { + + @SuppressWarnings("unchecked") + public static , T extends DataSourceProvider> T byDataSourceClass(Class c) { if (ALL == null) { throw new IllegalStateException("Not initialized"); } - return ALL.stream().filter(d -> d.getId().equals(name)).findAny(); + return (T) ALL.stream().filter(d -> d.getDescriptorClass().equals(c)).findAny() + .orElseThrow(() -> new IllegalArgumentException("Provider for " + c.getSimpleName() + " not found")); } - public static Optional byName(String name) { + public static Optional> byName(String name) { if (ALL == null) { throw new IllegalStateException("Not initialized"); } @@ -99,7 +107,7 @@ public class DataSourceProviders { .anyMatch(s -> s.equalsIgnoreCase(name))).findAny(); } - public static Optional byStore(DataStore store) { + public static Optional> byStore(DataStore store) { if (ALL == null) { throw new IllegalStateException("Not initialized"); } @@ -108,7 +116,7 @@ public class DataSourceProviders { .filter(d -> d.couldSupportStore(store)).findAny(); } - public static Set getAll() { + public static Set> getAll() { return ALL; } } diff --git a/extension/src/main/java/io/xpipe/extension/ExtensionException.java b/extension/src/main/java/io/xpipe/extension/ExtensionException.java new file mode 100644 index 000000000..fab3705c3 --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/ExtensionException.java @@ -0,0 +1,23 @@ +package io.xpipe.extension; + +public class ExtensionException extends RuntimeException { + + public ExtensionException() { + } + + public ExtensionException(String message) { + super(message); + } + + public ExtensionException(String message, Throwable cause) { + super(message, cause); + } + + public ExtensionException(Throwable cause) { + super(cause); + } + + public ExtensionException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/extension/src/main/java/io/xpipe/extension/SimpleFileDataSourceProvider.java b/extension/src/main/java/io/xpipe/extension/SimpleFileDataSourceProvider.java index f3a359045..cb8b34e3d 100644 --- a/extension/src/main/java/io/xpipe/extension/SimpleFileDataSourceProvider.java +++ b/extension/src/main/java/io/xpipe/extension/SimpleFileDataSourceProvider.java @@ -1,5 +1,6 @@ package io.xpipe.extension; +import io.xpipe.core.source.DataSourceDescriptor; import io.xpipe.core.store.DataStore; import io.xpipe.core.store.FileDataStore; import io.xpipe.core.store.StreamDataStore; @@ -7,20 +8,8 @@ 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(); - } +public interface SimpleFileDataSourceProvider> extends DataSourceProvider { @Override default boolean prefersStore(DataStore store) { diff --git a/extension/src/main/java/io/xpipe/extension/UniformDataSourceProvider.java b/extension/src/main/java/io/xpipe/extension/UniformDataSourceProvider.java index aa7a41ca2..d7d6780ae 100644 --- a/extension/src/main/java/io/xpipe/extension/UniformDataSourceProvider.java +++ b/extension/src/main/java/io/xpipe/extension/UniformDataSourceProvider.java @@ -1,35 +1,25 @@ 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 { +public interface UniformDataSourceProvider> extends DataSourceProvider { @Override - default ConfigProvider getConfigProvider() { + default ConfigProvider getConfigProvider() { return ConfigProvider.empty(List.of(getId()), this::createDefaultDescriptor); } @Override - default DataSourceDescriptor createDefaultDescriptor() { + @SuppressWarnings("unchecked") + default T createDefaultDescriptor(DataStore input) { try { - return (DataSourceDescriptor) getDescriptorClass().getDeclaredConstructors()[0].newInstance(); + return (T) getDescriptorClass().getDeclaredConstructors()[0].newInstance(input); } 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/io/xpipe/extension/comp/DynamicOptionsBuilder.java b/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsBuilder.java new file mode 100644 index 000000000..b0bbadd01 --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsBuilder.java @@ -0,0 +1,33 @@ +package io.xpipe.extension.comp; + +import io.xpipe.core.source.DataSourceDescriptor; +import io.xpipe.fxcomps.Comp; +import javafx.beans.Observable; +import javafx.beans.binding.Bindings; +import javafx.beans.property.Property; +import javafx.beans.value.ObservableValue; +import javafx.scene.control.TextField; +import javafx.scene.layout.Region; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +public class DynamicOptionsBuilder> { + + private final List entries = new ArrayList<>(); + private final List> props = new ArrayList<>(); + + public DynamicOptionsBuilder addText(ObservableValue name, Property prop) { + var comp = new TextField(); + comp.textProperty().bindBidirectional(prop); + entries.add(new DynamicOptionsComp.Entry(name, Comp.of(() -> comp))); + return this; + } + + public Region build(Supplier creator, Property toBind) { + var bind = Bindings.createObjectBinding(() -> creator.get(), props.toArray(Observable[]::new)); + toBind.bind(bind); + return new DynamicOptionsComp(entries).createRegion(); + } +} diff --git a/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsComp.java b/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsComp.java index 8a36adf56..ba20fd93a 100644 --- a/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsComp.java +++ b/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsComp.java @@ -4,6 +4,7 @@ import io.xpipe.fxcomps.Comp; import io.xpipe.fxcomps.CompStructure; import javafx.beans.Observable; import javafx.beans.binding.Bindings; +import javafx.beans.value.ObservableValue; import javafx.geometry.Orientation; import javafx.geometry.Pos; import javafx.scene.control.Label; @@ -13,7 +14,6 @@ import javafx.scene.layout.Region; import java.util.ArrayList; import java.util.List; -import java.util.function.Supplier; public class DynamicOptionsComp extends Comp> { @@ -37,7 +37,8 @@ public class DynamicOptionsComp extends Comp> { var line = new HBox(); line.setSpacing(5); - var name = new Label(entry.name().get()); + var name = new Label(); + name.textProperty().bind(entry.name()); name.prefHeightProperty().bind(line.heightProperty()); name.setMinWidth(Region.USE_PREF_SIZE); name.setAlignment(Pos.CENTER_LEFT); @@ -78,7 +79,7 @@ public class DynamicOptionsComp extends Comp> { return entries; } - public static record Entry(Supplier name, Comp comp) { + public static record Entry(ObservableValue name, Comp comp) { } } diff --git a/samples/sample_extensions/file_data_source_sample/build.gradle b/samples/sample_extensions/file_data_source_sample/build.gradle index 35438ca8d..ce10adc05 100644 --- a/samples/sample_extensions/file_data_source_sample/build.gradle +++ b/samples/sample_extensions/file_data_source_sample/build.gradle @@ -3,8 +3,6 @@ plugins { 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"