Improve type safety of the fluent interface of Overlay<T>

Refactor all the unchecked casts from Overlay<T> to T into a single
private cast() method. Also add a runtime type check to the constructor
to prevent creation of window objects of the form "A extends Overlay<B>"
for unrelated A & B, as such casts would then subvert the type system.
This commit is contained in:
Steven Barclay 2019-11-15 15:06:07 +00:00
parent 6b0da3d08b
commit 2f61b025f5
No known key found for this signature in database
GPG Key ID: 9FED6BF1176D500B
4 changed files with 100 additions and 73 deletions

View File

@ -29,14 +29,16 @@ import bisq.desktop.util.Transitions;
import bisq.core.app.BisqEnvironment;
import bisq.core.locale.GlobalSettings;
import bisq.core.locale.LanguageUtil;
import bisq.core.locale.Res;
import bisq.core.user.DontShowAgainLookup;
import bisq.core.locale.LanguageUtil;
import bisq.common.Timer;
import bisq.common.UserThread;
import bisq.common.util.Utilities;
import com.google.common.reflect.TypeToken;
import org.apache.commons.lang3.StringUtils;
import de.jensd.fx.fontawesome.AwesomeIcon;
@ -68,8 +70,8 @@ import javafx.scene.transform.Rotate;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.NodeOrientation;
import javafx.geometry.Pos;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
@ -93,7 +95,7 @@ import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public abstract class Overlay<T extends Overlay> {
public abstract class Overlay<T extends Overlay<T>> {
///////////////////////////////////////////////////////////////////////////////////////////
// Enum
@ -185,12 +187,23 @@ public abstract class Overlay<T extends Overlay> {
protected int maxChar = 1800;
private T cast() {
//noinspection unchecked
return (T) this;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Public API
///////////////////////////////////////////////////////////////////////////////////////////
public Overlay() {
//noinspection UnstableApiUsage
TypeToken<T> typeToken = new TypeToken<>(getClass()) {
};
if (!typeToken.isSupertypeOf(getClass())) {
throw new RuntimeException("Subclass of Overlay<T> should be castable to T");
}
}
public void show(boolean showAgainChecked) {
@ -266,26 +279,22 @@ public abstract class Overlay<T extends Overlay> {
public T onClose(Runnable closeHandler) {
this.closeHandlerOptional = Optional.of(closeHandler);
//noinspection unchecked
return (T) this;
return cast();
}
public T onAction(Runnable actionHandler) {
this.actionHandlerOptional = Optional.of(actionHandler);
//noinspection unchecked
return (T) this;
return cast();
}
public T onSecondaryAction(Runnable secondaryActionHandlerOptional) {
this.secondaryActionHandlerOptional = Optional.of(secondaryActionHandlerOptional);
//noinspection unchecked
return (T) this;
return cast();
}
public T headLine(String headLine) {
this.headLine = headLine;
//noinspection unchecked
return (T) this;
return cast();
}
public T notification(String message) {
@ -294,8 +303,7 @@ public abstract class Overlay<T extends Overlay> {
this.headLine = Res.get("popup.headline.notification");
this.message = message;
setTruncatedMessage();
//noinspection unchecked
return (T) this;
return cast();
}
public T instruction(String message) {
@ -304,8 +312,7 @@ public abstract class Overlay<T extends Overlay> {
this.headLine = Res.get("popup.headline.instruction");
this.message = message;
setTruncatedMessage();
//noinspection unchecked
return (T) this;
return cast();
}
public T attention(String message) {
@ -314,8 +321,7 @@ public abstract class Overlay<T extends Overlay> {
this.headLine = Res.get("popup.headline.attention");
this.message = message;
setTruncatedMessage();
//noinspection unchecked
return (T) this;
return cast();
}
public T backgroundInfo(String message) {
@ -324,8 +330,7 @@ public abstract class Overlay<T extends Overlay> {
this.headLine = Res.get("popup.headline.backgroundInfo");
this.message = message;
setTruncatedMessage();
//noinspection unchecked
return (T) this;
return cast();
}
public T feedback(String message) {
@ -334,8 +339,7 @@ public abstract class Overlay<T extends Overlay> {
this.headLine = Res.get("popup.headline.feedback");
this.message = message;
setTruncatedMessage();
//noinspection unchecked
return (T) this;
return cast();
}
public T confirmation(String message) {
@ -344,8 +348,7 @@ public abstract class Overlay<T extends Overlay> {
this.headLine = Res.get("popup.headline.confirmation");
this.message = message;
setTruncatedMessage();
//noinspection unchecked
return (T) this;
return cast();
}
public T information(String message) {
@ -354,8 +357,7 @@ public abstract class Overlay<T extends Overlay> {
this.headLine = Res.get("popup.headline.information");
this.message = message;
setTruncatedMessage();
//noinspection unchecked
return (T) this;
return cast();
}
public T warning(String message) {
@ -365,8 +367,7 @@ public abstract class Overlay<T extends Overlay> {
this.headLine = Res.get("popup.headline.warning");
this.message = message;
setTruncatedMessage();
//noinspection unchecked
return (T) this;
return cast();
}
public T error(String message) {
@ -377,136 +378,116 @@ public abstract class Overlay<T extends Overlay> {
this.headLine = Res.get("popup.headline.error");
this.message = message;
setTruncatedMessage();
//noinspection unchecked
return (T) this;
return cast();
}
@SuppressWarnings("UnusedReturnValue")
public T showReportErrorButtons() {
this.showReportErrorButtons = true;
//noinspection unchecked
return (T) this;
return cast();
}
public T message(String message) {
this.message = message;
setTruncatedMessage();
//noinspection unchecked
return (T) this;
return cast();
}
public T closeButtonText(String closeButtonText) {
this.closeButtonText = closeButtonText;
//noinspection unchecked
return (T) this;
return cast();
}
public T useReportBugButton() {
this.closeButtonText = Res.get("shared.reportBug");
this.closeHandlerOptional = Optional.of(() -> GUIUtil.openWebPage("https://bisq.network/source/bisq/issues"));
//noinspection unchecked
return (T) this;
return cast();
}
public T useIUnderstandButton() {
this.closeButtonText = Res.get("shared.iUnderstand");
//noinspection unchecked
return (T) this;
return cast();
}
public T actionButtonTextWithGoTo(String target) {
this.actionButtonText = Res.get("shared.goTo", Res.get(target));
//noinspection unchecked
return (T) this;
return cast();
}
public T secondaryActionButtonTextWithGoTo(String target) {
this.secondaryActionButtonText = Res.get("shared.goTo", Res.get(target));
//noinspection unchecked
return (T) this;
return cast();
}
public T closeButtonTextWithGoTo(String target) {
this.closeButtonText = Res.get("shared.goTo", Res.get(target));
//noinspection unchecked
return (T) this;
return cast();
}
public T actionButtonText(String actionButtonText) {
this.actionButtonText = actionButtonText;
//noinspection unchecked
return (T) this;
return cast();
}
public T secondaryActionButtonText(String secondaryActionButtonText) {
this.secondaryActionButtonText = secondaryActionButtonText;
//noinspection unchecked
return (T) this;
return cast();
}
public T useShutDownButton() {
this.actionButtonText = Res.get("shared.shutDown");
this.actionHandlerOptional = Optional.ofNullable(BisqApp.getShutDownHandler());
//noinspection unchecked
return (T) this;
return cast();
}
public T buttonAlignment(HPos pos) {
this.buttonAlignment = pos;
return (T) this;
return cast();
}
public T width(double width) {
this.width = width;
//noinspection unchecked
return (T) this;
return cast();
}
public T maxMessageLength(int maxChar) {
this.maxChar = maxChar;
return (T) this;
return cast();
}
public T showBusyAnimation() {
this.showBusyAnimation = true;
//noinspection unchecked
return (T) this;
return cast();
}
public T dontShowAgainId(String key) {
this.dontShowAgainId = key;
//noinspection unchecked
return (T) this;
return cast();
}
public T dontShowAgainText(String dontShowAgainText) {
this.dontShowAgainText = dontShowAgainText;
//noinspection unchecked
return (T) this;
return cast();
}
public T hideCloseButton() {
this.hideCloseButton = true;
//noinspection unchecked
return (T) this;
return cast();
}
public T useAnimation(boolean useAnimation) {
this.useAnimation = useAnimation;
//noinspection unchecked
return (T) this;
return cast();
}
public T setHeadlineStyle(String headlineStyle) {
this.headlineStyle = headlineStyle;
//noinspection unchecked
return (T) this;
return cast();
}
public T disableActionButton() {
this.disableActionButton = true;
//noinspection unchecked
return (T) this;
return cast();
}

View File

@ -4,7 +4,7 @@ import com.jfoenix.controls.JFXTabPane;
import javafx.scene.layout.Region;
public abstract class TabbedOverlay<T extends TabbedOverlay> extends Overlay {
public abstract class TabbedOverlay<T extends TabbedOverlay<T>> extends Overlay<T> {
protected JFXTabPane tabPane;

View File

@ -22,7 +22,8 @@ import bisq.desktop.main.overlays.Overlay;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Popup<T extends Overlay> extends Overlay<Popup> {
// TODO: Type parameter is unused - remove:
public class Popup<T> extends Overlay<Popup<T>> {
protected final Logger log = LoggerFactory.getLogger(this.getClass());
public Popup() {
@ -41,6 +42,4 @@ public class Popup<T extends Overlay> extends Overlay<Popup> {
protected void onHidden() {
PopupManager.onHidden(this);
}
}

View File

@ -0,0 +1,47 @@
/*
* This file is part of Bisq.
*
* Bisq 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.
*
* Bisq 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 Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.desktop.main.overlays;
import org.junit.Test;
public class OverlayTest {
@Test
public void typeSafeCreation() {
new A();
new C();
new D<>();
}
@Test(expected = RuntimeException.class)
public void typeUnsafeCreation() {
new B();
}
private static class A extends Overlay<A> {
}
private static class B extends Overlay<A> {
}
private static class C extends TabbedOverlay<C> {
}
private static class D<T> extends Overlay<D<T>> {
}
}