Browse Source

added update notification

Sebastian Stenzel 10 years ago
parent
commit
c7ecd612c9

+ 2 - 2
main/core/pom.xml

@@ -18,8 +18,8 @@
 	<name>Cryptomator WebDAV and I/O module</name>
 
 	<properties>
-		<jetty.version>9.2.5.v20141112</jetty.version>
-		<jackrabbit.version>2.9.0</jackrabbit.version>
+		<jetty.version>9.2.10.v20150310</jetty.version>
+		<jackrabbit.version>2.9.1</jackrabbit.version>
 		<commons.transaction.version>1.2</commons.transaction.version>
 		<jta.version>1.1</jta.version>
 	</properties>

+ 7 - 0
main/pom.xml

@@ -39,6 +39,7 @@
 		<commons-collections.version>4.0</commons-collections.version>
 		<commons-lang3.version>3.3.2</commons-lang3.version>
 		<commons-codec.version>1.10</commons-codec.version>
+		<commons-httpclient.version>3.1</commons-httpclient.version>
 		<jackson-databind.version>2.4.4</jackson-databind.version>
 		<mockito.version>1.10.19</mockito.version>
 	</properties>
@@ -110,6 +111,12 @@
 				<artifactId>commons-codec</artifactId>
 				<version>${commons-codec.version}</version>
 			</dependency>
+			<dependency>
+				<!-- org.apache.httpcomponents:httpclient is newer, but jackrabbit uses this version. We don't have a reason to upgrade --> 
+				<groupId>commons-httpclient</groupId>
+				<artifactId>commons-httpclient</artifactId>
+				<version>${commons-httpclient.version}</version>
+			</dependency>
 
 			<!-- Guava -->
 			<dependency>

+ 6 - 1
main/ui/pom.xml

@@ -48,7 +48,11 @@
 			<groupId>org.apache.commons</groupId>
 			<artifactId>commons-lang3</artifactId>
 		</dependency>
-		
+		<dependency>
+			<groupId>commons-httpclient</groupId>
+			<artifactId>commons-httpclient</artifactId>
+		</dependency>
+
 		<!-- DI -->
 		<dependency>
 			<groupId>com.google.inject</groupId>
@@ -78,6 +82,7 @@
 					<archive>
 						<manifestEntries>
 							<Main-Class>${exec.mainClass}</Main-Class>
+							<Implementation-Version>${project.version}</Implementation-Version>
 						</manifestEntries>
 					</archive>
 				</configuration>

+ 24 - 2
main/ui/src/main/java/org/cryptomator/ui/MainApplication.java

@@ -57,14 +57,15 @@ public class MainApplication extends Application {
 	}
 
 	public MainApplication(Injector injector) {
-		this(injector.getInstance(ExecutorService.class), injector.getInstance(ControllerFactory.class), injector.getInstance(DeferredCloser.class));
+		this(injector.getInstance(ExecutorService.class), injector.getInstance(ControllerFactory.class), injector.getInstance(DeferredCloser.class), injector.getInstance(MainApplicationReference.class));
 	}
 
-	public MainApplication(ExecutorService executorService, ControllerFactory controllerFactory, DeferredCloser closer) {
+	public MainApplication(ExecutorService executorService, ControllerFactory controllerFactory, DeferredCloser closer, MainApplicationReference appRef) {
 		super();
 		this.executorService = executorService;
 		this.controllerFactory = controllerFactory;
 		this.closer = closer;
+		appRef.set(this);
 	}
 
 	@Override
@@ -175,4 +176,25 @@ public class MainApplication extends Application {
 		}
 	}
 
+	/**
+	 * Needed to inject MainApplication. Problem: Application needs to be set asap after injector creation.
+	 */
+	static class MainApplicationReference {
+
+		private Application application;
+
+		private void set(Application application) {
+			this.application = application;
+		}
+
+		public Application get() {
+			if (application == null) {
+				throw new IllegalStateException("not yet ready.");
+			} else {
+				return application;
+			}
+		}
+
+	}
+
 }

+ 23 - 0
main/ui/src/main/java/org/cryptomator/ui/MainModule.java

@@ -8,22 +8,27 @@
  ******************************************************************************/
 package org.cryptomator.ui;
 
+import java.util.Comparator;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
+import javafx.application.Application;
 import javafx.util.Callback;
 
