diff --git a/build.gradle b/build.gradle
index dd43bf1e..2982b9bf 100644
--- a/build.gradle
+++ b/build.gradle
@@ -5,7 +5,7 @@ plugins {
id 'org.beryx.jlink' version '2.22.0'
}
-def sparrowVersion = '1.4.1'
+def sparrowVersion = '1.4.2'
def os = org.gradle.internal.os.OperatingSystem.current()
def osName = os.getFamilyName()
if(os.macOsX) {
@@ -50,7 +50,7 @@ dependencies {
exclude group: 'org.slf4j'
}
implementation('org.jdbi:jdbi3-sqlobject:3.20.0')
- implementation('org.flywaydb:flyway-core:7.10.1-SNAPSHOT')
+ implementation('org.flywaydb:flyway-core:7.10.5-SNAPSHOT')
implementation('org.fxmisc.richtext:richtextfx:0.10.4')
implementation('no.tornado:tornadofx-controls:1.0.4')
implementation('com.google.zxing:javase:3.4.0')
@@ -138,6 +138,8 @@ jlink {
requires 'com.fasterxml.jackson.databind'
requires 'jdk.crypto.cryptoki'
requires 'java.management'
+ uses 'org.flywaydb.core.extensibility.FlywayExtension'
+ uses 'org.flywaydb.core.internal.database.DatabaseType'
}
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png']
@@ -161,7 +163,8 @@ jlink {
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
"--add-reads=com.sparrowwallet.merged.module=java.desktop",
- "--add-reads=com.sparrowwallet.merged.module=java.sql"]
+ "--add-reads=com.sparrowwallet.merged.module=java.sql",
+ "--add-reads=com.sparrowwallet.merged.module=com.sparrowwallet.sparrow"]
if(os.macOsX) {
jvmArgs += "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"
diff --git a/src/main/deploy/package/osx/Info.plist b/src/main/deploy/package/osx/Info.plist
index 9771859b..5182aabd 100644
--- a/src/main/deploy/package/osx/Info.plist
+++ b/src/main/deploy/package/osx/Info.plist
@@ -21,7 +21,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.4.1
+ 1.4.2
CFBundleSignature
????
diff --git a/src/main/java/com/sparrowwallet/sparrow/AboutController.java b/src/main/java/com/sparrowwallet/sparrow/AboutController.java
index 4ae2d8d3..b3dcbc23 100644
--- a/src/main/java/com/sparrowwallet/sparrow/AboutController.java
+++ b/src/main/java/com/sparrowwallet/sparrow/AboutController.java
@@ -12,7 +12,7 @@ public class AboutController {
private Label title;
public void initializeView() {
- title.setText(MainApp.APP_NAME + " " + MainApp.APP_VERSION);
+ title.setText(MainApp.APP_NAME + " " + MainApp.APP_VERSION + MainApp.APP_VERSION_SUFFIX);
}
public void setStage(Stage stage) {
diff --git a/src/main/java/com/sparrowwallet/sparrow/MainApp.java b/src/main/java/com/sparrowwallet/sparrow/MainApp.java
index a2f5fed9..6717c22f 100644
--- a/src/main/java/com/sparrowwallet/sparrow/MainApp.java
+++ b/src/main/java/com/sparrowwallet/sparrow/MainApp.java
@@ -31,7 +31,8 @@ import java.util.stream.Collectors;
public class MainApp extends Application {
public static final String APP_ID = "com.sparrowwallet.sparrow";
public static final String APP_NAME = "Sparrow";
- public static final String APP_VERSION = "1.4.1";
+ public static final String APP_VERSION = "1.4.2";
+ public static final String APP_VERSION_SUFFIX = "-beta";
public static final String APP_HOME_PROPERTY = "sparrow.home";
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java b/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java
index 52a538e8..c9e1bb8b 100644
--- a/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java
+++ b/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java
@@ -230,7 +230,7 @@ public class Hwi {
String hwiPath = hwiExecutable.getAbsolutePath();
if(command.isTestFirst() && (hwiPath.contains(tmpDir) || hwiPath.startsWith(homeDir.getAbsolutePath())) && (!hwiPath.contains(HWI_VERSION_DIR) || !testHwi(hwiExecutable))) {
if(Platform.getCurrent() == Platform.OSX) {
- deleteDirectory(hwiExecutable.getParentFile());
+ IOUtils.deleteDirectory(hwiExecutable.getParentFile());
} else {
hwiExecutable.delete();
}
@@ -339,17 +339,6 @@ public class Hwi {
}
}
- private boolean deleteDirectory(File directoryToBeDeleted) {
- File[] allContents = directoryToBeDeleted.listFiles();
- if (allContents != null) {
- for (File file : allContents) {
- deleteDirectory(file);
- }
- }
-
- return directoryToBeDeleted.delete();
- }
-
public static File newDirectory(File destinationDir, ZipEntry zipEntry, Set setFilePermissions) throws IOException {
String destDirPath = destinationDir.getCanonicalPath();
diff --git a/src/main/java/com/sparrowwallet/sparrow/io/IOUtils.java b/src/main/java/com/sparrowwallet/sparrow/io/IOUtils.java
index 4524588c..c93ac253 100644
--- a/src/main/java/com/sparrowwallet/sparrow/io/IOUtils.java
+++ b/src/main/java/com/sparrowwallet/sparrow/io/IOUtils.java
@@ -1,8 +1,17 @@
package com.sparrowwallet.sparrow.io;
import java.io.*;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystems;
import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
public class IOUtils {
public static FileType getFileType(File file) {
@@ -38,4 +47,75 @@ public class IOUtils {
return FileType.UNKNOWN;
}
+
+ /**
+ * List directory contents for a resource folder. Not recursive.
+ * This is basically a brute-force implementation.
+ * Works for regular files, JARs and Java modules.
+ *
+ * @param clazz Any java class that lives in the same place as the resources you want.
+ * @param path Should end with "/", but not start with one.
+ * @return Just the name of each member item, not the full paths.
+ * @throws URISyntaxException
+ * @throws IOException
+ */
+ public static String[] getResourceListing(Class clazz, String path) throws URISyntaxException, IOException {
+ URL dirURL = clazz.getClassLoader().getResource(path);
+ if(dirURL != null && dirURL.getProtocol().equals("file")) {
+ /* A file path: easy enough */
+ return new File(dirURL.toURI()).list();
+ }
+
+ if(dirURL == null) {
+ /*
+ * In case of a jar file, we can't actually find a directory.
+ * Have to assume the same jar as clazz.
+ */
+ String me = clazz.getName().replace(".", "/")+".class";
+ dirURL = clazz.getClassLoader().getResource(me);
+ }
+
+ if(dirURL.getProtocol().equals("jar")) {
+ /* A JAR path */
+ String jarPath = dirURL.getPath().substring(5, dirURL.getPath().indexOf("!")); //strip out only the JAR file
+ JarFile jar = new JarFile(URLDecoder.decode(jarPath, "UTF-8"));
+ Enumeration entries = jar.entries(); //gives ALL entries in jar
+ Set result = new HashSet(); //avoid duplicates in case it is a subdirectory
+ while(entries.hasMoreElements()) {
+ String name = entries.nextElement().getName();
+ if(name.startsWith(path)) { //filter according to the path
+ String entry = name.substring(path.length());
+ int checkSubdir = entry.indexOf("/");
+ if (checkSubdir >= 0) {
+ // if it is a subdirectory, we just return the directory name
+ entry = entry.substring(0, checkSubdir);
+ }
+ result.add(entry);
+ }
+ }
+
+ return result.toArray(new String[result.size()]);
+ }
+
+ if(dirURL.getProtocol().equals("jrt")) {
+ java.nio.file.FileSystem jrtFs = FileSystems.newFileSystem(URI.create("jrt:/"), Collections.emptyMap());
+ Path resourcePath = jrtFs.getPath("modules/com.sparrowwallet.sparrow", path);
+ return Files.list(resourcePath).map(filePath -> filePath.getFileName().toString()).toArray(String[]::new);
+ }
+
+ throw new UnsupportedOperationException("Cannot list files for URL " + dirURL);
+ }
+
+ public static boolean deleteDirectory(File directory) {
+ try {
+ Files.walk(directory.toPath())
+ .sorted(Comparator.reverseOrder())
+ .map(Path::toFile)
+ .forEach(File::delete);
+ } catch(IOException e) {
+ return false;
+ }
+
+ return true;
+ }
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java b/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java
index a030f870..0fc69c95 100644
--- a/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java
+++ b/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java
@@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.io.db;
import com.google.common.eventbus.Subscribe;
+import com.google.common.io.Files;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.crypto.Argon2KeyDeriver;
import com.sparrowwallet.drongo.crypto.AsymmetricKeyDeriver;
@@ -29,6 +30,7 @@ import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
+import java.nio.file.StandardCopyOption;
import java.security.SecureRandom;
import java.util.*;
import java.util.stream.Collectors;
@@ -46,6 +48,7 @@ public class DbPersistence implements Persistence {
public static final byte[] HEADER_MAGIC_1 = "SPRW1\n".getBytes(StandardCharsets.UTF_8);
private static final String H2_USER = "sa";
private static final String H2_PASSWORD = "";
+ public static final String MIGRATION_RESOURCES_DIR = "com/sparrowwallet/sparrow/sql/";
private HikariDataSource dataSource;
private AsymmetricKeyDeriver keyDeriver;
@@ -299,8 +302,9 @@ public class DbPersistence implements Persistence {
}
private void migrate(Storage storage, String schema, ECKey encryptionKey) throws StorageException {
+ File migrationDir = getMigrationDir();
try {
- Flyway flyway = getFlyway(storage, schema, getFilePassword(encryptionKey));
+ Flyway flyway = getFlyway(storage, schema, getFilePassword(encryptionKey), migrationDir);
flyway.migrate();
} catch(FlywayValidateException e) {
log.error("Failed to open wallet file. Validation error during schema migration.", e);
@@ -308,20 +312,22 @@ public class DbPersistence implements Persistence {
} catch(FlywayException e) {
log.error("Failed to open wallet file. ", e);
throw new StorageException("Failed to open wallet file.\n" + e.getMessage(), e);
+ } finally {
+ IOUtils.deleteDirectory(migrationDir);
}
}
private void cleanAndMigrate(Storage storage, String schema, String password) throws StorageException {
+ File migrationDir = getMigrationDir();
try {
- boolean existing = (dataSource == null);
- Flyway flyway = getFlyway(storage, schema, password);
- if(existing) {
- flyway.clean();
- }
+ Flyway flyway = getFlyway(storage, schema, password, migrationDir);
+ flyway.clean();
flyway.migrate();
} catch(FlywayException e) {
log.error("Failed to save wallet file.", e);
throw new StorageException("Failed to save wallet file.\n" + e.getMessage(), e);
+ } finally {
+ IOUtils.deleteDirectory(migrationDir);
}
}
@@ -504,8 +510,30 @@ public class DbPersistence implements Persistence {
return jdbi;
}
- private Flyway getFlyway(Storage storage, String schema, String password) throws StorageException {
- return Flyway.configure().dataSource(getDataSource(storage, password)).locations("com/sparrowwallet/sparrow/sql").schemas(schema).load();
+ private Flyway getFlyway(Storage storage, String schema, String password, File resourcesDir) throws StorageException {
+ return Flyway.configure().dataSource(getDataSource(storage, password)).locations("filesystem:" + resourcesDir.getAbsolutePath()).schemas(schema).failOnMissingLocations(true).load();
+ }
+
+ //Flyway does not support JPMS yet, so the migration files are extracted to a temp dir in order to avoid classloader encapsulation issues
+ private File getMigrationDir() {
+ File migrationDir = Files.createTempDir();
+ try {
+ String[] files = IOUtils.getResourceListing(DbPersistence.class, MIGRATION_RESOURCES_DIR);
+ for(String name : files) {
+ File targetFile = new File(migrationDir, name);
+ try(InputStream inputStream = DbPersistence.class.getResourceAsStream("/" + MIGRATION_RESOURCES_DIR + name)) {
+ if(inputStream != null) {
+ java.nio.file.Files.copy(inputStream, targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ } else {
+ log.error("Could not load resource at /" + MIGRATION_RESOURCES_DIR + name);
+ }
+ }
+ }
+ } catch(Exception e) {
+ log.error("Could not extract migration resources", e);
+ }
+
+ return migrationDir;
}
private HikariDataSource getDataSource(Storage storage, String password) throws StorageException {
@@ -518,11 +546,15 @@ public class DbPersistence implements Persistence {
private HikariDataSource createDataSource(File walletFile, String password) throws StorageException {
try {
+ Class.forName("org.h2.Driver");
HikariConfig config = new HikariConfig();
config.setJdbcUrl(getUrl(walletFile, password));
config.setUsername(H2_USER);
config.setPassword(password == null ? H2_PASSWORD : password + " " + H2_PASSWORD);
return new HikariDataSource(config);
+ } catch(ClassNotFoundException e) {
+ log.error("Cannot find H2 driver", e);
+ throw new StorageException("Cannot find H2 driver", e);
} catch(HikariPool.PoolInitializationException e) {
if(e.getMessage() != null && e.getMessage().contains("Database may be already in use")) {
log.error("Wallet file may already be in use. Make sure the application is not running elsewhere.", e);
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
index 7291e7f0..7216bbc9 100644
--- a/src/main/resources/logback.xml
+++ b/src/main/resources/logback.xml
@@ -9,6 +9,7 @@
+