diff --git a/.gitignore b/.gitignore index dbca16b9b..bb66ac789 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ build/ .idea dev.properties extensions.txt -local/ \ No newline at end of file +local/ +.vscode +bin \ No newline at end of file diff --git a/api/build.gradle b/api/build.gradle index 73c97d0c2..2cd5969fe 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -7,6 +7,7 @@ plugins { apply from: "$rootDir/deps/java.gradle" apply from: "$rootDir/deps/junit.gradle" +apply from: "$rootDir/deps/jackson.gradle" apply from: 'publish.gradle' apply from: "$rootDir/deps/publish-base.gradle" @@ -19,18 +20,18 @@ repositories { } test { - enabled = false + enabled = true } dependencies { - // Fix warnings about missing annotations - compileOnly group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.13.0" - testCompileOnly group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.13.0" - implementation project(':core') implementation project(':beacon') } +configurations { + testImplementation.extendsFrom(dep) +} + def canTestWithDev = findProject(':app') != null def home = System.getenv('XPIPE_HOME') String daemonCommand = canTestWithDev ? @@ -52,7 +53,7 @@ test { " -Dio.xpipe.app.logLevel=trace" // API properties - // systemProperty 'io.xpipe.beacon.debugOutput', "true" + systemProperty 'io.xpipe.beacon.debugOutput', "true" systemProperty 'io.xpipe.beacon.debugExecOutput', "true" systemProperty "io.xpipe.beacon.port", "21722" } diff --git a/api/src/main/java/io/xpipe/api/DataSource.java b/api/src/main/java/io/xpipe/api/DataSource.java index 393d17efb..0130110f7 100644 --- a/api/src/main/java/io/xpipe/api/DataSource.java +++ b/api/src/main/java/io/xpipe/api/DataSource.java @@ -1,7 +1,9 @@ package io.xpipe.api; import io.xpipe.api.impl.DataSourceImpl; -import io.xpipe.core.source.*; +import io.xpipe.core.source.DataSourceId; +import io.xpipe.core.source.DataSourceReference; +import io.xpipe.core.source.DataSourceType; import java.io.IOException; import java.io.InputStream; @@ -9,7 +11,6 @@ import java.io.UncheckedIOException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Map; /** * Represents a reference to a data source that is managed by X-Pipe. @@ -85,36 +86,36 @@ public interface DataSource { } /** - * Wrapper for {@link #create(DataSourceId, String, Map, InputStream)} that creates an anonymous data source. + * Wrapper for {@link #create(DataSourceId, String, InputStream)} that creates an anonymous data source. */ - public static DataSource createAnonymous(String type, Map config, Path path) { - return create(null, type, config, path); + public static DataSource createAnonymous(String type, Path path) { + return create(null, type, path); } /** - * Wrapper for {@link #create(DataSourceId, String, Map, InputStream)}. + * Wrapper for {@link #create(DataSourceId, String, InputStream)}. */ - public static DataSource create(DataSourceId id, String type, Map config, Path path) { + public static DataSource create(DataSourceId id, String type, Path path) { try (var in = Files.newInputStream(path)) { - return create(id, type, config, in); + return create(id, type, in); } catch (IOException e) { throw new UncheckedIOException(e); } } /** - * Wrapper for {@link #create(DataSourceId, String, Map, InputStream)} that creates an anonymous data source. + * Wrapper for {@link #create(DataSourceId, String, InputStream)} that creates an anonymous data source. */ - public static DataSource createAnonymous(String type, Map config, URL url) { - return create(null, type, config, url); + public static DataSource createAnonymous(String type, URL url) { + return create(null, type, url); } /** - * Wrapper for {@link #create(DataSourceId, String, Map, InputStream)}. + * Wrapper for {@link #create(DataSourceId, String, InputStream)}. */ - public static DataSource create(DataSourceId id, String type, Map config, URL url) { + public static DataSource create(DataSourceId id, String type, URL url) { try (var in = url.openStream()) { - return create(id, type, config, in); + return create(id, type, in); } catch (IOException e) { throw new UncheckedIOException(e); } @@ -122,10 +123,10 @@ public interface DataSource { /** - * Wrapper for {@link #create(DataSourceId, String, Map, InputStream)} that creates an anonymous data source. + * Wrapper for {@link #create(DataSourceId, String, InputStream)} that creates an anonymous data source. */ - public static DataSource createAnonymous(String type, Map config, InputStream in) { - return create(null, type, config, in); + public static DataSource createAnonymous(String type, InputStream in) { + return create(null, type, in); } /** @@ -133,12 +134,11 @@ public interface DataSource { * * @param id the data source id * @param type the data source type - * @param config additional configuration options for the specific data source type * @param in the input stream to read * @return a {@link DataSource} instances that can be used to access the underlying data */ - public static DataSource create(DataSourceId id, String type, Map config, InputStream in) { - return DataSourceImpl.create(id, type, config, in); + public static DataSource create(DataSourceId id, String type, InputStream in) { + return DataSourceImpl.create(id, type, in); } /** diff --git a/api/src/main/java/io/xpipe/api/connector/XPipeConnection.java b/api/src/main/java/io/xpipe/api/connector/XPipeConnection.java index b5bc5911f..428828268 100644 --- a/api/src/main/java/io/xpipe/api/connector/XPipeConnection.java +++ b/api/src/main/java/io/xpipe/api/connector/XPipeConnection.java @@ -1,6 +1,8 @@ package io.xpipe.api.connector; import io.xpipe.beacon.*; +import io.xpipe.beacon.exchange.cli.DialogExchange; +import io.xpipe.core.dialog.DialogReference; import io.xpipe.core.util.JacksonHelper; import java.util.Optional; @@ -13,6 +15,22 @@ public final class XPipeConnection extends BeaconConnection { return con; } + public static void finishDialog(DialogReference reference) { + try (var con = new XPipeConnection()) { + con.constructSocket(); + while (true) { + DialogExchange.Response response = con.performSimpleExchange(DialogExchange.Request.builder().dialogKey(reference.getDialogId()).build()); + if (response.getElement() == null) { + break; + } + } + } catch (BeaconException e) { + throw e; + } catch (Exception e) { + throw new BeaconException(e); + } + } + public static void execute(Handler handler) { try (var con = new XPipeConnection()) { con.constructSocket(); @@ -74,7 +92,7 @@ public final class XPipeConnection extends BeaconConnection { } public static Optional waitForStartup() { - for (int i = 0; i < 40; i++) { + for (int i = 0; i < 80; i++) { try { Thread.sleep(500); } catch (InterruptedException ignored) { 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 0b6e3dbd4..825006504 100644 --- a/api/src/main/java/io/xpipe/api/impl/DataSourceImpl.java +++ b/api/src/main/java/io/xpipe/api/impl/DataSourceImpl.java @@ -10,7 +10,6 @@ import io.xpipe.core.source.DataSourceId; import io.xpipe.core.source.DataSourceReference; import java.io.InputStream; -import java.util.Map; public abstract class DataSourceImpl implements DataSource { @@ -19,29 +18,30 @@ public abstract class DataSourceImpl implements DataSource { var req = QueryDataSourceExchange.Request.builder().ref(ds).build(); QueryDataSourceExchange.Response res = con.performSimpleExchange(req); var config = new DataSourceConfig(res.getProvider(), res.getConfig()); - switch (res.getInfo().getType()) { + return switch (res.getInfo().getType()) { case TABLE -> { var data = res.getInfo().asTable(); - return new DataTableImpl(res.getId(), config, data); + yield new DataTableImpl(res.getId(), config, data); } case STRUCTURE -> { var info = res.getInfo().asStructure(); - return new DataStructureImpl(res.getId(), config, info); + yield new DataStructureImpl(res.getId(), config, info); } case TEXT -> { var info = res.getInfo().asText(); - return new DataTextImpl(res.getId(), config, info); + yield new DataTextImpl(res.getId(), config, info); } case RAW -> { var info = res.getInfo().asRaw(); - return new DataRawImpl(res.getId(), config, info); + yield new DataRawImpl(res.getId(), config, info); } - } - throw new AssertionError(); + case COLLECTION -> throw new UnsupportedOperationException("Unimplemented case: " + res.getInfo().getType()); + default -> throw new IllegalArgumentException("Unexpected value: " + res.getInfo().getType()); + }; }); } - public static DataSource create(DataSourceId id, String type, Map config, InputStream in) { + public static DataSource create(DataSourceId id, String type, InputStream in) { var res = XPipeConnection.execute(con -> { var req = StoreStreamExchange.Request.builder().build(); StoreStreamExchange.Response r = con.performOutputExchange(req, out -> in.transferTo(out)); @@ -53,6 +53,8 @@ public abstract class DataSourceImpl implements DataSource { var startReq = ReadExchange.Request.builder() .provider(type) .store(store) + .target(id) + .configureAll(false) .build(); var startRes = XPipeConnection.execute(con -> { ReadExchange.Response r = con.performSimpleExchange(startReq); @@ -60,13 +62,8 @@ public abstract class DataSourceImpl implements DataSource { }); var configInstance = startRes.getConfig(); - //TODO -// configInstance.getConfigInstance().getCurrentValues().putAll(config); -// var endReq = ReadExecuteExchange.Request.builder() -// .target(id).dataStore(store).config(configInstance).build(); -// XPipeConnection.execute(con -> { -// con.performSimpleExchange(endReq); -// }); + XPipeConnection.finishDialog(configInstance); + var ref = id != null ? DataSourceReference.id(id) : DataSourceReference.latest(); return get(ref); } diff --git a/api/src/main/java/io/xpipe/api/impl/DataTableAccumulatorImpl.java b/api/src/main/java/io/xpipe/api/impl/DataTableAccumulatorImpl.java index 1c201cdba..4b427ba9b 100644 --- a/api/src/main/java/io/xpipe/api/impl/DataTableAccumulatorImpl.java +++ b/api/src/main/java/io/xpipe/api/impl/DataTableAccumulatorImpl.java @@ -5,6 +5,8 @@ import io.xpipe.api.DataTable; import io.xpipe.api.DataTableAccumulator; import io.xpipe.api.connector.XPipeConnection; import io.xpipe.api.util.TypeDescriptor; +import io.xpipe.beacon.BeaconException; +import io.xpipe.beacon.exchange.ReadExchange; import io.xpipe.beacon.exchange.StoreStreamExchange; import io.xpipe.core.data.node.DataStructureNode; import io.xpipe.core.data.node.DataStructureNodeAcceptor; @@ -14,6 +16,7 @@ import io.xpipe.core.data.typed.TypedDataStreamWriter; import io.xpipe.core.source.DataSourceId; import io.xpipe.core.source.DataSourceReference; +import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; @@ -23,25 +26,35 @@ public class DataTableAccumulatorImpl implements DataTableAccumulator { private final TupleType type; private int rows; private TupleType writtenDescriptor; + private OutputStream bodyOutput; public DataTableAccumulatorImpl(TupleType type) { this.type = type; connection = XPipeConnection.open(); connection.sendRequest(StoreStreamExchange.Request.builder().build()); - connection.sendBody(); + bodyOutput = connection.sendBody(); } @Override public synchronized DataTable finish(DataSourceId id) { - connection.withOutputStream(OutputStream::close); + try { + bodyOutput.close(); + } catch (IOException e) { + throw new BeaconException(e); + } + StoreStreamExchange.Response res = connection.receiveResponse(); connection.close(); -// var req = ReadExecuteExchange.Request.builder() -// .target(id).dataStore(res.getStore()).build(); -// XPipeConnection.execute(con -> { -// con.performSimpleExchange(req); -// }); + var req = ReadExchange.Request.builder() + .target(id).store(res.getStore()).provider("xpbt").configureAll(false).build(); + ReadExchange.Response response = XPipeConnection.execute(con -> { + return con.performSimpleExchange(req); + }); + + var configInstance = response.getConfig(); + XPipeConnection.finishDialog(configInstance); + return DataSource.get(DataSourceReference.id(id)).asTable(); } diff --git a/api/src/test/java/io/xpipe/api/test/DataTableTest.java b/api/src/test/java/io/xpipe/api/test/DataTableTest.java index c5f1b80f2..c84c60c1d 100644 --- a/api/src/test/java/io/xpipe/api/test/DataTableTest.java +++ b/api/src/test/java/io/xpipe/api/test/DataTableTest.java @@ -5,13 +5,11 @@ import io.xpipe.core.source.DataSourceId; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import java.util.Map; - public class DataTableTest extends DaemonControl { @BeforeAll public static void setupStorage() throws Exception { - DataSource.create(DataSourceId.fromString(":usernames"), "csv", Map.of(), DataTableTest.class.getResource("username.csv")); + DataSource.create(DataSourceId.fromString(":usernames"), "csv", DataTableTest.class.getResource("username.csv")); } @Test diff --git a/beacon/src/main/java/module-info.java b/beacon/src/main/java/module-info.java index e835f4f22..5fdc84ef7 100644 --- a/beacon/src/main/java/module-info.java +++ b/beacon/src/main/java/module-info.java @@ -15,8 +15,8 @@ module io.xpipe.beacon { opens io.xpipe.beacon.exchange.data; opens io.xpipe.beacon.exchange.cli; - requires com.fasterxml.jackson.core; - requires com.fasterxml.jackson.databind; + requires static com.fasterxml.jackson.core; + requires static com.fasterxml.jackson.databind; requires transitive io.xpipe.core; requires static lombok; diff --git a/build.gradle b/build.gradle index 5e24c6109..83a130644 100644 --- a/build.gradle +++ b/build.gradle @@ -2,10 +2,8 @@ plugins { id 'org.jreleaser' version '1.0.0' } -if (project == rootProject) { - plugins { - id "io.codearte.nexus-staging" version "0.30.0" - } +if(project == rootProject) { + apply plugin: "io.codearte.nexus-staging" } version file('misc/version').text diff --git a/core/build.gradle b/core/build.gradle index 00b2e1f30..7014c910f 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -24,3 +24,7 @@ archivesBaseName = 'core' repositories { mavenCentral() } + +dependencies{ + compileOnly 'org.apache.commons:commons-exec:1.3' +} diff --git a/core/src/main/java/io/xpipe/core/data/node/DataStructureNode.java b/core/src/main/java/io/xpipe/core/data/node/DataStructureNode.java index e668ec3c7..6fd475486 100644 --- a/core/src/main/java/io/xpipe/core/data/node/DataStructureNode.java +++ b/core/src/main/java/io/xpipe/core/data/node/DataStructureNode.java @@ -86,7 +86,7 @@ public abstract class DataStructureNode implements Iterable { } public boolean isNull() { - throw unsupported("null check"); + return false; } public final ValueNode asValue() { 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 fcc41926f..9fca269ba 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 @@ -106,4 +106,10 @@ public abstract class ValueNode extends DataStructureNode { } public abstract byte[] getRawData(); + + @Override + public boolean isNull() { + return getRawData() == null; + } + } diff --git a/core/src/main/java/io/xpipe/core/dialog/Dialog.java b/core/src/main/java/io/xpipe/core/dialog/Dialog.java index 4eb49453d..7535db57c 100644 --- a/core/src/main/java/io/xpipe/core/dialog/Dialog.java +++ b/core/src/main/java/io/xpipe/core/dialog/Dialog.java @@ -10,12 +10,12 @@ import java.util.function.Supplier; /** * A Dialog is a sequence of questions and answers. - * + *

* The dialogue API is only used for the command line interface. * Therefore, the actual implementation is handled by the command line component. * This API provides a way of creating server-side dialogues which makes * it possible to create extensions that provide a commandline configuration component. - * + *

* When a Dialog is completed, it can also be optionally evaluated to a value, which can be queried by calling {@link #getResult()}. * The evaluation function can be set with {@link #evaluateTo(Supplier)}. * Alternatively, a dialogue can also copy the evaluation function of another dialogue with {@link #evaluateTo(Dialog)}. @@ -74,9 +74,9 @@ public abstract class Dialog { * Creates a choice dialogue. * * @param description the shown question description - * @param elements the available elements to choose from - * @param required signals whether a choice is required or can be left empty - * @param selected the selected element index + * @param elements the available elements to choose from + * @param required signals whether a choice is required or can be left empty + * @param selected the selected element index */ public static Dialog.Choice choice(String description, List elements, boolean required, int selected) { Dialog.Choice c = new Dialog.Choice(description, elements, required, selected); @@ -88,10 +88,10 @@ public abstract class Dialog { * Creates a choice dialogue from a set of objects. * * @param description the shown question description - * @param toString a function that maps the objects to a string - * @param required signals whether choices required or can be left empty - * @param def the element which is selected by default - * @param vals the range of possible elements + * @param toString a function that maps the objects to a string + * @param required signals whether choices required or can be left empty + * @param def the element which is selected by default + * @param vals the range of possible elements */ @SafeVarargs public static Dialog.Choice choice(String description, Function toString, boolean required, T def, T... vals) { @@ -133,7 +133,7 @@ public abstract class Dialog { return element; } - private T getConvertedValue() { + private T getConvertedValue() { return element.getConvertedValue(); } } @@ -142,13 +142,13 @@ public abstract class Dialog { * Creates a simple query dialogue. * * @param description the shown question description - * @param newLine signals whether the query should be done on a new line or not - * @param required signals whether the query can be left empty or not - * @param quiet signals whether the user should be explicitly queried for the value. - * In case the user is not queried, a value can still be set using the command line arguments - * that allow to set the specific value for a configuration query parameter - * @param value the default value - * @param converter the converter + * @param newLine signals whether the query should be done on a new line or not + * @param required signals whether the query can be left empty or not + * @param quiet signals whether the user should be explicitly queried for the value. + * In case the user is not queried, a value can still be set using the command line arguments + * that allow to set the specific value for a configuration query parameter + * @param value the default value + * @param converter the converter */ public static Dialog.Query query(String description, boolean newLine, boolean required, boolean quiet, T value, QueryConverter converter) { var q = new Dialog.Query(description, newLine, required, quiet, value, converter, false); @@ -197,7 +197,8 @@ public abstract class Dialog { if (currentElement == null) { DialogElement next = null; while (current < ds.length - 1 && (next = ds[++current].start()) == null) { - }; + } + ; return next; } @@ -378,10 +379,10 @@ public abstract class Dialog { * Creates a dialogue that will fork the control flow. * * @param description the shown question description - * @param elements the available elements to choose from - * @param required signals whether a choice is required or not - * @param selected the index of the element that is selected by default - * @param c the dialogue index mapping function + * @param elements the available elements to choose from + * @param required signals whether a choice is required or not + * @param selected the index of the element that is selected by default + * @param c the dialogue index mapping function */ public static Dialog fork(String description, List elements, boolean required, int selected, Function c) { var choice = new ChoiceElement(description, elements, required, selected); @@ -424,7 +425,7 @@ public abstract class Dialog { public abstract DialogElement start() throws Exception; public Dialog evaluateTo(Dialog d) { - evaluation = () -> d.evaluation.get(); + evaluation = () -> d.evaluation != null ? d.evaluation.get() : null; return this; } diff --git a/core/src/main/java/io/xpipe/core/source/DataSourceId.java b/core/src/main/java/io/xpipe/core/source/DataSourceId.java index 9f9707d26..4f9662399 100644 --- a/core/src/main/java/io/xpipe/core/source/DataSourceId.java +++ b/core/src/main/java/io/xpipe/core/source/DataSourceId.java @@ -32,6 +32,11 @@ public class DataSourceId { this.entryName = entryName; } + public static String cleanString(String input) { + var replaced = input.replaceAll(":", ""); + return replaced.length() == 0 ? "-" : replaced; + } + /** * Creates a new data source id from a collection name and an entry name. * diff --git a/core/src/main/java/io/xpipe/core/store/DataStore.java b/core/src/main/java/io/xpipe/core/store/DataStore.java index 0e02dd2de..a408b4c9b 100644 --- a/core/src/main/java/io/xpipe/core/store/DataStore.java +++ b/core/src/main/java/io/xpipe/core/store/DataStore.java @@ -41,12 +41,16 @@ public interface DataStore { /** * Creates a display string of this store. - * This can be a multiline string. */ - default String toDisplay() { + default String toSummaryString() { return null; } + default String queryInformationString() throws Exception { + return null; + } + + /** * Casts this instance to the required type without checking whether a cast is possible. */ diff --git a/core/src/main/java/io/xpipe/core/store/DataStoreFormatter.java b/core/src/main/java/io/xpipe/core/store/DataStoreFormatter.java new file mode 100644 index 000000000..ac8c93f53 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/store/DataStoreFormatter.java @@ -0,0 +1,27 @@ +package io.xpipe.core.store; + +public class DataStoreFormatter { + + public static String ellipsis(String input, int length) { + var end = Math.min(input.length(), length); + if (end < input.length()) { + return input.substring(0, end) + "..."; + } + return input; + } + + + public static String specialFormatHostName(String input) { + if (input.contains(":")) { + input = input.split(":")[0]; + } + + if (input.endsWith(".rds.amazonaws.com")) { + var split = input.split("\\."); + var name = split[0]; + var region = split[2]; + return String.format("RDS %s @ %s", name, region); + } + return null; + } +} diff --git a/core/src/main/java/io/xpipe/core/store/FileStore.java b/core/src/main/java/io/xpipe/core/store/FileStore.java index 12d71eb30..3acbe51c2 100644 --- a/core/src/main/java/io/xpipe/core/store/FileStore.java +++ b/core/src/main/java/io/xpipe/core/store/FileStore.java @@ -16,14 +16,14 @@ import java.nio.file.Path; public class FileStore implements StreamDataStore, FilenameStore { public static FileStore local(Path p) { - return new FileStore(MachineFileStore.local(), p.toString()); + return new FileStore(new LocalStore(), p.toString()); } /** * Creates a file store for a file that is local to the callers machine. */ public static FileStore local(String p) { - return new FileStore(MachineFileStore.local(), p); + return new FileStore(new LocalStore(), p); } MachineFileStore machine; @@ -38,7 +38,7 @@ public class FileStore implements StreamDataStore, FilenameStore { @Override public void validate() throws Exception { if (!machine.exists(file)) { - throw new IllegalStateException("File " + file + " could not be found on machine " + machine.toDisplay()); + throw new IllegalStateException("File " + file + " could not be found on machine " + machine.toSummaryString()); } } @@ -58,8 +58,8 @@ public class FileStore implements StreamDataStore, FilenameStore { } @Override - public String toDisplay() { - return file + "@" + machine.toDisplay(); + public String toSummaryString() { + return file + "@" + machine.toSummaryString(); } @Override @@ -69,6 +69,10 @@ public class FileStore implements StreamDataStore, FilenameStore { @Override public String getFileName() { - return file; + var split = file.split("[\\\\/]"); + if (split.length == 0) { + return ""; + } + return split[split.length - 1]; } } diff --git a/core/src/main/java/io/xpipe/core/store/InMemoryStore.java b/core/src/main/java/io/xpipe/core/store/InMemoryStore.java index 6430c312d..3e9ce281e 100644 --- a/core/src/main/java/io/xpipe/core/store/InMemoryStore.java +++ b/core/src/main/java/io/xpipe/core/store/InMemoryStore.java @@ -37,7 +37,7 @@ public class InMemoryStore implements StreamDataStore { } @Override - public String toDisplay() { + public String toSummaryString() { return "inMemory"; } } diff --git a/core/src/main/java/io/xpipe/core/store/LocalStore.java b/core/src/main/java/io/xpipe/core/store/LocalStore.java index ffbadb9be..71efe2cd8 100644 --- a/core/src/main/java/io/xpipe/core/store/LocalStore.java +++ b/core/src/main/java/io/xpipe/core/store/LocalStore.java @@ -1,33 +1,39 @@ package io.xpipe.core.store; import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.core.util.Secret; import lombok.Value; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.UncheckedIOException; +import java.io.*; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; @JsonTypeName("local") @Value -public class LocalStore implements ShellProcessStore { +public class LocalStore extends StandardShellStore implements MachineFileStore { + @Override + public boolean isLocal() { + return true; + } static class LocalProcessControl extends ProcessControl { - private final InputStream input; + private final List input; private final ProcessBuilder builder; + private final Integer timeout; private Process process; - LocalProcessControl(InputStream input, List cmd) { + LocalProcessControl(List input, List cmd, Integer timeout) { this.input = input; + this.timeout = timeout; var l = new ArrayList(); l.add("cmd"); l.add("/c"); @@ -35,13 +41,19 @@ public class LocalStore implements ShellProcessStore { builder = new ProcessBuilder(l); } + private InputStream createInputStream() { + var string = input.stream().map(secret -> secret.getSecretValue()).collect(Collectors.joining("\n")) + "\r\n"; + return new ByteArrayInputStream(string.getBytes(StandardCharsets.US_ASCII)); + } + @Override public void start() throws IOException { process = builder.start(); var t = new Thread(() -> { - try { - input.transferTo(process.getOutputStream()); + try (var inputStream = createInputStream()){ + process.getOutputStream().flush(); + inputStream.transferTo(process.getOutputStream()); process.getOutputStream().close(); } catch (IOException e) { throw new UncheckedIOException(e); @@ -53,7 +65,11 @@ public class LocalStore implements ShellProcessStore { @Override public int waitFor() throws Exception { - return process.waitFor(); + if (timeout != null) { + return process.waitFor(timeout, TimeUnit.SECONDS) ? 0 : -1; + } else { + return process.waitFor(); + } } @Override @@ -68,7 +84,11 @@ public class LocalStore implements ShellProcessStore { @Override public Charset getCharset() { - return StandardCharsets.UTF_8; + return StandardCharsets.US_ASCII; + } + + public Integer getTimeout() { + return timeout; } } @@ -78,8 +98,8 @@ public class LocalStore implements ShellProcessStore { } @Override - public String toDisplay() { - return "local"; + public String toSummaryString() { + return "localhost"; } @Override @@ -95,17 +115,17 @@ public class LocalStore implements ShellProcessStore { } @Override - public ProcessControl prepareCommand(InputStream input, List cmd) { - return new LocalProcessControl(input, cmd); + public ProcessControl prepareCommand(List input, List cmd, Integer timeout) { + return new LocalProcessControl(input, cmd, getEffectiveTimeOut(timeout)); } @Override - public ProcessControl preparePrivilegedCommand(InputStream input, List cmd) throws Exception { - return new LocalProcessControl(input, cmd); + public ProcessControl preparePrivilegedCommand(List input, List cmd, Integer timeOut) throws Exception { + return new LocalProcessControl(input, cmd, getEffectiveTimeOut(timeOut)); } @Override - public ShellType determineType() { - return ShellTypes.CMD; + public ShellType determineType() throws Exception { + return ShellTypes.determine(this); } } diff --git a/core/src/main/java/io/xpipe/core/store/MachineFileStore.java b/core/src/main/java/io/xpipe/core/store/MachineFileStore.java index ffe1b3537..a1e88073b 100644 --- a/core/src/main/java/io/xpipe/core/store/MachineFileStore.java +++ b/core/src/main/java/io/xpipe/core/store/MachineFileStore.java @@ -5,10 +5,11 @@ import java.io.OutputStream; public interface MachineFileStore extends DataStore { - static MachineFileStore local() { - return new LocalStore(); + default boolean isLocal(){ + return false; } + InputStream openInput(String file) throws Exception; OutputStream openOutput(String file) throws Exception; diff --git a/core/src/main/java/io/xpipe/core/store/NamedStore.java b/core/src/main/java/io/xpipe/core/store/NamedStore.java index 3e968e83d..e103b0fe3 100644 --- a/core/src/main/java/io/xpipe/core/store/NamedStore.java +++ b/core/src/main/java/io/xpipe/core/store/NamedStore.java @@ -33,7 +33,7 @@ public final class NamedStore implements DataStore { } @Override - public String toDisplay() { + public String toSummaryString() { throw new UnsupportedOperationException(); } diff --git a/core/src/main/java/io/xpipe/core/store/ProcessControl.java b/core/src/main/java/io/xpipe/core/store/ProcessControl.java index d936feda1..6c167c518 100644 --- a/core/src/main/java/io/xpipe/core/store/ProcessControl.java +++ b/core/src/main/java/io/xpipe/core/store/ProcessControl.java @@ -14,7 +14,6 @@ public abstract class ProcessControl { start(); var errT = discardErr(); var string = new String(getStdout().readAllBytes(), getCharset()); - errT.join(); waitFor(); return string; } @@ -34,9 +33,6 @@ public abstract class ProcessControl { t.setDaemon(true); t.start(); - outT.join(); - t.join(); - var ec = waitFor(); return ec != 0 ? Optional.of(read.get()) : Optional.empty(); } diff --git a/core/src/main/java/io/xpipe/core/store/ProcessOutputException.java b/core/src/main/java/io/xpipe/core/store/ProcessOutputException.java new file mode 100644 index 000000000..523dc91c1 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/store/ProcessOutputException.java @@ -0,0 +1,23 @@ +package io.xpipe.core.store; + +public class ProcessOutputException extends Exception{ + public ProcessOutputException() { + super(); + } + + public ProcessOutputException(String message) { + super(message); + } + + public ProcessOutputException(String message, Throwable cause) { + super(message, cause); + } + + public ProcessOutputException(Throwable cause) { + super(cause); + } + + protected ProcessOutputException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/core/src/main/java/io/xpipe/core/store/ShellProcessStore.java b/core/src/main/java/io/xpipe/core/store/ShellProcessStore.java deleted file mode 100644 index ae22ca1ff..000000000 --- a/core/src/main/java/io/xpipe/core/store/ShellProcessStore.java +++ /dev/null @@ -1,37 +0,0 @@ -package io.xpipe.core.store; - -import java.io.InputStream; -import java.io.OutputStream; - -public interface ShellProcessStore extends StandardShellStore { - - ShellType determineType() throws Exception; - - @Override - default InputStream openInput(String file) throws Exception { - var type = determineType(); - var cmd = type.createFileReadCommand(file); - var p = prepareCommand(InputStream.nullInputStream(), cmd); - p.start(); - return p.getStdout(); - } - - @Override - default OutputStream openOutput(String file) throws Exception { - return null; -// var type = determineType(); -// var cmd = type.createFileWriteCommand(file); -// var p = prepare(cmd).redirectErrorStream(true); -// var proc = p.start(); -// return proc.getOutputStream(); - } - - @Override - default boolean exists(String file) throws Exception { - var type = determineType(); - var cmd = type.createFileExistsCommand(file); - var p = prepareCommand(InputStream.nullInputStream(), cmd); - p.start(); - return p.waitFor() == 0; - } -} diff --git a/core/src/main/java/io/xpipe/core/store/ShellStore.java b/core/src/main/java/io/xpipe/core/store/ShellStore.java index af0a64bca..dd60f0159 100644 --- a/core/src/main/java/io/xpipe/core/store/ShellStore.java +++ b/core/src/main/java/io/xpipe/core/store/ShellStore.java @@ -1,35 +1,61 @@ package io.xpipe.core.store; +import io.xpipe.core.util.Secret; + +import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; import java.io.UncheckedIOException; import java.util.List; import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; -public interface ShellStore extends MachineFileStore { +public abstract class ShellStore implements DataStore { - static StandardShellStore local() { - return new LocalStore(); + public Integer getTimeout() { + return null; } - default String executeAndRead(List cmd) throws Exception { - var pc = prepareCommand(InputStream.nullInputStream(), cmd); + public List getInput() { + return List.of(); + } + + public boolean isLocal() { + return false; + } + + public String executeAndRead(List cmd, Integer timeout) throws Exception { + var pc = prepareCommand(List.of(), cmd, getEffectiveTimeOut(timeout)); pc.start(); pc.discardErr(); var string = new String(pc.getStdout().readAllBytes(), pc.getCharset()); return string; } - default Optional executeAndCheckOut(InputStream in, List cmd) throws Exception { - var pc = prepareCommand(in, cmd); + public String executeAndCheckOut(List in, List cmd, Integer timeout) throws ProcessOutputException, Exception { + var pc = prepareCommand(in, cmd, getEffectiveTimeOut(timeout)); pc.start(); - var outT = pc.discardErr(); - AtomicReference read = new AtomicReference<>(); + AtomicReference readError = new AtomicReference<>(); + var errorThread = new Thread(() -> { + try { + + readError.set(new String(pc.getStderr().readAllBytes(), pc.getCharset())); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + errorThread.setDaemon(true); + errorThread.start(); + + var read = new ByteArrayOutputStream(); var t = new Thread(() -> { try { - read.set(new String(pc.getStdout().readAllBytes(), pc.getCharset())); + final byte[] buf = new byte[1]; + + int length; + while ((length = pc.getStdout().read(buf)) > 0) { + read.write(buf, 0, length); + } } catch (IOException e) { throw new UncheckedIOException(e); } @@ -37,15 +63,21 @@ public interface ShellStore extends MachineFileStore { t.setDaemon(true); t.start(); - outT.join(); - t.join(); - var ec = pc.waitFor(); - return ec == 0 ? Optional.of(read.get()) : Optional.empty(); + var readOut = read.toString(pc.getCharset()); + if (ec == -1) { + throw new ProcessOutputException("Command timed out"); + } + + if (ec == 0) { + return readOut; + } else { + throw new ProcessOutputException("Command returned with " + ec + ": " + readError.get()); + } } - default Optional executeAndCheckErr(InputStream in, List cmd) throws Exception { - var pc = prepareCommand(in, cmd); + public Optional executeAndCheckErr(List in, List cmd) throws Exception { + var pc = prepareCommand(in, cmd, getTimeout()); pc.start(); var outT = pc.discardOut(); @@ -67,17 +99,27 @@ public interface ShellStore extends MachineFileStore { return ec != 0 ? Optional.of(read.get()) : Optional.empty(); } - default ProcessControl prepareCommand(List cmd) throws Exception { - return prepareCommand(InputStream.nullInputStream(), cmd); + public Integer getEffectiveTimeOut(Integer timeout) { + if (this.getTimeout() == null) { + return timeout; + } + if (timeout == null) { + return getTimeout(); + } + return Math.min(getTimeout(), timeout); } - ProcessControl prepareCommand(InputStream input, List cmd) throws Exception; - - default ProcessControl preparePrivilegedCommand(List cmd) throws Exception { - return preparePrivilegedCommand(InputStream.nullInputStream(), cmd); + public ProcessControl prepareCommand(List cmd, Integer timeout) throws Exception { + return prepareCommand(List.of(), cmd, timeout); } - default ProcessControl preparePrivilegedCommand(InputStream input, List cmd) throws Exception { + public abstract ProcessControl prepareCommand(List input, List cmd, Integer timeout) throws Exception; + + public ProcessControl preparePrivilegedCommand(List cmd, Integer timeout) throws Exception { + return preparePrivilegedCommand(List.of(), cmd, timeout); + } + + public ProcessControl preparePrivilegedCommand(List input, List cmd, Integer timeout) throws Exception { throw new UnsupportedOperationException(); } } diff --git a/core/src/main/java/io/xpipe/core/store/ShellTypes.java b/core/src/main/java/io/xpipe/core/store/ShellTypes.java index 5753e0ae6..4686e1d2f 100644 --- a/core/src/main/java/io/xpipe/core/store/ShellTypes.java +++ b/core/src/main/java/io/xpipe/core/store/ShellTypes.java @@ -1,7 +1,9 @@ package io.xpipe.core.store; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.xpipe.core.util.Secret; + import java.io.ByteArrayInputStream; -import java.io.InputStream; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -10,28 +12,28 @@ import java.util.List; public class ShellTypes { public static StandardShellStore.ShellType determine(ShellStore store) throws Exception { - var o = store.executeAndCheckOut(InputStream.nullInputStream(), List.of("echo", "$0")); - if (o.isPresent() && !o.get().equals("$0")) { + var o = store.executeAndCheckOut(List.of(), List.of("echo", "$0"), null).strip(); + if (!o.equals("$0")) { return SH; } else { - o = store.executeAndCheckOut(InputStream.nullInputStream(), List.of("(dir 2>&1 *`|echo CMD);&<# rem #>echo PowerShell")); - if (o.isPresent() && o.get().equals("PowerShell")) { + o = store.executeAndCheckOut(List.of(), List.of("(dir 2>&1 *`|echo CMD);&<# rem #>echo PowerShell"), null).trim(); + if (o.equals("PowerShell")) { return POWERSHELL; - } else { + } else return CMD; } } - } public static StandardShellStore.ShellType[] getAvailable(ShellStore store) throws Exception { - var o = store.executeAndCheckOut(InputStream.nullInputStream(), List.of("echo", "$0")); - if (o.isPresent() && !o.get().trim().equals("$0")) { + var o = store.executeAndCheckOut(List.of(), List.of("echo", "$0"), null); + if (!o.trim().equals("$0")) { return getLinuxShells(); } else { return getWindowsShells(); } } + @JsonProperty("powershell") public static final StandardShellStore.ShellType POWERSHELL = new StandardShellStore.ShellType() { @Override @@ -70,8 +72,14 @@ public class ShellTypes { public String getDisplayName() { return "PowerShell"; } + + @Override + public List getOperatingSystemNameCommand() { + return List.of("systeminfo", "|", "findstr", "/B", "/C:\"OS Name\""); + } }; + @JsonProperty("cmd") public static final StandardShellStore.ShellType CMD = new StandardShellStore.ShellType() { @Override @@ -83,9 +91,9 @@ public class ShellTypes { } @Override - public ProcessControl prepareElevatedCommand(ShellStore st, InputStream in, List cmd, String pw) throws Exception { + public ProcessControl prepareElevatedCommand(ShellStore st, List in, List cmd, Integer timeout, String pw) throws Exception { var l = List.of("net", "session", ";", "if", "%errorLevel%", "!=", "0"); - return st.prepareCommand(InputStream.nullInputStream(), l); + return st.prepareCommand(List.of(), l, timeout); } @Override @@ -117,8 +125,14 @@ public class ShellTypes { public String getDisplayName() { return "cmd.exe"; } + + @Override + public List getOperatingSystemNameCommand() { + return List.of("Get-ComputerInfo"); + } }; + @JsonProperty("sh") public static final StandardShellStore.ShellType SH = new StandardShellStore.ShellType() { @Override @@ -127,12 +141,12 @@ public class ShellTypes { } @Override - public ProcessControl prepareElevatedCommand(ShellStore st, InputStream in, List cmd, String pw) throws Exception { + public ProcessControl prepareElevatedCommand(ShellStore st, List in, List cmd, Integer timeout, String pw) throws Exception { var l = new ArrayList<>(cmd); l.add(0, "sudo"); l.add(1, "-S"); var pws = new ByteArrayInputStream(pw.getBytes(getCharset())); - return st.prepareCommand(pws, l); + return st.prepareCommand(List.of(Secret.createForSecretValue(pw)), l, timeout); } @Override @@ -164,6 +178,11 @@ public class ShellTypes { public String getDisplayName() { return "/bin/sh"; } + + @Override + public List getOperatingSystemNameCommand() { + return List.of("uname", "-o"); + } }; public static StandardShellStore.ShellType getDefault() { diff --git a/core/src/main/java/io/xpipe/core/store/StandardShellStore.java b/core/src/main/java/io/xpipe/core/store/StandardShellStore.java index 6b8321ba9..74e590797 100644 --- a/core/src/main/java/io/xpipe/core/store/StandardShellStore.java +++ b/core/src/main/java/io/xpipe/core/store/StandardShellStore.java @@ -1,17 +1,21 @@ package io.xpipe.core.store; +import io.xpipe.core.util.Secret; + import java.io.InputStream; +import java.io.OutputStream; import java.nio.charset.Charset; import java.util.List; -public interface StandardShellStore extends ShellStore { +public abstract class StandardShellStore extends ShellStore implements MachineFileStore { - static interface ShellType { + + public static interface ShellType { List switchTo(List cmd); - default ProcessControl prepareElevatedCommand(ShellStore st, InputStream in, List cmd, String pw) throws Exception { - return st.prepareCommand(in, cmd); + default ProcessControl prepareElevatedCommand(ShellStore st, List in, List cmd, Integer timeout, String pw) throws Exception { + return st.prepareCommand(in, cmd, timeout); } List createFileReadCommand(String file); @@ -25,7 +29,42 @@ public interface StandardShellStore extends ShellStore { String getName(); String getDisplayName(); + + List getOperatingSystemNameCommand(); } - ShellType determineType() throws Exception; + public abstract ShellType determineType() throws Exception; + + public final String querySystemName() throws Exception { + var result = executeAndCheckOut(List.of(), determineType().getOperatingSystemNameCommand(), getTimeout()); + return result.strip(); + } + + @Override + public InputStream openInput(String file) throws Exception { + var type = determineType(); + var cmd = type.createFileReadCommand(file); + var p = prepareCommand(List.of(), cmd, null); + p.start(); + return p.getStdout(); + } + + @Override + public OutputStream openOutput(String file) throws Exception { + return null; +// var type = determineType(); +// var cmd = type.createFileWriteCommand(file); +// var p = prepare(cmd).redirectErrorStream(true); +// var proc = p.start(); +// return proc.getOutputStream(); + } + + @Override + public boolean exists(String file) throws Exception { + var type = determineType(); + var cmd = type.createFileExistsCommand(file); + var p = prepareCommand(List.of(), cmd, null); + p.start(); + return p.waitFor() == 0; + } } diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java index 89680e832..6b1e90485 100644 --- a/core/src/main/java/module-info.java +++ b/core/src/main/java/module-info.java @@ -20,8 +20,8 @@ module io.xpipe.core { opens io.xpipe.core.data.typed; opens io.xpipe.core.dialog; - requires com.fasterxml.jackson.core; - requires com.fasterxml.jackson.databind; + requires static com.fasterxml.jackson.core; + requires static com.fasterxml.jackson.databind; requires java.net.http; requires static lombok; diff --git a/deps b/deps deleted file mode 160000 index 49a1ad06b..000000000 --- a/deps +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 49a1ad06bc6872f72c1d20ea864d24f3df59b7c5 diff --git a/extension/build.gradle b/extension/build.gradle index 4a9790cab..822806c95 100644 --- a/extension/build.gradle +++ b/extension/build.gradle @@ -25,17 +25,13 @@ version = file('../misc/version').text group = 'io.xpipe' archivesBaseName = 'extension' -repositories { - mavenCentral() -} - dependencies { compileOnly 'net.synedra:validatorfx:0.3.1' compileOnly 'org.junit.jupiter:junit-jupiter-api:5.8.2' compileOnly 'com.jfoenix:jfoenix:9.0.10' implementation project(':core') - implementation project(':fxcomps') - //implementation 'io.xpipe:fxcomps:0.2' + // implementation project(':fxcomps') + implementation 'io.xpipe:fxcomps:0.2.1' implementation 'org.controlsfx:controlsfx:11.1.1' } diff --git a/extension/src/main/java/io/xpipe/extension/DataSourceProvider.java b/extension/src/main/java/io/xpipe/extension/DataSourceProvider.java index 2c8c5875a..6a78be5ce 100644 --- a/extension/src/main/java/io/xpipe/extension/DataSourceProvider.java +++ b/extension/src/main/java/io/xpipe/extension/DataSourceProvider.java @@ -15,7 +15,7 @@ import java.util.Map; public interface DataSourceProvider> { - static enum GeneralType { + static enum Category { FILE, DATABASE; } @@ -25,13 +25,13 @@ public interface DataSourceProvider> { getSourceClass(); } - default GeneralType getGeneralType() { + default Category getGeneralType() { if (getFileProvider() != null) { - return GeneralType.FILE; + return Category.FILE; } if (getDatabaseProvider() != null) { - return GeneralType.DATABASE; + return Category.DATABASE; } throw new ExtensionException("Provider has no general type"); @@ -56,7 +56,7 @@ public interface DataSourceProvider> { return getId() + "." + key; } - default Region createConfigGui(Property source) { + default Region createConfigGui(Property source, Property appliedSource) { return null; } diff --git a/extension/src/main/java/io/xpipe/extension/DataStoreProvider.java b/extension/src/main/java/io/xpipe/extension/DataStoreProvider.java index 4ee6e0471..f07ff5869 100644 --- a/extension/src/main/java/io/xpipe/extension/DataStoreProvider.java +++ b/extension/src/main/java/io/xpipe/extension/DataStoreProvider.java @@ -1,10 +1,7 @@ package io.xpipe.extension; import io.xpipe.core.dialog.Dialog; -import io.xpipe.core.store.DataStore; -import io.xpipe.core.store.MachineFileStore; -import io.xpipe.core.store.ShellStore; -import io.xpipe.core.store.StreamDataStore; +import io.xpipe.core.store.*; import javafx.beans.property.Property; import java.net.URI; @@ -35,9 +32,14 @@ public interface DataStoreProvider { throw new ExtensionException("Gui Dialog is not implemented by provider " + getId()); } - default void init() throws Exception { + default boolean init() throws Exception { + return true; } + String queryInformationString(DataStore store, int length) throws Exception; + + public String toSummaryString(DataStore store, int length); + default boolean isHidden() { return false; } @@ -81,7 +83,7 @@ public interface DataStoreProvider { } default String display(DataStore store) { - return store.toDisplay(); + return store.toSummaryString(); } List getPossibleNames(); diff --git a/extension/src/main/java/io/xpipe/extension/DataStoreProviders.java b/extension/src/main/java/io/xpipe/extension/DataStoreProviders.java index 512cc7fef..247cf05dd 100644 --- a/extension/src/main/java/io/xpipe/extension/DataStoreProviders.java +++ b/extension/src/main/java/io/xpipe/extension/DataStoreProviders.java @@ -1,6 +1,7 @@ package io.xpipe.extension; import io.xpipe.core.dialog.Dialog; +import io.xpipe.core.store.DataStore; import io.xpipe.extension.event.ErrorEvent; import java.net.URI; @@ -53,6 +54,11 @@ public class DataStoreProviders { return ALL.stream().map(d -> d.dialogForString(s)).filter(Objects::nonNull).findAny(); } + + public static T byStore(DataStore store) { + return byStoreClass(store.getClass()); + } + @SuppressWarnings("unchecked") public static T byStoreClass(Class c) { if (ALL == null) { diff --git a/extension/src/main/java/io/xpipe/extension/SupportedApplicationProvider.java b/extension/src/main/java/io/xpipe/extension/SupportedApplicationProvider.java index 9113344ca..4545604e4 100644 --- a/extension/src/main/java/io/xpipe/extension/SupportedApplicationProvider.java +++ b/extension/src/main/java/io/xpipe/extension/SupportedApplicationProvider.java @@ -1,28 +1,57 @@ package io.xpipe.extension; +import io.xpipe.core.source.DataSource; import javafx.beans.value.ObservableValue; import javafx.scene.layout.Region; - -import java.util.function.Supplier; +import lombok.AllArgsConstructor; +import lombok.Value; public interface SupportedApplicationProvider { enum Category { PROGRAMMING_LANGUAGE, - APPLICATION + APPLICATION, + OTHER } - Region createRetrieveInstructions(ObservableValue id); + enum AccessType { + ACTIVE, + PASSIVE + } + + @Value + @AllArgsConstructor + public static class InstructionsDisplay { + Region region; + Runnable onFinish; + + public InstructionsDisplay(Region region) { + this.region = region; + onFinish = null; + } + } + + + default InstructionsDisplay createRetrievalInstructions(DataSource source, ObservableValue id) { + return null; + } + + default InstructionsDisplay createUpdateInstructions(DataSource source, ObservableValue id) { + return null; + } String getId(); - Supplier getName(); + ObservableValue getName(); Category getCategory(); + AccessType getAccessType(); + String getSetupGuideURL(); default String getGraphicIcon() { return null; } } + diff --git a/extension/src/main/java/io/xpipe/extension/UniformDataSourceProvider.java b/extension/src/main/java/io/xpipe/extension/UniformDataSourceProvider.java index 5b9ee6237..316eb7ae9 100644 --- a/extension/src/main/java/io/xpipe/extension/UniformDataSourceProvider.java +++ b/extension/src/main/java/io/xpipe/extension/UniformDataSourceProvider.java @@ -10,7 +10,7 @@ public interface UniformDataSourceProvider> extends Data @Override default Dialog configDialog(T source, boolean all) { - return Dialog.empty(); + return Dialog.empty().evaluateTo(() -> source); } @Override @@ -19,7 +19,7 @@ public interface UniformDataSourceProvider> extends Data try { return (T) getSourceClass().getDeclaredConstructors()[0].newInstance(input); } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { - throw new AssertionError(e); + throw new ExtensionException(e); } } } diff --git a/extension/src/main/java/io/xpipe/extension/Validators.java b/extension/src/main/java/io/xpipe/extension/Validators.java index 6ab3d02d8..932f18e14 100644 --- a/extension/src/main/java/io/xpipe/extension/Validators.java +++ b/extension/src/main/java/io/xpipe/extension/Validators.java @@ -1,8 +1,11 @@ package io.xpipe.extension; +import io.xpipe.core.store.ShellStore; import javafx.beans.value.ObservableValue; import net.synedra.validatorfx.Check; +import java.util.function.Predicate; + public class Validators { public static Check nonNull(Validator v, ObservableValue name, ObservableValue s) { @@ -12,4 +15,16 @@ public class Validators { } }); } + + public static void nonNull(Object object, String name) { + if (object == null) { + throw new IllegalArgumentException(I18n.get("extension.null", name)); + } + } + + public static void hostFeature(ShellStore host, Predicate predicate, String name) { + if (!predicate.test(host)) { + throw new IllegalArgumentException(I18n.get("extension.hostFeatureUnsupported", name)); + } + } } 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 7f03c3248..ac1b00a6c 100644 --- a/extension/src/main/java/io/xpipe/extension/comp/CharChoiceComp.java +++ b/extension/src/main/java/io/xpipe/extension/comp/CharChoiceComp.java @@ -4,42 +4,33 @@ import io.xpipe.fxcomps.Comp; import io.xpipe.fxcomps.CompStructure; import io.xpipe.fxcomps.SimpleCompStructure; 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.HashMap; +import java.util.Map; public class CharChoiceComp extends Comp> { private final Property value; - private final Property charChoiceValue; - private final BidiMap> range; + private final Map> range; private final ObservableValue customName; - public CharChoiceComp(Property value, BidiMap> range, ObservableValue customName) { + public CharChoiceComp(Property value, Map> range, ObservableValue customName) { this.value = value; this.range = range; this.customName = customName; - this.charChoiceValue = new SimpleObjectProperty<>(range.containsKey(value.getValue()) ? value.getValue() : null); - value.addListener((c, o, n) -> { - if (!range.containsKey(n)) { - charChoiceValue.setValue(null); - } else { - charChoiceValue.setValue(n); - } - }); } @Override public CompStructure createBase() { var charChoice = new CharComp(value); - var rangeCopy = new DualLinkedHashBidiMap<>(range); + var rangeCopy = new HashMap<>(range); if (customName != null) { rangeCopy.put(null, customName); } - var choice = new ChoiceComp(charChoiceValue, rangeCopy); + var choice = new ChoiceComp(value, rangeCopy); var charChoiceR = charChoice.createRegion(); var choiceR = choice.createRegion(); var box = new HBox(charChoiceR, choiceR); 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 9e22d2dec..3c2658c89 100644 --- a/extension/src/main/java/io/xpipe/extension/comp/CharsetChoiceComp.java +++ b/extension/src/main/java/io/xpipe/extension/comp/CharsetChoiceComp.java @@ -7,10 +7,11 @@ 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.nio.charset.StandardCharsets; import java.util.LinkedHashMap; +import java.util.List; public class CharsetChoiceComp extends ReplacementComp>> { @@ -23,9 +24,10 @@ public class CharsetChoiceComp extends ReplacementComp>> createComp() { var map = new LinkedHashMap>(); - for (var e : Charset.availableCharsets().entrySet()) { - map.put(e.getValue(), new SimpleStringProperty(e.getKey())); + for (var e : List.of(StandardCharsets.UTF_8, StandardCharsets.UTF_16, + StandardCharsets.UTF_16BE, StandardCharsets.ISO_8859_1, Charset.forName("Windows-1251"), Charset.forName("Windows-1252"), StandardCharsets.US_ASCII)) { + map.put(e, new SimpleStringProperty(e.displayName())); } - return new ChoiceComp<>(charset, new DualLinkedHashBidiMap<>(map)); + return new ChoiceComp<>(charset, 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 e038972ae..8de041390 100644 --- a/extension/src/main/java/io/xpipe/extension/comp/ChoiceComp.java +++ b/extension/src/main/java/io/xpipe/extension/comp/ChoiceComp.java @@ -5,42 +5,65 @@ import io.xpipe.fxcomps.Comp; import io.xpipe.fxcomps.CompStructure; import io.xpipe.fxcomps.SimpleCompStructure; import io.xpipe.fxcomps.util.PlatformThread; +import io.xpipe.fxcomps.util.SimpleChangeListener; import javafx.beans.property.Property; +import javafx.beans.property.SimpleObjectProperty; 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 lombok.Value; +import java.util.Map; + +@Value public class ChoiceComp extends Comp>> { - private final Property value; - private final BidiMap> range; + Property value; + ObservableValue>> range; - public ChoiceComp(Property value, BidiMap> range) { + public ChoiceComp(Property value, Map> range) { + this.value = value; + this.range = new SimpleObjectProperty<>(range); + } + + public ChoiceComp(Property value, ObservableValue>> range) { this.value = value; this.range = range; } @Override public CompStructure> createBase() { - var list = FXCollections.observableArrayList(range.keySet()); - var cb = new ComboBox<>(list); - cb.setConverter(new StringConverter<>() { + var cb = new ComboBox(); + cb.setConverter(new StringConverter() { @Override public String toString(T object) { if (object == null) { return I18n.get("extension.none"); } - return range.get(object).getValue(); + var found = range.getValue().get(object); + if (found == null) { + return ""; + } + + return found.getValue(); } + @Override public T fromString(String string) { throw new UnsupportedOperationException(); } }); + SimpleChangeListener.apply(PlatformThread.sync(range), c -> { + + var list = FXCollections.observableArrayList(c.keySet()); + if (!list.contains(null)) { + list.add(null); + } + cb.setItems(list); + }); PlatformThread.connect(value, cb.valueProperty()); cb.getStyleClass().add("choice-comp"); return new SimpleCompStructure<>(cb); 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 a30badda8..dc2dbfe3a 100644 --- a/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsBuilder.java +++ b/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsBuilder.java @@ -11,7 +11,6 @@ import javafx.beans.value.ObservableValue; import javafx.scene.control.Label; import javafx.scene.layout.Region; import net.synedra.validatorfx.Check; -import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap; import java.nio.charset.Charset; import java.util.ArrayList; @@ -24,6 +23,7 @@ public class DynamicOptionsBuilder { private final List entries = new ArrayList<>(); private final List> props = new ArrayList<>(); + private final List> lazyProperties = new ArrayList<>(); private final ObservableValue title; private final boolean wrap; @@ -43,7 +43,15 @@ public class DynamicOptionsBuilder { this.title = title; } + public DynamicOptionsBuilder makeLazy() { + var p = props.get(props.size() - 1); + props.remove(p); + lazyProperties.add(p); + return this; + } + public DynamicOptionsBuilder decorate(Check c) { + entries.get(entries.size() - 1).comp().apply(s -> c.decorates(s.get())); return this; } @@ -59,28 +67,42 @@ public class DynamicOptionsBuilder { for (var e : NewLine.values()) { map.put(e, I18n.observable("extension." + e.getId())); } - var comp = new ChoiceComp<>(prop, new DualLinkedHashBidiMap<>(map)); + var comp = new ChoiceComp<>(prop,map); entries.add(new DynamicOptionsComp.Entry(I18n.observable("extension.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); + var comp = new CharChoiceComp(prop, 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); + var comp = new CharChoiceComp(prop, 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)); + var comp = new ToggleGroupComp<>(prop, names); + entries.add(new DynamicOptionsComp.Entry(name, comp)); + props.add(prop); + return this; + } + + public DynamicOptionsBuilder addChoice(Property prop, ObservableValue name, Map> names) { + var comp = new ChoiceComp<>(prop, names); + entries.add(new DynamicOptionsComp.Entry(name, comp)); + props.add(prop); + return this; + } + + public DynamicOptionsBuilder addChoice(Property prop, ObservableValue name, ObservableValue>> names) { + var comp = new ChoiceComp<>(prop, names); entries.add(new DynamicOptionsComp.Entry(name, comp)); props.add(prop); return this; @@ -107,6 +129,14 @@ public class DynamicOptionsBuilder { return this; } + public DynamicOptionsBuilder addLazyString(String nameKey, Property prop, Property lazy) { + var comp = new TextFieldComp(prop, lazy); + entries.add(new DynamicOptionsComp.Entry(I18n.observable(nameKey), comp)); + props.add(prop); + lazyProperties.add(lazy); + return this; + } + public DynamicOptionsBuilder addString(ObservableValue name, Property prop) { var comp = new TextFieldComp(prop); entries.add(new DynamicOptionsComp.Entry(name, comp)); @@ -120,6 +150,10 @@ public class DynamicOptionsBuilder { return this; } + public DynamicOptionsBuilder addSecret(String nameKey, Property prop) { + return addSecret(I18n.observable(nameKey), prop); + } + public DynamicOptionsBuilder addSecret(ObservableValue name, Property prop) { var comp = new SecretFieldComp(prop); entries.add(new DynamicOptionsComp.Entry(name, comp)); @@ -134,20 +168,61 @@ public class DynamicOptionsBuilder { return this; } - public Comp buildComp(Supplier creator, Property toSet) { + public DynamicOptionsBuilder addInteger(String nameKey, Property prop) { + var comp = new IntFieldComp(prop); + entries.add(new DynamicOptionsComp.Entry(I18n.observable(nameKey), comp)); + props.add(prop); + return this; + } + + @SafeVarargs + public final DynamicOptionsBuilder bind(Supplier creator, Property... toSet) { props.forEach(prop -> { - prop.addListener((c,o,n) -> { - toSet.setValue(creator.get()); + prop.addListener((c, o, n) -> { + for (Property p : toSet) { + p.setValue(creator.get()); + } }); }); - toSet.setValue(creator.get()); + for (Property p : toSet) { + p.setValue(creator.get()); + } + return this; + } + + public DynamicOptionsBuilder bindLazy(Supplier creator, Property toLazySet) { + lazyProperties.forEach(prop -> { + prop.addListener((c,o,n) -> { + toLazySet.setValue(creator.get()); + }); + }); + toLazySet.setValue(creator.get()); + + return this; + } + + public Comp buildComp() { if (title != null) { entries.add(0, new DynamicOptionsComp.Entry(null, Comp.of(() -> new Label(title.getValue())).styleClass("title"))); } return new DynamicOptionsComp(entries, wrap); } - public Region build(Supplier creator, Property toSet) { - return buildComp(creator, toSet).createRegion(); + public Comp buildBindingComp(Supplier> creator, Property toSet) { + props.forEach(prop -> { + prop.addListener((c,o,n) -> { + toSet.unbind(); + toSet.bind(creator.get()); + }); + }); + toSet.bind(creator.get()); + if (title != null) { + entries.add(0, new DynamicOptionsComp.Entry(null, Comp.of(() -> new Label(title.getValue())).styleClass("title"))); + } + return new DynamicOptionsComp(entries, wrap); + } + + public Region build() { + return buildComp().createRegion(); } } diff --git a/extension/src/main/java/io/xpipe/extension/comp/SecretFieldComp.java b/extension/src/main/java/io/xpipe/extension/comp/SecretFieldComp.java index 3ab7d80b3..f301dd6ee 100644 --- a/extension/src/main/java/io/xpipe/extension/comp/SecretFieldComp.java +++ b/extension/src/main/java/io/xpipe/extension/comp/SecretFieldComp.java @@ -22,7 +22,7 @@ public class SecretFieldComp extends Comp> { var text = new PasswordField(); text.setText(value.getValue() != null ? value.getValue().getSecretValue() : null); text.textProperty().addListener((c, o, n) -> { - value.setValue(n.length() > 0 ? Secret.createForSecretValue(n) : null); + value.setValue(n != null && n.length() > 0 ? Secret.createForSecretValue(n) : null); }); value.addListener((c, o, n) -> { PlatformThread.runLaterIfNeeded(() -> { diff --git a/extension/src/main/java/io/xpipe/extension/comp/TabPaneComp.java b/extension/src/main/java/io/xpipe/extension/comp/TabPaneComp.java index 45b355dc6..ac26b4e03 100644 --- a/extension/src/main/java/io/xpipe/extension/comp/TabPaneComp.java +++ b/extension/src/main/java/io/xpipe/extension/comp/TabPaneComp.java @@ -25,7 +25,10 @@ public class TabPaneComp extends Comp> { for (var e : entries) { Tab tab = new Tab(); - var ll = new Label(null, new FontIcon(e.graphic())); + var ll = new Label(null); + if (e.graphic != null) { + ll.setGraphic(new FontIcon(e.graphic())); + } ll.textProperty().bind(e.name()); ll.getStyleClass().add("name"); ll.setAlignment(Pos.CENTER); diff --git a/extension/src/main/java/io/xpipe/extension/comp/TextFieldComp.java b/extension/src/main/java/io/xpipe/extension/comp/TextFieldComp.java index 8556c77e7..9e4010bb8 100644 --- a/extension/src/main/java/io/xpipe/extension/comp/TextFieldComp.java +++ b/extension/src/main/java/io/xpipe/extension/comp/TextFieldComp.java @@ -5,14 +5,25 @@ import io.xpipe.fxcomps.CompStructure; import io.xpipe.fxcomps.SimpleCompStructure; import io.xpipe.fxcomps.util.PlatformThread; import javafx.beans.property.Property; +import javafx.event.EventHandler; import javafx.scene.control.TextField; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; public class TextFieldComp extends Comp> { private final Property value; + private final Property lazyValue; public TextFieldComp(Property value) { this.value = value; + this.lazyValue = value; + + } + + public TextFieldComp(Property value, Property lazyValue) { + this.value = value; + this.lazyValue = lazyValue; } @Override @@ -26,6 +37,15 @@ public class TextFieldComp extends Comp> { text.setText(n); }); }); + text.setOnKeyPressed(new EventHandler() { + @Override + public void handle(KeyEvent ke) { + if (ke.getCode().equals(KeyCode.ENTER)) { + lazyValue.setValue(value.getValue()); + } + ke.consume(); + } + }); return new SimpleCompStructure<>(text); } } 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 8072a6c8b..176e9fbc3 100644 --- a/extension/src/main/java/io/xpipe/extension/comp/ToggleGroupComp.java +++ b/extension/src/main/java/io/xpipe/extension/comp/ToggleGroupComp.java @@ -9,22 +9,19 @@ 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.Map; public class ToggleGroupComp extends Comp> { private final Property value; - private final BidiMap> range; + private final Map> range; - public ToggleGroupComp(Property value, BidiMap> range) { + public ToggleGroupComp(Property value, Map> range) { this.value = value; this.range = range; } - public BidiMap> getRange() { - return range; - } - @Override public CompStructure createBase() { var box = new HBox(); diff --git a/extension/src/main/java/io/xpipe/extension/event/ErrorEvent.java b/extension/src/main/java/io/xpipe/extension/event/ErrorEvent.java index 726ada190..97588d26b 100644 --- a/extension/src/main/java/io/xpipe/extension/event/ErrorEvent.java +++ b/extension/src/main/java/io/xpipe/extension/event/ErrorEvent.java @@ -33,6 +33,11 @@ public class ErrorEvent { .description(msg); } + public static ErrorEventBuilder fromMessage(String msg) { + return builder() + .description(msg); + } + public void handle() { EventHandler.get().handle(this); } diff --git a/extension/src/main/java/io/xpipe/extension/prefs/PrefsChoiceValue.java b/extension/src/main/java/io/xpipe/extension/prefs/PrefsChoiceValue.java index 093617051..fbd645c7a 100644 --- a/extension/src/main/java/io/xpipe/extension/prefs/PrefsChoiceValue.java +++ b/extension/src/main/java/io/xpipe/extension/prefs/PrefsChoiceValue.java @@ -2,9 +2,45 @@ package io.xpipe.extension.prefs; import io.xpipe.extension.I18n; import io.xpipe.extension.Translatable; +import lombok.SneakyThrows; + +import java.util.Arrays; +import java.util.List; public interface PrefsChoiceValue extends Translatable { + @SuppressWarnings("unchecked") + @SneakyThrows + static List getAll(Class type) { + if (Enum.class.isAssignableFrom(type)) { + return Arrays.asList(type.getEnumConstants()); + } + + try { + type.getDeclaredField("ALL"); + } catch (NoSuchFieldException e) { + return null; + } + + try { + return (List) type.getDeclaredField("ALL").get(null); + } catch (IllegalAccessException | NoSuchFieldException e) { + return List.of(type.getEnumConstants()); + } + } + + static List getSupported(Class type) { + try { + return (List) type.getDeclaredField("SUPPORTED").get(null); + } catch (IllegalAccessException | NoSuchFieldException e) { + return getAll(type).stream().filter(t -> t.isSupported()).toList(); + } + } + + default boolean isSupported() { + return true; + } + @Override default String toTranslatedString() { return I18n.get(getId()); diff --git a/extension/src/main/java/module-info.java b/extension/src/main/java/module-info.java index 2f2c92d6b..88594f465 100644 --- a/extension/src/main/java/module-info.java +++ b/extension/src/main/java/module-info.java @@ -13,7 +13,6 @@ module io.xpipe.extension { requires javafx.graphics; requires transitive javafx.controls; requires io.xpipe.fxcomps; - requires static org.apache.commons.collections4; requires static lombok; requires static com.dlsc.preferencesfx; requires static com.dlsc.formsfx; diff --git a/extension/src/main/resources/io/xpipe/extension/resources/lang/translations_en.properties b/extension/src/main/resources/io/xpipe/extension/resources/lang/translations_en.properties index 86cbe9125..576f3552f 100644 --- a/extension/src/main/resources/io/xpipe/extension/resources/lang/translations_en.properties +++ b/extension/src/main/resources/io/xpipe/extension/resources/lang/translations_en.properties @@ -4,4 +4,9 @@ crlf=CRLF (Windows) lf=LF (Linux) none=None nullPointer=Null Pointer: $MSG$ -mustNotBeEmpty=$NAME$ must not be empty \ No newline at end of file +mustNotBeEmpty=$NAME$ must not be empty +null=$VALUE$ must be not null +hostFeatureUnsupported=Host does not support the feature $FEATURE$ +namedHostFeatureUnsupported=$HOST$ does not support this feature +namedHostNotActive=$HOST$ is not active +noInformationAvailable=No information available \ No newline at end of file diff --git a/gradlew b/gradlew old mode 100755 new mode 100644 diff --git a/jreleaser.gradle b/jreleaser.gradle index a5723a988..2d97f2c81 100644 --- a/jreleaser.gradle +++ b/jreleaser.gradle @@ -5,8 +5,8 @@ def canonicalVersion = file('misc/canonical_version').text jreleaser { environment { - properties.put('rawChangelog', file("misc/changelogs/${canonicalVersion}.txt").exists() ? - file("misc/changelogs/${canonicalVersion}.txt").text.replace('\r\n', '\n') : "") + properties.put('rawChangelog', file("misc/changelogs/${version}.txt").exists() ? + file("misc/changelogs/${version}.txt").text.replace('\r\n', '\n') : "") } project { diff --git a/misc/canonical_version b/misc/canonical_version deleted file mode 100644 index ceab6e11e..000000000 --- a/misc/canonical_version +++ /dev/null @@ -1 +0,0 @@ -0.1 \ No newline at end of file diff --git a/misc/changelogs/0.1.txt b/misc/changelogs/0.0.1.txt similarity index 100% rename from misc/changelogs/0.1.txt rename to misc/changelogs/0.0.1.txt diff --git a/misc/discord_full.tpl b/misc/discord_full.tpl index 1038d4c31..5a0b2504a 100644 --- a/misc/discord_full.tpl +++ b/misc/discord_full.tpl @@ -2,10 +2,7 @@ 🚀 {{projectName}} {{projectVersion}} has been released: {{releaseNotesUrl}} -If you have already installed {{projectName}} and have updates enabled, -this update will be automatically installed when launching it the next time. - -You can get the standalone version here: {{releaseNotesUrl}} +The documentation and maven repositories should be automatically updated within the next couple of hours. Changes in {{projectVersion}}: {{{rawChangelog}}} \ No newline at end of file diff --git a/misc/discord_pre.tpl b/misc/discord_pre.tpl index 2d4ee9371..4756bf337 100644 --- a/misc/discord_pre.tpl +++ b/misc/discord_pre.tpl @@ -5,8 +5,7 @@ Note that as this is not a final release, there might still be some small issues with it. Please report them if you stumble upon one. -Also note that pre-releases are not downloaded by the automatic updater -until they are ready for a full release or you have opted in to receiving pre-releases. +The documentation and maven repositories should be automatically updated within the next couple of hours. Changes in {{projectVersion}}: {{{rawChangelog}}} \ No newline at end of file diff --git a/misc/version b/misc/version index ce4bf16c2..4adba7ed0 100644 --- a/misc/version +++ b/misc/version @@ -1 +1 @@ -0.1-SNAPSHOT \ No newline at end of file +0.0.1-SNAPSHOT \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index aaa48c0b8..3c0355943 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,3 +4,12 @@ include 'api' include 'core' include 'beacon' include 'extension' + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath "io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.30.0" + } +}