+import javax.inject.Named;
 import javax.inject.Singleton;
 
 import org.cryptomator.crypto.Cryptor;
 import org.cryptomator.crypto.SamplingDecorator;
 import org.cryptomator.crypto.aes256.Aes256Cryptor;
+import org.cryptomator.ui.MainApplication.MainApplicationReference;
 import org.cryptomator.ui.model.VaultFactory;
 import org.cryptomator.ui.model.VaultObjectMapperProvider;
 import org.cryptomator.ui.settings.Settings;
 import org.cryptomator.ui.settings.SettingsProvider;
 import org.cryptomator.ui.util.DeferredCloser;
 import org.cryptomator.ui.util.DeferredCloser.Closer;
+import org.cryptomator.ui.util.SemVerComparator;
 import org.cryptomator.ui.util.mount.WebDavMounter;
 import org.cryptomator.ui.util.mount.WebDavMounterProvider;
 import org.cryptomator.webdav.WebDavServer;
@@ -57,6 +62,24 @@ public class MainModule extends AbstractModule {
 		return cls -> injector.getInstance(cls);
 	}
 
+	@Provides
+	@Singleton
+	MainApplicationReference getApplicationBinding() {
+		return new MainApplicationReference();
+	}
+
+	@Provides
+	Application getApplication(MainApplicationReference ref) {
+		return ref.get();
+	}
+
+	@Provides
+	@Named("SemVer")
+	@Singleton
+	Comparator<String> getSemVerComparator() {
+		return new SemVerComparator();
+	}
+
 	@Provides
 	@Singleton
 	ExecutorService getExec() {

+ 83 - 0
main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java

@@ -8,22 +8,105 @@
  ******************************************************************************/
 package org.cryptomator.ui.controllers;
 
+import java.io.IOException;
 import java.net.URL;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.ResourceBundle;
+import java.util.concurrent.ExecutorService;
 
+import javafx.application.Application;
+import javafx.application.Platform;
+import javafx.event.ActionEvent;
 import javafx.fxml.FXML;
 import javafx.fxml.Initializable;
+import javafx.scene.control.Hyperlink;
 import javafx.scene.image.Image;
 import javafx.scene.image.ImageView;
 
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpMethod;
+import org.apache.commons.httpclient.HttpStatus;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.apache.commons.lang3.SystemUtils;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
 public class WelcomeController implements Initializable {
 
 	@FXML
 	private ImageView botImageView;
 
+	@FXML
+	private Hyperlink updateLink;
+
+	private final Application app;
+	private final Comparator<String> semVerComparator;
+	private final ExecutorService executor;
+	private ResourceBundle rb;
+
+	@Inject
+	public WelcomeController(Application app, @Named("SemVer") Comparator<String> semVerComparator, ExecutorService executor) {
+		this.app = app;
+		this.semVerComparator = semVerComparator;
+		this.executor = executor;
+	}
+
 	@Override
 	public void initialize(URL url, ResourceBundle rb) {
+		this.rb = rb;
 		this.botImageView.setImage(new Image(WelcomeController.class.getResource("/bot_welcome.png").toString()));
+		executor.execute(this::checkForUpdates);
+	}
+
+	private void checkForUpdates() {
+		final HttpClient client = new HttpClient();
+		final HttpMethod method = new GetMethod("https://cryptomator.org/downloads/latestVersion.json");
+		client.getParams().setConnectionManagerTimeout(5000);
+		try {
+			client.executeMethod(method);
+			if (method.getStatusCode() == HttpStatus.SC_OK) {
+				final byte[] responseData = method.getResponseBody();
+				final ObjectMapper mapper = new ObjectMapper();
+				final Map<String, String> map = mapper.readValue(responseData, new TypeReference<HashMap<String, String>>() {
+				});
+				this.compareVersions(map);
+			}
+		} catch (IOException e) {
+			// no error handling required. Maybe next time the version check is successful.
+		}
+	}
+
+	private void compareVersions(final Map<String, String> latestVersions) {
+		final String latestVersion;
+		if (SystemUtils.IS_OS_MAC_OSX) {
+			latestVersion = latestVersions.get("mac");
+		} else if (SystemUtils.IS_OS_WINDOWS) {
+			latestVersion = latestVersions.get("win");
+		} else if (SystemUtils.IS_OS_LINUX) {
+			latestVersion = latestVersions.get("linux");
+		} else {
+			// no version check possible on unsupported OS
+			return;
+		}
+		final String currentVersion = WelcomeController.class.getPackage().getImplementationVersion();
+		if (currentVersion != null && semVerComparator.compare(currentVersion, latestVersion) < 0) {
+			final String msg = String.format(rb.getString("welcome.newVersionMessage"), latestVersion, currentVersion);
+			Platform.runLater(() -> {
+				this.updateLink.setText(msg);
+				this.updateLink.setVisible(true);
+			});
+		}
+	}
+
+	@FXML
+	public void didClickUpdateLink(ActionEvent event) {
+		app.getHostServices().showDocument("https://cryptomator.org/#download");
 	}
 
 }

+ 34 - 0
main/ui/src/main/java/org/cryptomator/ui/util/SemVerComparator.java

@@ -0,0 +1,34 @@
+package org.cryptomator.ui.util;
+
+import java.util.Comparator;
+
+import org.apache.commons.lang3.StringUtils;
+
+public class SemVerComparator implements Comparator<String> {
+
+	@Override
+	public int compare(String version1, String version2) {
+		final String[] vComps1 = StringUtils.split(version1, '.');
+		final String[] vComps2 = StringUtils.split(version2, '.');
+		final int commonCompCount = Math.min(vComps1.length, vComps2.length);
+
+		for (int i = 0; i < commonCompCount; i++) {
+			int subversionComparisionResult = 0;
+			try {
+				final int v1 = Integer.parseInt(vComps1[i]);
+				final int v2 = Integer.parseInt(vComps2[i]);
+				subversionComparisionResult = v1 - v2;
+			} catch (NumberFormatException ex) {
+				// ok, lets compare this fragment lexicographically
+				subversionComparisionResult = vComps1[i].compareTo(vComps2[i]);
+			}
+			if (subversionComparisionResult != 0) {
+				return subversionComparisionResult;
+			}
+		}
+
+		// all in common so far? longest version string wins:
+		return vComps1.length - vComps2.length;
+	}
+
+}

+ 14 - 0
main/ui/src/main/resources/css/mac_theme.css

@@ -293,6 +293,20 @@
     -fx-text-fill: -fx-text-background-color;
 }
 
