diff --git a/build.gradle b/build.gradle index fa3606ee32..aa82ce434e 100644 --- a/build.gradle +++ b/build.gradle @@ -17,6 +17,7 @@ version = '0.1.0-SNAPSHOT' sourceCompatibility = 1.8 sourceSets.main.resources.srcDirs += 'src/main/java' +sourceSets.test.resources.srcDirs += 'src/test/java' mainClassName = "io.bitsquare.app.gui.BitsquareAppMain" @@ -58,6 +59,7 @@ dependencies { compile 'org.jetbrains:annotations:13.0' compile 'eu.hansolo.enzo:Enzo:0.1.5' testCompile 'junit:junit:4.11' + testCompile "org.mockito:mockito-core:1.+" testCompile 'org.springframework:spring-test:4.1.1.RELEASE' } diff --git a/src/main/java/viewfx/ViewfxException.java b/src/main/java/viewfx/ViewfxException.java index 2b1a72f019..b837d500ac 100644 --- a/src/main/java/viewfx/ViewfxException.java +++ b/src/main/java/viewfx/ViewfxException.java @@ -17,9 +17,15 @@ package viewfx; +import static java.lang.String.format; + public class ViewfxException extends RuntimeException { public ViewfxException(Throwable cause, String format, Object... args) { - super(String.format(format, args), cause); + super(format(format, args), cause); + } + + public ViewfxException(String format, Object... args) { + super(format(format, args)); } } diff --git a/src/main/java/viewfx/view/DefaultPathConvention.java b/src/main/java/viewfx/view/DefaultPathConvention.java new file mode 100644 index 0000000000..cd7b17bc1e --- /dev/null +++ b/src/main/java/viewfx/view/DefaultPathConvention.java @@ -0,0 +1,27 @@ +/* + * This file is part of Bitsquare. + * + * Bitsquare is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bitsquare is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bitsquare. If not, see . + */ + +package viewfx.view; + +import org.springframework.util.ClassUtils; + +public class DefaultPathConvention implements FxmlView.PathConvention { + @Override + public String apply(Class viewClass) { + return ClassUtils.convertClassNameToResourcePath(viewClass.getName()).concat(".fxml"); + } +} diff --git a/src/main/java/viewfx/view/FxmlView.java b/src/main/java/viewfx/view/FxmlView.java index b8c046c55c..56d9f5c1a1 100644 --- a/src/main/java/viewfx/view/FxmlView.java +++ b/src/main/java/viewfx/view/FxmlView.java @@ -23,7 +23,6 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.springframework.util.ClassUtils; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @@ -40,12 +39,8 @@ public @interface FxmlView { * By default it is the fully-qualified view class name, converted to a resource path, replacing the * {@code .class} suffix replaced with {@code .fxml}. */ - Class, String>> convention() default DefaultFxmlPathConvention.class; + Class convention() default DefaultPathConvention.class; - static class DefaultFxmlPathConvention implements Function, String> { - @Override - public String apply(Class viewClass) { - return ClassUtils.convertClassNameToResourcePath(viewClass.getName()).concat(".fxml"); - } + static interface PathConvention extends Function, String> { } } diff --git a/src/main/java/viewfx/view/support/fxml/FxmlViewLoader.java b/src/main/java/viewfx/view/support/fxml/FxmlViewLoader.java index 997a894dc1..cad8c8bb68 100644 --- a/src/main/java/viewfx/view/support/fxml/FxmlViewLoader.java +++ b/src/main/java/viewfx/view/support/fxml/FxmlViewLoader.java @@ -33,7 +33,13 @@ import viewfx.view.ViewLoader; import javafx.fxml.FXMLLoader; +import java.lang.reflect.Constructor; +import org.springframework.cglib.core.ReflectUtils; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.ReflectionUtils; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.springframework.core.annotation.AnnotationUtils.getDefaultValue; public class FxmlViewLoader implements ViewLoader { @@ -46,30 +52,70 @@ public class FxmlViewLoader implements ViewLoader { this.resourceBundle = resourceBundle; } + @SuppressWarnings("unchecked") public View load(Class clazz) { if (!View.class.isAssignableFrom(clazz)) throw new IllegalArgumentException("Class must be of generic type Class: " + clazz); - @SuppressWarnings("unchecked") Class viewClass = (Class) clazz; FxmlView fxmlView = AnnotationUtils.getAnnotation(viewClass, FxmlView.class); + + final Class convention; + final Class defaultConvention = + (Class) getDefaultValue(FxmlView.class, "convention"); + + final String specifiedLocation; + final String defaultLocation = (String) getDefaultValue(FxmlView.class, "location"); + + if (fxmlView == null) { + convention = defaultConvention; + specifiedLocation = defaultLocation; + } + else { + convention = fxmlView.convention(); + specifiedLocation = fxmlView.location(); + } + + if (convention == null || specifiedLocation == null) + throw new IllegalStateException("Convention and location should never be null."); + + try { - String path = fxmlView.convention().newInstance().apply(viewClass); - return load(viewClass.getClassLoader().getResource(path)); + final String resolvedLocation; + if (specifiedLocation.equals(defaultLocation)) + resolvedLocation = convention.newInstance().apply(viewClass); + else + resolvedLocation = specifiedLocation; + + URL fxmlUrl = viewClass.getClassLoader().getResource(resolvedLocation); + if (fxmlUrl == null) + throw new ViewfxException( + "Failed to load view class [%s] because FXML file at [%s] could not be loaded " + + "as a classpath resource. Does it exist?", viewClass, specifiedLocation); + + return load(fxmlUrl); } catch (InstantiationException | IllegalAccessException ex) { - throw new ViewfxException(ex, "Failed to load View from class %s", viewClass); + throw new ViewfxException(ex, "Failed to load view from class %s", viewClass); } } public View load(URL url) { + checkNotNull(url, "FXML URL must not be null"); try { FXMLLoader loader = new FXMLLoader(url, resourceBundle); loader.setControllerFactory(viewFactory); loader.load(); - return loader.getController(); + Object controller = loader.getController(); + if (controller == null) + throw new ViewfxException("Failed to load view from FXML file at [%s]. " + + "Does it declare an fx:controller attribute?", url); + if (!(controller instanceof View)) + throw new ViewfxException("Controller of type [%s] loaded from FXML file at [%s] " + + "does not implement [%s] as expected.", controller.getClass(), url, View.class); + return (View) controller; } catch (IOException ex) { - throw new ViewfxException(ex, "Failed to load View at location %s", url); + throw new ViewfxException(ex, "Failed to load view from FXML file at [%s]", url); } } diff --git a/src/test/java/viewfx/view/support/fxml/FxmlViewLoaderTests$Malformed.fxml b/src/test/java/viewfx/view/support/fxml/FxmlViewLoaderTests$Malformed.fxml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/test/java/viewfx/view/support/fxml/FxmlViewLoaderTests$MissingFxController.fxml b/src/test/java/viewfx/view/support/fxml/FxmlViewLoaderTests$MissingFxController.fxml new file mode 100644 index 0000000000..cd9e284182 --- /dev/null +++ b/src/test/java/viewfx/view/support/fxml/FxmlViewLoaderTests$MissingFxController.fxml @@ -0,0 +1,20 @@ + + + + + diff --git a/src/test/java/viewfx/view/support/fxml/FxmlViewLoaderTests$MissingFxmlViewAnnotation.fxml b/src/test/java/viewfx/view/support/fxml/FxmlViewLoaderTests$MissingFxmlViewAnnotation.fxml new file mode 100644 index 0000000000..e868889b54 --- /dev/null +++ b/src/test/java/viewfx/view/support/fxml/FxmlViewLoaderTests$MissingFxmlViewAnnotation.fxml @@ -0,0 +1,21 @@ + + + + + diff --git a/src/test/java/viewfx/view/support/fxml/FxmlViewLoaderTests$WellFormed.fxml b/src/test/java/viewfx/view/support/fxml/FxmlViewLoaderTests$WellFormed.fxml new file mode 100644 index 0000000000..06a3949e56 --- /dev/null +++ b/src/test/java/viewfx/view/support/fxml/FxmlViewLoaderTests$WellFormed.fxml @@ -0,0 +1,21 @@ + + + + + diff --git a/src/test/java/viewfx/view/support/fxml/FxmlViewLoaderTests.java b/src/test/java/viewfx/view/support/fxml/FxmlViewLoaderTests.java new file mode 100644 index 0000000000..bbd482e221 --- /dev/null +++ b/src/test/java/viewfx/view/support/fxml/FxmlViewLoaderTests.java @@ -0,0 +1,142 @@ +/* + * This file is part of Bitsquare. + * + * Bitsquare is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bitsquare is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bitsquare. If not, see . + */ + +package viewfx.view.support.fxml; + +import java.util.ResourceBundle; + +import viewfx.ViewfxException; +import viewfx.view.FxmlView; +import viewfx.view.View; +import viewfx.view.ViewFactory; +import viewfx.view.ViewLoader; +import viewfx.view.support.AbstractView; + +import javafx.fxml.LoadException; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.contains; +import static org.mockito.Mockito.mock; + +public class FxmlViewLoaderTests { + + private ViewLoader viewLoader; + private ViewFactory viewFactory; + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Before + public void setUp() { + viewFactory = mock(ViewFactory.class); + ResourceBundle resourceBundle = mock(ResourceBundle.class); + viewLoader = new FxmlViewLoader(viewFactory, resourceBundle); + } + + + @FxmlView + static class WellFormed extends AbstractView { + } + + @Test + public void wellFormedFxmlFileShouldSucceed() { + given(viewFactory.call(WellFormed.class)).willReturn(new WellFormed()); + View view = viewLoader.load(WellFormed.class); + assertThat(view, instanceOf(WellFormed.class)); + } + + + @FxmlView + static class MissingFxController extends AbstractView { + } + + @Test + public void fxmlFileMissingFxControllerAttributeShouldThrow() { + thrown.expect(ViewfxException.class); + thrown.expectMessage("Does it declare an fx:controller attribute?"); + viewLoader.load(MissingFxController.class); + } + + + static class MissingFxmlViewAnnotation extends AbstractView { + } + + @Test + public void fxmlViewAnnotationShouldBeOptional() { + given(viewFactory.call(MissingFxmlViewAnnotation.class)).willReturn(new MissingFxmlViewAnnotation()); + View view = viewLoader.load(MissingFxmlViewAnnotation.class); + assertThat(view, instanceOf(MissingFxmlViewAnnotation.class)); + } + + + static class NonView { + } + + @Test + public void nonViewClassShouldThrow() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Class must be of generic type"); + viewLoader.load(NonView.class); + } + + + @FxmlView + static class Malformed extends AbstractView { + } + + @Test + public void malformedFxmlFileShouldThrow() { + thrown.expect(ViewfxException.class); + thrown.expectMessage("Failed to load view from FXML file"); + thrown.expectCause(instanceOf(LoadException.class)); + viewLoader.load(Malformed.class); + } + + + @FxmlView + static class MissingFxmlFile extends AbstractView { + } + + @Test + public void missingFxmlFileShouldThrow() { + thrown.expect(ViewfxException.class); + thrown.expectMessage("Does it exist?"); + viewLoader.load(MissingFxmlFile.class); + } + + + @FxmlView(location = "unconventionally/located.fxml") + static class CustomLocation extends AbstractView { + } + + @Test + public void customFxmlFileLocationShouldOverrideDefaultConvention() { + thrown.expect(ViewfxException.class); + thrown.expectMessage("Failed to load view class"); + thrown.expectMessage("CustomLocation"); + thrown.expectMessage("[unconventionally/located.fxml] could not be loaded"); + viewLoader.load(CustomLocation.class); + } +} +