+/*******************************************************************************
+ *                                                                             *
+ * Hyperlink                                                                   *
+ *                                                                             *
+ ******************************************************************************/
+
+.hyperlink {
+    -fx-cursor: hand;
+    -fx-text-fill: #0069D9;
+}
+.hyperlink:hover {
+    -fx-underline: true;
+}
+
 /*******************************************************************************
  *                                                                             *
  * Button & ToggleButton                                                       *

+ 14 - 0
main/ui/src/main/resources/css/win_theme.css

@@ -295,6 +295,20 @@
     -fx-text-fill: -fx-text-background-color;
 }
 
+/*******************************************************************************
+ *                                                                             *
+ * Hyperlink                                                                   *
+ *                                                                             *
+ ******************************************************************************/
+
+.hyperlink {
+    -fx-cursor: hand;
+    -fx-text-fill: #3399FF;
+}
+.hyperlink:hover {
+    -fx-underline: true;
+}
+
 /*******************************************************************************
  *                                                                             *
  * Button & ToggleButton                                                       *

+ 3 - 1
main/ui/src/main/resources/fxml/welcome.fxml

@@ -16,11 +16,13 @@
 <?import javafx.scene.layout.AnchorPane?>
 <?import javafx.scene.control.Label?>
 <?import javafx.scene.image.ImageView?>
+<?import javafx.scene.control.Hyperlink?>
 
 <AnchorPane xmlns:fx="http://javafx.com/fxml" fx:controller="org.cryptomator.ui.controllers.WelcomeController">
 	
 	<children>
-		<Label AnchorPane.leftAnchor="100.0" AnchorPane.topAnchor="20.0" style="-fx-font-size: 1.5em;" text="%welcome.welcomeLabel"/>
+		<Label AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="20.0" prefWidth="400.0" alignment="CENTER" style="-fx-font-size: 1.5em;" text="%welcome.welcomeLabel"/>
+		<Hyperlink AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="50.0" prefWidth="400.0" alignment="CENTER" fx:id="updateLink" onAction="#didClickUpdateLink" visible="false"/>
 		
 		<ImageView fx:id="botImageView" AnchorPane.leftAnchor="100.0" AnchorPane.topAnchor="200.0" fitHeight="200.0" preserveRatio="true" smooth="false"/>
 		

+ 1 - 0
main/ui/src/main/resources/localization.properties

@@ -18,6 +18,7 @@ main.addDirectory.contextMenu.open=Add existing vault
 # welcome.fxml
 welcome.welcomeLabel=Welcome to Cryptomator
 welcome.addButtonInstructionLabel=Start by adding a new vault
+welcome.newVersionMessage=Version %s can be downloaded. This is %s.
 
 # initialize.fxml
 initialize.label.password=Password

+ 2 - 2
main/ui/src/test/java/org/cryptomator/ui/MainApplicationTest.java

@@ -1,12 +1,12 @@
 package org.cryptomator.ui;
 
-import static org.junit.Assert.*;
-
 import org.junit.Test;
 
 public class MainApplicationTest {
+
 	@Test
 	public void testInjection() throws Exception {
 		new MainApplication();
 	}
+
 }

+ 62 - 0
main/ui/src/test/java/org/cryptomator/ui/test/GuiceJUnitRunner.java

@@ -0,0 +1,62 @@
+package org.cryptomator.ui.test;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.InitializationError;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+
+/**
+ * Taken from http://fabiostrozzi.eu/2011/03/27/junit-tests-easy-guice/
+ */
+public class GuiceJUnitRunner extends BlockJUnit4ClassRunner {
+	private final Injector injector;
+
+	@Target(ElementType.TYPE)
+	@Retention(RetentionPolicy.RUNTIME)
+	@Inherited
+	public @interface GuiceModules {
+		Class<?>[] value();
+	}
+
+	@Override
+	public Object createTest() throws Exception {
+		Object obj = super.createTest();
+		injector.injectMembers(obj);
+		return obj;
+	}
+
+	public GuiceJUnitRunner(Class<?> klass) throws InitializationError {
+		super(klass);
+		Class<?>[] classes = getModulesFor(klass);
+		injector = createInjectorFor(classes);
+	}
+
+	private Injector createInjectorFor(Class<?>[] classes) throws InitializationError {
+		Module[] modules = new Module[classes.length];
+		for (int i = 0; i < classes.length; i++) {
+			try {
+				modules[i] = (Module) (classes[i]).newInstance();
+			} catch (InstantiationException e) {
+				throw new InitializationError(e);
+			} catch (IllegalAccessException e) {
+				throw new InitializationError(e);
+			}
+		}
+		return Guice.createInjector(modules);
+	}
+
+	private Class<?>[] getModulesFor(Class<?> klass) throws InitializationError {
+		GuiceModules annotation = klass.getAnnotation(GuiceModules.class);
+		if (annotation == null)
+			throw new InitializationError("Missing @GuiceModules annotation for unit test '" + klass.getName() + "'");
+		return annotation.value();
+	}
+}

+ 51 - 0
main/ui/src/test/java/org/cryptomator/ui/util/SemVerComparatorTest.java

@@ -0,0 +1,51 @@
+package org.cryptomator.ui.util;
+
+import java.util.Comparator;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.cryptomator.ui.MainModule;
+import org.cryptomator.ui.test.GuiceJUnitRunner;
+import org.cryptomator.ui.test.GuiceJUnitRunner.GuiceModules;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(GuiceJUnitRunner.class)
+@GuiceModules(MainModule.class)
+public class SemVerComparatorTest {
+
+	@Inject
+	@Named("SemVer")
+	private Comparator<String> semVerComparator;
+
+	// equal versions
+
+	@Test
+	public void compareEqualVersions() {
+		final int comparisonResult = semVerComparator.compare("1.23.4", "1.23.4");
+		Assert.assertEquals(0, Integer.signum(comparisonResult));
+	}
+
+	// newer versions in first argument
+
+	@Test
+	public void compareHigherToLowerVersions() {
+		Assert.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.5", "1.23.4")));
+		Assert.assertEquals(1, Integer.signum(semVerComparator.compare("1.24.4", "1.23.4")));
+		Assert.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4", "1.23")));
+		Assert.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4a", "1.23.4")));
+	}
+
+	// newer versions in second argument
+
+	@Test
+	public void compareLowerToHigherVersions() {
+		Assert.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4", "1.23.5")));
+		Assert.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4", "1.24.4")));
+		Assert.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23", "1.23.4")));
+		Assert.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4", "1.23.4a")));
+	}
+
+}