diff --git a/pom.xml b/pom.xml index a0a822a03..7192afa72 100644 --- a/pom.xml +++ b/pom.xml @@ -59,6 +59,7 @@ webfx-stack-authn-login-ui-gateway webfx-stack-authn-login-ui-gateway-facebook-plugin webfx-stack-authn-login-ui-gateway-google-plugin + webfx-stack-authn-login-ui-gateway-magiclink-plugin webfx-stack-authn-login-ui-gateway-mojoauth-plugin webfx-stack-authn-login-ui-gateway-password-plugin webfx-stack-authn-login-ui-gateway-webviewbased @@ -134,7 +135,9 @@ webfx-stack-orm-dql-querypush-interceptor webfx-stack-orm-dql-submit-interceptor webfx-stack-orm-entity + webfx-stack-orm-entity-binding webfx-stack-orm-entity-controls + webfx-stack-orm-entity-messaging webfx-stack-orm-expression webfx-stack-orm-reactive-call webfx-stack-orm-reactive-dql @@ -159,6 +162,7 @@ webfx-stack-session-state-server webfx-stack-session-vertx webfx-stack-ui-action + webfx-stack-ui-action-tuner webfx-stack-ui-controls webfx-stack-ui-dialog webfx-stack-ui-exceptions diff --git a/webfx-stack-authn-login-server-gateway-facebook-plugin/src/main/java/dev/webfx/stack/authn/login/spi/impl/server/gateway/facebook/FacebookServerLoginGatewayProvider.java b/webfx-stack-authn-login-server-gateway-facebook-plugin/src/main/java/dev/webfx/stack/authn/login/spi/impl/server/gateway/facebook/FacebookServerLoginGateway.java similarity index 99% rename from webfx-stack-authn-login-server-gateway-facebook-plugin/src/main/java/dev/webfx/stack/authn/login/spi/impl/server/gateway/facebook/FacebookServerLoginGatewayProvider.java rename to webfx-stack-authn-login-server-gateway-facebook-plugin/src/main/java/dev/webfx/stack/authn/login/spi/impl/server/gateway/facebook/FacebookServerLoginGateway.java index 028313fb5..6dd90c43e 100644 --- a/webfx-stack-authn-login-server-gateway-facebook-plugin/src/main/java/dev/webfx/stack/authn/login/spi/impl/server/gateway/facebook/FacebookServerLoginGatewayProvider.java +++ b/webfx-stack-authn-login-server-gateway-facebook-plugin/src/main/java/dev/webfx/stack/authn/login/spi/impl/server/gateway/facebook/FacebookServerLoginGateway.java @@ -5,7 +5,7 @@ import dev.webfx.platform.conf.ConfigLoader; import dev.webfx.platform.console.Console; import dev.webfx.stack.authn.AuthenticationService; -import dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGatewayProvider; +import dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGateway; import dev.webfx.stack.push.server.PushServerService; import dev.webfx.stack.routing.router.Router; import dev.webfx.stack.routing.router.RoutingContext; @@ -17,7 +17,7 @@ /** * @author Bruno Salmon */ -public class FacebookServerLoginGatewayProvider implements ServerLoginGatewayProvider { +public class FacebookServerLoginGateway implements ServerLoginGateway { private final static String CONFIG_PATH = "webfx.stack.authn.server.facebook"; private final static String CLIENT_ID_CONF_KEY = "clientId"; diff --git a/webfx-stack-authn-login-server-gateway-facebook-plugin/src/main/java/module-info.java b/webfx-stack-authn-login-server-gateway-facebook-plugin/src/main/java/module-info.java index daf564624..9d8f44bab 100644 --- a/webfx-stack-authn-login-server-gateway-facebook-plugin/src/main/java/module-info.java +++ b/webfx-stack-authn-login-server-gateway-facebook-plugin/src/main/java/module-info.java @@ -18,6 +18,6 @@ exports dev.webfx.stack.authn.login.spi.impl.server.gateway.facebook; // Provided services - provides dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGatewayProvider with dev.webfx.stack.authn.login.spi.impl.server.gateway.facebook.FacebookServerLoginGatewayProvider; + provides dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGateway with dev.webfx.stack.authn.login.spi.impl.server.gateway.facebook.FacebookServerLoginGateway; } \ No newline at end of file diff --git a/webfx-stack-authn-login-server-gateway-facebook-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGatewayProvider b/webfx-stack-authn-login-server-gateway-facebook-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGateway similarity index 66% rename from webfx-stack-authn-login-server-gateway-facebook-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGatewayProvider rename to webfx-stack-authn-login-server-gateway-facebook-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGateway index 1b3d52c51..25c2fc301 100644 --- a/webfx-stack-authn-login-server-gateway-facebook-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGatewayProvider +++ b/webfx-stack-authn-login-server-gateway-facebook-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGateway @@ -1 +1 @@ -dev.webfx.stack.authn.login.spi.impl.server.gateway.facebook.FacebookServerLoginGatewayProvider +dev.webfx.stack.authn.login.spi.impl.server.gateway.facebook.FacebookServerLoginGateway diff --git a/webfx-stack-authn-login-server-gateway-facebook-plugin/webfx.xml b/webfx-stack-authn-login-server-gateway-facebook-plugin/webfx.xml index 3d4cdbefc..c1940df56 100644 --- a/webfx-stack-authn-login-server-gateway-facebook-plugin/webfx.xml +++ b/webfx-stack-authn-login-server-gateway-facebook-plugin/webfx.xml @@ -11,7 +11,7 @@ - dev.webfx.stack.authn.login.spi.impl.server.gateway.facebook.FacebookServerLoginGatewayProvider + dev.webfx.stack.authn.login.spi.impl.server.gateway.facebook.FacebookServerLoginGateway diff --git a/webfx-stack-authn-login-server-gateway-google-plugin/src/main/java/dev/webfx/stack/authn/login/spi/impl/server/gateway/google/GoogleServerLoginGatewayProvider.java b/webfx-stack-authn-login-server-gateway-google-plugin/src/main/java/dev/webfx/stack/authn/login/spi/impl/server/gateway/google/GoogleServerLoginGateway.java similarity index 98% rename from webfx-stack-authn-login-server-gateway-google-plugin/src/main/java/dev/webfx/stack/authn/login/spi/impl/server/gateway/google/GoogleServerLoginGatewayProvider.java rename to webfx-stack-authn-login-server-gateway-google-plugin/src/main/java/dev/webfx/stack/authn/login/spi/impl/server/gateway/google/GoogleServerLoginGateway.java index 54cce4382..f7299265a 100644 --- a/webfx-stack-authn-login-server-gateway-google-plugin/src/main/java/dev/webfx/stack/authn/login/spi/impl/server/gateway/google/GoogleServerLoginGatewayProvider.java +++ b/webfx-stack-authn-login-server-gateway-google-plugin/src/main/java/dev/webfx/stack/authn/login/spi/impl/server/gateway/google/GoogleServerLoginGateway.java @@ -5,7 +5,7 @@ import dev.webfx.platform.conf.ConfigLoader; import dev.webfx.platform.console.Console; import dev.webfx.stack.authn.AuthenticationService; -import dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGatewayProvider; +import dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGateway; import dev.webfx.stack.authn.oauth2.OAuth2Service; import dev.webfx.stack.push.server.PushServerService; import dev.webfx.stack.routing.router.Router; @@ -18,7 +18,7 @@ /** * @author Bruno Salmon */ -public class GoogleServerLoginGatewayProvider implements ServerLoginGatewayProvider { +public class GoogleServerLoginGateway implements ServerLoginGateway { private final static String CONFIG_PATH = "webfx.stack.authn.server.google"; private final static String CLIENT_ID_CONF_KEY = "clientId"; diff --git a/webfx-stack-authn-login-server-gateway-google-plugin/src/main/java/module-info.java b/webfx-stack-authn-login-server-gateway-google-plugin/src/main/java/module-info.java index 57966ad5f..90f8f450d 100644 --- a/webfx-stack-authn-login-server-gateway-google-plugin/src/main/java/module-info.java +++ b/webfx-stack-authn-login-server-gateway-google-plugin/src/main/java/module-info.java @@ -19,6 +19,6 @@ exports dev.webfx.stack.authn.login.spi.impl.server.gateway.google; // Provided services - provides dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGatewayProvider with dev.webfx.stack.authn.login.spi.impl.server.gateway.google.GoogleServerLoginGatewayProvider; + provides dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGateway with dev.webfx.stack.authn.login.spi.impl.server.gateway.google.GoogleServerLoginGateway; } \ No newline at end of file diff --git a/webfx-stack-authn-login-server-gateway-google-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGatewayProvider b/webfx-stack-authn-login-server-gateway-google-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGateway similarity index 69% rename from webfx-stack-authn-login-server-gateway-google-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGatewayProvider rename to webfx-stack-authn-login-server-gateway-google-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGateway index 0c3ea77e6..11e54f453 100644 --- a/webfx-stack-authn-login-server-gateway-google-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGatewayProvider +++ b/webfx-stack-authn-login-server-gateway-google-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGateway @@ -1 +1 @@ -dev.webfx.stack.authn.login.spi.impl.server.gateway.google.GoogleServerLoginGatewayProvider +dev.webfx.stack.authn.login.spi.impl.server.gateway.google.GoogleServerLoginGateway diff --git a/webfx-stack-authn-login-server-gateway-google-plugin/webfx.xml b/webfx-stack-authn-login-server-gateway-google-plugin/webfx.xml index 5eef97a5d..0a66202c8 100644 --- a/webfx-stack-authn-login-server-gateway-google-plugin/webfx.xml +++ b/webfx-stack-authn-login-server-gateway-google-plugin/webfx.xml @@ -11,7 +11,7 @@ - dev.webfx.stack.authn.login.spi.impl.server.gateway.google.GoogleServerLoginGatewayProvider + dev.webfx.stack.authn.login.spi.impl.server.gateway.google.GoogleServerLoginGateway diff --git a/webfx-stack-authn-login-server-gateway-mojoauth-plugin/src/main/java/dev/webfx/stack/authn/login/spi/impl/server/gateway/mojoauth/MojoAuthServerLoginGatewayProvider.java b/webfx-stack-authn-login-server-gateway-mojoauth-plugin/src/main/java/dev/webfx/stack/authn/login/spi/impl/server/gateway/mojoauth/MojoAuthServerLoginGateway.java similarity index 98% rename from webfx-stack-authn-login-server-gateway-mojoauth-plugin/src/main/java/dev/webfx/stack/authn/login/spi/impl/server/gateway/mojoauth/MojoAuthServerLoginGatewayProvider.java rename to webfx-stack-authn-login-server-gateway-mojoauth-plugin/src/main/java/dev/webfx/stack/authn/login/spi/impl/server/gateway/mojoauth/MojoAuthServerLoginGateway.java index 81129d8e1..406659d2b 100644 --- a/webfx-stack-authn-login-server-gateway-mojoauth-plugin/src/main/java/dev/webfx/stack/authn/login/spi/impl/server/gateway/mojoauth/MojoAuthServerLoginGatewayProvider.java +++ b/webfx-stack-authn-login-server-gateway-mojoauth-plugin/src/main/java/dev/webfx/stack/authn/login/spi/impl/server/gateway/mojoauth/MojoAuthServerLoginGateway.java @@ -6,7 +6,7 @@ import dev.webfx.platform.console.Console; import dev.webfx.platform.util.tuples.Pair; import dev.webfx.stack.authn.AuthenticationService; -import dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGatewayProvider; +import dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGateway; import dev.webfx.stack.push.server.PushServerService; import dev.webfx.stack.routing.router.Router; import dev.webfx.stack.routing.router.RoutingContext; @@ -19,7 +19,7 @@ /** * @author Bruno Salmon */ -public class MojoAuthServerLoginGatewayProvider implements ServerLoginGatewayProvider { +public class MojoAuthServerLoginGateway implements ServerLoginGateway { private final static String CONFIG_PATH = "webfx.stack.authn.server.mojoauth"; private final static String API_KEY_CONF_KEY = "apiKey"; diff --git a/webfx-stack-authn-login-server-gateway-mojoauth-plugin/src/main/java/module-info.java b/webfx-stack-authn-login-server-gateway-mojoauth-plugin/src/main/java/module-info.java index 10119e0de..f5f05e3e9 100644 --- a/webfx-stack-authn-login-server-gateway-mojoauth-plugin/src/main/java/module-info.java +++ b/webfx-stack-authn-login-server-gateway-mojoauth-plugin/src/main/java/module-info.java @@ -19,6 +19,6 @@ exports dev.webfx.stack.authn.login.spi.impl.server.gateway.mojoauth; // Provided services - provides dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGatewayProvider with dev.webfx.stack.authn.login.spi.impl.server.gateway.mojoauth.MojoAuthServerLoginGatewayProvider; + provides dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGateway with dev.webfx.stack.authn.login.spi.impl.server.gateway.mojoauth.MojoAuthServerLoginGateway; } \ No newline at end of file diff --git a/webfx-stack-authn-login-server-gateway-mojoauth-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGatewayProvider b/webfx-stack-authn-login-server-gateway-mojoauth-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGateway similarity index 66% rename from webfx-stack-authn-login-server-gateway-mojoauth-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGatewayProvider rename to webfx-stack-authn-login-server-gateway-mojoauth-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGateway index 752be2a47..14b0d2823 100644 --- a/webfx-stack-authn-login-server-gateway-mojoauth-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGatewayProvider +++ b/webfx-stack-authn-login-server-gateway-mojoauth-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGateway @@ -1 +1 @@ -dev.webfx.stack.authn.login.spi.impl.server.gateway.mojoauth.MojoAuthServerLoginGatewayProvider +dev.webfx.stack.authn.login.spi.impl.server.gateway.mojoauth.MojoAuthServerLoginGateway diff --git a/webfx-stack-authn-login-server-gateway-mojoauth-plugin/webfx.xml b/webfx-stack-authn-login-server-gateway-mojoauth-plugin/webfx.xml index daf6424b5..ecefa032c 100644 --- a/webfx-stack-authn-login-server-gateway-mojoauth-plugin/webfx.xml +++ b/webfx-stack-authn-login-server-gateway-mojoauth-plugin/webfx.xml @@ -11,7 +11,7 @@ - dev.webfx.stack.authn.login.spi.impl.server.gateway.mojoauth.MojoAuthServerLoginGatewayProvider + dev.webfx.stack.authn.login.spi.impl.server.gateway.mojoauth.MojoAuthServerLoginGateway diff --git a/webfx-stack-authn-login-server-gateway/src/main/java/dev/webfx/stack/authn/login/spi/impl/server/gateway/ServerLoginGatewayProvider.java b/webfx-stack-authn-login-server-gateway/src/main/java/dev/webfx/stack/authn/login/spi/impl/server/gateway/ServerLoginGateway.java similarity index 83% rename from webfx-stack-authn-login-server-gateway/src/main/java/dev/webfx/stack/authn/login/spi/impl/server/gateway/ServerLoginGatewayProvider.java rename to webfx-stack-authn-login-server-gateway/src/main/java/dev/webfx/stack/authn/login/spi/impl/server/gateway/ServerLoginGateway.java index 5edd932c7..266bd531c 100644 --- a/webfx-stack-authn-login-server-gateway/src/main/java/dev/webfx/stack/authn/login/spi/impl/server/gateway/ServerLoginGatewayProvider.java +++ b/webfx-stack-authn-login-server-gateway/src/main/java/dev/webfx/stack/authn/login/spi/impl/server/gateway/ServerLoginGateway.java @@ -5,7 +5,7 @@ /** * @author Bruno Salmon */ -public interface ServerLoginGatewayProvider { +public interface ServerLoginGateway { void boot(); diff --git a/webfx-stack-authn-login-server-portal/src/main/java/dev/webfx/stack/authn/login/spi/impl/server/portal/ServerLoginPortalProvider.java b/webfx-stack-authn-login-server-portal/src/main/java/dev/webfx/stack/authn/login/spi/impl/server/portal/ServerLoginPortalProvider.java index 4a27a451f..02f662473 100644 --- a/webfx-stack-authn-login-server-portal/src/main/java/dev/webfx/stack/authn/login/spi/impl/server/portal/ServerLoginPortalProvider.java +++ b/webfx-stack-authn-login-server-portal/src/main/java/dev/webfx/stack/authn/login/spi/impl/server/portal/ServerLoginPortalProvider.java @@ -4,7 +4,7 @@ import dev.webfx.platform.service.MultipleServiceProviders; import dev.webfx.stack.authn.login.LoginUiContext; import dev.webfx.stack.authn.login.spi.LoginServiceProvider; -import dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGatewayProvider; +import dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGateway; import java.util.List; import java.util.Objects; @@ -15,23 +15,23 @@ */ public class ServerLoginPortalProvider implements LoginServiceProvider { - private static List getGatewayProviders() { - return MultipleServiceProviders.getProviders(ServerLoginGatewayProvider.class, () -> ServiceLoader.load(ServerLoginGatewayProvider.class)); + private static List getGateways() { + return MultipleServiceProviders.getProviders(ServerLoginGateway.class, () -> ServiceLoader.load(ServerLoginGateway.class)); } public ServerLoginPortalProvider() { // Called first time on server start through LoginService.getProvider() call in GetLoginUiInputMethodEndpoint. // We instantiate the gateways (such as Google, Facebook, etc...) and call their boot() method, which they will // probably use to register their callback route (must be done as soon as possible, i.e. on server start). - for (ServerLoginGatewayProvider gatewayProvider : getGatewayProviders()) - gatewayProvider.boot(); + for (ServerLoginGateway gateway : getGateways()) + gateway.boot(); } @Override public Future getLoginUiInput(LoginUiContext loginUiContext) { Object gatewayId = loginUiContext.getGatewayId(); - for (ServerLoginGatewayProvider gatewayProvider : getGatewayProviders()) { - if (Objects.equals(gatewayProvider.getGatewayId(), gatewayId)) - return gatewayProvider.getLoginUiInput(loginUiContext.getGatewayContext()); + for (ServerLoginGateway gateway : getGateways()) { + if (Objects.equals(gateway.getGatewayId(), gatewayId)) + return gateway.getLoginUiInput(loginUiContext.getGatewayContext()); } return Future.failedFuture("No server login gateway found with id='" + gatewayId + "'"); } diff --git a/webfx-stack-authn-login-server-portal/src/main/java/module-info.java b/webfx-stack-authn-login-server-portal/src/main/java/module-info.java index f58273f62..b5fd05f24 100644 --- a/webfx-stack-authn-login-server-portal/src/main/java/module-info.java +++ b/webfx-stack-authn-login-server-portal/src/main/java/module-info.java @@ -12,7 +12,7 @@ exports dev.webfx.stack.authn.login.spi.impl.server.portal; // Used services - uses dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGatewayProvider; + uses dev.webfx.stack.authn.login.spi.impl.server.gateway.ServerLoginGateway; // Provided services provides dev.webfx.stack.authn.login.spi.LoginServiceProvider with dev.webfx.stack.authn.login.spi.impl.server.portal.ServerLoginPortalProvider; diff --git a/webfx-stack-authn-login-ui-gateway-facebook-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/facebook/FacebookUiLoginGatewayProvider.java b/webfx-stack-authn-login-ui-gateway-facebook-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/facebook/FacebookUiLoginGateway.java similarity index 76% rename from webfx-stack-authn-login-ui-gateway-facebook-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/facebook/FacebookUiLoginGatewayProvider.java rename to webfx-stack-authn-login-ui-gateway-facebook-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/facebook/FacebookUiLoginGateway.java index 67e07a992..92507cc6a 100644 --- a/webfx-stack-authn-login-ui-gateway-facebook-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/facebook/FacebookUiLoginGatewayProvider.java +++ b/webfx-stack-authn-login-ui-gateway-facebook-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/facebook/FacebookUiLoginGateway.java @@ -1,18 +1,18 @@ package dev.webfx.stack.authn.login.ui.spi.impl.gateway.facebook; import dev.webfx.platform.resource.Resource; -import dev.webfx.stack.authn.login.ui.spi.impl.gateway.webview.WebViewBasedUiLoginGatewayProvider; +import dev.webfx.stack.authn.login.ui.spi.impl.gateway.webview.WebViewBasedUiLoginGateway; import javafx.scene.Node; import javafx.scene.image.ImageView; /** * @author Bruno Salmon */ -public final class FacebookUiLoginGatewayProvider extends WebViewBasedUiLoginGatewayProvider { +public final class FacebookUiLoginGateway extends WebViewBasedUiLoginGateway { private final static String GATEWAY_ID = "Facebook"; - public FacebookUiLoginGatewayProvider() { + public FacebookUiLoginGateway() { super(GATEWAY_ID); } diff --git a/webfx-stack-authn-login-ui-gateway-facebook-plugin/src/main/java/module-info.java b/webfx-stack-authn-login-ui-gateway-facebook-plugin/src/main/java/module-info.java index 41154b7d0..01fa90321 100644 --- a/webfx-stack-authn-login-ui-gateway-facebook-plugin/src/main/java/module-info.java +++ b/webfx-stack-authn-login-ui-gateway-facebook-plugin/src/main/java/module-info.java @@ -15,6 +15,6 @@ opens dev.webfx.stack.authn.login.ui.spi.impl.gateway.facebook; // Provided services - provides dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGatewayProvider with dev.webfx.stack.authn.login.ui.spi.impl.gateway.facebook.FacebookUiLoginGatewayProvider; + provides dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGateway with dev.webfx.stack.authn.login.ui.spi.impl.gateway.facebook.FacebookUiLoginGateway; } \ No newline at end of file diff --git a/webfx-stack-authn-login-ui-gateway-facebook-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGatewayProvider b/webfx-stack-authn-login-ui-gateway-facebook-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGateway similarity index 72% rename from webfx-stack-authn-login-ui-gateway-facebook-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGatewayProvider rename to webfx-stack-authn-login-ui-gateway-facebook-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGateway index dd69f9806..cd1e47433 100644 --- a/webfx-stack-authn-login-ui-gateway-facebook-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGatewayProvider +++ b/webfx-stack-authn-login-ui-gateway-facebook-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGateway @@ -1 +1 @@ -dev.webfx.stack.authn.login.ui.spi.impl.gateway.facebook.FacebookUiLoginGatewayProvider +dev.webfx.stack.authn.login.ui.spi.impl.gateway.facebook.FacebookUiLoginGateway diff --git a/webfx-stack-authn-login-ui-gateway-facebook-plugin/webfx.xml b/webfx-stack-authn-login-ui-gateway-facebook-plugin/webfx.xml index 6d10d1593..138011cf6 100644 --- a/webfx-stack-authn-login-ui-gateway-facebook-plugin/webfx.xml +++ b/webfx-stack-authn-login-ui-gateway-facebook-plugin/webfx.xml @@ -11,7 +11,7 @@ - dev.webfx.stack.authn.login.ui.spi.impl.gateway.facebook.FacebookUiLoginGatewayProvider + dev.webfx.stack.authn.login.ui.spi.impl.gateway.facebook.FacebookUiLoginGateway \ No newline at end of file diff --git a/webfx-stack-authn-login-ui-gateway-google-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/google/GoogleUiLoginGatewayProvider.java b/webfx-stack-authn-login-ui-gateway-google-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/google/GoogleUiLoginGateway.java similarity index 93% rename from webfx-stack-authn-login-ui-gateway-google-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/google/GoogleUiLoginGatewayProvider.java rename to webfx-stack-authn-login-ui-gateway-google-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/google/GoogleUiLoginGateway.java index 37b63909b..47e1cc370 100644 --- a/webfx-stack-authn-login-ui-gateway-google-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/google/GoogleUiLoginGatewayProvider.java +++ b/webfx-stack-authn-login-ui-gateway-google-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/google/GoogleUiLoginGateway.java @@ -1,18 +1,18 @@ package dev.webfx.stack.authn.login.ui.spi.impl.gateway.google; import dev.webfx.platform.resource.Resource; -import dev.webfx.stack.authn.login.ui.spi.impl.gateway.webview.WebViewBasedUiLoginGatewayProvider; +import dev.webfx.stack.authn.login.ui.spi.impl.gateway.webview.WebViewBasedUiLoginGateway; import javafx.scene.Node; import javafx.scene.image.ImageView; /** * @author Bruno Salmon */ -public final class GoogleUiLoginGatewayProvider extends WebViewBasedUiLoginGatewayProvider { +public final class GoogleUiLoginGateway extends WebViewBasedUiLoginGateway { private final static String GATEWAY_ID = "Google"; - public GoogleUiLoginGatewayProvider() { + public GoogleUiLoginGateway() { super(GATEWAY_ID); } diff --git a/webfx-stack-authn-login-ui-gateway-google-plugin/src/main/java/module-info.java b/webfx-stack-authn-login-ui-gateway-google-plugin/src/main/java/module-info.java index 5fd6caf0f..c0b0117e5 100644 --- a/webfx-stack-authn-login-ui-gateway-google-plugin/src/main/java/module-info.java +++ b/webfx-stack-authn-login-ui-gateway-google-plugin/src/main/java/module-info.java @@ -15,6 +15,6 @@ opens dev.webfx.stack.authn.login.ui.spi.impl.gateway.google; // Provided services - provides dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGatewayProvider with dev.webfx.stack.authn.login.ui.spi.impl.gateway.google.GoogleUiLoginGatewayProvider; + provides dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGateway with dev.webfx.stack.authn.login.ui.spi.impl.gateway.google.GoogleUiLoginGateway; } \ No newline at end of file diff --git a/webfx-stack-authn-login-ui-gateway-google-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGatewayProvider b/webfx-stack-authn-login-ui-gateway-google-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGateway similarity index 76% rename from webfx-stack-authn-login-ui-gateway-google-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGatewayProvider rename to webfx-stack-authn-login-ui-gateway-google-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGateway index 7e3396e84..e5e9856f0 100644 --- a/webfx-stack-authn-login-ui-gateway-google-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGatewayProvider +++ b/webfx-stack-authn-login-ui-gateway-google-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGateway @@ -1 +1 @@ -dev.webfx.stack.authn.login.ui.spi.impl.gateway.google.GoogleUiLoginGatewayProvider +dev.webfx.stack.authn.login.ui.spi.impl.gateway.google.GoogleUiLoginGateway diff --git a/webfx-stack-authn-login-ui-gateway-google-plugin/webfx.xml b/webfx-stack-authn-login-ui-gateway-google-plugin/webfx.xml index bfd96a204..2ac83f021 100644 --- a/webfx-stack-authn-login-ui-gateway-google-plugin/webfx.xml +++ b/webfx-stack-authn-login-ui-gateway-google-plugin/webfx.xml @@ -11,7 +11,7 @@ - dev.webfx.stack.authn.login.ui.spi.impl.gateway.google.GoogleUiLoginGatewayProvider + dev.webfx.stack.authn.login.ui.spi.impl.gateway.google.GoogleUiLoginGateway \ No newline at end of file diff --git a/webfx-stack-authn-login-ui-gateway-magiclink-plugin/pom.xml b/webfx-stack-authn-login-ui-gateway-magiclink-plugin/pom.xml new file mode 100644 index 000000000..15d6e69dc --- /dev/null +++ b/webfx-stack-authn-login-ui-gateway-magiclink-plugin/pom.xml @@ -0,0 +1,112 @@ + + + + 4.0.0 + + + dev.webfx + webfx-stack + 0.1.0-SNAPSHOT + + + webfx-stack-authn-login-ui-gateway-magiclink-plugin + + + + + org.openjfx + javafx-base + provided + + + + org.openjfx + javafx-controls + provided + + + + org.openjfx + javafx-graphics + provided + + + + dev.webfx + webfx-extras-styles-bootstrap + 0.1.0-SNAPSHOT + + + + dev.webfx + webfx-kit-util + 0.1.0-SNAPSHOT + + + + dev.webfx + webfx-platform-console + 0.1.0-SNAPSHOT + + + + dev.webfx + webfx-platform-javatime-emul-j2cl + 0.1.0-SNAPSHOT + runtime + true + + + + dev.webfx + webfx-platform-uischeduler + 0.1.0-SNAPSHOT + + + + dev.webfx + webfx-stack-authn + 0.1.0-SNAPSHOT + + + + dev.webfx + webfx-stack-authn-login-ui-gateway-password-plugin + 0.1.0-SNAPSHOT + + + + dev.webfx + webfx-stack-i18n + 0.1.0-SNAPSHOT + + + + dev.webfx + webfx-stack-i18n-controls + 0.1.0-SNAPSHOT + + + + dev.webfx + webfx-stack-ui-controls + 0.1.0-SNAPSHOT + + + + dev.webfx + webfx-stack-ui-operation + 0.1.0-SNAPSHOT + + + + dev.webfx + webfx-stack-ui-validation + 0.1.0-SNAPSHOT + + + + + \ No newline at end of file diff --git a/webfx-stack-authn-login-ui-gateway-magiclink-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/magiclink/MagicLinkI18nKeys.java b/webfx-stack-authn-login-ui-gateway-magiclink-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/magiclink/MagicLinkI18nKeys.java new file mode 100644 index 000000000..d35a12f72 --- /dev/null +++ b/webfx-stack-authn-login-ui-gateway-magiclink-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/magiclink/MagicLinkI18nKeys.java @@ -0,0 +1,32 @@ +// File managed by WebFX (DO NOT EDIT MANUALLY) +package dev.webfx.stack.authn.login.ui.spi.impl.gateway.magiclink; + +public interface MagicLinkI18nKeys { + + String ConfirmChange = "ConfirmChange"; + String BackToNavigation = "BackToNavigation"; + String MagicLinkBusClosedErrorTitle = "MagicLinkBusClosedErrorTitle"; + String Recovery = "Recovery"; + String MagicLinkUnrecognisedError = "MagicLinkUnrecognisedError"; + String MagicLinkUnexpectedErrorTitle = "MagicLinkUnexpectedErrorTitle"; + String MagicLinkPushErrorTitle = "MagicLinkPushErrorTitle"; + String MagicLinkInitialMessage = "MagicLinkInitialMessage"; + String CaseSensitive = "CaseSensitive"; + String MagicLinkAlreadyUsedErrorTitle = "MagicLinkAlreadyUsedErrorTitle"; + String ChangeYourPassword = "ChangeYourPassword"; + String PasswordStrength = "PasswordStrength"; + String NewPassword = "NewPassword"; + String MagicLinkSentCheckYourMailBox = "MagicLinkSentCheckYourMailBox"; + String PasswordChanged = "PasswordChanged"; + String MagicLinkBusClosedError = "MagicLinkBusClosedError"; + String MagicLinkExpiredError = "MagicLinkExpiredError"; + String MagicLinkAlreadyUsedError = "MagicLinkAlreadyUsedError"; + String MagicLinkUnrecognisedErrorTitle = "MagicLinkUnrecognisedErrorTitle"; + String GoToLogin = "GoToLogin"; + String MagicLinkUnexpectedError = "MagicLinkUnexpectedError"; + String ErrorWhileUpdatingPassword = "ErrorWhileUpdatingPassword"; + String MagicLinkPushError = "MagicLinkPushError"; + String MagicLinkExpiredErrorTitle = "MagicLinkExpiredErrorTitle"; + String MagicLinkSuccessMessage = "MagicLinkSuccessMessage"; + +} \ No newline at end of file diff --git a/webfx-stack-authn-login-ui-gateway-magiclink-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/magiclink/MagicLinkUi.java b/webfx-stack-authn-login-ui-gateway-magiclink-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/magiclink/MagicLinkUi.java new file mode 100644 index 000000000..9dda2ef68 --- /dev/null +++ b/webfx-stack-authn-login-ui-gateway-magiclink-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/magiclink/MagicLinkUi.java @@ -0,0 +1,181 @@ +package dev.webfx.stack.authn.login.ui.spi.impl.gateway.magiclink; + +import dev.webfx.extras.styles.bootstrap.Bootstrap; +import dev.webfx.kit.util.properties.FXProperties; +import dev.webfx.platform.console.Console; +import dev.webfx.platform.uischeduler.UiScheduler; +import dev.webfx.stack.authn.*; +import dev.webfx.stack.authn.login.ui.spi.impl.gateway.password.PasswordI18nKeys; +import dev.webfx.stack.authn.login.ui.spi.impl.gateway.password.UILoginView; +import dev.webfx.stack.i18n.I18n; +import dev.webfx.stack.i18n.controls.I18nControls; +import dev.webfx.stack.ui.controls.MaterialFactoryMixin; +import dev.webfx.stack.ui.operation.OperationUtil; +import dev.webfx.stack.ui.validation.ValidationSupport; +import javafx.application.Platform; +import javafx.beans.property.StringProperty; +import javafx.scene.Node; +import javafx.scene.control.Button; + +import java.util.function.Consumer; + +/** + * @author Bruno Salmon + */ +public class MagicLinkUi implements MaterialFactoryMixin { + + private final UILoginView uiLoginView; + private final StringProperty tokenProperty; + private final Consumer requestedPathConsumer; + private final ValidationSupport validationSupport = new ValidationSupport(); + + public MagicLinkUi(StringProperty tokenProperty, Consumer requestedPathConsumer) { + this.tokenProperty = tokenProperty; + this.requestedPathConsumer = requestedPathConsumer; + uiLoginView = new UILoginView(null); + uiLoginView.initializeComponents(); + uiLoginView.setTitle(MagicLinkI18nKeys.Recovery); + uiLoginView.setMainMessage(MagicLinkI18nKeys.ChangeYourPassword, Bootstrap.STRONG); + uiLoginView.setLabelOnActionButton(MagicLinkI18nKeys.ConfirmChange); + uiLoginView.showMainMessage(); + uiLoginView.hideEmailField(); + uiLoginView.hideForgetPasswordHyperlink(); + uiLoginView.showMessageForPasswordField(); + uiLoginView.hideGraphicFromActionButton(); + Button actionButton = uiLoginView.getActionButton(); + actionButton.setDisable(false); + + FXProperties.runNowAndOnPropertyChange(token -> { + if (token == null) { + uiLoginView.setMainMessage(MagicLinkI18nKeys.MagicLinkUnrecognisedError, Bootstrap.TEXT_DANGER); + } else { + AuthenticationService.authenticate(new AuthenticateWithMagicLinkCredentials(token)) + .onFailure(e -> UiScheduler.runInUiThread(() -> onFailure(e))) + .onSuccess(requestedPath -> UiScheduler.runInUiThread(() -> onSuccess((String) requestedPath))); + } + }, tokenProperty); + } + + public Node getUi() { + return uiLoginView.getContainer(); + } + + private void onSuccess(String requestedPath) { + uiLoginView.setTitle(MagicLinkI18nKeys.Recovery); + uiLoginView.setMainMessage(MagicLinkI18nKeys.ChangeYourPassword, Bootstrap.STRONG); + uiLoginView.setLabelOnActionButton(MagicLinkI18nKeys.ConfirmChange); + uiLoginView.showMainMessage(); + uiLoginView.hideEmailField(); + uiLoginView.hideForgetPasswordHyperlink(); + uiLoginView.showMessageForPasswordField(); + uiLoginView.showPasswordField(); + uiLoginView.showMessageForPasswordField(); + uiLoginView.hideGraphicFromActionButton(); + uiLoginView.getActionButton().setDisable(false); + uiLoginView.getActionButton().setOnAction(l -> { + if (validateForm()) { + AuthenticationService.updateCredentials(new UpdatePasswordFromMagicLinkCredentials(uiLoginView.getPasswordField().getText())) + .onFailure(e -> { + Console.log("Error Updating password: " + e); + Platform.runLater(() -> onFailure(e)); + }) + .onSuccess(ignored -> { + Console.log("Password Updated"); + Platform.runLater(() -> { + uiLoginView.setMainMessage(PasswordI18nKeys.PasswordUpdated, Bootstrap.TEXT_SUCCESS); + uiLoginView.showMainMessage(); + I18nControls.bindI18nProperties(uiLoginView.getActionButton(), PasswordI18nKeys.GoToLogin); + uiLoginView.getActionButton().setOnAction(e2 -> requestedPathConsumer.accept(requestedPath)); + uiLoginView.getPasswordField().setDisable(true); + uiLoginView.setForgetRememberPasswordHyperlink(MagicLinkI18nKeys.BackToNavigation); + uiLoginView.hideForgetPasswordHyperlink(); + //uiLoginView.getForgetRememberPasswordHyperlink().setOnAction(e2-> pathConsumer.accept(pathToBeRedirected)); + uiLoginView.hideGraphicFromActionButton(); + }); + }); + } + }); + } + + + private void onFailure(Throwable e) { + String technicalMessage = e.getMessage(); + Console.log("Technical error: " + technicalMessage); + + if (technicalMessage != null) { + //The error Message are defined in ModalityMagicLinkAuthenticationGatewayProvider + if (technicalMessage.contains("not found")) { + uiLoginView.getInfoMessageForPasswordFieldLabel().setVisible(false); + uiLoginView.setTitle(MagicLinkI18nKeys.MagicLinkUnrecognisedErrorTitle); + uiLoginView.setMainMessage(MagicLinkI18nKeys.MagicLinkUnexpectedError, Bootstrap.STRONG); + uiLoginView.displayOnlyTitleAndMainMessage(); + } else if (technicalMessage.contains("used") || technicalMessage.contains("expired")) { + if (technicalMessage.contains("used")) { + uiLoginView.setTitle(MagicLinkI18nKeys.MagicLinkAlreadyUsedErrorTitle); + uiLoginView.setMainMessage(MagicLinkI18nKeys.MagicLinkAlreadyUsedError, Bootstrap.STRONG); + } + if (technicalMessage.contains("expired")) { + uiLoginView.setTitle(MagicLinkI18nKeys.MagicLinkExpiredErrorTitle); + uiLoginView.setMainMessage(MagicLinkI18nKeys.MagicLinkExpiredError, Bootstrap.STRONG); + } + uiLoginView.setLabelOnActionButton(PasswordI18nKeys.SendLink); + uiLoginView.showMainMessage(); + uiLoginView.hideForgetPasswordHyperlink(); + uiLoginView.hideMessageForPasswordField(); + uiLoginView.hidePasswordField(); + uiLoginView.showEmailField(); + uiLoginView.hideGraphicFromActionButton(); + uiLoginView.getActionButton().setDisable(false); + uiLoginView.getActionButton().setOnAction(event -> { + Object credentials = new RenewMagicLinkCredentials(tokenProperty.get()); + OperationUtil.turnOnButtonsWaitMode(uiLoginView.getActionButton()); + new AuthenticationRequest() + .setUserCredentials(credentials) + .executeAsync() + .onComplete(ar -> UiScheduler.runInUiThread(() -> OperationUtil.turnOffButtonsWaitMode(uiLoginView.getActionButton()))) + .onFailure(failure -> Console.log("Fail to renew Magik Link:" + failure.getMessage())) + .onSuccess(ignored -> UiScheduler.runInUiThread(() -> { + uiLoginView.setMainMessage(PasswordI18nKeys.LinkSent, Bootstrap.TEXT_SUCCESS); + uiLoginView.showMainMessage(); + uiLoginView.getActionButton().setDisable(true); + uiLoginView.getEmailTextField().setDisable(true); + uiLoginView.hideForgetPasswordHyperlink(); + uiLoginView.showGraphicFromActionButton(); + })); + }); + } + if (technicalMessage.contains("address")) { + uiLoginView.getInfoMessageForPasswordFieldLabel().setVisible(false); + uiLoginView.setTitle(MagicLinkI18nKeys.MagicLinkPushErrorTitle); + uiLoginView.setMainMessage(MagicLinkI18nKeys.MagicLinkPushError, Bootstrap.STRONG); + uiLoginView.displayOnlyTitleAndMainMessage(); + } + if (technicalMessage.contains("closed")) { + uiLoginView.getInfoMessageForPasswordFieldLabel().setVisible(false); + uiLoginView.setTitle(MagicLinkI18nKeys.MagicLinkBusClosedErrorTitle); + uiLoginView.setMainMessage(MagicLinkI18nKeys.MagicLinkBusClosedError, Bootstrap.STRONG); + uiLoginView.displayOnlyTitleAndMainMessage(); + } + } + } + + /** + * This method is used to initialise the parameters for the form validation + */ + private void initFormValidation() { + if (validationSupport.isEmpty()) { + validationSupport.addPasswordStrengthValidation(uiLoginView.getPasswordField(), I18n.i18nTextProperty(MagicLinkI18nKeys.PasswordStrength)); + } + } + + /** + * We validate the form + * + * @return true if all the validation is success, false otherwise + */ + public boolean validateForm() { + initFormValidation(); // does nothing if already initialised + return validationSupport.isValid(); + } + +} diff --git a/webfx-stack-authn-login-ui-gateway-magiclink-plugin/src/main/java/module-info.java b/webfx-stack-authn-login-ui-gateway-magiclink-plugin/src/main/java/module-info.java new file mode 100644 index 000000000..15c13dfd9 --- /dev/null +++ b/webfx-stack-authn-login-ui-gateway-magiclink-plugin/src/main/java/module-info.java @@ -0,0 +1,24 @@ +// File managed by WebFX (DO NOT EDIT MANUALLY) + +module webfx.stack.authn.login.ui.gateway.magiclink.plugin { + + // Direct dependencies modules + requires javafx.base; + requires javafx.controls; + requires javafx.graphics; + requires webfx.extras.styles.bootstrap; + requires webfx.kit.util; + requires webfx.platform.console; + requires webfx.platform.uischeduler; + requires webfx.stack.authn; + requires webfx.stack.authn.login.ui.gateway.password.plugin; + requires webfx.stack.i18n; + requires webfx.stack.i18n.controls; + requires webfx.stack.ui.controls; + requires webfx.stack.ui.operation; + requires webfx.stack.ui.validation; + + // Exported packages + exports dev.webfx.stack.authn.login.ui.spi.impl.gateway.magiclink; + +} \ No newline at end of file diff --git a/webfx-stack-authn-login-ui-gateway-magiclink-plugin/src/main/webfx/i18n/webfx-magiclink@en.properties b/webfx-stack-authn-login-ui-gateway-magiclink-plugin/src/main/webfx/i18n/webfx-magiclink@en.properties new file mode 100644 index 000000000..9460cbd45 --- /dev/null +++ b/webfx-stack-authn-login-ui-gateway-magiclink-plugin/src/main/webfx/i18n/webfx-magiclink@en.properties @@ -0,0 +1,26 @@ +MagicLinkInitialMessage = We are checking the magic link... +MagicLinkSuccessMessage = You are now logged in! You may close this window and go back to your original session or update your password below. +MagicLinkUnrecognisedErrorTitle = Link not recognised +MagicLinkUnrecognisedError = This link is not recognised by the system. Please generate a new one from the login page. +MagicLinkAlreadyUsedErrorTitle = Link already used +MagicLinkAlreadyUsedError = This link has already been used. Please send a new one below. +MagicLinkExpiredErrorTitle = Link Expired +MagicLinkExpiredError = Your link has expired.\n Please send a new one below. +MagicLinkPushErrorTitle = Error +MagicLinkPushError = We were unable to reach your original session. Please make sure it is still open and connected to internet. +MagicLinkUnexpectedErrorTitle = Error +MagicLinkBusClosedErrorTitle = Error on server +MagicLinkBusClosedError = The server is unreachable, please try later. +MagicLinkUnexpectedError = An error occurred on the server, sorry! +ChangeYourPassword = Please update your password below +NewPassword = New password +Recovery = Recovery +MagicLinkSentCheckYourMailBox = The link has been sent. +ConfirmChange = Confirm Change +PasswordChanged = Your password has been changed +ErrorWhileUpdatingPassword = An error occurred while updating the password +GoToLogin = Go to login +CaseSensitive = Case-sensitive +BackToNavigation = Back to where I was +PasswordStrength: "Password must be at least 8 characters, and contains:\n - uppercase\n - lowercase\n - a number\n - a special character" + diff --git a/webfx-stack-authn-login-ui-gateway-magiclink-plugin/webfx.xml b/webfx-stack-authn-login-ui-gateway-magiclink-plugin/webfx.xml new file mode 100644 index 000000000..ea02ce307 --- /dev/null +++ b/webfx-stack-authn-login-ui-gateway-magiclink-plugin/webfx.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/webfx-stack-authn-login-ui-gateway-mojoauth-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/mojoauth/MojoAuthUiLoginGatewayProvider.java b/webfx-stack-authn-login-ui-gateway-mojoauth-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/mojoauth/MojoAuthUiLoginGateway.java similarity index 94% rename from webfx-stack-authn-login-ui-gateway-mojoauth-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/mojoauth/MojoAuthUiLoginGatewayProvider.java rename to webfx-stack-authn-login-ui-gateway-mojoauth-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/mojoauth/MojoAuthUiLoginGateway.java index 1111689f4..67c20a1d2 100644 --- a/webfx-stack-authn-login-ui-gateway-mojoauth-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/mojoauth/MojoAuthUiLoginGatewayProvider.java +++ b/webfx-stack-authn-login-ui-gateway-mojoauth-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/mojoauth/MojoAuthUiLoginGateway.java @@ -1,6 +1,6 @@ package dev.webfx.stack.authn.login.ui.spi.impl.gateway.mojoauth; -import dev.webfx.stack.authn.login.ui.spi.impl.gateway.webview.WebViewBasedUiLoginGatewayProvider; +import dev.webfx.stack.authn.login.ui.spi.impl.gateway.webview.WebViewBasedUiLoginGateway; import javafx.scene.Node; import javafx.scene.paint.Color; import javafx.scene.shape.SVGPath; @@ -8,11 +8,11 @@ /** * @author Bruno Salmon */ -public final class MojoAuthUiLoginGatewayProvider extends WebViewBasedUiLoginGatewayProvider { +public final class MojoAuthUiLoginGateway extends WebViewBasedUiLoginGateway { private final static String GATEWAY_ID = "MojoAuth"; - public MojoAuthUiLoginGatewayProvider() { + public MojoAuthUiLoginGateway() { super(GATEWAY_ID); } diff --git a/webfx-stack-authn-login-ui-gateway-mojoauth-plugin/src/main/java/module-info.java b/webfx-stack-authn-login-ui-gateway-mojoauth-plugin/src/main/java/module-info.java index 6f792fee3..18a2d3807 100644 --- a/webfx-stack-authn-login-ui-gateway-mojoauth-plugin/src/main/java/module-info.java +++ b/webfx-stack-authn-login-ui-gateway-mojoauth-plugin/src/main/java/module-info.java @@ -11,6 +11,6 @@ exports dev.webfx.stack.authn.login.ui.spi.impl.gateway.mojoauth; // Provided services - provides dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGatewayProvider with dev.webfx.stack.authn.login.ui.spi.impl.gateway.mojoauth.MojoAuthUiLoginGatewayProvider; + provides dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGateway with dev.webfx.stack.authn.login.ui.spi.impl.gateway.mojoauth.MojoAuthUiLoginGateway; } \ No newline at end of file diff --git a/webfx-stack-authn-login-ui-gateway-mojoauth-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGatewayProvider b/webfx-stack-authn-login-ui-gateway-mojoauth-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGateway similarity index 72% rename from webfx-stack-authn-login-ui-gateway-mojoauth-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGatewayProvider rename to webfx-stack-authn-login-ui-gateway-mojoauth-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGateway index 51c615325..04327d137 100644 --- a/webfx-stack-authn-login-ui-gateway-mojoauth-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGatewayProvider +++ b/webfx-stack-authn-login-ui-gateway-mojoauth-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGateway @@ -1 +1 @@ -dev.webfx.stack.authn.login.ui.spi.impl.gateway.mojoauth.MojoAuthUiLoginGatewayProvider +dev.webfx.stack.authn.login.ui.spi.impl.gateway.mojoauth.MojoAuthUiLoginGateway diff --git a/webfx-stack-authn-login-ui-gateway-mojoauth-plugin/webfx.xml b/webfx-stack-authn-login-ui-gateway-mojoauth-plugin/webfx.xml index c43e2e5a4..7502b23dd 100644 --- a/webfx-stack-authn-login-ui-gateway-mojoauth-plugin/webfx.xml +++ b/webfx-stack-authn-login-ui-gateway-mojoauth-plugin/webfx.xml @@ -10,7 +10,7 @@ - dev.webfx.stack.authn.login.ui.spi.impl.gateway.mojoauth.MojoAuthUiLoginGatewayProvider + dev.webfx.stack.authn.login.ui.spi.impl.gateway.mojoauth.MojoAuthUiLoginGateway \ No newline at end of file diff --git a/webfx-stack-authn-login-ui-gateway-password-plugin/pom.xml b/webfx-stack-authn-login-ui-gateway-password-plugin/pom.xml index 86d0dc68a..a234f6743 100644 --- a/webfx-stack-authn-login-ui-gateway-password-plugin/pom.xml +++ b/webfx-stack-authn-login-ui-gateway-password-plugin/pom.xml @@ -41,13 +41,7 @@ dev.webfx - webfx-extras-util-animation - 0.1.0-SNAPSHOT - - - - dev.webfx - webfx-extras-util-layout + webfx-extras-styles-bootstrap 0.1.0-SNAPSHOT @@ -113,6 +107,12 @@ 0.1.0-SNAPSHOT + + dev.webfx + webfx-stack-session-state-client-fx + 0.1.0-SNAPSHOT + + dev.webfx webfx-stack-ui-controls @@ -125,6 +125,12 @@ 0.1.0-SNAPSHOT + + dev.webfx + webfx-stack-ui-validation + 0.1.0-SNAPSHOT + + \ No newline at end of file diff --git a/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/password/PasswordI18nKeys.java b/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/password/PasswordI18nKeys.java new file mode 100644 index 000000000..a2ddebb52 --- /dev/null +++ b/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/password/PasswordI18nKeys.java @@ -0,0 +1,33 @@ +// File managed by WebFX (DO NOT EDIT MANUALLY) +package dev.webfx.stack.authn.login.ui.spi.impl.gateway.password; + +public interface PasswordI18nKeys { + + String Email = "Email"; + String Password = "Password"; + String ForgotPassword = "ForgotPassword"; + String RememberPassword = "RememberPassword"; + String CreateAccount = "CreateAccount"; + String CreateAccountTitle = "CreateAccountTitle"; + String CreateAccountInfoMessage = "CreateAccountInfoMessage"; + String SignIn = "SignIn"; + String SendLink = "SendLink"; + String Next = "Next"; + String Continue = "Continue"; + String IncorrectLoginOrPassword = "IncorrectLoginOrPassword"; + String ErrorOccurred = "ErrorOccurred"; + String Login = "Login"; + String Back = "Back"; + String Recovery = "Recovery"; + String LinkSent = "LinkSent"; + String AppTitle = "AppTitle"; + String CaseSensitive = "CaseSensitive"; + String GoToLogin = "GoToLogin"; + String PasswordUpdated = "PasswordUpdated"; + String AccountCreationLinkSent = "AccountCreationLinkSent"; + String SendEmailToValidate = "SendEmailToValidate"; + String InvalidEmail = "InvalidEmail"; + String YouMayCloseThisWindow = "YouMayCloseThisWindow"; + String EditUserAccount = "EditUserAccount"; + +} \ No newline at end of file diff --git a/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/password/PasswordUiLoginGateway.java b/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/password/PasswordUiLoginGateway.java new file mode 100644 index 000000000..219aebfa0 --- /dev/null +++ b/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/password/PasswordUiLoginGateway.java @@ -0,0 +1,85 @@ +package dev.webfx.stack.authn.login.ui.spi.impl.gateway.password; + +import dev.webfx.extras.util.scene.SceneUtil; +import dev.webfx.kit.util.properties.FXProperties; +import dev.webfx.platform.uischeduler.UiScheduler; +import dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGatewayBase; +import dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginPortalCallback; +import dev.webfx.stack.i18n.controls.I18nControls; +import dev.webfx.stack.session.state.client.fx.FXUserId; +import dev.webfx.stack.ui.controls.MaterialFactoryMixin; +import dev.webfx.stack.ui.controls.button.ButtonFactory; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleObjectProperty; +import javafx.scene.Node; +import javafx.scene.text.Text; + +import java.util.function.Consumer; + +/** + * @author Bruno Salmon + */ +public final class PasswordUiLoginGateway extends UiLoginGatewayBase implements MaterialFactoryMixin { + + private final static String GATEWAY_ID = "Password"; + + private static Consumer CREATE_ACCOUNT_EMAIL_CONSUMER; + + private UILoginView uiLoginView; + + private UiLoginPortalCallback uiLoginPortalcallback; + + // SignInMode = true => username/password, false => magic link + private final Property signInModeProperty = new SimpleObjectProperty<>(true); + + public static void setCreateAccountEmailConsumer(Consumer createAccountEmailConsumer) { + CREATE_ACCOUNT_EMAIL_CONSUMER = createAccountEmailConsumer; + } + + public PasswordUiLoginGateway() { + super(GATEWAY_ID); + FXProperties.runOnPropertiesChange(() -> { + if (FXUserId.getUserId() != null) + resetUXToLogin(); + }, FXUserId.userIdProperty()); + } + + @Override + public Node createLoginButton() { + return new Text("Password"); + } + + @Override + public Node createLoginUi(UiLoginPortalCallback callback) { + uiLoginPortalcallback = callback; + uiLoginView = new UILoginView(CREATE_ACCOUNT_EMAIL_CONSUMER); + uiLoginView.initializeComponents(); + + + FXProperties.runNowAndOnPropertiesChange(() -> { + boolean signIn = signInModeProperty.getValue(); + if (!signIn) { + uiLoginView.transformPaneToForgetPasswordState(callback); + } + else { + uiLoginView.transformPaneToInitialState(callback); + } + }, signInModeProperty); + FXProperties.runNowAndOnPropertiesChange(this::prepareShowing, uiLoginView.getContainer().sceneProperty()); + return uiLoginView.getContainer(); + } + + private void resetUXToLogin() { + signInModeProperty.setValue(true); + //We wait one second to reset the UXLogin (otherwise it change to quickly, and we notice it on the UI if we have go to the password page to the home page (which take 1s), this change occurs between the two, and we don't want it to be noticed + UiScheduler.scheduleDelay(1000,()->uiLoginView.transformPaneToInitialState(uiLoginPortalcallback)); + } + + public void prepareShowing() { + I18nControls.bindI18nProperties(uiLoginView.getActionButton(), signInModeProperty.getValue() ? "Continue" : "SendLink>>"); + // Resetting the default button (required for JavaFX if displayed a second time) + ButtonFactory.resetDefaultButton(uiLoginView.getActionButton()); + SceneUtil.autoFocusIfEnabled(uiLoginView.getEmailTextField()); + UiScheduler.scheduleDelay(500, () -> SceneUtil.autoFocusIfEnabled(uiLoginView.getEmailTextField())); + } +} diff --git a/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/password/PasswordUiLoginGatewayProvider.java b/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/password/PasswordUiLoginGatewayProvider.java deleted file mode 100644 index cdb722fa1..000000000 --- a/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/password/PasswordUiLoginGatewayProvider.java +++ /dev/null @@ -1,135 +0,0 @@ -package dev.webfx.stack.authn.login.ui.spi.impl.gateway.password; - -import dev.webfx.extras.panes.ScalePane; -import dev.webfx.extras.util.animation.Animations; -import dev.webfx.extras.util.layout.LayoutUtil; -import dev.webfx.extras.util.scene.SceneUtil; -import dev.webfx.kit.util.properties.FXProperties; -import dev.webfx.platform.uischeduler.UiScheduler; -import dev.webfx.platform.windowlocation.WindowLocation; -import dev.webfx.stack.authn.AuthenticationRequest; -import dev.webfx.stack.authn.MagicLinkRequest; -import dev.webfx.stack.authn.UsernamePasswordCredentials; -import dev.webfx.stack.authn.login.ui.FXLoginContext; -import dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGatewayProviderBase; -import dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginPortalCallback; -import dev.webfx.stack.i18n.I18n; -import dev.webfx.stack.i18n.controls.I18nControls; -import dev.webfx.stack.ui.controls.MaterialFactoryMixin; -import dev.webfx.stack.ui.controls.button.ButtonFactory; -import dev.webfx.stack.ui.controls.dialog.GridPaneBuilder; -import dev.webfx.stack.ui.operation.OperationUtil; -import javafx.beans.property.Property; -import javafx.beans.property.SimpleObjectProperty; -import javafx.geometry.HPos; -import javafx.geometry.Insets; -import javafx.scene.Node; -import javafx.scene.control.Button; -import javafx.scene.control.Hyperlink; -import javafx.scene.control.PasswordField; -import javafx.scene.control.TextField; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.GridPane; -import javafx.scene.shape.SVGPath; -import javafx.scene.text.Text; - -/** - * @author Bruno Salmon - */ -public final class PasswordUiLoginGatewayProvider extends UiLoginGatewayProviderBase implements MaterialFactoryMixin { - - private final static String GATEWAY_ID = "Password"; - - private TextField usernameField; - private PasswordField passwordField; - private double passwordPrefHeight; - private Button button; - // SignInMode = true => username/password, false => magic link - private final Property signInMode = new SimpleObjectProperty<>(true); - //private final ModalityValidationSupport validationSupport = new ModalityValidationSupport(); - - public PasswordUiLoginGatewayProvider() { - super(GATEWAY_ID); - } - - @Override - public Node createLoginButton() { - return new Text("Password"); - } - - @Override - public Node createLoginUi(UiLoginPortalCallback callback) { - BorderPane loginWindow = new BorderPane(); - Hyperlink hyperLink = newHyperlink(null, e -> signInMode.setValue(!signInMode.getValue())); - GridPane.setMargin(hyperLink, new Insets(20)); - GridPane gridPane = new GridPaneBuilder() - .addNodeFillingRow(usernameField = newMaterialTextField("Email")) - .addNodeFillingRow(passwordField = newMaterialPasswordField("Password")) - .addNewRow(hyperLink) - .addNodeFillingRow(button = new Button()) - .build(); - LayoutUtil.setMaxWidthToInfinite(button); - button.setPadding(new Insets(15)); - LayoutUtil.setPrefWidthToInfinite(gridPane); - loginWindow.setCenter(gridPane); - GridPane.setHalignment(hyperLink, HPos.CENTER); - hyperLink.setOnAction(e -> signInMode.setValue(!signInMode.getValue())); - FXProperties.runNowAndOnPropertiesChange(() -> { - boolean signIn = signInMode.getValue(); - I18nControls.bindI18nProperties(button, signIn ? "SignIn>>" : "SendLink>>"); - I18nControls.bindI18nProperties(hyperLink, signIn ? "ForgotPassword?" : "RememberPassword?"); - passwordField.setVisible(signIn); - if (!signIn) { - if (passwordPrefHeight == 0) { - passwordPrefHeight = passwordField.getHeight(); - passwordField.setPrefHeight(passwordPrefHeight); - } - passwordField.setMinHeight(0); - } - Animations.animateProperty(passwordField.prefHeightProperty(), signIn ? passwordPrefHeight : 0) - .setOnFinished(e -> passwordField.setMinHeight(signIn ? -1 : 0)); - }, signInMode); - //initValidation(); - button.setOnAction(event -> { - //if (validationSupport.isValid()) - Object credentials = signInMode.getValue() ? - new UsernamePasswordCredentials(usernameField.getText(), passwordField.getText()) - : new MagicLinkRequest(usernameField.getText(), WindowLocation.getOrigin(), I18n.getLanguage(), FXLoginContext.getLoginContext()); - OperationUtil.turnOnButtonsWaitMode(button); - new AuthenticationRequest() - .setUserCredentials(credentials) - .executeAsync() - .onComplete(ar -> UiScheduler.runInUiThread(() -> OperationUtil.turnOffButtonsWaitMode(button))) - .onFailure(callback::notifyUserLoginFailed) - .onSuccess(ignored -> UiScheduler.runInUiThread(() -> { - if (signInMode.getValue()) - usernameField.clear(); - passwordField.clear(); - //callback.notifyUserLoginSuccessful(); - SVGPath checkMark = new SVGPath(); - checkMark.setContent("M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8z M14.7 8.39l-3.78 5-1.63-2.11a1 1 0 0 0-1.58 1.23l2.43 3.11a1 1 0 0 0 .79.38 1 1 0 0 0 .79-.39l4.57-6a1 1 0 1 0-1.6-1.22z"); - ScalePane scalePane = new ScalePane(checkMark); - button.graphicProperty().unbind(); - button.setGraphic(scalePane); - //button.textProperty().unbind(); - //button.setText(null); - })); - }); - FXProperties.runNowAndOnPropertiesChange(this::prepareShowing, loginWindow.sceneProperty()); - return loginWindow; - } - - /*private void initValidation() { - validationSupport.addRequiredInput(usernameField, "Username is required"); - validationSupport.addRequiredInput(passwordField, "Password is required"); - }*/ - - public void prepareShowing() { - I18nControls.bindI18nProperties(button, signInMode.getValue() ? "SignIn>>" : "SendLink>>"); - // Resetting the default button (required for JavaFX if displayed a second time) - ButtonFactory.resetDefaultButton(button); - SceneUtil.autoFocusIfEnabled(usernameField); - UiScheduler.scheduleDelay(500, () -> SceneUtil.autoFocusIfEnabled(usernameField)); - } - -} diff --git a/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/password/UILoginView.java b/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/password/UILoginView.java new file mode 100644 index 000000000..ffac7713d --- /dev/null +++ b/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/password/UILoginView.java @@ -0,0 +1,423 @@ +package dev.webfx.stack.authn.login.ui.spi.impl.gateway.password; + + +import dev.webfx.extras.panes.ScalePane; +import dev.webfx.extras.styles.bootstrap.Bootstrap; +import dev.webfx.platform.uischeduler.UiScheduler; +import dev.webfx.platform.windowlocation.WindowLocation; +import dev.webfx.stack.authn.AuthenticateWithUsernamePasswordCredentials; +import dev.webfx.stack.authn.AuthenticationRequest; +import dev.webfx.stack.authn.InitiateAccountCreationCredentials; +import dev.webfx.stack.authn.SendMagicLinkCredentials; +import dev.webfx.stack.authn.login.ui.FXLoginContext; +import dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginPortalCallback; +import dev.webfx.stack.i18n.I18n; +import dev.webfx.stack.i18n.controls.I18nControls; +import dev.webfx.stack.ui.controls.MaterialFactoryMixin; +import dev.webfx.stack.ui.operation.OperationUtil; +import dev.webfx.stack.ui.validation.ValidationSupport; +import javafx.application.Platform; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.VBox; +import javafx.scene.shape.SVGPath; +import javafx.scene.text.TextAlignment; + +import java.util.function.Consumer; + +public class UILoginView implements MaterialFactoryMixin { + + private Label loginTitleLabel; + private Label mainMessageLabel; + private TextField emailTextField; + private PasswordField passwordField; + private Label infoMessageForPasswordFieldLabel; + private Hyperlink forgetRememberPasswordHyperlink; + private Hyperlink createAccountHyperlink; + private Button actionButton; + private VBox mainVBox; + private VBox passwordFieldAndMessageVbox; + private VBox emailAndPasswordContainer; + private final ValidationSupport validationSupport = new ValidationSupport(); + + private BorderPane container; + private final Consumer createAccountEmailConsumer; + + + private static final String CHECKMARK_PATH = "M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8z M14.7 8.39l-3.78 5-1.63-2.11a1 1 0 0 0-1.58 1.23l2.43 3.11a1 1 0 0 0 .79.38 1 1 0 0 0 .79-.39l4.57-6a1 1 0 1 0-1.6-1.22z"; + + public UILoginView(Consumer emailConsumer) { + createAccountEmailConsumer = emailConsumer; + } + + public void initializeComponents() { + container = new BorderPane(); + container.setMaxWidth(Double.MAX_VALUE); // so it fills the whole width of the main frame VBox (with text centered) + VBox loginVBox = new VBox(); + loginVBox.setAlignment(Pos.CENTER); + initialiseMainVBox(loginVBox); + loginVBox.getChildren().addAll(mainVBox); + loginVBox.setSpacing(60); + container.setCenter(loginVBox); + } + + + private void initialiseMainVBox(VBox container) { + mainVBox = new VBox(); + mainVBox.setMinWidth(container.getMinWidth()); + //mainVBox.getStyleClass().add("login"); + mainVBox.setAlignment(Pos.TOP_CENTER); + + loginTitleLabel = Bootstrap.h2Primary(I18nControls.newLabel(PasswordI18nKeys.Recovery)); + int vSpacing = 10; + loginTitleLabel.setPadding(new Insets(vSpacing, 0, 0, 0)); + + mainMessageLabel = Bootstrap.textSuccess(new Label("Success Message")); + mainMessageLabel.setPadding(new Insets(60, 0, 0, 0)); + mainMessageLabel.setTextAlignment(TextAlignment.CENTER); + mainMessageLabel.setWrapText(true); + mainMessageLabel.setGraphicTextGap(15); + hideMainMessage(); + + emailAndPasswordContainer = new VBox(); + emailAndPasswordContainer.setAlignment(Pos.CENTER); + int vBoxHeight = 150; + emailAndPasswordContainer.setMinHeight(vBoxHeight); + emailAndPasswordContainer.setMaxHeight(vBoxHeight); + emailTextField = newMaterialTextField(PasswordI18nKeys.Email); + VBox.setMargin(emailTextField, new Insets(40, 0, 0, 0)); + emailTextField.setMaxWidth(370); + emailTextField.setMinWidth(370); + + + passwordFieldAndMessageVbox = new VBox(10); + passwordField = newMaterialPasswordField(PasswordI18nKeys.Password); + passwordField.setMaxWidth(300); + passwordField.setMaxWidth(370); + VBox.setMargin(passwordField, new Insets(15, 0, 0, 0)); + passwordFieldAndMessageVbox.setMaxWidth(370); + passwordFieldAndMessageVbox.setMinWidth(370); + + + infoMessageForPasswordFieldLabel = Bootstrap.small(I18nControls.newLabel(PasswordI18nKeys.CaseSensitive)); + infoMessageForPasswordFieldLabel.setVisible(true); + passwordFieldAndMessageVbox.getChildren().addAll(passwordField, infoMessageForPasswordFieldLabel); + + emailAndPasswordContainer.getChildren().setAll(emailTextField, passwordFieldAndMessageVbox); + forgetRememberPasswordHyperlink = Bootstrap.textSecondary(I18nControls.newHyperlink(PasswordI18nKeys.GoToLogin)); + + VBox.setMargin(forgetRememberPasswordHyperlink, new Insets(40, 0, 0, 0)); + + createAccountHyperlink = new Hyperlink(); + //Here we display in a transition pane the content + createAccountHyperlink.setOnAction(e -> { + }); + I18nControls.bindI18nProperties(createAccountHyperlink, PasswordI18nKeys.CreateAccount); + createAccountHyperlink.getStyleClass().setAll(Bootstrap.TEXT_SECONDARY); + createAccountHyperlink.setVisible(true); + if (createAccountEmailConsumer == null) { + createAccountHyperlink.setVisible(false); + createAccountHyperlink.setManaged(false); + } + createAccountHyperlink.setOnAction(null); + VBox.setMargin(createAccountHyperlink, new Insets(20, 0, 0, 0)); + + actionButton = Bootstrap.largePrimaryButton(I18nControls.newButton(PasswordI18nKeys.Continue)); + VBox.setMargin(actionButton, new Insets(30, 0, 0, 0)); + mainVBox.getChildren().addAll(loginTitleLabel, mainMessageLabel, emailAndPasswordContainer, forgetRememberPasswordHyperlink, createAccountHyperlink, actionButton); + } + + private void initFormValidation() { + if (validationSupport.isEmpty()) { + validationSupport.addEmailValidation(emailTextField, emailTextField, I18n.i18nTextProperty(PasswordI18nKeys.InvalidEmail)); + validationSupport.addRequiredInput(emailTextField); + } + } + + public boolean validateForm() { + initFormValidation(); + return validationSupport.isValid(); + } + + private void transformPaneToCreateAccount(UiLoginPortalCallback callback) { + hidePasswordField(); + hideMessageForPasswordField(); + I18nControls.bindI18nProperties(forgetRememberPasswordHyperlink, PasswordI18nKeys.Back); + showForgetPasswordHyperlink(); + forgetRememberPasswordHyperlink.setOnAction(e-> transformPaneToInitialState(callback)); + loginTitleLabel.setWrapText(true); + I18nControls.bindI18nProperties(mainMessageLabel, PasswordI18nKeys.CreateAccountInfoMessage); + showMainMessage(); + I18nControls.bindI18nProperties(loginTitleLabel, PasswordI18nKeys.CreateAccountTitle); + hideCreateAccountHyperlink(); + I18nControls.bindI18nProperties(actionButton, PasswordI18nKeys.SendEmailToValidate); + actionButton.setOnAction(event -> { + if (validateForm()) { + Object credentials = new InitiateAccountCreationCredentials(emailTextField.getText().trim().toLowerCase(), WindowLocation.getOrigin(), WindowLocation.getPath(), I18n.getLanguage(), FXLoginContext.getLoginContext()); + OperationUtil.turnOnButtonsWaitMode(actionButton); + new AuthenticationRequest() + .setUserCredentials(credentials) + .executeAsync() + .onComplete(ar -> UiScheduler.runInUiThread(() -> OperationUtil.turnOffButtonsWaitMode(actionButton))) + .onFailure(failure -> { + callback.notifyUserLoginFailed(failure); + Platform.runLater(() -> { + setInfoMessageForPasswordFieldLabel(PasswordI18nKeys.ErrorOccurred, Bootstrap.TEXT_DANGER); + showMessageForPasswordField(); + }); + }) + .onSuccess(ignored -> UiScheduler.runInUiThread(() -> { + I18nControls.bindI18nProperties(mainMessageLabel, PasswordI18nKeys.AccountCreationLinkSent); + mainMessageLabel.setPadding(new Insets(100,15,0,15)); + showMainMessage(); + Label closeWindowLabel = Bootstrap.textSecondary(I18nControls.newLabel(PasswordI18nKeys.YouMayCloseThisWindow)); + closeWindowLabel.setPadding(new Insets(100,15,0,15)); + closeWindowLabel.setWrapText(true); + int indexToInsert = mainVBox.getChildren().indexOf(mainMessageLabel) + 1; + mainVBox.getChildren().add(indexToInsert, closeWindowLabel); + hideActionButton(); + actionButton.setManaged(false); + hideEmailField(); + emailTextField.setManaged(false); + hidePasswordField(); + passwordField.setManaged(false); + hideForgetPasswordHyperlink(); + forgetRememberPasswordHyperlink.setManaged(false); + })); + } + }); + } + + public void transformPaneToInitialState(UiLoginPortalCallback callback) { + I18nControls.bindI18nProperties(loginTitleLabel, PasswordI18nKeys.Login); + hidePasswordField(); + hideMainMessage(); + mainMessageLabel.setManaged(false); + hideMessageForPasswordField(); + hideForgetPasswordHyperlink(); + I18nControls.bindI18nProperties(actionButton, PasswordI18nKeys.Continue); + showCreateAccountHyperlink(); + createAccountHyperlink.setOnAction(e-> transformPaneToCreateAccount(callback)); + actionButton.setOnAction(e->{ + if (validateForm()) { + transformPaneToLoginAndPasswordState(callback); + } + }); + moveActionButtonUnderEmail(); + } + + private void moveActionButtonUnderEmail() { + mainVBox.getChildren().setAll(loginTitleLabel, mainMessageLabel, emailAndPasswordContainer, actionButton, forgetRememberPasswordHyperlink, createAccountHyperlink); + } + + private void moveActionButtonAtTheBottom() { + mainVBox.getChildren().setAll(loginTitleLabel, mainMessageLabel, emailAndPasswordContainer, forgetRememberPasswordHyperlink, createAccountHyperlink, actionButton); + } + + public void transformPaneToLoginAndPasswordState(UiLoginPortalCallback callback) { + showPasswordField(); + showMessageForPasswordField(); + showForgetPasswordHyperlink(); + I18nControls.bindI18nProperties(loginTitleLabel, PasswordI18nKeys.Login); + hideCreateAccountHyperlink(); + I18nControls.bindI18nProperties(actionButton, PasswordI18nKeys.Login); + passwordField.requestFocus(); + + I18nControls.bindI18nProperties(forgetRememberPasswordHyperlink, PasswordI18nKeys.ForgotPassword); + forgetRememberPasswordHyperlink.setOnAction(e -> transformPaneToForgetPasswordState(callback)); + + actionButton.setOnAction(event -> { + if (validateForm()) { + Object credentials = new AuthenticateWithUsernamePasswordCredentials(emailTextField.getText().trim().toLowerCase(), passwordField.getText().trim()); + OperationUtil.turnOnButtonsWaitMode(actionButton); + new AuthenticationRequest() + .setUserCredentials(credentials) + .executeAsync() + .onComplete(ar -> UiScheduler.runInUiThread(() -> OperationUtil.turnOffButtonsWaitMode(actionButton))) + .onFailure(failure -> { + callback.notifyUserLoginFailed(failure); + Platform.runLater(() -> { + setInfoMessageForPasswordFieldLabel(PasswordI18nKeys.IncorrectLoginOrPassword, Bootstrap.TEXT_DANGER); + showMessageForPasswordField(); + }); + }) + .onSuccess(ignored -> UiScheduler.scheduleDelay(1000,() -> passwordField.setText(""))); + } + }); + moveActionButtonAtTheBottom(); + } + + + + + public void transformPaneToForgetPasswordState(UiLoginPortalCallback callback) { + hidePasswordField(); + hideMessageForPasswordField(); + I18nControls.bindI18nProperties(loginTitleLabel, PasswordI18nKeys.Recovery); + hideCreateAccountHyperlink(); + I18nControls.bindI18nProperties(forgetRememberPasswordHyperlink, PasswordI18nKeys.RememberPassword); + forgetRememberPasswordHyperlink.setOnAction(e -> transformPaneToLoginAndPasswordState(callback)); + I18nControls.bindI18nProperties(actionButton, PasswordI18nKeys.SendLink+">>"); + + actionButton.setOnAction(event -> { + if(validateForm()) { + Object credentials = new SendMagicLinkCredentials(emailTextField.getText().trim().toLowerCase(), WindowLocation.getOrigin(), WindowLocation.getPath(), I18n.getLanguage(), FXLoginContext.getLoginContext()); + OperationUtil.turnOnButtonsWaitMode(actionButton); + new AuthenticationRequest() + .setUserCredentials(credentials) + .executeAsync() + .onComplete(ar -> UiScheduler.runInUiThread(() -> OperationUtil.turnOffButtonsWaitMode(actionButton))) + .onFailure(failure -> { + callback.notifyUserLoginFailed(failure); + Platform.runLater(() -> { + setInfoMessageForPasswordFieldLabel(PasswordI18nKeys.IncorrectLoginOrPassword, Bootstrap.TEXT_DANGER); + showMessageForPasswordField(); + }); + }) + .onSuccess(ignored -> UiScheduler.runInUiThread(() -> { + setTitle(PasswordI18nKeys.Recovery); + setMainMessage(PasswordI18nKeys.LinkSent, Bootstrap.TEXT_SUCCESS); + showMainMessage(); + actionButton.setDisable(true); + emailTextField.setDisable(true); + hidePasswordField(); + hideForgetPasswordHyperlink(); + showGraphicFromActionButton(); + })); + } + }); + } + + + public void hideCreateAccountHyperlink() { + createAccountHyperlink.setVisible(false); + createAccountHyperlink.setManaged(false); + } + + + public void showCreateAccountHyperlink() { + createAccountHyperlink.setVisible(true); + createAccountHyperlink.setManaged(true); + } + + + public void hideForgetPasswordHyperlink() { + forgetRememberPasswordHyperlink.setVisible(false); + } + + public void showForgetPasswordHyperlink() { + forgetRememberPasswordHyperlink.setVisible(true); + } + + public void setInfoMessageForPasswordFieldLabel(String I18nKey, String bootStrapStyle) { + I18nControls.bindI18nProperties(infoMessageForPasswordFieldLabel, I18nKey); + infoMessageForPasswordFieldLabel.getStyleClass().setAll(bootStrapStyle); + } + + public void setForgetRememberPasswordHyperlink(String I18nKey) { + I18nControls.bindI18nProperties(forgetRememberPasswordHyperlink, I18nKey); + } + + public void hideMessageForPasswordField() { + infoMessageForPasswordFieldLabel.setVisible(false); + } + + public void showMessageForPasswordField() { + infoMessageForPasswordFieldLabel.setVisible(true); + } + + public void hideEmailField() { + emailTextField.setVisible(false); + emailTextField.setManaged(false); + } + + public void showEmailField() { + emailTextField.setVisible(true); + emailTextField.setManaged(true); + } + + public void setMainMessage(String I18nKey, String bootStrapStyle) { + I18nControls.bindI18nProperties(mainMessageLabel, I18nKey); + mainMessageLabel.getStyleClass().setAll(bootStrapStyle); + } + + public void setLabelOnActionButton(String I18nKey) { + I18nControls.bindI18nProperties(actionButton, I18nKey); + } + + public void setTitle(String I18nKey) { + I18nControls.bindI18nProperties(loginTitleLabel, I18nKey); + } + + public void showMainMessage() { + mainMessageLabel.setVisible(true); + mainMessageLabel.setManaged(true); + } + + public void hideMainMessage() { + mainMessageLabel.setVisible(false); + mainMessageLabel.setManaged(false); + } + + public void showPasswordField() { + getPasswordFieldAndMessageVbox().setVisible(true); + getPasswordFieldAndMessageVbox().setManaged(true); + } + + public void hidePasswordField() { + getPasswordFieldAndMessageVbox().setVisible(false); + getPasswordFieldAndMessageVbox().setManaged(false); + } + + public void hideGraphicFromActionButton() { + actionButton.setGraphic(null); + } + + public void showGraphicFromActionButton() { + SVGPath svgPath = new SVGPath(); + svgPath.setContent(CHECKMARK_PATH); + actionButton.setGraphic(new ScalePane(svgPath)); + } + + public void hideActionButton() { + actionButton.setVisible(false); + } + + public Button getActionButton() { + return actionButton; + } + + public void displayOnlyTitleAndMainMessage() { + showMainMessage(); + hideEmailField(); + hidePasswordField(); + hideMessageForPasswordField(); + hideForgetPasswordHyperlink(); + hideActionButton(); + } + + + public Label getInfoMessageForPasswordFieldLabel() { + return infoMessageForPasswordFieldLabel; + } + + + public BorderPane getContainer() { + return container; + } + + public TextField getEmailTextField() { + return emailTextField; + } + + public VBox getPasswordFieldAndMessageVbox() { + return passwordFieldAndMessageVbox; + } + + public PasswordField getPasswordField() { + return passwordField; + } + +} diff --git a/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/java/module-info.java b/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/java/module-info.java index 38f87b553..ed9972ebb 100644 --- a/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/java/module-info.java +++ b/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/java/module-info.java @@ -7,8 +7,7 @@ requires javafx.controls; requires javafx.graphics; requires webfx.extras.panes; - requires webfx.extras.util.animation; - requires webfx.extras.util.layout; + requires webfx.extras.styles.bootstrap; requires webfx.extras.util.scene; requires webfx.kit.util; requires webfx.platform.uischeduler; @@ -18,13 +17,15 @@ requires webfx.stack.authn.login.ui.gateway; requires webfx.stack.i18n; requires webfx.stack.i18n.controls; + requires webfx.stack.session.state.client.fx; requires webfx.stack.ui.controls; requires webfx.stack.ui.operation; + requires webfx.stack.ui.validation; // Exported packages exports dev.webfx.stack.authn.login.ui.spi.impl.gateway.password; // Provided services - provides dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGatewayProvider with dev.webfx.stack.authn.login.ui.spi.impl.gateway.password.PasswordUiLoginGatewayProvider; + provides dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGateway with dev.webfx.stack.authn.login.ui.spi.impl.gateway.password.PasswordUiLoginGateway; } \ No newline at end of file diff --git a/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGatewayProvider b/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGateway similarity index 72% rename from webfx-stack-authn-login-ui-gateway-password-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGatewayProvider rename to webfx-stack-authn-login-ui-gateway-password-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGateway index 33c31984d..f5d45d866 100644 --- a/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGatewayProvider +++ b/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGateway @@ -1 +1 @@ -dev.webfx.stack.authn.login.ui.spi.impl.gateway.password.PasswordUiLoginGatewayProvider +dev.webfx.stack.authn.login.ui.spi.impl.gateway.password.PasswordUiLoginGateway diff --git a/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/webfx/css/webfx-stack-password-javafx@main.css b/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/webfx/css/webfx-stack-password-javafx@main.css new file mode 100644 index 000000000..d9f79de24 --- /dev/null +++ b/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/webfx/css/webfx-stack-password-javafx@main.css @@ -0,0 +1,4 @@ +/* We don't want the default white background for WebFX Extras material text fields in the login window */ +.login * { + -webfx-material-background-color: transparent; +} \ No newline at end of file diff --git a/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/webfx/css/webfx-stack-password-web@main.css b/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/webfx/css/webfx-stack-password-web@main.css new file mode 100644 index 000000000..e1c619a6b --- /dev/null +++ b/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/webfx/css/webfx-stack-password-web@main.css @@ -0,0 +1,4 @@ +/* We don't want the default white background for WebFX Extras material text fields in the login window */ +.login * { + --webfx-material-background-color: transparent; +} \ No newline at end of file diff --git a/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/webfx/i18n/webfx-stack-password@en.yaml b/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/webfx/i18n/webfx-stack-password@en.yaml index c18486d70..4859728b8 100644 --- a/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/webfx/i18n/webfx-stack-password@en.yaml +++ b/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/webfx/i18n/webfx-stack-password@en.yaml @@ -4,7 +4,48 @@ Email: Password: text: "Password" prompt: "Your password" -ForgotPassword: "Forgot password" -RememberPassword: "Remember password" +ForgotPassword: "Forgot password?" +RememberPassword: "Go to Login" +CreateAccount: "or create an account here" +CreateAccountTitle: "Create an account" +CreateAccountInfoMessage: + text: "Enter your email to start creating your account." + textFill: "#000000" SignIn: "Sign In" SendLink: "Send link" +Next: "Next" +Continue: "Continue" +IncorrectLoginOrPassword : + text: "Your password or email does not match" +ErrorOccurred: "An error has occurred, please retry later." +Login: "Login" +Back: "go back" +Recovery: "Recovery" +LinkSent: + text: "Your link to reset your password has been sent in your mailbox.\n You may now close this windows and check your emails." + graphic: + fill: "#41BA4D" + svgPath: "M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8z M14.7 8.39l-3.78 5-1.63-2.11a1 1 0 0 0-1.58 1.23l2.43 3.11a1 1 0 0 0 .79.38 1 1 0 0 0 .79-.39l4.57-6a1 1 0 1 0-1.6-1.22z" +AppTitle: + text: "Kadampa
Booking System" + graphic: + fill: "#0096D6" + svgPath: "M54.6002 10.561C30.3226 10.561 10.5752 30.3084 10.5752 54.6C10.5752 78.8915 30.3226 98.6249 54.6002 98.6249C78.8777 98.6249 98.6392 78.8775 98.6392 54.6C98.6392 30.3224 78.8777 10.561 54.6002 10.561Z M80.5186 49.621C80.5046 48.7936 80.3924 48.2045 80.3924 48.2045C74.3336 54.5859 69.2845 51.8791 67.8539 50.8833C67.4893 49.607 66.9563 48.4008 66.2691 47.3069C66.6758 45.7781 68.6674 40.6029 77.1947 40.5748C77.1947 40.5748 76.7739 39.9577 76.0867 39.2705C77.0404 32.4683 81.9913 30.8413 83.5481 30.5047C88.2185 36.1148 91.304 43.0853 92.0894 50.743C91.0515 51.4724 86.2549 54.2914 80.5046 49.6351L80.5186 49.621Z M81.5144 54.628C80.6028 54.9927 80.1119 55.5817 79.8595 56.2269C79.5088 55.5537 78.9899 54.9786 78.1765 54.6841C78.9759 54.3335 79.5088 53.7304 79.8595 53.0432C80.1119 53.6883 80.6028 54.2774 81.5004 54.642L81.5144 54.628Z M73.9689 73.562C73.0573 73.1833 72.2999 73.2394 71.6688 73.5199C71.9072 72.7906 71.9353 72.0192 71.5846 71.2338C72.3981 71.5564 73.1975 71.5003 73.9409 71.2619C73.6603 71.907 73.6042 72.6644 73.9689 73.548V73.562Z M78.7094 83.5619C73.0993 88.2323 66.1288 91.3178 58.4711 92.1032C57.7418 91.0654 54.9227 86.2688 59.5791 80.5185C60.4066 80.5044 60.9956 80.3922 60.9956 80.3922C54.6142 74.3334 57.321 69.2843 58.3168 67.8537C59.5931 67.4891 60.7993 66.9561 61.8932 66.2689C63.422 66.6756 68.5973 68.6672 68.6253 77.1945C68.6253 77.1945 69.2424 76.7737 69.9297 76.0865C76.7319 77.0402 78.3588 81.9911 78.6954 83.5479L78.7094 83.5619Z M35.4979 71.4442C36.2272 71.6826 36.9986 71.7107 37.784 71.36C37.4754 72.1735 37.5175 72.9729 37.756 73.7163C37.1108 73.4358 36.3534 73.3797 35.4698 73.7443C35.8626 72.8327 35.7924 72.0753 35.5119 71.4582L35.4979 71.4442Z M39.7054 76.5914C40.2945 77.1664 40.7854 77.503 40.7854 77.503C40.547 68.1903 46.69 66.886 48.0083 66.7037C49.0181 67.2507 50.0981 67.6854 51.2341 67.9659C52.0897 69.5087 54.0111 74.4596 48.1907 80.3081C48.1907 80.3081 48.92 80.4483 49.9017 80.4624C54.2495 86.2127 51.4305 91.0513 50.7152 92.1032C43.1977 91.3318 36.3254 88.3445 30.7714 83.8003C31.094 82.2576 32.6929 77.3207 39.7054 76.5914Z M35.2174 35.638C36.129 36.0166 36.8864 35.9605 37.5175 35.68C37.2791 36.4093 37.251 37.1807 37.6017 37.9661C36.7882 37.6435 35.9888 37.6996 35.2454 37.9381C35.5259 37.2929 35.582 36.5356 35.2174 35.652V35.638Z M30.4909 25.638C36.101 20.9676 43.0715 17.8821 50.7292 17.0967C51.4585 18.1346 54.2776 22.9312 49.6212 28.6815C48.7937 28.6955 48.2047 28.8077 48.2047 28.8077C54.5861 34.8666 51.8793 39.9156 50.8835 41.3462C49.6072 41.7108 48.401 42.2438 47.3071 42.931C45.7783 42.5243 40.6031 40.5327 40.575 32.0054C40.575 32.0054 39.9579 32.4262 39.2707 33.1134C32.4685 32.1597 30.8415 27.2088 30.5049 25.652L30.4909 25.638Z M73.5201 37.5173C72.7908 37.2789 72.0194 37.2508 71.234 37.6015C71.5426 36.788 71.5005 35.9886 71.262 35.2452C71.9072 35.5258 72.6646 35.5819 73.5481 35.2172C73.1554 36.1288 73.2256 36.8862 73.5061 37.5033L73.5201 37.5173Z M69.3125 32.3841C68.7235 31.8091 68.2326 31.4725 68.2326 31.4725C68.4851 41.5706 61.2481 42.2578 60.7712 42.2859C59.8736 41.8371 58.9339 41.4864 57.9522 41.234C57.0966 39.6912 55.1752 34.7403 60.9956 28.8919C60.9956 28.8919 60.2663 28.7516 59.2846 28.7376C54.9368 22.9873 57.7558 18.1486 58.4711 17.0967C65.9044 17.8541 72.7066 20.7993 78.2325 25.2593C77.8679 26.8863 76.2129 31.6688 69.3266 32.3841H69.3125Z M54.516 78.1903C54.8666 78.9897 55.4697 79.5227 56.157 79.8733C55.5118 80.1258 54.9227 80.6166 54.5581 81.5142C54.1934 80.6026 53.6044 80.1117 52.9592 79.8733C53.6324 79.5227 54.2075 79.0037 54.502 78.1903H54.516Z M54.6002 62.0473C50.4908 62.0473 47.1528 58.7093 47.1528 54.6C47.1528 50.4906 50.4908 47.1526 54.6002 47.1526C58.7095 47.1526 62.0475 50.4906 62.0475 54.6C62.0475 58.7093 58.7095 62.0473 54.6002 62.0473Z M54.6843 31.0097C54.3337 30.2102 53.7306 29.6773 53.0434 29.3266C53.6885 29.0742 54.2776 28.5833 54.6422 27.6857C55.0069 28.5973 55.5959 29.0882 56.2411 29.3266C55.5679 29.6773 54.9929 30.1962 54.6983 31.0097H54.6843Z M25.2455 30.9676C26.8724 31.3322 31.655 32.9872 32.3703 39.8736C31.7953 40.4626 31.4587 40.9535 31.4587 40.9535C41.5568 40.701 42.244 47.938 42.272 48.4149C41.8092 49.3265 41.4586 50.2802 41.2061 51.29C39.9719 52.0334 34.8107 54.5158 28.6396 48.3728C28.6396 48.3728 28.4993 49.1021 28.4853 50.0839C23.0436 54.1932 18.4293 51.8931 17.0548 51.0236C17.7701 43.464 20.7154 36.5636 25.2315 30.9536L25.2455 30.9676Z M30.7714 54.6981C29.972 55.0488 29.439 55.6519 29.0884 56.3391C28.8359 55.6939 28.3451 55.1049 27.4475 54.7402C28.3591 54.3756 28.85 53.7865 29.1024 53.1413C29.4531 53.8146 29.972 54.3896 30.7854 54.6841L30.7714 54.6981Z M17.0969 58.4849C18.4994 57.6014 23.0576 55.3713 28.4432 59.7472C28.4573 60.5747 28.5695 61.1637 28.5695 61.1637C35.4278 53.9408 40.9817 58.3447 41.4726 58.7654C41.7952 59.7753 42.23 60.729 42.7629 61.6266C42.3843 63.0992 40.4207 68.3727 31.8373 68.4007C31.8373 68.4007 32.2581 69.0178 32.9453 69.7051C31.9916 76.5353 26.9987 78.1342 25.4699 78.4708C20.9117 72.9028 17.8963 66.0305 17.1249 58.4849H17.0969Z M83.9408 78.2324C82.3139 77.8677 77.5313 76.2127 76.816 69.3264C77.391 68.7373 77.7276 68.2464 77.7276 68.2464C67.6436 68.4989 66.9423 61.2619 66.9142 60.7851C67.3631 59.8875 67.7137 58.9478 67.9661 57.966C69.5089 57.1105 74.4598 55.189 80.3083 61.0095C80.3083 61.0095 80.4485 60.2802 80.4625 59.2984C86.2129 54.9506 91.0515 57.7697 92.1034 58.4849C91.3461 65.9183 88.4008 72.7205 83.9408 78.2464V78.2324Z M54.6284 8.92C55.3156 7.12478 56.6901 5.49786 59.2427 4.46C56.7602 3.43616 55.3857 1.80925 54.6705 0C53.6747 1.93547 52.188 3.60447 49.958 4.60025C52.2161 5.44176 53.6887 7.04063 54.6424 8.93402L54.6284 8.92Z M86.9139 22.3281C88.667 21.5426 90.7988 21.3603 93.3374 22.4403C92.2995 19.9718 92.4959 17.84 93.2672 16.0448C91.2056 16.718 88.9615 16.8442 86.6895 15.9606C87.6852 18.1486 87.6011 20.3225 86.9419 22.3421L86.9139 22.3281Z M104.6 49.9576V49.9436C103.758 52.2016 102.16 53.6743 100.266 54.628C102.061 55.3152 103.688 56.6897 104.726 59.2423C105.75 56.7598 107.377 55.3853 109.186 54.67C107.251 53.6743 105.582 52.1876 104.586 49.9576H104.6Z M86.8715 86.9139C87.6569 88.667 87.8392 90.7989 86.7593 93.3374C89.2277 92.2995 91.3595 92.4959 93.1547 93.2673C92.4815 91.2056 92.3553 88.9616 93.2389 86.6895C91.051 87.6853 88.8771 87.6011 86.8575 86.9419L86.8715 86.9139Z M54.5576 100.28C53.8704 102.075 52.4959 103.702 49.9434 104.74C52.4258 105.764 53.8003 107.391 54.5156 109.2C55.5113 107.264 56.998 105.595 59.228 104.6C56.97 103.758 55.4973 102.159 54.5436 100.266L54.5576 100.28Z M22.2717 86.8718C20.5185 87.6572 18.3867 87.8395 15.8481 86.7596C16.886 89.228 16.6897 91.3598 15.9183 93.1551C17.98 92.4818 20.224 92.3556 22.4961 93.2392C21.5003 91.0513 21.5844 88.8774 22.2436 86.8578L22.2717 86.8718Z M4.46 49.9436C3.43616 52.426 1.80925 53.8005 0 54.5158C1.93547 55.5116 3.60447 56.9982 4.60025 59.2282V59.2423C5.44176 56.9842 7.04063 55.5116 8.93402 54.5579C7.1388 53.8706 5.51189 52.4962 4.47403 49.9436H4.46Z M22.3279 22.2719C21.5425 20.5188 21.3601 18.387 22.4401 15.8484C19.9716 16.8863 17.8398 16.6899 16.0446 15.9185C16.7178 17.9802 16.844 20.2243 15.9604 22.4963C18.1484 21.5006 20.3223 21.5847 22.3419 22.2439L22.3279 22.2719Z" +CaseSensitive: "Case sensitive" +GoToLogin: "Go to login" +PasswordUpdated: + text: "Your password has been updated" + graphic: + fill: "#41BA4D" + svgPath: "M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8z M14.7 8.39l-3.78 5-1.63-2.11a1 1 0 0 0-1.58 1.23l2.43 3.11a1 1 0 0 0 .79.38 1 1 0 0 0 .79-.39l4.57-6a1 1 0 1 0-1.6-1.22z" +AccountCreationLinkSent: + text: "Thank you! An email with a link has been sent. Please continue by accessing the link from your email account." + textFill: "#41BA4D" + graphic: + fill: "#41BA4D" + svgPath: "M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8z M14.7 8.39l-3.78 5-1.63-2.11a1 1 0 0 0-1.58 1.23l2.43 3.11a1 1 0 0 0 .79.38 1 1 0 0 0 .79-.39l4.57-6a1 1 0 1 0-1.6-1.22z" +SendEmailToValidate: "Send email to validate" +InvalidEmail: "Please enter a valid email address" +YouMayCloseThisWindow: "You may close this window and continue from the validate email sent" +EditUserAccount: "Edit account information" + diff --git a/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/webfx/i18n/webfx-stack-password@fr.yaml b/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/webfx/i18n/webfx-stack-password@fr.yaml index aa228b48e..4d4e2d438 100644 --- a/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/webfx/i18n/webfx-stack-password@fr.yaml +++ b/webfx-stack-authn-login-ui-gateway-password-plugin/src/main/webfx/i18n/webfx-stack-password@fr.yaml @@ -8,3 +8,4 @@ ForgotPassword: "Mot de passe oublié" RememberPassword: "Mot de passe retrouvé" SignIn: "Identifiez-vous" SendLink: "Envoyer le lien" +InvalidEmail: "Veuillez entrer une adresse email valide" diff --git a/webfx-stack-authn-login-ui-gateway-password-plugin/webfx.xml b/webfx-stack-authn-login-ui-gateway-password-plugin/webfx.xml index e62787abd..c7d5714bb 100644 --- a/webfx-stack-authn-login-ui-gateway-password-plugin/webfx.xml +++ b/webfx-stack-authn-login-ui-gateway-password-plugin/webfx.xml @@ -10,7 +10,10 @@ - dev.webfx.stack.authn.login.ui.spi.impl.gateway.password.PasswordUiLoginGatewayProvider + dev.webfx.stack.authn.login.ui.spi.impl.gateway.password.PasswordUiLoginGateway + + + \ No newline at end of file diff --git a/webfx-stack-authn-login-ui-gateway-webviewbased-openjfx/pom.xml b/webfx-stack-authn-login-ui-gateway-webviewbased-openjfx/pom.xml index e187f492f..4444e1d60 100644 --- a/webfx-stack-authn-login-ui-gateway-webviewbased-openjfx/pom.xml +++ b/webfx-stack-authn-login-ui-gateway-webviewbased-openjfx/pom.xml @@ -25,6 +25,12 @@ javafx-web + + dev.webfx + webfx-kit-util + 0.1.0-SNAPSHOT + + dev.webfx webfx-platform-ast diff --git a/webfx-stack-authn-login-ui-gateway-webviewbased-openjfx/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/webview/spi/impl/openjfx/FXLoginWebViewProvider.java b/webfx-stack-authn-login-ui-gateway-webviewbased-openjfx/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/webview/spi/impl/openjfx/FXLoginWebViewProvider.java index bc9fe50c6..e1264fad9 100644 --- a/webfx-stack-authn-login-ui-gateway-webviewbased-openjfx/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/webview/spi/impl/openjfx/FXLoginWebViewProvider.java +++ b/webfx-stack-authn-login-ui-gateway-webviewbased-openjfx/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/webview/spi/impl/openjfx/FXLoginWebViewProvider.java @@ -1,5 +1,6 @@ package dev.webfx.stack.authn.login.ui.spi.impl.gateway.webview.spi.impl.openjfx; +import dev.webfx.kit.util.properties.FXProperties; import dev.webfx.stack.authn.login.ui.spi.impl.gateway.webview.spi.LoginWebViewProvider; import dev.webfx.stack.ui.dialog.DialogCallback; import dev.webfx.stack.ui.dialog.DialogUtil; @@ -40,7 +41,7 @@ public WebView createLoginWebView() { }); // When there is a state change on the main web engine, this can indicate the final success callback: - mainWebEngine.getLoadWorker().stateProperty().addListener((ov,oldState,newState) -> { + FXProperties.runOnPropertyChange(webEngineState -> { if (popupDialogCallback != null) { // indicates that there was login popup dialog popupDialogCallback.closeDialog(); // we close that dialog, because this state change must indicate the success callback popupDialogCallback = null; // No need to do it again @@ -56,7 +57,7 @@ public WebView createLoginWebView() { Element headerNotices = document == null ? null : document.getElementById("header-notices"); if (headerNotices != null) headerNotices.getParentNode().removeChild(headerNotices); - }); + }, mainWebEngine.getLoadWorker().stateProperty()); // Setting a cookie handler with a persistent cookie store. This is mainly for the Facebook login which prompts // an annoying cookie window. Thanks to the cookie persistence, this should now happen only once, on first time. diff --git a/webfx-stack-authn-login-ui-gateway-webviewbased-openjfx/src/main/java/module-info.java b/webfx-stack-authn-login-ui-gateway-webviewbased-openjfx/src/main/java/module-info.java index 49722bbb4..3725c1733 100644 --- a/webfx-stack-authn-login-ui-gateway-webviewbased-openjfx/src/main/java/module-info.java +++ b/webfx-stack-authn-login-ui-gateway-webviewbased-openjfx/src/main/java/module-info.java @@ -6,6 +6,7 @@ requires java.xml; requires javafx.graphics; requires javafx.web; + requires webfx.kit.util; requires webfx.platform.ast; requires webfx.platform.ast.json.plugin; requires webfx.platform.storagelocation; diff --git a/webfx-stack-authn-login-ui-gateway-webviewbased/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/webview/WebViewBasedUiLoginGatewayProvider.java b/webfx-stack-authn-login-ui-gateway-webviewbased/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/webview/WebViewBasedUiLoginGateway.java similarity index 92% rename from webfx-stack-authn-login-ui-gateway-webviewbased/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/webview/WebViewBasedUiLoginGatewayProvider.java rename to webfx-stack-authn-login-ui-gateway-webviewbased/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/webview/WebViewBasedUiLoginGateway.java index c01a84232..5b3ca1bc0 100644 --- a/webfx-stack-authn-login-ui-gateway-webviewbased/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/webview/WebViewBasedUiLoginGatewayProvider.java +++ b/webfx-stack-authn-login-ui-gateway-webviewbased/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/webview/WebViewBasedUiLoginGateway.java @@ -4,7 +4,7 @@ import dev.webfx.platform.uischeduler.UiScheduler; import dev.webfx.stack.authn.login.LoginService; import dev.webfx.stack.authn.login.LoginUiContext; -import dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGatewayProviderBase; +import dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGatewayBase; import dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginPortalCallback; import javafx.scene.Node; import javafx.scene.web.WebEngine; @@ -13,11 +13,11 @@ /** * @author Bruno Salmon */ -public abstract class WebViewBasedUiLoginGatewayProvider extends UiLoginGatewayProviderBase { +public abstract class WebViewBasedUiLoginGateway extends UiLoginGatewayBase { private final static String ERROR_HTML_TEMPLATE = "
{{ERROR}}
"; - public WebViewBasedUiLoginGatewayProvider(Object gatewayId) { + public WebViewBasedUiLoginGateway(Object gatewayId) { super(gatewayId); } diff --git a/webfx-stack-authn-login-ui-gateway/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/UiLoginGatewayProvider.java b/webfx-stack-authn-login-ui-gateway/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/UiLoginGateway.java similarity index 82% rename from webfx-stack-authn-login-ui-gateway/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/UiLoginGatewayProvider.java rename to webfx-stack-authn-login-ui-gateway/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/UiLoginGateway.java index 521882a38..87ee4473e 100644 --- a/webfx-stack-authn-login-ui-gateway/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/UiLoginGatewayProvider.java +++ b/webfx-stack-authn-login-ui-gateway/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/UiLoginGateway.java @@ -2,7 +2,7 @@ import javafx.scene.Node; -public interface UiLoginGatewayProvider { +public interface UiLoginGateway { Object getGatewayId(); diff --git a/webfx-stack-authn-login-ui-gateway/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/UiLoginGatewayProviderBase.java b/webfx-stack-authn-login-ui-gateway/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/UiLoginGatewayBase.java similarity index 64% rename from webfx-stack-authn-login-ui-gateway/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/UiLoginGatewayProviderBase.java rename to webfx-stack-authn-login-ui-gateway/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/UiLoginGatewayBase.java index 2ad3e160c..5cc4a8e8a 100644 --- a/webfx-stack-authn-login-ui-gateway/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/UiLoginGatewayProviderBase.java +++ b/webfx-stack-authn-login-ui-gateway/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/gateway/UiLoginGatewayBase.java @@ -3,11 +3,11 @@ /** * @author Bruno Salmon */ -public abstract class UiLoginGatewayProviderBase implements UiLoginGatewayProvider { +public abstract class UiLoginGatewayBase implements UiLoginGateway { private final Object gatewayId; - public UiLoginGatewayProviderBase(Object gatewayId) { + public UiLoginGatewayBase(Object gatewayId) { this.gatewayId = gatewayId; } diff --git a/webfx-stack-authn-login-ui-portal/pom.xml b/webfx-stack-authn-login-ui-portal/pom.xml index 4bb4fc079..d12072cbf 100644 --- a/webfx-stack-authn-login-ui-portal/pom.xml +++ b/webfx-stack-authn-login-ui-portal/pom.xml @@ -15,6 +15,12 @@ + + org.openjfx + javafx-base + provided + + org.openjfx javafx-controls @@ -97,6 +103,12 @@ 0.1.0-SNAPSHOT + + dev.webfx + webfx-stack-authn-login-ui-gateway-magiclink-plugin + 0.1.0-SNAPSHOT + + \ No newline at end of file diff --git a/webfx-stack-authn-login-ui-portal/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/portal/LoginPortalUi.java b/webfx-stack-authn-login-ui-portal/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/portal/LoginPortalUi.java index 41be60440..c60dff7d9 100644 --- a/webfx-stack-authn-login-ui-portal/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/portal/LoginPortalUi.java +++ b/webfx-stack-authn-login-ui-portal/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/portal/LoginPortalUi.java @@ -1,15 +1,18 @@ package dev.webfx.stack.authn.login.ui.spi.impl.portal; import dev.webfx.extras.panes.FlipPane; +import dev.webfx.extras.panes.GoldenRatioPane; import dev.webfx.extras.panes.ScalePane; import dev.webfx.extras.util.animation.Animations; import dev.webfx.kit.util.properties.FXProperties; import dev.webfx.platform.console.Console; import dev.webfx.platform.os.OperatingSystem; import dev.webfx.platform.uischeduler.UiScheduler; -import dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGatewayProvider; +import dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGateway; import dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginPortalCallback; +import dev.webfx.stack.authn.login.ui.spi.impl.gateway.magiclink.MagicLinkUi; import javafx.application.Platform; +import javafx.beans.property.StringProperty; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Pos; @@ -23,6 +26,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; /** * @author Bruno Salmon @@ -34,9 +38,10 @@ final class LoginPortalUi implements UiLoginPortalCallback { private final Region leftLine = new Region(); private final Text orText = new Text("OR"); private final Region rightLine = new Region(); - private Node userPasswordUI; + private Node userUI; private final List otherLoginButtons = new ArrayList<>(); + private final GoldenRatioPane loginPaneContainer = new GoldenRatioPane(); private final Pane loginPane = new Pane(backgroundRegion, leftLine, orText, rightLine) { @Override @@ -44,7 +49,7 @@ protected void layoutChildren() { double width = getWidth(), height = getHeight(); double margin = 40, x = margin, y = margin, w = width - 2 * margin, h, wor = orText.prefWidth(w), wl = w * 0.5 - wor; layoutInArea(backgroundRegion, 0, 0, width, height, 0, null, HPos.LEFT, VPos.TOP); - layoutInArea(userPasswordUI, x, y, w, h = Math.min(userPasswordUI.prefHeight(w), height - 2 * margin), 0, Insets.EMPTY, false, false, HPos.CENTER, VPos.TOP); + layoutInArea(userUI, x, y, w, h = Math.min(userUI.prefHeight(w), height - 2 * margin), 0, Insets.EMPTY, false, false, HPos.CENTER, VPos.TOP); int n = otherLoginButtons.size(); boolean hasOtherLoginButtons = n > 0; orText.setVisible(hasOtherLoginButtons); @@ -71,13 +76,13 @@ protected void layoutChildren() { { setMinHeight(USE_PREF_SIZE); - setMaxSize(400, Region.USE_PREF_SIZE); + setMaxSize(586, Region.USE_PREF_SIZE); } @Override protected double computePrefHeight(double width) { double margin = 40, y = margin, w = Math.max(-1 , width - 2 * margin); - double h = userPasswordUI.prefHeight(w); // userPasswordUI + double h = userUI.prefHeight(w); // userPasswordUI y += h; if (!otherLoginButtons.isEmpty()) { y += margin; // orText @@ -87,41 +92,49 @@ protected double computePrefHeight(double width) { } }; - public LoginPortalUi() { - for (UiLoginGatewayProvider gatewayProvider : UiLoginPortalProvider.getProviders()) { - if ("Password".equals(gatewayProvider.getGatewayId())) { - userPasswordUI = gatewayProvider.createLoginUi(this); - } else { - StackPane loginButton = new StackPane(gatewayProvider.createLoginButton()); - loginButton.setPadding(new Insets(13)); - loginButton.setPrefSize(50, 50); - loginButton.setOnMouseClicked(e -> { - Button backButton = new Button("« Use another method to sign in"); - backButton.setPadding(new Insets(15)); - BorderPane.setAlignment(backButton, Pos.CENTER); - BorderPane.setMargin(backButton, new Insets(10)); - backButton.setOnAction(e2 -> showLoginHome()); - BorderPane borderPane = new BorderPane(); - borderPane.setBottom(backButton); - flipPane.setBack(borderPane); - Node loginUi = gatewayProvider.createLoginUi(this); // probably a web-view - if (!OperatingSystem.isMobile()) { - borderPane.setCenter(loginUi); - flipPane.flipToBack(); - } else { - // On mobiles, we wait the flip animation to be finished (borderPane in stable position) before - // attaching the web-view (otherwise the web view is not visible). - flipPane.flipToBack(() -> borderPane.setCenter(loginUi)); - // Now the "Sign in with Google" button appears, but it is not reacting (no popup)... - // 2 possible causes: - // 1) Popup is not working on the web-view (or required setup) - // 2) Google doesn't allow SSO in web-view (see https://developers.googleblog.com/2016/08/modernizing-oauth-interactions-in-native-apps.html) - } - }); - otherLoginButtons.add(loginButton); + public LoginPortalUi(StringProperty magicLinkTokenProperty, Consumer requestedPathConsumer) { + //If MagicLink, we display only the MagikLink panel + if (magicLinkTokenProperty != null) + userUI = new MagicLinkUi(magicLinkTokenProperty, requestedPathConsumer).getUi(); + else { + for (UiLoginGateway gateway : UiLoginPortalProvider.getGateways()) { + Object gatewayId = gateway.getGatewayId(); + if ("Password".equals(gatewayId)) { + userUI = gateway.createLoginUi(this); + } else { + //If we have magicklink to true, we do nothing + StackPane loginButton = new StackPane(gateway.createLoginButton()); + loginButton.setPadding(new Insets(13)); + loginButton.setPrefSize(50, 50); + loginButton.setOnMouseClicked(e -> { + Button backButton = new Button("« Use another method to sign in"); + backButton.setPadding(new Insets(15)); + BorderPane.setAlignment(backButton, Pos.CENTER); + BorderPane.setMargin(backButton, new Insets(10)); + backButton.setOnAction(e2 -> showLoginHome()); + BorderPane borderPane = new BorderPane(); + borderPane.setBottom(backButton); + flipPane.setBack(borderPane); + Node loginUi = gateway.createLoginUi(this); // probably a web-view + if (!OperatingSystem.isMobile()) { + borderPane.setCenter(loginUi); + flipPane.flipToBack(); + } else { + // On mobiles, we wait the flip animation to be finished (borderPane in stable position) before + // attaching the web-view (otherwise the web view is not visible). + flipPane.flipToBack(() -> borderPane.setCenter(loginUi)); + // Now the "Sign in with Google" button appears, but it is not reacting (no popup)... + // 2 possible causes: + // 1) Popup is not working on the web-view (or required setup) + // 2) Google doesn't allow SSO in web-view (see https://developers.googleblog.com/2016/08/modernizing-oauth-interactions-in-native-apps.html) + } + }); + otherLoginButtons.add(loginButton); + } } } - loginPane.getChildren().add(userPasswordUI); + loginPaneContainer.setContent(loginPane); + loginPane.getChildren().add(userUI); loginPane.getChildren().addAll(otherLoginButtons); orText.getStyleClass().add("or"); leftLine.setMinHeight(1); @@ -129,8 +142,9 @@ public LoginPortalUi() { rightLine.setMinHeight(1); rightLine.getStyleClass().add("line"); backgroundRegion.getStyleClass().addAll("background", "fx-border"); - FXProperties.runNowAndOnPropertiesChange(this::showLoginHome, flipPane.sceneProperty()); + FXProperties.runNowAndOnPropertyChange(this::showLoginHome, flipPane.sceneProperty()); flipPane.getStyleClass().add("login"); + loginPane.getStyleClass().add("login-child"); } public FlipPane getFlipPane() { @@ -138,7 +152,7 @@ public FlipPane getFlipPane() { } void showLoginHome() { - flipPane.setFront(loginPane); + flipPane.setFront(loginPaneContainer); if (flipPane.getScene() != null) flipPane.flipToFront(); } diff --git a/webfx-stack-authn-login-ui-portal/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/portal/UiLoginPortalProvider.java b/webfx-stack-authn-login-ui-portal/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/portal/UiLoginPortalProvider.java index 00cdba0bb..1ed79cbb0 100644 --- a/webfx-stack-authn-login-ui-portal/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/portal/UiLoginPortalProvider.java +++ b/webfx-stack-authn-login-ui-portal/src/main/java/dev/webfx/stack/authn/login/ui/spi/impl/portal/UiLoginPortalProvider.java @@ -2,8 +2,9 @@ import dev.webfx.platform.service.MultipleServiceProviders; import dev.webfx.stack.authn.login.ui.spi.UiLoginServiceProvider; -import dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGatewayProvider; +import dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGateway; import dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginPortalCallback; +import javafx.beans.property.StringProperty; import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.Scene; @@ -11,14 +12,15 @@ import java.util.ArrayList; import java.util.List; import java.util.ServiceLoader; +import java.util.function.Consumer; /** * @author Bruno Salmon */ public class UiLoginPortalProvider implements UiLoginServiceProvider, UiLoginPortalCallback { - static List getProviders() { - return MultipleServiceProviders.getProviders(UiLoginGatewayProvider.class, () -> ServiceLoader.load(UiLoginGatewayProvider.class)); + static List getGateways() { + return MultipleServiceProviders.getProviders(UiLoginGateway.class, () -> ServiceLoader.load(UiLoginGateway.class)); } // Several login ui may be instantiated in different activities, but only one should be visible at a time (= active login) @@ -26,7 +28,16 @@ static List getProviders() { @Override public Node createLoginUi() { // Called each time a login window is required - LoginPortalUi loginPortalUi = new LoginPortalUi(); + return createLoginPortalUi(null, null); + } + + @Override + public Node createMagicLinkUi(StringProperty magicLinkTokenProperty, Consumer requestedPathConsumer) { // Called each time a magic link window is required + return createLoginPortalUi(magicLinkTokenProperty, requestedPathConsumer); + } + + private Node createLoginPortalUi(StringProperty magicLinkTokenProperty, Consumer requestedPathConsumer) { + LoginPortalUi loginPortalUi = new LoginPortalUi(magicLinkTokenProperty, requestedPathConsumer); loginPortalUis.add(loginPortalUi); return loginPortalUi.getFlipPane(); } diff --git a/webfx-stack-authn-login-ui-portal/src/main/java/module-info.java b/webfx-stack-authn-login-ui-portal/src/main/java/module-info.java index 65976b05d..9d698ec34 100644 --- a/webfx-stack-authn-login-ui-portal/src/main/java/module-info.java +++ b/webfx-stack-authn-login-ui-portal/src/main/java/module-info.java @@ -3,6 +3,7 @@ module webfx.stack.authn.login.ui.portal { // Direct dependencies modules + requires javafx.base; requires javafx.controls; requires javafx.graphics; requires webfx.extras.panes; @@ -14,6 +15,7 @@ requires webfx.platform.uischeduler; requires webfx.stack.authn.login.ui; requires webfx.stack.authn.login.ui.gateway; + requires webfx.stack.authn.login.ui.gateway.magiclink.plugin; // Exported packages exports dev.webfx.stack.authn.login.ui.spi.impl.portal; @@ -22,7 +24,7 @@ opens dev.webfx.kit.css.fonts.password; // Used services - uses dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGatewayProvider; + uses dev.webfx.stack.authn.login.ui.spi.impl.gateway.UiLoginGateway; // Provided services provides dev.webfx.stack.authn.login.ui.spi.UiLoginServiceProvider with dev.webfx.stack.authn.login.ui.spi.impl.portal.UiLoginPortalProvider; diff --git a/webfx-stack-authn-login-ui-portal/src/main/webfx/css/webfx-stack-authn-login-ui-portal-javafx@main.css b/webfx-stack-authn-login-ui-portal/src/main/webfx/css/webfx-stack-authn-login-ui-portal-javafx@main.css index 8a9b6627c..5e204d0e9 100644 --- a/webfx-stack-authn-login-ui-portal/src/main/webfx/css/webfx-stack-authn-login-ui-portal-javafx@main.css +++ b/webfx-stack-authn-login-ui-portal/src/main/webfx/css/webfx-stack-authn-login-ui-portal-javafx@main.css @@ -1,31 +1,24 @@ - -.login { - -fx-font-size: 18px; -} - -.login Button { - -fx-background-color: #0096D6FF; - -fx-background-radius: 10px; - -fx-text-fill: white; - -fx-curson: hand; -} - -.login Button SVGPath { - -fx-fill: white; -} - -.login .or { - -fx-fill: #888; +* { + -webfx-login-portal-background-image: none; /* Specify the path to your image */ } -.login .line { - -fx-background-color: lightgray; +.login { + -fx-background-image: none; /*-webfx-login-portal-background-image; not working*/ + -fx-background-repeat: no-repeat; /* Prevents the image from repeating */ + -fx-background-size: cover; /* Scales the image to cover the area */ + -fx-background-position: center; } .login .background { - -fx-background-color: white; + -fx-background-image: none; + -fx-background-color: rgba(255, 255, 255, 0.7); -fx-background-radius: 20px; -fx-border-color: lightgray; -fx-border-radius: 20px; - -fx-effect: dropshadow(gaussian, lightgray, 10, 0, 0, 0) + -fx-effect: dropshadow(gaussian, lightgray, 10, 0, 0, 0); } + +.login .h2 { + -fx-font-family: 'Poppins', sans-serif; + -fx-line-spacing: 0; +} \ No newline at end of file diff --git a/webfx-stack-authn-login-ui-portal/src/main/webfx/css/webfx-stack-authn-login-ui-portal-web@main.css b/webfx-stack-authn-login-ui-portal/src/main/webfx/css/webfx-stack-authn-login-ui-portal-web@main.css index b0fe5c765..bf047f4d0 100644 --- a/webfx-stack-authn-login-ui-portal/src/main/webfx/css/webfx-stack-authn-login-ui-portal-web@main.css +++ b/webfx-stack-authn-login-ui-portal/src/main/webfx/css/webfx-stack-authn-login-ui-portal-web@main.css @@ -1,26 +1,5 @@ -.login * { - font-size: 18px; -} - -.login fx-button { - cursor: pointer; -} - -.login fx-button > fx-background { - background: #0096D6FF; - border-radius: 15px; -} - -.login fx-button > fx-border { - --fx-border-color: transparent; -} - -.login fx-button .text { - color: white; -} - -.login fx-button fx-svgpath { - --fx-svg-path-fill: white; +:root { + --webfx-login-portal-background-image: none; } /* Montserrat password dots are tiny! So we use another font for passwords, but we don't want that font for the placeholder! @@ -38,6 +17,32 @@ input[type="password"]:not([value=""]) { font-weight: 100 900; } +.login { + background-image: var(--webfx-login-portal-background-image); /* Specify the path to your image */ + background-repeat: no-repeat; /* Prevents the image from repeating */ + background-size: cover; /* Scales the image to cover the area */ + background-position: center; /* Centers the image */ + --fx-border-radius: 21px; +} + +.login input { + font-size: 13px; + } + +.login-child { + background-color: rgba(255, 255, 255, 0.8); + border-radius: 21px; + /* border: 1px solid lightgray;*/ + /* box-shadow: 0px 0px 10px lightgray;*/ + width: 586px; /* Sets the fixed width */ + height: 506px; /* Sets the fixed height */ +} + +.login .h2 { + font-family: 'Poppins', sans-serif; /* Sets the font to Poppins, with a fallback to sans-serif */ + line-height: 0.5; +} + .login .or { color: #888; } @@ -46,13 +51,11 @@ input[type="password"]:not([value=""]) { background-color: lightgray; } -.login .background > fx-background { - background-color: white; - border-radius: 20px; +.transparent-input > fx-background{ + background-color: transparent; } -.login .fx-border > fx-border { - --fx-border-color: lightgray; - --fx-border-radius: 20px; - box-shadow: lightgray 0 0 10px; +.transparent-input > fx-border{ + border-style: none; } + diff --git a/webfx-stack-authn-login-ui/src/main/java/dev/webfx/stack/authn/login/ui/LoginUiService.java b/webfx-stack-authn-login-ui/src/main/java/dev/webfx/stack/authn/login/ui/LoginUiService.java index 8a6d1cfff..bbd649e35 100644 --- a/webfx-stack-authn-login-ui/src/main/java/dev/webfx/stack/authn/login/ui/LoginUiService.java +++ b/webfx-stack-authn-login-ui/src/main/java/dev/webfx/stack/authn/login/ui/LoginUiService.java @@ -2,9 +2,11 @@ import dev.webfx.platform.service.SingleServiceProvider; import dev.webfx.stack.authn.login.ui.spi.UiLoginServiceProvider; +import javafx.beans.property.StringProperty; import javafx.scene.Node; import java.util.ServiceLoader; +import java.util.function.Consumer; /** * @author Bruno Salmon @@ -19,4 +21,8 @@ public static Node createLoginUI() { return getProvider().createLoginUi(); } + public static Node createMagicLinkUi(StringProperty magicLinkTokenProperty, Consumer requestedPathConsumer) { + return getProvider().createMagicLinkUi(magicLinkTokenProperty, requestedPathConsumer); + } + } diff --git a/webfx-stack-authn-login-ui/src/main/java/dev/webfx/stack/authn/login/ui/spi/UiLoginServiceProvider.java b/webfx-stack-authn-login-ui/src/main/java/dev/webfx/stack/authn/login/ui/spi/UiLoginServiceProvider.java index db33800f9..0842693d9 100644 --- a/webfx-stack-authn-login-ui/src/main/java/dev/webfx/stack/authn/login/ui/spi/UiLoginServiceProvider.java +++ b/webfx-stack-authn-login-ui/src/main/java/dev/webfx/stack/authn/login/ui/spi/UiLoginServiceProvider.java @@ -1,9 +1,14 @@ package dev.webfx.stack.authn.login.ui.spi; +import javafx.beans.property.StringProperty; import javafx.scene.Node; +import java.util.function.Consumer; + public interface UiLoginServiceProvider { Node createLoginUi(); + Node createMagicLinkUi(StringProperty magicLinkTokenProperty, Consumer requestedPathConsumer); + } diff --git a/webfx-stack-authn-logout-client/pom.xml b/webfx-stack-authn-logout-client/pom.xml index 4aedffcfd..259530d97 100644 --- a/webfx-stack-authn-logout-client/pom.xml +++ b/webfx-stack-authn-logout-client/pom.xml @@ -35,6 +35,12 @@ 0.1.0-SNAPSHOT
+ + dev.webfx + webfx-stack-i18n + 0.1.0-SNAPSHOT + + dev.webfx webfx-stack-ui-operation diff --git a/webfx-stack-authn-logout-client/src/main/java/dev/webfx/stack/authn/logout/client/operation/LogoutI18nKeys.java b/webfx-stack-authn-logout-client/src/main/java/dev/webfx/stack/authn/logout/client/operation/LogoutI18nKeys.java new file mode 100644 index 000000000..be23ed5c7 --- /dev/null +++ b/webfx-stack-authn-logout-client/src/main/java/dev/webfx/stack/authn/logout/client/operation/LogoutI18nKeys.java @@ -0,0 +1,8 @@ +// File managed by WebFX (DO NOT EDIT MANUALLY) +package dev.webfx.stack.authn.logout.client.operation; + +public interface LogoutI18nKeys { + + String Logout = "Logout"; + +} \ No newline at end of file diff --git a/webfx-stack-authn-logout-client/src/main/java/dev/webfx/stack/authn/logout/client/operation/LogoutRequest.java b/webfx-stack-authn-logout-client/src/main/java/dev/webfx/stack/authn/logout/client/operation/LogoutRequest.java index 2393bf3a6..e2a716a3e 100644 --- a/webfx-stack-authn-logout-client/src/main/java/dev/webfx/stack/authn/logout/client/operation/LogoutRequest.java +++ b/webfx-stack-authn-logout-client/src/main/java/dev/webfx/stack/authn/logout/client/operation/LogoutRequest.java @@ -1,13 +1,14 @@ package dev.webfx.stack.authn.logout.client.operation; import dev.webfx.platform.async.AsyncFunction; +import dev.webfx.stack.i18n.HasI18nKey; import dev.webfx.stack.ui.operation.HasOperationCode; import dev.webfx.stack.ui.operation.HasOperationExecutor; /** * @author Bruno Salmon */ -public final class LogoutRequest implements HasOperationCode, HasOperationExecutor { +public final class LogoutRequest implements HasOperationCode, HasI18nKey, HasOperationExecutor { private final static String OPERATION_CODE = "Logout"; @@ -17,6 +18,11 @@ public Object getOperationCode() { return OPERATION_CODE; } + @Override + public Object getI18nKey() { + return LogoutI18nKeys.Logout; + } + @Override public AsyncFunction getOperationExecutor() { return LogoutExecutor::executeRequest; diff --git a/webfx-stack-authn-logout-client/src/main/java/module-info.java b/webfx-stack-authn-logout-client/src/main/java/module-info.java index 2c805f58b..7647a8987 100644 --- a/webfx-stack-authn-logout-client/src/main/java/module-info.java +++ b/webfx-stack-authn-logout-client/src/main/java/module-info.java @@ -5,6 +5,7 @@ // Direct dependencies modules requires webfx.platform.async; requires webfx.stack.authn; + requires webfx.stack.i18n; requires webfx.stack.ui.operation; // Exported packages diff --git a/webfx-stack-authn-logout-client/src/main/webfx/i18n/webfx-stack-logout@en.yaml b/webfx-stack-authn-logout-client/src/main/webfx/i18n/webfx-stack-logout@en.yaml index b0e86fa6c..3d6f5cfab 100644 --- a/webfx-stack-authn-logout-client/src/main/webfx/i18n/webfx-stack-logout@en.yaml +++ b/webfx-stack-authn-logout-client/src/main/webfx/i18n/webfx-stack-logout@en.yaml @@ -2,4 +2,4 @@ Logout: text: "Logout" graphic: fill: "#828788" - svgPath: "M 0 8.86855 C 0 6.933 0.783871 5.1813 2.05208 3.91397 L 3.29051 5.15241 C 2.55555 5.88732 2.05503 6.82364 1.85223 7.84307 C 1.64944 8.86244 1.75348 9.91906 2.15121 10.8793 C 2.54894 11.8396 3.22249 12.6603 4.08668 13.2377 C 4.95085 13.8152 5.9669 14.1234 7.00622 14.1234 C 8.04559 14.1234 9.06159 13.8152 9.92581 13.2377 C 10.79 12.6603 11.4635 11.8396 11.8613 10.8793 C 12.259 9.91906 12.3631 8.86244 12.1602 7.84307 C 11.9575 6.82364 11.4569 5.88732 10.722 5.15241 L 11.9613 3.91397 C 12.776 4.72875 13.3772 5.73187 13.7117 6.8345 C 14.0461 7.93712 14.1035 9.10525 13.8787 10.2353 C 13.6539 11.3654 13.1539 12.4226 12.4229 13.3133 C 11.6919 14.204 10.7526 14.9007 9.68804 15.3416 C 8.62354 15.7826 7.46671 15.9542 6.31999 15.8412 C 5.17332 15.7283 4.07216 15.3344 3.1141 14.6942 C 2.15603 14.0541 1.37062 13.1876 0.827437 12.1714 C 0.284245 11.1552 4.19861e-05 10.0208 0 8.86855 Z M 7.88256 1.00084 C 7.88256 0.76855 7.79027 0.545777 7.62602 0.381526 C 7.46177 0.217275 7.23901 0.125 7.00671 0.125 C 6.77441 0.125 6.55165 0.217275 6.3874 0.381526 C 6.22315 0.545777 6.13091 0.76855 6.13091 1.00084 L 6.12827 8.01015 C 6.12827 8.24245 6.22051 8.46521 6.38476 8.62946 C 6.54901 8.79371 6.77182 8.88595 7.00407 8.88595 C 7.23637 8.88595 7.45913 8.79371 7.62338 8.62946 C 7.78763 8.46521 7.87992 8.24245 7.87992 8.01015 L 7.88256 1.00171 Z" + svgPath: "M 2.3 21 C 1.7 21 1.1 20.7 0.7 20.3 C 0.2 19.8 0 19.3 0 18.6 V 2.4 C 0 1.7 0.2 1.2 0.7 0.8 C 1.1 0.3 1.7 0.1 2.3 0.1 H 9.2 C 9.6 0.1 9.8 0.2 10.1 0.4 C 10.3 0.7 10.4 0.9 10.4 1.2 C 10.4 1.6 10.3 1.8 10.1 2.1 C 9.8 2.3 9.6 2.4 9.2 2.4 H 2.3 V 18.6 H 9.2 C 9.6 18.6 9.8 18.8 10.1 19 C 10.3 19.2 10.4 19.5 10.4 19.8 C 10.4 20.1 10.3 20.4 10.1 20.6 C 9.8 20.8 9.6 21 9.2 21 H 2.3 Z M 14.6 11.7 H 6.3 C 5.9 11.7 5.7 11.6 5.5 11.3 C 5.2 11.1 5.1 10.9 5.1 10.5 C 5.1 10.2 5.2 9.9 5.5 9.7 C 5.7 9.5 5.9 9.4 6.3 9.4 H 14.6 L 12.4 7.2 C 12.2 6.9 12.1 6.7 12.1 6.5 C 12.1 6.1 12.2 5.9 12.4 5.6 C 12.7 5.3 12.9 5.2 13.2 5.2 C 13.6 5.2 13.8 5.3 14.1 5.5 L 18.2 9.7 C 18.5 9.9 18.6 10.2 18.6 10.5 C 18.6 10.9 18.5 11.1 18.2 11.3 L 14.1 15.5 C 13.8 15.7 13.6 15.9 13.2 15.9 C 12.9 15.9 12.7 15.7 12.4 15.5 C 12.2 15.3 12.1 14.9 12.1 14.6 C 12.1 14.3 12.2 14 12.4 13.8 L 14.6 11.7 Z" diff --git a/webfx-stack-authn-logout-client/webfx.xml b/webfx-stack-authn-logout-client/webfx.xml index 9e8facde4..277f0343f 100644 --- a/webfx-stack-authn-logout-client/webfx.xml +++ b/webfx-stack-authn-logout-client/webfx.xml @@ -9,4 +9,7 @@ + + + \ No newline at end of file diff --git a/webfx-stack-authn-server-gateway-facebook-plugin/src/main/java/dev/webfx/stack/authn/server/gateway/spi/impl/facebook/FacebookServerAuthenticationGatewayProvider.java b/webfx-stack-authn-server-gateway-facebook-plugin/src/main/java/dev/webfx/stack/authn/server/gateway/spi/impl/facebook/FacebookServerAuthenticationGateway.java similarity index 94% rename from webfx-stack-authn-server-gateway-facebook-plugin/src/main/java/dev/webfx/stack/authn/server/gateway/spi/impl/facebook/FacebookServerAuthenticationGatewayProvider.java rename to webfx-stack-authn-server-gateway-facebook-plugin/src/main/java/dev/webfx/stack/authn/server/gateway/spi/impl/facebook/FacebookServerAuthenticationGateway.java index 5b90c10e2..f68cc7d0a 100644 --- a/webfx-stack-authn-server-gateway-facebook-plugin/src/main/java/dev/webfx/stack/authn/server/gateway/spi/impl/facebook/FacebookServerAuthenticationGatewayProvider.java +++ b/webfx-stack-authn-server-gateway-facebook-plugin/src/main/java/dev/webfx/stack/authn/server/gateway/spi/impl/facebook/FacebookServerAuthenticationGateway.java @@ -6,15 +6,15 @@ import dev.webfx.platform.console.Console; import dev.webfx.platform.fetch.json.JsonFetch; import dev.webfx.stack.authn.UserClaims; -import dev.webfx.stack.authn.server.gateway.spi.impl.ServerAuthenticationGatewayProviderBase; +import dev.webfx.stack.authn.server.gateway.spi.impl.ServerAuthenticationGatewayBase; import dev.webfx.stack.session.state.ThreadLocalStateHolder; -import static dev.webfx.stack.authn.login.spi.impl.server.gateway.facebook.FacebookServerLoginGatewayProvider.*; +import static dev.webfx.stack.authn.login.spi.impl.server.gateway.facebook.FacebookServerLoginGateway.*; /** * @author Bruno Salmon */ -public final class FacebookServerAuthenticationGatewayProvider extends ServerAuthenticationGatewayProviderBase { +public final class FacebookServerAuthenticationGateway extends ServerAuthenticationGatewayBase { private final static String FACEBOOK_AUTH_TOKEN_PREFIX = "Facebook."; private static final String FB_JSON_API_APP_TOKEN_URL_TEMPLATE = "https://graph.facebook.com/oauth/access_token?client_id={{CLIENT_ID}}&client_secret={{CLIENT_SECRET}}&grant_type=client_credentials"; @@ -24,7 +24,7 @@ public final class FacebookServerAuthenticationGatewayProvider extends ServerAut private String appAccessToken; - public FacebookServerAuthenticationGatewayProvider() { + public FacebookServerAuthenticationGateway() { super(FACEBOOK_AUTH_TOKEN_PREFIX); } diff --git a/webfx-stack-authn-server-gateway-facebook-plugin/src/main/java/module-info.java b/webfx-stack-authn-server-gateway-facebook-plugin/src/main/java/module-info.java index 6107ef6d5..a7622ddb1 100644 --- a/webfx-stack-authn-server-gateway-facebook-plugin/src/main/java/module-info.java +++ b/webfx-stack-authn-server-gateway-facebook-plugin/src/main/java/module-info.java @@ -16,6 +16,6 @@ exports dev.webfx.stack.authn.server.gateway.spi.impl.facebook; // Provided services - provides dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGatewayProvider with dev.webfx.stack.authn.server.gateway.spi.impl.facebook.FacebookServerAuthenticationGatewayProvider; + provides dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGateway with dev.webfx.stack.authn.server.gateway.spi.impl.facebook.FacebookServerAuthenticationGateway; } \ No newline at end of file diff --git a/webfx-stack-authn-server-gateway-facebook-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGatewayProvider b/webfx-stack-authn-server-gateway-facebook-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGateway similarity index 64% rename from webfx-stack-authn-server-gateway-facebook-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGatewayProvider rename to webfx-stack-authn-server-gateway-facebook-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGateway index 32bed572c..9e9c5e467 100644 --- a/webfx-stack-authn-server-gateway-facebook-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGatewayProvider +++ b/webfx-stack-authn-server-gateway-facebook-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGateway @@ -1 +1 @@ -dev.webfx.stack.authn.server.gateway.spi.impl.facebook.FacebookServerAuthenticationGatewayProvider +dev.webfx.stack.authn.server.gateway.spi.impl.facebook.FacebookServerAuthenticationGateway diff --git a/webfx-stack-authn-server-gateway-facebook-plugin/webfx.xml b/webfx-stack-authn-server-gateway-facebook-plugin/webfx.xml index 935563d0b..021f0d9fd 100644 --- a/webfx-stack-authn-server-gateway-facebook-plugin/webfx.xml +++ b/webfx-stack-authn-server-gateway-facebook-plugin/webfx.xml @@ -10,7 +10,7 @@ - dev.webfx.stack.authn.server.gateway.spi.impl.facebook.FacebookServerAuthenticationGatewayProvider + dev.webfx.stack.authn.server.gateway.spi.impl.facebook.FacebookServerAuthenticationGateway \ No newline at end of file diff --git a/webfx-stack-authn-server-gateway-google-plugin/src/main/java/dev/webfx/stack/authn/server/gateway/spi/impl/google/GoogleServerAuthenticationGatewayProvider.java b/webfx-stack-authn-server-gateway-google-plugin/src/main/java/dev/webfx/stack/authn/server/gateway/spi/impl/google/GoogleServerAuthenticationGateway.java similarity index 84% rename from webfx-stack-authn-server-gateway-google-plugin/src/main/java/dev/webfx/stack/authn/server/gateway/spi/impl/google/GoogleServerAuthenticationGatewayProvider.java rename to webfx-stack-authn-server-gateway-google-plugin/src/main/java/dev/webfx/stack/authn/server/gateway/spi/impl/google/GoogleServerAuthenticationGateway.java index 8835ee71c..d53cc7b76 100644 --- a/webfx-stack-authn-server-gateway-google-plugin/src/main/java/dev/webfx/stack/authn/server/gateway/spi/impl/google/GoogleServerAuthenticationGatewayProvider.java +++ b/webfx-stack-authn-server-gateway-google-plugin/src/main/java/dev/webfx/stack/authn/server/gateway/spi/impl/google/GoogleServerAuthenticationGateway.java @@ -4,16 +4,16 @@ import dev.webfx.platform.ast.ReadOnlyAstObject; import dev.webfx.stack.authn.UserClaims; import dev.webfx.stack.authn.server.gateway.spi.impl.Jwt; -import dev.webfx.stack.authn.server.gateway.spi.impl.ServerAuthenticationGatewayProviderBase; +import dev.webfx.stack.authn.server.gateway.spi.impl.ServerAuthenticationGatewayBase; /** * @author Bruno Salmon */ -public final class GoogleServerAuthenticationGatewayProvider extends ServerAuthenticationGatewayProviderBase { +public final class GoogleServerAuthenticationGateway extends ServerAuthenticationGatewayBase { private final static String GOOGLE_GATEWAY_AUTH_PREFIX = "Google."; - public GoogleServerAuthenticationGatewayProvider() { + public GoogleServerAuthenticationGateway() { super(GOOGLE_GATEWAY_AUTH_PREFIX); } diff --git a/webfx-stack-authn-server-gateway-google-plugin/src/main/java/module-info.java b/webfx-stack-authn-server-gateway-google-plugin/src/main/java/module-info.java index ae71259a3..4285161cf 100644 --- a/webfx-stack-authn-server-gateway-google-plugin/src/main/java/module-info.java +++ b/webfx-stack-authn-server-gateway-google-plugin/src/main/java/module-info.java @@ -12,6 +12,6 @@ exports dev.webfx.stack.authn.server.gateway.spi.impl.google; // Provided services - provides dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGatewayProvider with dev.webfx.stack.authn.server.gateway.spi.impl.google.GoogleServerAuthenticationGatewayProvider; + provides dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGateway with dev.webfx.stack.authn.server.gateway.spi.impl.google.GoogleServerAuthenticationGateway; } \ No newline at end of file diff --git a/webfx-stack-authn-server-gateway-google-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGatewayProvider b/webfx-stack-authn-server-gateway-google-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGateway similarity index 67% rename from webfx-stack-authn-server-gateway-google-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGatewayProvider rename to webfx-stack-authn-server-gateway-google-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGateway index 0a149184f..da203d2c7 100644 --- a/webfx-stack-authn-server-gateway-google-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGatewayProvider +++ b/webfx-stack-authn-server-gateway-google-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGateway @@ -1 +1 @@ -dev.webfx.stack.authn.server.gateway.spi.impl.google.GoogleServerAuthenticationGatewayProvider +dev.webfx.stack.authn.server.gateway.spi.impl.google.GoogleServerAuthenticationGateway diff --git a/webfx-stack-authn-server-gateway-google-plugin/webfx.xml b/webfx-stack-authn-server-gateway-google-plugin/webfx.xml index 48c68cc3d..ff8415af7 100644 --- a/webfx-stack-authn-server-gateway-google-plugin/webfx.xml +++ b/webfx-stack-authn-server-gateway-google-plugin/webfx.xml @@ -10,7 +10,7 @@ - dev.webfx.stack.authn.server.gateway.spi.impl.google.GoogleServerAuthenticationGatewayProvider + dev.webfx.stack.authn.server.gateway.spi.impl.google.GoogleServerAuthenticationGateway \ No newline at end of file diff --git a/webfx-stack-authn-server-gateway-mojoauth-plugin/src/main/java/dev/webfx/stack/authn/server/gateway/spi/mojoauth/MojoAuthServerAuthenticationGatewayProvider.java b/webfx-stack-authn-server-gateway-mojoauth-plugin/src/main/java/dev/webfx/stack/authn/server/gateway/spi/mojoauth/MojoAuthServerAuthenticationGateway.java similarity index 92% rename from webfx-stack-authn-server-gateway-mojoauth-plugin/src/main/java/dev/webfx/stack/authn/server/gateway/spi/mojoauth/MojoAuthServerAuthenticationGatewayProvider.java rename to webfx-stack-authn-server-gateway-mojoauth-plugin/src/main/java/dev/webfx/stack/authn/server/gateway/spi/mojoauth/MojoAuthServerAuthenticationGateway.java index 14bdee873..2724ce126 100644 --- a/webfx-stack-authn-server-gateway-mojoauth-plugin/src/main/java/dev/webfx/stack/authn/server/gateway/spi/mojoauth/MojoAuthServerAuthenticationGatewayProvider.java +++ b/webfx-stack-authn-server-gateway-mojoauth-plugin/src/main/java/dev/webfx/stack/authn/server/gateway/spi/mojoauth/MojoAuthServerAuthenticationGateway.java @@ -10,24 +10,24 @@ import dev.webfx.platform.async.Promise; import dev.webfx.platform.ast.ReadOnlyAstObject; import dev.webfx.stack.authn.UserClaims; -import dev.webfx.stack.authn.login.spi.impl.server.gateway.mojoauth.MojoAuthServerLoginGatewayProvider; +import dev.webfx.stack.authn.login.spi.impl.server.gateway.mojoauth.MojoAuthServerLoginGateway; import dev.webfx.stack.authn.server.gateway.spi.impl.Jwt; -import dev.webfx.stack.authn.server.gateway.spi.impl.ServerAuthenticationGatewayProviderBase; +import dev.webfx.stack.authn.server.gateway.spi.impl.ServerAuthenticationGatewayBase; /** * @author Bruno Salmon */ -public final class MojoAuthServerAuthenticationGatewayProvider extends ServerAuthenticationGatewayProviderBase { +public final class MojoAuthServerAuthenticationGateway extends ServerAuthenticationGatewayBase { private final static String MOJO_AUTH_PREFIX = "MojoAuth."; //private final static String USERS_STATUS_URL = "https://api.mojoauth.com/users/status"; private MojoAuthApi mojoAuthApi; - public MojoAuthServerAuthenticationGatewayProvider() { + public MojoAuthServerAuthenticationGateway() { super(MOJO_AUTH_PREFIX); - MojoAuthServerLoginGatewayProvider.onValidConfig(() -> { - MojoAuthSDK.Initialize.setApiKey(MojoAuthServerLoginGatewayProvider.MOJO_AUTH_API_KEY); + MojoAuthServerLoginGateway.onValidConfig(() -> { + MojoAuthSDK.Initialize.setApiKey(MojoAuthServerLoginGateway.MOJO_AUTH_API_KEY); mojoAuthApi = new MojoAuthApi(); }); } diff --git a/webfx-stack-authn-server-gateway-mojoauth-plugin/src/main/java/module-info.java b/webfx-stack-authn-server-gateway-mojoauth-plugin/src/main/java/module-info.java index fc2d1870b..faa2d056f 100644 --- a/webfx-stack-authn-server-gateway-mojoauth-plugin/src/main/java/module-info.java +++ b/webfx-stack-authn-server-gateway-mojoauth-plugin/src/main/java/module-info.java @@ -14,6 +14,6 @@ exports dev.webfx.stack.authn.server.gateway.spi.mojoauth; // Provided services - provides dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGatewayProvider with dev.webfx.stack.authn.server.gateway.spi.mojoauth.MojoAuthServerAuthenticationGatewayProvider; + provides dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGateway with dev.webfx.stack.authn.server.gateway.spi.mojoauth.MojoAuthServerAuthenticationGateway; } \ No newline at end of file diff --git a/webfx-stack-authn-server-gateway-mojoauth-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGatewayProvider b/webfx-stack-authn-server-gateway-mojoauth-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGateway similarity index 68% rename from webfx-stack-authn-server-gateway-mojoauth-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGatewayProvider rename to webfx-stack-authn-server-gateway-mojoauth-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGateway index 0c68143d2..5b8a3564b 100644 --- a/webfx-stack-authn-server-gateway-mojoauth-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGatewayProvider +++ b/webfx-stack-authn-server-gateway-mojoauth-plugin/src/main/resources/META-INF/services/dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGateway @@ -1 +1 @@ -dev.webfx.stack.authn.server.gateway.spi.mojoauth.MojoAuthServerAuthenticationGatewayProvider +dev.webfx.stack.authn.server.gateway.spi.mojoauth.MojoAuthServerAuthenticationGateway diff --git a/webfx-stack-authn-server-gateway-mojoauth-plugin/webfx.xml b/webfx-stack-authn-server-gateway-mojoauth-plugin/webfx.xml index 07f175b44..ae4b2c495 100644 --- a/webfx-stack-authn-server-gateway-mojoauth-plugin/webfx.xml +++ b/webfx-stack-authn-server-gateway-mojoauth-plugin/webfx.xml @@ -10,7 +10,7 @@ - dev.webfx.stack.authn.server.gateway.spi.mojoauth.MojoAuthServerAuthenticationGatewayProvider + dev.webfx.stack.authn.server.gateway.spi.mojoauth.MojoAuthServerAuthenticationGateway diff --git a/webfx-stack-authn-server-gateway/src/main/java/dev/webfx/stack/authn/server/gateway/spi/ServerAuthenticationGatewayProvider.java b/webfx-stack-authn-server-gateway/src/main/java/dev/webfx/stack/authn/server/gateway/spi/ServerAuthenticationGateway.java similarity index 91% rename from webfx-stack-authn-server-gateway/src/main/java/dev/webfx/stack/authn/server/gateway/spi/ServerAuthenticationGatewayProvider.java rename to webfx-stack-authn-server-gateway/src/main/java/dev/webfx/stack/authn/server/gateway/spi/ServerAuthenticationGateway.java index 526184108..4bdab77cf 100644 --- a/webfx-stack-authn-server-gateway/src/main/java/dev/webfx/stack/authn/server/gateway/spi/ServerAuthenticationGatewayProvider.java +++ b/webfx-stack-authn-server-gateway/src/main/java/dev/webfx/stack/authn/server/gateway/spi/ServerAuthenticationGateway.java @@ -3,7 +3,7 @@ import dev.webfx.platform.async.Future; import dev.webfx.stack.authn.UserClaims; -public interface ServerAuthenticationGatewayProvider { +public interface ServerAuthenticationGateway { default void boot() {} diff --git a/webfx-stack-authn-server-gateway/src/main/java/dev/webfx/stack/authn/server/gateway/spi/impl/ServerAuthenticationGatewayProviderBase.java b/webfx-stack-authn-server-gateway/src/main/java/dev/webfx/stack/authn/server/gateway/spi/impl/ServerAuthenticationGatewayBase.java similarity index 92% rename from webfx-stack-authn-server-gateway/src/main/java/dev/webfx/stack/authn/server/gateway/spi/impl/ServerAuthenticationGatewayProviderBase.java rename to webfx-stack-authn-server-gateway/src/main/java/dev/webfx/stack/authn/server/gateway/spi/impl/ServerAuthenticationGatewayBase.java index 7efb155b0..24971a20a 100644 --- a/webfx-stack-authn-server-gateway/src/main/java/dev/webfx/stack/authn/server/gateway/spi/impl/ServerAuthenticationGatewayProviderBase.java +++ b/webfx-stack-authn-server-gateway/src/main/java/dev/webfx/stack/authn/server/gateway/spi/impl/ServerAuthenticationGatewayBase.java @@ -4,15 +4,15 @@ import dev.webfx.platform.async.Promise; import dev.webfx.stack.authn.UserClaims; import dev.webfx.stack.authn.logout.server.LogoutPush; -import dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGatewayProvider; +import dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGateway; import dev.webfx.stack.session.state.ThreadLocalStateHolder; /** * @author Bruno Salmon */ -public abstract class ServerAuthenticationGatewayProviderBase implements ServerAuthenticationGatewayProvider { +public abstract class ServerAuthenticationGatewayBase implements ServerAuthenticationGateway { private final String gatewayAuthPrefix; - public ServerAuthenticationGatewayProviderBase(String gatewayAuthPrefix) { + public ServerAuthenticationGatewayBase(String gatewayAuthPrefix) { this.gatewayAuthPrefix = gatewayAuthPrefix; } diff --git a/webfx-stack-authn-server-portal/src/main/java/dev/webfx/stack/authn/spi/impl/server/portal/ServerAuthenticationPortalProvider.java b/webfx-stack-authn-server-portal/src/main/java/dev/webfx/stack/authn/spi/impl/server/portal/ServerAuthenticationPortalProvider.java index b0529df56..591d49923 100644 --- a/webfx-stack-authn-server-portal/src/main/java/dev/webfx/stack/authn/spi/impl/server/portal/ServerAuthenticationPortalProvider.java +++ b/webfx-stack-authn-server-portal/src/main/java/dev/webfx/stack/authn/spi/impl/server/portal/ServerAuthenticationPortalProvider.java @@ -2,37 +2,41 @@ import dev.webfx.platform.async.Future; import dev.webfx.platform.async.FutureBroadcaster; +import dev.webfx.platform.console.Console; import dev.webfx.platform.service.MultipleServiceProviders; import dev.webfx.stack.authn.UserClaims; -import dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGatewayProvider; +import dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGateway; import dev.webfx.stack.authn.spi.AuthenticationServiceProvider; import dev.webfx.stack.session.state.ThreadLocalStateHolder; -import java.util.*; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; /** * @author Bruno Salmon */ public class ServerAuthenticationPortalProvider implements AuthenticationServiceProvider { - private static List getGatewayProviders() { - return MultipleServiceProviders.getProviders(ServerAuthenticationGatewayProvider.class, () -> ServiceLoader.load(ServerAuthenticationGatewayProvider.class)); + private static List getGateways() { + return MultipleServiceProviders.getProviders(ServerAuthenticationGateway.class, () -> ServiceLoader.load(ServerAuthenticationGateway.class)); } public ServerAuthenticationPortalProvider() { // Called first time on server start through AuthenticationService.getProvider() call in AuthenticateMethodEndpoint. // We instantiate the gateways (such as Google, Facebook, etc...) and call their boot() method, which may do // some initialisation (ex: fetching Facebook application access token). This must be done as soon as possible, // i.e. on server start. - for (ServerAuthenticationGatewayProvider gatewayProvider : getGatewayProviders()) - gatewayProvider.boot(); + for (ServerAuthenticationGateway gateway : getGateways()) + gateway.boot(); } @Override public Future authenticate(Object userCredentials) { - for (ServerAuthenticationGatewayProvider gatewayProvider : getGatewayProviders()) { - boolean accepts = gatewayProvider.acceptsUserCredentials(userCredentials); + for (ServerAuthenticationGateway gateway : getGateways()) { + boolean accepts = gateway.acceptsUserCredentials(userCredentials); if (accepts) - return gatewayProvider.authenticate(userCredentials); + return gateway.authenticate(userCredentials); } return Future.failedFuture("No server authentication gateway found accepting credentials " + userCredentials); } @@ -44,13 +48,13 @@ public Future verifyAuthenticated() { Object userId = ThreadLocalStateHolder.getUserId(); FutureBroadcaster userVerificationBroadcaster = userVerificationBroadcasters.get(userId); if (userVerificationBroadcaster != null) { - dev.webfx.platform.console.Console.log("👮👮 Joining same user verification broadcaster"); + Console.log("👮👮 Joining same user verification broadcaster"); return userVerificationBroadcaster.newClient(); } - for (ServerAuthenticationGatewayProvider gatewayProvider : getGatewayProviders()) { - boolean accepts = gatewayProvider.acceptsUserId(); + for (ServerAuthenticationGateway gateway : getGateways()) { + boolean accepts = gateway.acceptsUserId(); if (accepts) { - userVerificationBroadcaster = new FutureBroadcaster<>(() -> gatewayProvider.verifyAuthenticated() + userVerificationBroadcaster = new FutureBroadcaster<>(() -> gateway.verifyAuthenticated() .map(uid -> { userVerificationBroadcasters.remove(userId); return uid; @@ -64,31 +68,31 @@ public Future verifyAuthenticated() { @Override public Future getUserClaims() { - for (ServerAuthenticationGatewayProvider gatewayProvider : getGatewayProviders()) { - boolean accepts = gatewayProvider.acceptsUserId(); + for (ServerAuthenticationGateway gateway : getGateways()) { + boolean accepts = gateway.acceptsUserId(); if (accepts) - return gatewayProvider.getUserClaims(); + return gateway.getUserClaims(); } return Future.failedFuture("getUserClaims() failed on server authentication portal because no server gateway accepted UserId " + ThreadLocalStateHolder.getUserId()); } @Override public Future updateCredentials(Object updateCredentialsArgument) { - for (ServerAuthenticationGatewayProvider gatewayProvider : getGatewayProviders()) { - //boolean acceptsUserId = gatewayProvider.acceptsUserId(); - boolean acceptsArgument = gatewayProvider.acceptsUpdateCredentialsArgument(updateCredentialsArgument); + for (ServerAuthenticationGateway gateway : getGateways()) { + //boolean acceptsUserId = gateway.acceptsUserId(); + boolean acceptsArgument = gateway.acceptsUpdateCredentialsArgument(updateCredentialsArgument); if (/*acceptsUserId &&*/ acceptsArgument) - return gatewayProvider.updateCredentials(updateCredentialsArgument); + return gateway.updateCredentials(updateCredentialsArgument); } return Future.failedFuture("No server authentication gateway found accepting credentials update " + updateCredentialsArgument); } @Override public Future logout() { - for (ServerAuthenticationGatewayProvider gatewayProvider : getGatewayProviders()) { - boolean accepts = gatewayProvider.acceptsUserId(); + for (ServerAuthenticationGateway gateway : getGateways()) { + boolean accepts = gateway.acceptsUserId(); if (accepts) - return gatewayProvider.logout(); + return gateway.logout(); } return Future.failedFuture("logout() failed on server authentication portal because no server gateway accepted UserId " + ThreadLocalStateHolder.getUserId()); } diff --git a/webfx-stack-authn-server-portal/src/main/java/module-info.java b/webfx-stack-authn-server-portal/src/main/java/module-info.java index 77f0d8386..392daec24 100644 --- a/webfx-stack-authn-server-portal/src/main/java/module-info.java +++ b/webfx-stack-authn-server-portal/src/main/java/module-info.java @@ -17,7 +17,7 @@ exports dev.webfx.stack.authn.spi.impl.server.portal; // Used services - uses dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGatewayProvider; + uses dev.webfx.stack.authn.server.gateway.spi.ServerAuthenticationGateway; // Provided services provides dev.webfx.platform.boot.spi.ApplicationJob with dev.webfx.stack.authn.spi.impl.server.ServerAuthenticationJob; diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/MagicLinkRequest.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/AlternativeLoginActionCredentials.java similarity index 70% rename from webfx-stack-authn/src/main/java/dev/webfx/stack/authn/MagicLinkRequest.java rename to webfx-stack-authn/src/main/java/dev/webfx/stack/authn/AlternativeLoginActionCredentials.java index a084d6e22..425a77a64 100644 --- a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/MagicLinkRequest.java +++ b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/AlternativeLoginActionCredentials.java @@ -3,16 +3,18 @@ /** * @author Bruno Salmon */ -public final class MagicLinkRequest { +public abstract class AlternativeLoginActionCredentials { private final String email; private final String clientOrigin; // ex: https://mydomain.com The magic link will start with the same origin, so it goes back to the same server + private final String requestedPath; private final Object language; private final Object context; // ex: ModalityContext with organization & event => used to select the correct mailbox and magic link letter template - public MagicLinkRequest(String email, String clientOrigin, Object language, Object context) { + public AlternativeLoginActionCredentials(String email, String clientOrigin, String requestedPath, Object language, Object context) { this.email = email; this.clientOrigin = clientOrigin; + this.requestedPath = requestedPath; this.language = language; this.context = context; } @@ -25,6 +27,10 @@ public String getClientOrigin() { return clientOrigin; } + public String getRequestedPath() { + return requestedPath; + } + public Object getLanguage() { return language; } @@ -32,4 +38,5 @@ public Object getLanguage() { public Object getContext() { return context; } + } diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/AuthenticateWithMagicLinkCredentials.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/AuthenticateWithMagicLinkCredentials.java new file mode 100644 index 000000000..d9594b683 --- /dev/null +++ b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/AuthenticateWithMagicLinkCredentials.java @@ -0,0 +1,17 @@ +package dev.webfx.stack.authn; + +/** + * @author Bruno Salmon + */ +public final class AuthenticateWithMagicLinkCredentials { + + private final String token; + + public AuthenticateWithMagicLinkCredentials(String token) { + this.token = token; + } + + public String getToken() { + return token; + } +} diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/UsernamePasswordCredentials.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/AuthenticateWithUsernamePasswordCredentials.java similarity index 68% rename from webfx-stack-authn/src/main/java/dev/webfx/stack/authn/UsernamePasswordCredentials.java rename to webfx-stack-authn/src/main/java/dev/webfx/stack/authn/AuthenticateWithUsernamePasswordCredentials.java index ba51c7186..481fd2e66 100644 --- a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/UsernamePasswordCredentials.java +++ b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/AuthenticateWithUsernamePasswordCredentials.java @@ -3,12 +3,12 @@ /** * @author Bruno Salmon */ -public final class UsernamePasswordCredentials { +public final class AuthenticateWithUsernamePasswordCredentials { private final String username; private final String password; - public UsernamePasswordCredentials(String username, String password) { + public AuthenticateWithUsernamePasswordCredentials(String username, String password) { this.username = username; this.password = password; } diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/AuthenticationArgument.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/AuthenticationArgument.java deleted file mode 100644 index aae4c30e9..000000000 --- a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/AuthenticationArgument.java +++ /dev/null @@ -1,23 +0,0 @@ -package dev.webfx.stack.authn; - -/** - * @author Bruno Salmon - */ -public final class AuthenticationArgument { - - private final Object identity; - private final Object credentials; - - public AuthenticationArgument(Object identity, Object credentials) { - this.identity = identity; - this.credentials = credentials; - } - - public Object getIdentity() { - return identity; - } - - public Object getCredentials() { - return credentials; - } -} diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/ContinueAccountCreationCredentials.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/ContinueAccountCreationCredentials.java new file mode 100644 index 000000000..3dae56522 --- /dev/null +++ b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/ContinueAccountCreationCredentials.java @@ -0,0 +1,17 @@ +package dev.webfx.stack.authn; + +/** + * @author Bruno Salmon + */ +public final class ContinueAccountCreationCredentials { + + private final String token; + + public ContinueAccountCreationCredentials(String token) { + this.token = token; + } + + public String getToken() { + return token; + } +} diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/FinaliseAccountCreationCredentials.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/FinaliseAccountCreationCredentials.java new file mode 100644 index 000000000..a020ca6fe --- /dev/null +++ b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/FinaliseAccountCreationCredentials.java @@ -0,0 +1,23 @@ +package dev.webfx.stack.authn; + +/** + * @author Bruno Salmon + */ +public final class FinaliseAccountCreationCredentials { + + private final String token; + private final String password; + + public FinaliseAccountCreationCredentials(String token, String password) { + this.token = token; + this.password = password; + } + + public String getToken() { + return token; + } + + public String getPassword() { + return password; + } +} diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/MagicLinkCredentials.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/FinaliseEmailUpdateCredentials.java similarity index 63% rename from webfx-stack-authn/src/main/java/dev/webfx/stack/authn/MagicLinkCredentials.java rename to webfx-stack-authn/src/main/java/dev/webfx/stack/authn/FinaliseEmailUpdateCredentials.java index 18c21f0a5..310f121e6 100644 --- a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/MagicLinkCredentials.java +++ b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/FinaliseEmailUpdateCredentials.java @@ -3,15 +3,16 @@ /** * @author Bruno Salmon */ -public class MagicLinkCredentials { +public final class FinaliseEmailUpdateCredentials { private final String token; - public MagicLinkCredentials(String token) { + public FinaliseEmailUpdateCredentials(String token) { this.token = token; } public String getToken() { return token; } + } diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/InitiateAccountCreationCredentials.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/InitiateAccountCreationCredentials.java new file mode 100644 index 000000000..8da5ba1c7 --- /dev/null +++ b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/InitiateAccountCreationCredentials.java @@ -0,0 +1,12 @@ +package dev.webfx.stack.authn; + +/** + * @author Bruno Salmon + */ +public final class InitiateAccountCreationCredentials extends AlternativeLoginActionCredentials { + + public InitiateAccountCreationCredentials(String email, String clientOrigin, String requestedPath, Object language, Object context) { + super(email, clientOrigin, requestedPath, language, context); + } +} + diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/InitiateEmailUpdateCredentials.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/InitiateEmailUpdateCredentials.java new file mode 100644 index 000000000..4aee0cf07 --- /dev/null +++ b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/InitiateEmailUpdateCredentials.java @@ -0,0 +1,13 @@ +package dev.webfx.stack.authn; + +/** + * @author Bruno Salmon + */ +public final class InitiateEmailUpdateCredentials extends AlternativeLoginActionCredentials { + + public InitiateEmailUpdateCredentials(String email, String clientOrigin, String requestedPath, Object language, Object context) { + super(email, clientOrigin, requestedPath, language, context); + } + +} + diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/RenewMagicLinkCredentials.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/RenewMagicLinkCredentials.java new file mode 100644 index 000000000..74a04cb14 --- /dev/null +++ b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/RenewMagicLinkCredentials.java @@ -0,0 +1,17 @@ +package dev.webfx.stack.authn; + +/** + * @author Bruno Salmon + */ +public final class RenewMagicLinkCredentials { + + private final String previousToken; + + public RenewMagicLinkCredentials(String previousToken) { + this.previousToken = previousToken; + } + + public String getPreviousToken() { + return previousToken; + } +} diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/SendMagicLinkCredentials.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/SendMagicLinkCredentials.java new file mode 100644 index 000000000..d34661828 --- /dev/null +++ b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/SendMagicLinkCredentials.java @@ -0,0 +1,11 @@ +package dev.webfx.stack.authn; + +/** + * @author Bruno Salmon + */ +public final class SendMagicLinkCredentials extends AlternativeLoginActionCredentials { + + public SendMagicLinkCredentials(String email, String clientOrigin, String requestedPath, Object language, Object context) { + super(email, clientOrigin, requestedPath, language, context); + } +} diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/PasswordUpdate.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/UpdatePasswordCredentials.java similarity index 74% rename from webfx-stack-authn/src/main/java/dev/webfx/stack/authn/PasswordUpdate.java rename to webfx-stack-authn/src/main/java/dev/webfx/stack/authn/UpdatePasswordCredentials.java index 814c5b25a..390385e9e 100644 --- a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/PasswordUpdate.java +++ b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/UpdatePasswordCredentials.java @@ -3,12 +3,12 @@ /** * @author Bruno Salmon */ -public class PasswordUpdate { +public final class UpdatePasswordCredentials { private final String oldPassword; private final String newPassword; - public PasswordUpdate(String oldPassword, String newPassword) { + public UpdatePasswordCredentials(String oldPassword, String newPassword) { this.oldPassword = oldPassword; this.newPassword = newPassword; } diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/MagicLinkPasswordUpdate.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/UpdatePasswordFromMagicLinkCredentials.java similarity index 52% rename from webfx-stack-authn/src/main/java/dev/webfx/stack/authn/MagicLinkPasswordUpdate.java rename to webfx-stack-authn/src/main/java/dev/webfx/stack/authn/UpdatePasswordFromMagicLinkCredentials.java index 6ba4ac652..1c8a719db 100644 --- a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/MagicLinkPasswordUpdate.java +++ b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/UpdatePasswordFromMagicLinkCredentials.java @@ -3,11 +3,11 @@ /** * @author Bruno Salmon */ -public class MagicLinkPasswordUpdate { +public final class UpdatePasswordFromMagicLinkCredentials { - private String newPassword; + private final String newPassword; - public MagicLinkPasswordUpdate(String newPassword) { + public UpdatePasswordFromMagicLinkCredentials(String newPassword) { this.newPassword = newPassword; } diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/UserClaims.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/UserClaims.java index 8e93e779e..02696bca4 100644 --- a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/UserClaims.java +++ b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/UserClaims.java @@ -5,7 +5,7 @@ /** * @author Bruno Salmon */ -public class UserClaims { +public final class UserClaims { private final String username; private final String email; diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/AlternativeLoginActionCredentialsSerialCodec.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/AlternativeLoginActionCredentialsSerialCodec.java new file mode 100644 index 000000000..b2b51a103 --- /dev/null +++ b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/AlternativeLoginActionCredentialsSerialCodec.java @@ -0,0 +1,31 @@ +package dev.webfx.stack.authn.serial; + +import dev.webfx.platform.ast.AstObject; +import dev.webfx.stack.authn.AlternativeLoginActionCredentials; +import dev.webfx.stack.com.serial.spi.impl.SerialCodecBase; + +/** + * @author Bruno Salmon + */ +public abstract class AlternativeLoginActionCredentialsSerialCodec extends SerialCodecBase { + + protected static final String EMAIL_KEY = "email"; + protected static final String CLIENT_ORIGIN_KEY = "origin"; + protected static final String REQUESTED_PATH_KEY = "path"; + protected static final String LANGUAGE_KEY = "lang"; + protected static final String CONTEXT_KEY = "context"; + + public AlternativeLoginActionCredentialsSerialCodec(Class javaClass, String codecId) { + super(javaClass, codecId); + } + + @Override + public void encode(T arg, AstObject serial) { + encodeString(serial, EMAIL_KEY, arg.getEmail()); + encodeString(serial, CLIENT_ORIGIN_KEY, arg.getClientOrigin()); + encodeString(serial, REQUESTED_PATH_KEY, arg.getRequestedPath()); + encodeObject(serial, LANGUAGE_KEY, arg.getLanguage()); + encodeObject(serial, CONTEXT_KEY, arg.getContext()); + } + +} \ No newline at end of file diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/AuthenticateWithMagicLinkCredentialsSerialCodec.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/AuthenticateWithMagicLinkCredentialsSerialCodec.java new file mode 100644 index 000000000..8bf87898c --- /dev/null +++ b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/AuthenticateWithMagicLinkCredentialsSerialCodec.java @@ -0,0 +1,31 @@ +package dev.webfx.stack.authn.serial; + +import dev.webfx.platform.ast.AstObject; +import dev.webfx.platform.ast.ReadOnlyAstObject; +import dev.webfx.stack.authn.AuthenticateWithMagicLinkCredentials; +import dev.webfx.stack.com.serial.spi.impl.SerialCodecBase; + +/** + * @author Bruno Salmon + */ +public final class AuthenticateWithMagicLinkCredentialsSerialCodec extends SerialCodecBase { + + private static final String CODEC_ID = "AuthenticateWithMagicLinkCredentials"; + private static final String TOKEN_KEY = "token"; + + public AuthenticateWithMagicLinkCredentialsSerialCodec() { + super(AuthenticateWithMagicLinkCredentials.class, CODEC_ID); + } + + @Override + public void encode(AuthenticateWithMagicLinkCredentials arg, AstObject serial) { + encodeString(serial, TOKEN_KEY, arg.getToken()); + } + + @Override + public AuthenticateWithMagicLinkCredentials decode(ReadOnlyAstObject serial) { + return new AuthenticateWithMagicLinkCredentials( + decodeString(serial, TOKEN_KEY) + ); + } +} diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/AuthenticateWithUsernamePasswordCredentialsSerialCodec.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/AuthenticateWithUsernamePasswordCredentialsSerialCodec.java new file mode 100644 index 000000000..851b8ae70 --- /dev/null +++ b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/AuthenticateWithUsernamePasswordCredentialsSerialCodec.java @@ -0,0 +1,34 @@ +package dev.webfx.stack.authn.serial; + +import dev.webfx.platform.ast.AstObject; +import dev.webfx.platform.ast.ReadOnlyAstObject; +import dev.webfx.stack.authn.AuthenticateWithUsernamePasswordCredentials; +import dev.webfx.stack.com.serial.spi.impl.SerialCodecBase; + +/** + * @author Bruno Salmon + */ +public final class AuthenticateWithUsernamePasswordCredentialsSerialCodec extends SerialCodecBase { + + private static final String CODEC_ID = "AuthenticateWithUsernamePasswordCredentials"; + private static final String USERNAME_KEY = "username"; + private static final String PASSWORD_KEY = "password"; + + public AuthenticateWithUsernamePasswordCredentialsSerialCodec() { + super(AuthenticateWithUsernamePasswordCredentials.class, CODEC_ID); + } + + @Override + public void encode(AuthenticateWithUsernamePasswordCredentials arg, AstObject serial) { + encodeString(serial, USERNAME_KEY, arg.getUsername()); + encodeString(serial, PASSWORD_KEY, arg.getPassword()); + } + + @Override + public AuthenticateWithUsernamePasswordCredentials decode(ReadOnlyAstObject serial) { + return new AuthenticateWithUsernamePasswordCredentials( + decodeString(serial, USERNAME_KEY), + decodeString(serial, PASSWORD_KEY) + ); + } +} diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/ContinueAccountCreationCredentialsSerialCodec.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/ContinueAccountCreationCredentialsSerialCodec.java new file mode 100644 index 000000000..799bd5594 --- /dev/null +++ b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/ContinueAccountCreationCredentialsSerialCodec.java @@ -0,0 +1,31 @@ +package dev.webfx.stack.authn.serial; + +import dev.webfx.platform.ast.AstObject; +import dev.webfx.platform.ast.ReadOnlyAstObject; +import dev.webfx.stack.authn.ContinueAccountCreationCredentials; +import dev.webfx.stack.com.serial.spi.impl.SerialCodecBase; + +/** + * @author Bruno Salmon + */ +public final class ContinueAccountCreationCredentialsSerialCodec extends SerialCodecBase { + + private static final String CODEC_ID = "ContinueAccountCreationCredentials"; + private static final String TOKEN_KEY = "token"; + + public ContinueAccountCreationCredentialsSerialCodec() { + super(ContinueAccountCreationCredentials.class, CODEC_ID); + } + + @Override + public void encode(ContinueAccountCreationCredentials arg, AstObject serial) { + encodeString(serial, TOKEN_KEY, arg.getToken()); + } + + @Override + public ContinueAccountCreationCredentials decode(ReadOnlyAstObject serial) { + return new ContinueAccountCreationCredentials( + decodeString(serial, TOKEN_KEY) + ); + } +} diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/FinaliseAccountCreationCredentialsSerialCodec.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/FinaliseAccountCreationCredentialsSerialCodec.java new file mode 100644 index 000000000..32fc170f6 --- /dev/null +++ b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/FinaliseAccountCreationCredentialsSerialCodec.java @@ -0,0 +1,34 @@ +package dev.webfx.stack.authn.serial; + +import dev.webfx.platform.ast.AstObject; +import dev.webfx.platform.ast.ReadOnlyAstObject; +import dev.webfx.stack.authn.FinaliseAccountCreationCredentials; +import dev.webfx.stack.com.serial.spi.impl.SerialCodecBase; + +/** + * @author Bruno Salmon + */ +public final class FinaliseAccountCreationCredentialsSerialCodec extends SerialCodecBase { + + private static final String CODEC_ID = "FinaliseAccountCreationCredentials"; + private static final String TOKEN_KEY = "token"; + private static final String PASSWORD_KEY = "password"; + + public FinaliseAccountCreationCredentialsSerialCodec() { + super(FinaliseAccountCreationCredentials.class, CODEC_ID); + } + + @Override + public void encode(FinaliseAccountCreationCredentials arg, AstObject serial) { + encodeString(serial, TOKEN_KEY, arg.getToken()); + encodeString(serial, PASSWORD_KEY, arg.getPassword()); + } + + @Override + public FinaliseAccountCreationCredentials decode(ReadOnlyAstObject serial) { + return new FinaliseAccountCreationCredentials( + decodeString(serial, TOKEN_KEY), + decodeString(serial, PASSWORD_KEY) + ); + } +} diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/FinaliseEmailUpdateCredentialsSerialCodec.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/FinaliseEmailUpdateCredentialsSerialCodec.java new file mode 100644 index 000000000..6dff301ce --- /dev/null +++ b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/FinaliseEmailUpdateCredentialsSerialCodec.java @@ -0,0 +1,31 @@ +package dev.webfx.stack.authn.serial; + +import dev.webfx.platform.ast.AstObject; +import dev.webfx.platform.ast.ReadOnlyAstObject; +import dev.webfx.stack.authn.FinaliseEmailUpdateCredentials; +import dev.webfx.stack.com.serial.spi.impl.SerialCodecBase; + +/** + * @author Bruno Salmon + */ +public final class FinaliseEmailUpdateCredentialsSerialCodec extends SerialCodecBase { + + private static final String CODEC_ID = "FinaliseEmailUpdateCredentials"; + private static final String TOKEN_KEY = "token"; + + public FinaliseEmailUpdateCredentialsSerialCodec() { + super(FinaliseEmailUpdateCredentials.class, CODEC_ID); + } + + @Override + public void encode(FinaliseEmailUpdateCredentials arg, AstObject serial) { + encodeString(serial, TOKEN_KEY, arg.getToken()); + } + + @Override + public FinaliseEmailUpdateCredentials decode(ReadOnlyAstObject serial) { + return new FinaliseEmailUpdateCredentials( + decodeString(serial, TOKEN_KEY) + ); + } +} diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/InitiateAccountCreationCredentialsSerialCodec.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/InitiateAccountCreationCredentialsSerialCodec.java new file mode 100644 index 000000000..0d5f0d952 --- /dev/null +++ b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/InitiateAccountCreationCredentialsSerialCodec.java @@ -0,0 +1,27 @@ +package dev.webfx.stack.authn.serial; + +import dev.webfx.platform.ast.ReadOnlyAstObject; +import dev.webfx.stack.authn.InitiateAccountCreationCredentials; + +/** + * @author Bruno Salmon + */ +public final class InitiateAccountCreationCredentialsSerialCodec extends AlternativeLoginActionCredentialsSerialCodec { + + private static final String CODEC_ID = "InitiateAccountCreationCredentials"; + + public InitiateAccountCreationCredentialsSerialCodec() { + super(InitiateAccountCreationCredentials.class, CODEC_ID); + } + + @Override + public InitiateAccountCreationCredentials decode(ReadOnlyAstObject serial) { + return new InitiateAccountCreationCredentials( + decodeString(serial, EMAIL_KEY), + decodeString(serial, CLIENT_ORIGIN_KEY), + decodeString(serial, REQUESTED_PATH_KEY), + decodeObject(serial, LANGUAGE_KEY), + decodeObject(serial, CONTEXT_KEY) + ); + } +} \ No newline at end of file diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/InitiateEmailUpdateCredentialsSerialCodec.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/InitiateEmailUpdateCredentialsSerialCodec.java new file mode 100644 index 000000000..7d0b006e7 --- /dev/null +++ b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/InitiateEmailUpdateCredentialsSerialCodec.java @@ -0,0 +1,27 @@ +package dev.webfx.stack.authn.serial; + +import dev.webfx.platform.ast.ReadOnlyAstObject; +import dev.webfx.stack.authn.InitiateEmailUpdateCredentials; + +/** + * @author Bruno Salmon + */ +public final class InitiateEmailUpdateCredentialsSerialCodec extends AlternativeLoginActionCredentialsSerialCodec { + + private static final String CODEC_ID = "InitiateEmailUpdateCredentials"; + + public InitiateEmailUpdateCredentialsSerialCodec() { + super(InitiateEmailUpdateCredentials.class, CODEC_ID); + } + + @Override + public InitiateEmailUpdateCredentials decode(ReadOnlyAstObject serial) { + return new InitiateEmailUpdateCredentials( + decodeString(serial, EMAIL_KEY), + decodeString(serial, CLIENT_ORIGIN_KEY), + decodeString(serial, REQUESTED_PATH_KEY), + decodeObject(serial, LANGUAGE_KEY), + decodeObject(serial, CONTEXT_KEY) + ); + } +} \ No newline at end of file diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/MagicLinkCredentialsSerialCodec.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/MagicLinkCredentialsSerialCodec.java deleted file mode 100644 index bfa6e0490..000000000 --- a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/MagicLinkCredentialsSerialCodec.java +++ /dev/null @@ -1,31 +0,0 @@ -package dev.webfx.stack.authn.serial; - -import dev.webfx.platform.ast.AstObject; -import dev.webfx.platform.ast.ReadOnlyAstObject; -import dev.webfx.stack.authn.MagicLinkCredentials; -import dev.webfx.stack.com.serial.spi.impl.SerialCodecBase; - -/** - * @author Bruno Salmon - */ -public class MagicLinkCredentialsSerialCodec extends SerialCodecBase { - - private static final String CODEC_ID = "MagicLinkCredentials"; - private static final String TOKEN_KEY = "token"; - - public MagicLinkCredentialsSerialCodec() { - super(MagicLinkCredentials.class, CODEC_ID); - } - - @Override - public void encode(MagicLinkCredentials arg, AstObject serial) { - encodeString(serial, TOKEN_KEY, arg.getToken()); - } - - @Override - public MagicLinkCredentials decode(ReadOnlyAstObject serial) { - return new MagicLinkCredentials( - decodeString(serial, TOKEN_KEY) - ); - } -} diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/MagicLinkPasswordUpdateSerialCodec.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/MagicLinkPasswordUpdateSerialCodec.java deleted file mode 100644 index eb0f2553e..000000000 --- a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/MagicLinkPasswordUpdateSerialCodec.java +++ /dev/null @@ -1,31 +0,0 @@ -package dev.webfx.stack.authn.serial; - -import dev.webfx.platform.ast.AstObject; -import dev.webfx.platform.ast.ReadOnlyAstObject; -import dev.webfx.stack.authn.MagicLinkPasswordUpdate; -import dev.webfx.stack.com.serial.spi.impl.SerialCodecBase; - -/** - * @author Bruno Salmon - */ -public class MagicLinkPasswordUpdateSerialCodec extends SerialCodecBase { - - private static final String CODEC_ID = "MagicLinkPasswordUpdate"; - private static final String NEW_PASSWORD_KEY = "newPassword"; - - public MagicLinkPasswordUpdateSerialCodec() { - super(MagicLinkPasswordUpdate.class, CODEC_ID); - } - - @Override - public void encode(MagicLinkPasswordUpdate arg, AstObject serial) { - encodeString(serial, NEW_PASSWORD_KEY, arg.getNewPassword()); - } - - @Override - public MagicLinkPasswordUpdate decode(ReadOnlyAstObject serial) { - return new MagicLinkPasswordUpdate( - decodeString(serial, NEW_PASSWORD_KEY) - ); - } -} diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/MagicLinkRequestSerialCodec.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/MagicLinkRequestSerialCodec.java deleted file mode 100644 index 7c02be154..000000000 --- a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/MagicLinkRequestSerialCodec.java +++ /dev/null @@ -1,40 +0,0 @@ -package dev.webfx.stack.authn.serial; - -import dev.webfx.platform.ast.AstObject; -import dev.webfx.platform.ast.ReadOnlyAstObject; -import dev.webfx.stack.authn.MagicLinkRequest; -import dev.webfx.stack.com.serial.spi.impl.SerialCodecBase; - -/** - * @author Bruno Salmon - */ -public class MagicLinkRequestSerialCodec extends SerialCodecBase { - - private static final String CODEC_ID = "MagicLinkRequest"; - private static final String EMAIL_KEY = "email"; - private static final String CLIENT_ORIGIN_KEY = "clientOrigin"; - private static final String LANGUAGE_KEY = "lang"; - private static final String CONTEXT_KEY = "context"; - - public MagicLinkRequestSerialCodec() { - super(MagicLinkRequest.class, CODEC_ID); - } - - @Override - public void encode(MagicLinkRequest arg, AstObject serial) { - encodeString(serial, EMAIL_KEY, arg.getEmail()); - encodeString(serial, CLIENT_ORIGIN_KEY, arg.getClientOrigin()); - encodeObject(serial, LANGUAGE_KEY, arg.getLanguage()); - encodeObject(serial, CONTEXT_KEY, arg.getContext()); - } - - @Override - public MagicLinkRequest decode(ReadOnlyAstObject serial) { - return new MagicLinkRequest( - decodeString(serial, EMAIL_KEY), - decodeString(serial, CLIENT_ORIGIN_KEY), - decodeObject(serial, LANGUAGE_KEY), - decodeObject(serial, CONTEXT_KEY) - ); - } -} \ No newline at end of file diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/RenewMagicLinkCredentialsSerialCodec.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/RenewMagicLinkCredentialsSerialCodec.java new file mode 100644 index 000000000..54620b6bf --- /dev/null +++ b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/RenewMagicLinkCredentialsSerialCodec.java @@ -0,0 +1,31 @@ +package dev.webfx.stack.authn.serial; + +import dev.webfx.platform.ast.AstObject; +import dev.webfx.platform.ast.ReadOnlyAstObject; +import dev.webfx.stack.authn.RenewMagicLinkCredentials; +import dev.webfx.stack.com.serial.spi.impl.SerialCodecBase; + +/** + * @author Bruno Salmon + */ +public final class RenewMagicLinkCredentialsSerialCodec extends SerialCodecBase { + + private static final String CODEC_ID = "RenewMagicLinkCredentials"; + private static final String PREVIOUS_TOKEN_KEY = "previousToken"; + + public RenewMagicLinkCredentialsSerialCodec() { + super(RenewMagicLinkCredentials.class, CODEC_ID); + } + + @Override + public void encode(RenewMagicLinkCredentials arg, AstObject serial) { + encodeString(serial, PREVIOUS_TOKEN_KEY, arg.getPreviousToken()); + } + + @Override + public RenewMagicLinkCredentials decode(ReadOnlyAstObject serial) { + return new RenewMagicLinkCredentials( + decodeString(serial, PREVIOUS_TOKEN_KEY) + ); + } +} \ No newline at end of file diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/SendMagicLinkCredentialsSerialCodec.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/SendMagicLinkCredentialsSerialCodec.java new file mode 100644 index 000000000..23f97f93c --- /dev/null +++ b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/SendMagicLinkCredentialsSerialCodec.java @@ -0,0 +1,27 @@ +package dev.webfx.stack.authn.serial; + +import dev.webfx.platform.ast.ReadOnlyAstObject; +import dev.webfx.stack.authn.SendMagicLinkCredentials; + +/** + * @author Bruno Salmon + */ +public final class SendMagicLinkCredentialsSerialCodec extends AlternativeLoginActionCredentialsSerialCodec { + + private static final String CODEC_ID = "SendMagicLinkCredentials"; + + public SendMagicLinkCredentialsSerialCodec() { + super(SendMagicLinkCredentials.class, CODEC_ID); + } + + @Override + public SendMagicLinkCredentials decode(ReadOnlyAstObject serial) { + return new SendMagicLinkCredentials( + decodeString(serial, EMAIL_KEY), + decodeString(serial, CLIENT_ORIGIN_KEY), + decodeString(serial, REQUESTED_PATH_KEY), + decodeObject(serial, LANGUAGE_KEY), + decodeObject(serial, CONTEXT_KEY) + ); + } +} \ No newline at end of file diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/PasswordUpdateSerialCodec.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/UpdatePasswordCredentialsSerialCodec.java similarity index 55% rename from webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/PasswordUpdateSerialCodec.java rename to webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/UpdatePasswordCredentialsSerialCodec.java index 4ddcb335b..0eb554ba8 100644 --- a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/PasswordUpdateSerialCodec.java +++ b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/UpdatePasswordCredentialsSerialCodec.java @@ -2,31 +2,31 @@ import dev.webfx.platform.ast.AstObject; import dev.webfx.platform.ast.ReadOnlyAstObject; -import dev.webfx.stack.authn.PasswordUpdate; +import dev.webfx.stack.authn.UpdatePasswordCredentials; import dev.webfx.stack.com.serial.spi.impl.SerialCodecBase; /** * @author Bruno Salmon */ -public class PasswordUpdateSerialCodec extends SerialCodecBase { +public final class UpdatePasswordCredentialsSerialCodec extends SerialCodecBase { - private static final String CODEC_ID = "PasswordUpdate"; + private static final String CODEC_ID = "UpdatePasswordCredentials"; private static final String OLD_PASSWORD_KEY = "oldPassword"; private static final String NEW_PASSWORD_KEY = "newPassword"; - public PasswordUpdateSerialCodec() { - super(PasswordUpdate.class, CODEC_ID); + public UpdatePasswordCredentialsSerialCodec() { + super(UpdatePasswordCredentials.class, CODEC_ID); } @Override - public void encode(PasswordUpdate arg, AstObject serial) { + public void encode(UpdatePasswordCredentials arg, AstObject serial) { encodeString(serial, OLD_PASSWORD_KEY, arg.getOldPassword()); encodeString(serial, NEW_PASSWORD_KEY, arg.getNewPassword()); } @Override - public PasswordUpdate decode(ReadOnlyAstObject serial) { - return new PasswordUpdate( + public UpdatePasswordCredentials decode(ReadOnlyAstObject serial) { + return new UpdatePasswordCredentials( decodeString(serial, OLD_PASSWORD_KEY), decodeString(serial, NEW_PASSWORD_KEY) ); diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/UpdatePasswordFromMagicLinkCredentialsSerialCodec.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/UpdatePasswordFromMagicLinkCredentialsSerialCodec.java new file mode 100644 index 000000000..3336daa1f --- /dev/null +++ b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/UpdatePasswordFromMagicLinkCredentialsSerialCodec.java @@ -0,0 +1,31 @@ +package dev.webfx.stack.authn.serial; + +import dev.webfx.platform.ast.AstObject; +import dev.webfx.platform.ast.ReadOnlyAstObject; +import dev.webfx.stack.authn.UpdatePasswordFromMagicLinkCredentials; +import dev.webfx.stack.com.serial.spi.impl.SerialCodecBase; + +/** + * @author Bruno Salmon + */ +public final class UpdatePasswordFromMagicLinkCredentialsSerialCodec extends SerialCodecBase { + + private static final String CODEC_ID = "UpdatePasswordFromMagicLinkCredentials"; + private static final String NEW_PASSWORD_KEY = "newPassword"; + + public UpdatePasswordFromMagicLinkCredentialsSerialCodec() { + super(UpdatePasswordFromMagicLinkCredentials.class, CODEC_ID); + } + + @Override + public void encode(UpdatePasswordFromMagicLinkCredentials arg, AstObject serial) { + encodeString(serial, NEW_PASSWORD_KEY, arg.getNewPassword()); + } + + @Override + public UpdatePasswordFromMagicLinkCredentials decode(ReadOnlyAstObject serial) { + return new UpdatePasswordFromMagicLinkCredentials( + decodeString(serial, NEW_PASSWORD_KEY) + ); + } +} diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/UserClaimsSerialCodec.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/UserClaimsSerialCodec.java index 284756a37..91d46420b 100644 --- a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/UserClaimsSerialCodec.java +++ b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/UserClaimsSerialCodec.java @@ -8,7 +8,7 @@ /** * @author Bruno Salmon */ -public class UserClaimsSerialCodec extends SerialCodecBase { +public final class UserClaimsSerialCodec extends SerialCodecBase { private static final String CODEC_ID = "UserClaims"; private static final String USERNAME_KEY = "username"; diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/UsernamePasswordCredentialsSerialCodec.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/UsernamePasswordCredentialsSerialCodec.java deleted file mode 100644 index ae01af90d..000000000 --- a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/serial/UsernamePasswordCredentialsSerialCodec.java +++ /dev/null @@ -1,34 +0,0 @@ -package dev.webfx.stack.authn.serial; - -import dev.webfx.platform.ast.AstObject; -import dev.webfx.platform.ast.ReadOnlyAstObject; -import dev.webfx.stack.authn.UsernamePasswordCredentials; -import dev.webfx.stack.com.serial.spi.impl.SerialCodecBase; - -/** - * @author Bruno Salmon - */ -public class UsernamePasswordCredentialsSerialCodec extends SerialCodecBase { - - private static final String CODEC_ID = "UsernamePasswordCredentials"; - private static final String USERNAME_KEY = "username"; - private static final String PASSWORD_KEY = "password"; - - public UsernamePasswordCredentialsSerialCodec() { - super(UsernamePasswordCredentials.class, CODEC_ID); - } - - @Override - public void encode(UsernamePasswordCredentials arg, AstObject serial) { - encodeString(serial, USERNAME_KEY, arg.getUsername()); - encodeString(serial, PASSWORD_KEY, arg.getPassword()); - } - - @Override - public UsernamePasswordCredentials decode(ReadOnlyAstObject serial) { - return new UsernamePasswordCredentials( - decodeString(serial, USERNAME_KEY), - decodeString(serial, PASSWORD_KEY) - ); - } -} diff --git a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/spi/AuthenticatorInfo.java b/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/spi/AuthenticatorInfo.java deleted file mode 100644 index bcb33eab8..000000000 --- a/webfx-stack-authn/src/main/java/dev/webfx/stack/authn/spi/AuthenticatorInfo.java +++ /dev/null @@ -1,23 +0,0 @@ -package dev.webfx.stack.authn.spi; - -/** - * @author Bruno Salmon - */ -public class AuthenticatorInfo { - - private final Object authenticatorId; - private final Object authenticatorI18nKey; - - public AuthenticatorInfo(Object authenticatorId, Object authenticatorI18nKey) { - this.authenticatorId = authenticatorId; - this.authenticatorI18nKey = authenticatorI18nKey; - } - - public Object getAuthenticatorId() { - return authenticatorId; - } - - public Object getAuthenticatorI18nKey() { - return authenticatorI18nKey; - } -} diff --git a/webfx-stack-authn/src/main/java/module-info.java b/webfx-stack-authn/src/main/java/module-info.java index f4ede7e48..9e63f629d 100644 --- a/webfx-stack-authn/src/main/java/module-info.java +++ b/webfx-stack-authn/src/main/java/module-info.java @@ -17,6 +17,6 @@ uses dev.webfx.stack.authn.spi.AuthenticationServiceProvider; // Provided services - provides dev.webfx.stack.com.serial.spi.SerialCodec with dev.webfx.stack.authn.serial.MagicLinkRequestSerialCodec, dev.webfx.stack.authn.serial.MagicLinkCredentialsSerialCodec, dev.webfx.stack.authn.serial.MagicLinkPasswordUpdateSerialCodec, dev.webfx.stack.authn.serial.PasswordUpdateSerialCodec, dev.webfx.stack.authn.serial.UserClaimsSerialCodec, dev.webfx.stack.authn.serial.UsernamePasswordCredentialsSerialCodec; + provides dev.webfx.stack.com.serial.spi.SerialCodec with dev.webfx.stack.authn.serial.AuthenticateWithMagicLinkCredentialsSerialCodec, dev.webfx.stack.authn.serial.AuthenticateWithUsernamePasswordCredentialsSerialCodec, dev.webfx.stack.authn.serial.ContinueAccountCreationCredentialsSerialCodec, dev.webfx.stack.authn.serial.FinaliseAccountCreationCredentialsSerialCodec, dev.webfx.stack.authn.serial.FinaliseEmailUpdateCredentialsSerialCodec, dev.webfx.stack.authn.serial.InitiateAccountCreationCredentialsSerialCodec, dev.webfx.stack.authn.serial.InitiateEmailUpdateCredentialsSerialCodec, dev.webfx.stack.authn.serial.RenewMagicLinkCredentialsSerialCodec, dev.webfx.stack.authn.serial.SendMagicLinkCredentialsSerialCodec, dev.webfx.stack.authn.serial.UpdatePasswordCredentialsSerialCodec, dev.webfx.stack.authn.serial.UpdatePasswordFromMagicLinkCredentialsSerialCodec, dev.webfx.stack.authn.serial.UserClaimsSerialCodec; } \ No newline at end of file diff --git a/webfx-stack-authn/src/main/resources/META-INF/services/dev.webfx.stack.com.serial.spi.SerialCodec b/webfx-stack-authn/src/main/resources/META-INF/services/dev.webfx.stack.com.serial.spi.SerialCodec index dd4549743..1f69b807e 100644 --- a/webfx-stack-authn/src/main/resources/META-INF/services/dev.webfx.stack.com.serial.spi.SerialCodec +++ b/webfx-stack-authn/src/main/resources/META-INF/services/dev.webfx.stack.com.serial.spi.SerialCodec @@ -1,6 +1,12 @@ -dev.webfx.stack.authn.serial.MagicLinkRequestSerialCodec -dev.webfx.stack.authn.serial.MagicLinkCredentialsSerialCodec -dev.webfx.stack.authn.serial.MagicLinkPasswordUpdateSerialCodec -dev.webfx.stack.authn.serial.PasswordUpdateSerialCodec +dev.webfx.stack.authn.serial.AuthenticateWithMagicLinkCredentialsSerialCodec +dev.webfx.stack.authn.serial.AuthenticateWithUsernamePasswordCredentialsSerialCodec +dev.webfx.stack.authn.serial.ContinueAccountCreationCredentialsSerialCodec +dev.webfx.stack.authn.serial.FinaliseAccountCreationCredentialsSerialCodec +dev.webfx.stack.authn.serial.FinaliseEmailUpdateCredentialsSerialCodec +dev.webfx.stack.authn.serial.InitiateAccountCreationCredentialsSerialCodec +dev.webfx.stack.authn.serial.InitiateEmailUpdateCredentialsSerialCodec +dev.webfx.stack.authn.serial.RenewMagicLinkCredentialsSerialCodec +dev.webfx.stack.authn.serial.SendMagicLinkCredentialsSerialCodec +dev.webfx.stack.authn.serial.UpdatePasswordCredentialsSerialCodec +dev.webfx.stack.authn.serial.UpdatePasswordFromMagicLinkCredentialsSerialCodec dev.webfx.stack.authn.serial.UserClaimsSerialCodec -dev.webfx.stack.authn.serial.UsernamePasswordCredentialsSerialCodec diff --git a/webfx-stack-authn/webfx.xml b/webfx-stack-authn/webfx.xml index b0421df91..14cd0aeb0 100644 --- a/webfx-stack-authn/webfx.xml +++ b/webfx-stack-authn/webfx.xml @@ -12,12 +12,18 @@ - dev.webfx.stack.authn.serial.MagicLinkRequestSerialCodec - dev.webfx.stack.authn.serial.MagicLinkCredentialsSerialCodec - dev.webfx.stack.authn.serial.MagicLinkPasswordUpdateSerialCodec - dev.webfx.stack.authn.serial.PasswordUpdateSerialCodec + dev.webfx.stack.authn.serial.AuthenticateWithMagicLinkCredentialsSerialCodec + dev.webfx.stack.authn.serial.AuthenticateWithUsernamePasswordCredentialsSerialCodec + dev.webfx.stack.authn.serial.ContinueAccountCreationCredentialsSerialCodec + dev.webfx.stack.authn.serial.FinaliseAccountCreationCredentialsSerialCodec + dev.webfx.stack.authn.serial.FinaliseEmailUpdateCredentialsSerialCodec + dev.webfx.stack.authn.serial.InitiateAccountCreationCredentialsSerialCodec + dev.webfx.stack.authn.serial.InitiateEmailUpdateCredentialsSerialCodec + dev.webfx.stack.authn.serial.RenewMagicLinkCredentialsSerialCodec + dev.webfx.stack.authn.serial.SendMagicLinkCredentialsSerialCodec + dev.webfx.stack.authn.serial.UpdatePasswordCredentialsSerialCodec + dev.webfx.stack.authn.serial.UpdatePasswordFromMagicLinkCredentialsSerialCodec dev.webfx.stack.authn.serial.UserClaimsSerialCodec - dev.webfx.stack.authn.serial.UsernamePasswordCredentialsSerialCodec \ No newline at end of file diff --git a/webfx-stack-authz-client/src/main/java/dev/webfx/stack/authz/client/spi/impl/inmemory/InMemoryAuthorizationRuleRegistry.java b/webfx-stack-authz-client/src/main/java/dev/webfx/stack/authz/client/spi/impl/inmemory/InMemoryAuthorizationRuleRegistry.java index 847a66ac1..b64b4f9f9 100644 --- a/webfx-stack-authz-client/src/main/java/dev/webfx/stack/authz/client/spi/impl/inmemory/InMemoryAuthorizationRuleRegistry.java +++ b/webfx-stack-authz-client/src/main/java/dev/webfx/stack/authz/client/spi/impl/inmemory/InMemoryAuthorizationRuleRegistry.java @@ -69,12 +69,13 @@ public AuthorizationRuleResult computeRuleResult(Object operationRequest) { while (true) { Collection rules = registeredInMemoryAuthorizationRules.get(operationRequestClass); if (rules != null) - for (InMemoryAuthorizationRule rule : rules) // ConcurrentModificationException observed + for (InMemoryAuthorizationRule rule : rules) { // ConcurrentModificationException observed switch (rule.computeRuleResult(operationRequest)) { case DENIED: result = AuthorizationRuleResult.DENIED; break; // Breaking as it's a final decision case GRANTED: result = AuthorizationRuleResult.GRANTED; // Not breaking, as we need to check there is not another denying rule (which is priority) case OUT_OF_RULE_CONTEXT: // just ignoring it and looping to the next } + } if (result != AuthorizationRuleResult.OUT_OF_RULE_CONTEXT || operationRequestClass == null) break; operationRequestClass = operationRequestClass.getSuperclass(); diff --git a/webfx-stack-cache-client/src/main/java/dev/webfx/stack/cache/client/SessionClientCache.java b/webfx-stack-cache-client/src/main/java/dev/webfx/stack/cache/client/SessionClientCache.java index a6892c9cc..6d5428832 100644 --- a/webfx-stack-cache-client/src/main/java/dev/webfx/stack/cache/client/SessionClientCache.java +++ b/webfx-stack-cache-client/src/main/java/dev/webfx/stack/cache/client/SessionClientCache.java @@ -2,7 +2,6 @@ import dev.webfx.stack.cache.Cache; import dev.webfx.stack.session.Session; -import dev.webfx.stack.session.SessionService; import dev.webfx.stack.session.state.client.fx.FXSession; /** @@ -15,7 +14,7 @@ public void put(String key, Object value) { Session session = FXSession.getSession(); if (session != null) { session.put(key, value); - SessionService.getSessionStore().put(session); + session.store(); } } diff --git a/webfx-stack-cloud-image-cloudinary/src/main/java/dev/webfx/stack/cloud/image/impl/cloudinary/Cloudinary.java b/webfx-stack-cloud-image-cloudinary/src/main/java/dev/webfx/stack/cloud/image/impl/cloudinary/Cloudinary.java index fe28ab219..88b44513c 100644 --- a/webfx-stack-cloud-image-cloudinary/src/main/java/dev/webfx/stack/cloud/image/impl/cloudinary/Cloudinary.java +++ b/webfx-stack-cloud-image-cloudinary/src/main/java/dev/webfx/stack/cloud/image/impl/cloudinary/Cloudinary.java @@ -92,7 +92,7 @@ private static String apiSignRequest(FormData paramsToSign, String apiSecret) { Collection params = new ArrayList<>(); for (Map.Entry param : entries) { if (param.getValue() instanceof Collection) { - params.add(param.getKey() + "=" + Collections.toString((Collection) param.getValue(), false, false)); + params.add(param.getKey() + "=" + Collections.toStringCommaSeparated((Collection) param.getValue())); } /*else if (param.getValue() instanceof Object[]) { params.add(param.getKey() + "=" + StringUtils.join((Object[]) param.getValue(), ",")); }*/ else { @@ -103,7 +103,7 @@ private static String apiSignRequest(FormData paramsToSign, String apiSecret) { } } - String to_sign = Collections.toString(params, "&", false, false); + String to_sign = Collections.toStringAmpersandSeparated(params); String hash = Sha1.hash(to_sign + apiSecret); return hash; } diff --git a/webfx-stack-com-bus-call/pom.xml b/webfx-stack-com-bus-call/pom.xml index ff3a2ea66..b69220bda 100644 --- a/webfx-stack-com-bus-call/pom.xml +++ b/webfx-stack-com-bus-call/pom.xml @@ -15,12 +15,6 @@ - - org.openjfx - javafx-base - provided - - dev.webfx webfx-platform-ast diff --git a/webfx-stack-com-bus-call/src/main/java/dev/webfx/stack/com/bus/call/PendingBusCall.java b/webfx-stack-com-bus-call/src/main/java/dev/webfx/stack/com/bus/call/PendingBusCall.java index da328b8b9..10deca2d6 100644 --- a/webfx-stack-com-bus-call/src/main/java/dev/webfx/stack/com/bus/call/PendingBusCall.java +++ b/webfx-stack-com-bus-call/src/main/java/dev/webfx/stack/com/bus/call/PendingBusCall.java @@ -2,9 +2,8 @@ import dev.webfx.platform.async.AsyncResult; import dev.webfx.platform.async.Future; +import dev.webfx.platform.async.Handler; import dev.webfx.platform.async.Promise; -import javafx.beans.property.Property; -import javafx.beans.property.SimpleObjectProperty; import java.util.ArrayList; import java.util.List; @@ -14,12 +13,11 @@ */ public final class PendingBusCall { - private static final List pendingCalls = new ArrayList<>(); - private static final Property pendingCallsCountProperty = new SimpleObjectProperty<>(0); - // Note: this is the only javafx property used so far in the Platform module - // TODO: decide if we keep it or replace it with something else to remove the dependency to javafx bindings - public static Property pendingCallsCountProperty() { - return pendingCallsCountProperty; + private static final List PENDING_CALLS = new ArrayList<>(); + private static final List> PENDING_CALLS_COUNT_HANDLERS = new ArrayList<>(); + + public static void addPendingCallsCountHandler(Handler pendingCallsCountHandler) { + PENDING_CALLS_COUNT_HANDLERS.add(pendingCallsCountHandler); } private final Promise promise = Promise.promise(); @@ -57,9 +55,10 @@ public Future future() { private void updatePendingCalls(boolean addition) { if (addition) - pendingCalls.add(this); + PENDING_CALLS.add(this); else - pendingCalls.remove(this); - pendingCallsCountProperty.setValue(pendingCalls.size()); + PENDING_CALLS.remove(this); + int pendingCallsCount = PENDING_CALLS.size(); + PENDING_CALLS_COUNT_HANDLERS.forEach(h -> h.handle(pendingCallsCount)); } } diff --git a/webfx-stack-com-bus-call/src/main/java/module-info.java b/webfx-stack-com-bus-call/src/main/java/module-info.java index 802857579..3f0d741e4 100644 --- a/webfx-stack-com-bus-call/src/main/java/module-info.java +++ b/webfx-stack-com-bus-call/src/main/java/module-info.java @@ -3,7 +3,6 @@ module webfx.stack.com.bus.call { // Direct dependencies modules - requires javafx.base; requires webfx.platform.ast; requires webfx.platform.async; requires webfx.platform.boot; diff --git a/webfx-stack-com-bus-client/src/main/java/dev/webfx/stack/com/bus/spi/impl/client/SimpleBus.java b/webfx-stack-com-bus-client/src/main/java/dev/webfx/stack/com/bus/spi/impl/client/SimpleBus.java index 4bab6cdd8..20ce4bff3 100644 --- a/webfx-stack-com-bus-client/src/main/java/dev/webfx/stack/com/bus/spi/impl/client/SimpleBus.java +++ b/webfx-stack-com-bus-client/src/main/java/dev/webfx/stack/com/bus/spi/impl/client/SimpleBus.java @@ -275,7 +275,7 @@ void clearReplyHandlers() { Map.Entry>> entry = it.next(); if (shouldClearReplyHandlerNow(entry.getKey())) { entry.getValue().handle(Future.failedFuture(new Exception("Bus closed"))); - it.remove(); + it.remove(); // ConcurrentModificationException observed } } } diff --git a/webfx-stack-com-bus-json-client-websocket/src/main/java/dev/webfx/stack/com/bus/spi/impl/json/client/websocket/WebSocketBus.java b/webfx-stack-com-bus-json-client-websocket/src/main/java/dev/webfx/stack/com/bus/spi/impl/json/client/websocket/WebSocketBus.java index 727577f2c..177cf47b5 100644 --- a/webfx-stack-com-bus-json-client-websocket/src/main/java/dev/webfx/stack/com/bus/spi/impl/json/client/websocket/WebSocketBus.java +++ b/webfx-stack-com-bus-json-client-websocket/src/main/java/dev/webfx/stack/com/bus/spi/impl/json/client/websocket/WebSocketBus.java @@ -38,6 +38,9 @@ @SuppressWarnings("rawtypes") public class WebSocketBus extends JsonClientBus { + // Can be set to true when debugging Scheduler for other purposes than this class (reduces Scheduler sollicitations) + private static final boolean SKIP_SCHEDULER_DEBUG_FLAG = false; + private WebSocketListener internalWebSocketHandler; String serverUri; WebSocket webSocket; @@ -83,7 +86,8 @@ public void onMessage(String msg) { // link app (which can be open in a separate tab in the same browser, making this app hidden) to ensure // we send back the acknowledgement, so the magic link app can confirm the user it reached out this app // with a successful login. - Scheduler.wakeUp(); + if (!SKIP_SCHEDULER_DEBUG_FLAG) + Scheduler.wakeUp(); } @Override @@ -167,7 +171,8 @@ protected void sendPing() { private void scheduleNextPing() { cancelPingTimer(); - pingScheduled = Scheduler.scheduleDelay(pingInterval, this::sendPing); + if (!SKIP_SCHEDULER_DEBUG_FLAG) + pingScheduled = Scheduler.scheduleDelay(pingInterval, this::sendPing); } private void cancelPingTimer() { diff --git a/webfx-stack-com-bus-json-server/src/main/java/dev/webfx/stack/com/bus/spi/impl/json/server/ServerJsonBusStateManager.java b/webfx-stack-com-bus-json-server/src/main/java/dev/webfx/stack/com/bus/spi/impl/json/server/ServerJsonBusStateManager.java index 1d4cfc652..93da4e6c6 100644 --- a/webfx-stack-com-bus-json-server/src/main/java/dev/webfx/stack/com/bus/spi/impl/json/server/ServerJsonBusStateManager.java +++ b/webfx-stack-com-bus-json-server/src/main/java/dev/webfx/stack/com/bus/spi/impl/json/server/ServerJsonBusStateManager.java @@ -23,7 +23,6 @@ public final class ServerJsonBusStateManager implements JsonBusConstants { private final static boolean LOG_RAW_MESSAGES = false; private final static boolean LOG_STATES = false; - private final static String ASSOCIATED_SESSION_KEY = "$associatedSession"; public static void initialiseStateManagement(Bus serverJsonBus) { // We register at PING_STATE_ADDRESS a handler that just replies with an empty body (but the states mechanism will automatically apply - which is the main purpose of that call) diff --git a/webfx-stack-com-bus-json-vertx/src/main/java/dev/webfx/stack/com/bus/spi/impl/json/vertx/VertxBusModuleBooter.java b/webfx-stack-com-bus-json-vertx/src/main/java/dev/webfx/stack/com/bus/spi/impl/json/vertx/VertxBusModuleBooter.java index b315c7d84..546990922 100644 --- a/webfx-stack-com-bus-json-vertx/src/main/java/dev/webfx/stack/com/bus/spi/impl/json/vertx/VertxBusModuleBooter.java +++ b/webfx-stack-com-bus-json-vertx/src/main/java/dev/webfx/stack/com/bus/spi/impl/json/vertx/VertxBusModuleBooter.java @@ -6,6 +6,7 @@ import io.vertx.core.Handler; import io.vertx.core.json.JsonObject; import io.vertx.ext.bridge.PermittedOptions; +import io.vertx.ext.web.handler.BodyHandler; import io.vertx.ext.web.handler.sockjs.BridgeEvent; import io.vertx.ext.web.handler.sockjs.SockJSBridgeOptions; import io.vertx.ext.web.handler.sockjs.SockJSHandler; @@ -49,21 +50,22 @@ public void bootModule() { VertxInstance.setBridgeInstaller(() -> { VertxInstance.getHttpRouter() - .route("/" + busPrefix + "/*") - .subRouter(SockJSHandler.create(VertxInstance.getVertx()) - .bridge(new SockJSBridgeOptions() - .setPingTimeout(pingTimeout) // Should be higher than client WebSocketBusOptions.pingInterval (which is set to 30_000 at the time of writing this code) - .addInboundPermitted(new PermittedOptions(new JsonObject())) - .addOutboundPermitted(new PermittedOptions(new JsonObject())) - , bridgeEvent -> { // Calling the VertxInstance bridge event handler if set - Handler bridgeEventHandler = VertxInstance.getBridgeEventHandler(); - if (bridgeEventHandler != null) - bridgeEventHandler.handle(bridgeEvent); - else - bridgeEvent.complete(true); - } - ) - ); + .route("/" + busPrefix + "/*") + .handler(BodyHandler.create()) // Required since Vert.x 4.3.0 to handle XHR POST requests (see https://github.com/vert-x3/vertx-web/issues/2188) + .subRouter(SockJSHandler.create(VertxInstance.getVertx()) + .bridge(new SockJSBridgeOptions() + .setPingTimeout(pingTimeout) // Should be higher than client WebSocketBusOptions.pingInterval (which is set to 30_000 at the time of writing this code) + .addInboundPermitted(new PermittedOptions(new JsonObject())) + .addOutboundPermitted(new PermittedOptions(new JsonObject())) + , bridgeEvent -> { // Calling the VertxInstance bridge event handler if set + Handler bridgeEventHandler = VertxInstance.getBridgeEventHandler(); + if (bridgeEventHandler != null) + bridgeEventHandler.handle(bridgeEvent); + else + bridgeEvent.complete(true); + } + ) + ); log("✓ Vert.x bus configured with prefix = '" + busPrefix + "' & timeout = " + pingTimeout + " ms"); }); diff --git a/webfx-stack-db-query/src/main/java/dev/webfx/stack/db/query/QueryArgument.java b/webfx-stack-db-query/src/main/java/dev/webfx/stack/db/query/QueryArgument.java index 3e13c3472..8e02f9c90 100644 --- a/webfx-stack-db-query/src/main/java/dev/webfx/stack/db/query/QueryArgument.java +++ b/webfx-stack-db-query/src/main/java/dev/webfx/stack/db/query/QueryArgument.java @@ -1,5 +1,6 @@ package dev.webfx.stack.db.query; +import dev.webfx.platform.util.Arrays; import dev.webfx.platform.util.Numbers; import dev.webfx.stack.db.datascope.DataScope; @@ -91,11 +92,11 @@ public int hashCode() { @Override public String toString() { return "QueryArgument{" + - "dataSourceId=" + dataSourceId + - ", language='" + language + '\'' + - ", statement='" + statement + '\'' + - ", parameters=" + java.util.Arrays.toString(parameters) + - '}'; + "dataSourceId=" + dataSourceId + + ", language='" + language + '\'' + + ", statement='" + statement + '\'' + + ", parameters=" + Arrays.toString(parameters) + + '}'; } public static QueryArgumentBuilder builder() { diff --git a/webfx-stack-db-submit-buscall/src/main/java/dev/webfx/stack/db/submit/buscall/serial/GeneratedKeyReferenceSerialCodec.java b/webfx-stack-db-submit-buscall/src/main/java/dev/webfx/stack/db/submit/buscall/serial/GeneratedKeyReferenceSerialCodec.java index 3a7b89a33..253675b96 100644 --- a/webfx-stack-db-submit-buscall/src/main/java/dev/webfx/stack/db/submit/buscall/serial/GeneratedKeyReferenceSerialCodec.java +++ b/webfx-stack-db-submit-buscall/src/main/java/dev/webfx/stack/db/submit/buscall/serial/GeneratedKeyReferenceSerialCodec.java @@ -18,7 +18,7 @@ public GeneratedKeyReferenceSerialCodec() { @Override public void encode(GeneratedKeyReference arg, AstObject serial) { encodeInteger(serial, STATEMENT_BATCH_INDEX_KEY, arg.getStatementBatchIndex()); - encodeInteger(serial, STATEMENT_BATCH_INDEX_KEY, arg.getGeneratedKeyIndex(), 0); + encodeInteger(serial, GENERATED_KEY_INDEX_KEY, arg.getGeneratedKeyIndex(), 0); } @Override diff --git a/webfx-stack-i18n-ast/src/main/java/dev/webfx/stack/i18n/spi/impl/ast/AstDictionary.java b/webfx-stack-i18n-ast/src/main/java/dev/webfx/stack/i18n/spi/impl/ast/AstDictionary.java index 1ed19f406..be2d6640c 100644 --- a/webfx-stack-i18n-ast/src/main/java/dev/webfx/stack/i18n/spi/impl/ast/AstDictionary.java +++ b/webfx-stack-i18n-ast/src/main/java/dev/webfx/stack/i18n/spi/impl/ast/AstDictionary.java @@ -1,11 +1,15 @@ package dev.webfx.stack.i18n.spi.impl.ast; import dev.webfx.platform.ast.AST; +import dev.webfx.platform.ast.AstObject; +import dev.webfx.platform.ast.ReadOnlyAstArray; import dev.webfx.platform.ast.ReadOnlyAstObject; import dev.webfx.platform.util.Strings; import dev.webfx.stack.i18n.DefaultTokenKey; import dev.webfx.stack.i18n.Dictionary; +import dev.webfx.stack.i18n.I18n; import dev.webfx.stack.i18n.TokenKey; +import dev.webfx.stack.i18n.spi.impl.I18nProviderImpl; /** * @author Bruno Salmon @@ -23,11 +27,45 @@ final class AstDictionary implements Dictionary { } @Override - public & TokenKey> Object getMessageTokenValue(Object messageKey, TK tokenKey) { + public & TokenKey> Object getMessageTokenValue(Object messageKey, TK tokenKey, boolean ignoreCase) { String key = Strings.toString(messageKey); Object o = dictionary.get(key); - if (o instanceof ReadOnlyAstObject) - return ((ReadOnlyAstObject) o).get(tokenKey.toString()); - return tokenKey == DefaultTokenKey.TEXT ? Strings.toString(o) : null; + if (o == null && ignoreCase) { + for (Object k : dictionary.keys()) { + String sk = Strings.toString(k); + if (key.equalsIgnoreCase(sk)) { + o = dictionary.get(sk); + break; + } + } + } + Object value; + // If we have a multi-token message (coming from json, yaml, etc...), we return the value corresponding to the + // requested token + if (o instanceof ReadOnlyAstObject) { + value = ((ReadOnlyAstObject) o).get(tokenKey.toString()); + // If the value itself is again an object (which can happen with graphic defined as an svgPath with + // associated properties such as fill, stroke, etc...), we extend the i18n interpretation features (normally + // designed for simple text values) to this object too (ex: fill = [brandMainColor]) + if (value instanceof AstObject) { + interpretBracketsAndDefaultInAstObjectValues((AstObject) value, messageKey); + } + } else + value = tokenKey == DefaultTokenKey.TEXT ? Strings.toString(o) : null; + return value; + } + + private void interpretBracketsAndDefaultInAstObjectValues(AstObject o, Object messageKey) { + ReadOnlyAstArray keys = o.keys(); + for (int i = 0; i < keys.size(); i++) { + String key = keys.getElement(i); + Object value = o.get(key); + if (value instanceof ReadOnlyAstObject) + interpretBracketsAndDefaultInAstObjectValues((AstObject) value, messageKey); + else if (value instanceof String) { + Object newTokenValue = ((I18nProviderImpl) I18n.getProvider()).interpretBracketsAndDefaultInTokenValue(value, messageKey, "", DefaultTokenKey.TEXT, this, false, this, true); + o.set(key, newTokenValue); + } + } } } diff --git a/webfx-stack-i18n-controls/src/main/java/dev/webfx/stack/i18n/controls/I18nControls.java b/webfx-stack-i18n-controls/src/main/java/dev/webfx/stack/i18n/controls/I18nControls.java index e0a3d27af..01b2fdf44 100644 --- a/webfx-stack-i18n-controls/src/main/java/dev/webfx/stack/i18n/controls/I18nControls.java +++ b/webfx-stack-i18n-controls/src/main/java/dev/webfx/stack/i18n/controls/I18nControls.java @@ -1,9 +1,7 @@ package dev.webfx.stack.i18n.controls; import dev.webfx.stack.i18n.I18n; -import javafx.scene.control.Labeled; -import javafx.scene.control.Tab; -import javafx.scene.control.TextInputControl; +import javafx.scene.control.*; /** * @author Bruno Salmon @@ -70,4 +68,25 @@ public static T bindI18nGraphicProperty(T tab, Object i18nKey, O I18n.bindI18nGraphicProperty(tab.graphicProperty(), i18nKey, args); return tab; } + + public static Label newLabel(Object i18nKey, Object... args) { + return bindI18nProperties(new Label(), i18nKey, args); + } + + public static Button newButton(Object i18nKey, Object... args) { + return bindI18nProperties(new Button(), i18nKey, args); + } + + public static RadioButton newRadioButton(Object i18nKey, Object... args) { + return bindI18nProperties(new RadioButton(), i18nKey, args); + } + + public static Hyperlink newHyperlink(Object i18nKey, Object... args) { + return bindI18nProperties(new Hyperlink(), i18nKey, args); + } + + public static CheckBox newCheckBox(Object i18nKey, Object... args) { + return bindI18nProperties(new CheckBox(), i18nKey, args); + } + } diff --git a/webfx-stack-i18n-time-plugin/src/main/java/dev/webfx/stack/i18n/time/TimeI18nKeys.java b/webfx-stack-i18n-time-plugin/src/main/java/dev/webfx/stack/i18n/time/TimeI18nKeys.java new file mode 100644 index 000000000..f9c986a03 --- /dev/null +++ b/webfx-stack-i18n-time-plugin/src/main/java/dev/webfx/stack/i18n/time/TimeI18nKeys.java @@ -0,0 +1,27 @@ +// File managed by WebFX (DO NOT EDIT MANUALLY) +package dev.webfx.stack.i18n.time; + +public interface TimeI18nKeys { + + String JANUARY = "JANUARY"; + String JUNE = "JUNE"; + String THURSDAY = "THURSDAY"; + String SUNDAY = "SUNDAY"; + String MAY = "MAY"; + String OCTOBER = "OCTOBER"; + String TUESDAY = "TUESDAY"; + String FRIDAY = "FRIDAY"; + String DECEMBER = "DECEMBER"; + String FEBRUARY = "FEBRUARY"; + String SEPTEMBER = "SEPTEMBER"; + String NOVEMBER = "NOVEMBER"; + String WEDNESDAY = "WEDNESDAY"; + String MONDAY = "MONDAY"; + String SATURDAY = "SATURDAY"; + String MARCH = "MARCH"; + String AUGUST = "AUGUST"; + String JULY = "JULY"; + String WEEK = "WEEK"; + String APRIL = "APRIL"; + +} \ No newline at end of file diff --git a/webfx-stack-i18n-time-plugin/webfx.xml b/webfx-stack-i18n-time-plugin/webfx.xml index bb53f069d..1d24785ed 100644 --- a/webfx-stack-i18n-time-plugin/webfx.xml +++ b/webfx-stack-i18n-time-plugin/webfx.xml @@ -14,4 +14,6 @@ dev.webfx.stack.i18n.time.I18nTimeFormatProvider
+ + \ No newline at end of file diff --git a/webfx-stack-i18n/src/main/java/dev/webfx/stack/i18n/Dictionary.java b/webfx-stack-i18n/src/main/java/dev/webfx/stack/i18n/Dictionary.java index 9b3993ec0..51631648d 100644 --- a/webfx-stack-i18n/src/main/java/dev/webfx/stack/i18n/Dictionary.java +++ b/webfx-stack-i18n/src/main/java/dev/webfx/stack/i18n/Dictionary.java @@ -5,6 +5,6 @@ */ public interface Dictionary { - & TokenKey> Object getMessageTokenValue(Object messageKey, TK tokenKey); + & TokenKey> Object getMessageTokenValue(Object messageKey, TK tokenKey, boolean ignoreCase); } diff --git a/webfx-stack-i18n/src/main/java/dev/webfx/stack/i18n/HasI18nKey.java b/webfx-stack-i18n/src/main/java/dev/webfx/stack/i18n/HasI18nKey.java new file mode 100644 index 000000000..8312508b2 --- /dev/null +++ b/webfx-stack-i18n/src/main/java/dev/webfx/stack/i18n/HasI18nKey.java @@ -0,0 +1,12 @@ +package dev.webfx.stack.i18n; + +/** + * Not directly used by this module but can be convenient for extended I18n providers + * + * @author Bruno Salmon + */ +public interface HasI18nKey { + + Object getI18nKey(); + +} diff --git a/webfx-stack-i18n/src/main/java/dev/webfx/stack/i18n/I18n.java b/webfx-stack-i18n/src/main/java/dev/webfx/stack/i18n/I18n.java index aa4cdae8b..b59b387c1 100644 --- a/webfx-stack-i18n/src/main/java/dev/webfx/stack/i18n/I18n.java +++ b/webfx-stack-i18n/src/main/java/dev/webfx/stack/i18n/I18n.java @@ -179,4 +179,8 @@ public static T bindI18nProperties(T text, Object i18nKey, Obje return text; } + public static Text newText(Object i18nKey, Object... args) { + return bindI18nProperties(new Text(), i18nKey, args); + } + } diff --git a/webfx-stack-i18n/src/main/java/dev/webfx/stack/i18n/I18nKeys.java b/webfx-stack-i18n/src/main/java/dev/webfx/stack/i18n/I18nKeys.java new file mode 100644 index 000000000..f444a4844 --- /dev/null +++ b/webfx-stack-i18n/src/main/java/dev/webfx/stack/i18n/I18nKeys.java @@ -0,0 +1,46 @@ +package dev.webfx.stack.i18n; + +/** + * @author Bruno Salmon + */ +public final class I18nKeys { + + public static String appendEllipsis(String i18nKey) { + return i18nKey + "..."; + } + + public static String appendColons(String i18nKey) { + return i18nKey + ":"; + } + + public static String appendArrows(String i18nKey) { + return i18nKey + ">>"; + } + + public static String prependArrows(String i18nKey) { + return "<<" + i18nKey; + } + + public static String upperCase(String i18nKey) { + return i18nKey.toUpperCase(); + } + + public static String lowerCase(String i18nKey) { + return i18nKey.toLowerCase(); + } + + public static String upperCaseFirstChar(String i18nKey) { + char firstCharKey = i18nKey.charAt(0); + if (!Character.isUpperCase(firstCharKey)) + i18nKey = Character.toUpperCase(firstCharKey) + i18nKey.substring(1); + return i18nKey; + } + + public static String lowerCaseFirstChar(String i18nKey) { + char firstCharKey = i18nKey.charAt(0); + if (!Character.isLowerCase(firstCharKey)) + i18nKey = Character.toLowerCase(firstCharKey) + i18nKey.substring(1); + return i18nKey; + } + +} diff --git a/webfx-stack-i18n/src/main/java/dev/webfx/stack/i18n/spi/I18nProvider.java b/webfx-stack-i18n/src/main/java/dev/webfx/stack/i18n/spi/I18nProvider.java index e220ec3b6..ec004d1a1 100644 --- a/webfx-stack-i18n/src/main/java/dev/webfx/stack/i18n/spi/I18nProvider.java +++ b/webfx-stack-i18n/src/main/java/dev/webfx/stack/i18n/spi/I18nProvider.java @@ -48,7 +48,7 @@ default & TokenKey> Object getDictionaryTokenValue(Object i1 default & TokenKey> Object getDictionaryTokenValue(Object i18nKey, TK tokenKey, Dictionary dictionary) { if (dictionary == null) dictionary = getDictionary(); - return dictionary.getMessageTokenValue(i18nKeyToDictionaryMessageKey(i18nKey), tokenKey); + return dictionary.getMessageTokenValue(i18nKeyToDictionaryMessageKey(i18nKey), tokenKey, false); // Is it ok to always ignore case or should we add this to method signature } // Temporary (should be protected) diff --git a/webfx-stack-i18n/src/main/java/dev/webfx/stack/i18n/spi/impl/I18nProviderImpl.java b/webfx-stack-i18n/src/main/java/dev/webfx/stack/i18n/spi/impl/I18nProviderImpl.java index 490b07b9d..cf28245d9 100644 --- a/webfx-stack-i18n/src/main/java/dev/webfx/stack/i18n/spi/impl/I18nProviderImpl.java +++ b/webfx-stack-i18n/src/main/java/dev/webfx/stack/i18n/spi/impl/I18nProviderImpl.java @@ -1,10 +1,12 @@ package dev.webfx.stack.i18n.spi.impl; +import dev.webfx.kit.util.properties.FXProperties; import dev.webfx.platform.console.Console; import dev.webfx.platform.scheduler.Scheduled; import dev.webfx.platform.uischeduler.UiScheduler; import dev.webfx.platform.util.Strings; import dev.webfx.platform.util.collection.Collections; +import dev.webfx.stack.i18n.DefaultTokenKey; import dev.webfx.stack.i18n.Dictionary; import dev.webfx.stack.i18n.TokenKey; import dev.webfx.stack.i18n.spi.I18nProvider; @@ -103,12 +105,7 @@ private Object guessInitialLanguage() { return null; } - private final ObjectProperty languageProperty = new SimpleObjectProperty<>() { - @Override - protected void invalidated() { - onLanguageChanged(); - } - }; + private final ObjectProperty languageProperty = FXProperties.newObjectProperty(this::onLanguageChanged); @Override public ObjectProperty languageProperty() { @@ -140,76 +137,120 @@ public Dictionary getDefaultDictionary() { public & TokenKey> Object getDictionaryTokenValue(Object i18nKey, TK tokenKey, Dictionary dictionary) { if (dictionary == null) dictionary = getDictionary(); - return getDictionaryTokenValueImpl(i18nKey, tokenKey, dictionary, false, false, false); + return getDictionaryTokenValueImpl(i18nKey, tokenKey, dictionary, false, dictionary, false, false); } - protected & TokenKey> Object getDictionaryTokenValueImpl(Object i18nKey, TK tokenKey, Dictionary dictionary, boolean skipDefaultDictionary, boolean skipMessageKeyInterpretation, boolean skipMessageLoading) { + protected & TokenKey> Object getDictionaryTokenValueImpl(Object i18nKey, TK tokenKey, Dictionary dictionary, boolean skipDefaultDictionary, Dictionary originalDictionary, boolean skipMessageKeyInterpretation, boolean skipMessageLoading) { Object tokenValue = null; - if (dictionary != null && i18nKey != null) { + String tokenValuePrefix = null, tokenValueSuffix = null; + if (i18nKey != null) { Object messageKey = i18nKeyToDictionaryMessageKey(i18nKey); - tokenValue = dictionary.getMessageTokenValue(messageKey, tokenKey); + tokenValue = dictionary == null ? null : dictionary.getMessageTokenValue(messageKey, tokenKey, false); + // Message key prefix & suffix interpretation if (tokenValue == null && !skipMessageKeyInterpretation && messageKey instanceof String) { String sKey = (String) messageKey; int length = Strings.length(sKey); + // Prefix interpretation (only << for now) if (length > 1) { int index = 0; while (index < length && !Character.isLetterOrDigit(sKey.charAt(index))) index++; if (index > 0) { - String prefix = sKey.substring(0, index); - switch (prefix) { + String sKeyPrefix = sKey.substring(0, index); + switch (sKeyPrefix) { case "<<": // Reading the token value of the remaining key (after <<) - tokenValue = getDictionaryTokenValueImpl(new I18nSubKey(sKey.substring(prefix.length(), length), i18nKey), tokenKey, dictionary, skipDefaultDictionary, false, skipMessageLoading); + tokenValue = getDictionaryTokenValueImpl(new I18nSubKey(sKey.substring(sKeyPrefix.length(), length), i18nKey), tokenKey, dictionary, skipDefaultDictionary, originalDictionary, true, skipMessageLoading); if (tokenValue != null && isAssignableFrom(tokenKey.expectedClass(), String.class)) - tokenValue = "" + getDictionaryTokenValueImpl(prefix, tokenKey, dictionary, skipDefaultDictionary, true, skipMessageLoading) + tokenValue; + tokenValuePrefix = "" + getDictionaryTokenValueImpl(sKeyPrefix, tokenKey, dictionary, skipDefaultDictionary, originalDictionary, true, skipMessageLoading); } } } + // Suffix interpretation (:, ?, >>, ...) if (tokenValue == null && length > 1) { int index = length; while (index > 0 && !Character.isLetterOrDigit(sKey.charAt(index - 1))) index--; if (index < length) { - String suffix = sKey.substring(index, length); - switch (suffix) { + String sKeySuffix = sKey.substring(index, length); + switch (sKeySuffix) { case ":": case "?": case ">>": case "...": // Reading the token value of the remaining key (before the suffix) - tokenValue = getDictionaryTokenValueImpl(new I18nSubKey(sKey.substring(0, length - suffix.length()), i18nKey), tokenKey, dictionary, skipDefaultDictionary, true, skipMessageLoading); + tokenValue = getDictionaryTokenValueImpl(new I18nSubKey(sKey.substring(0, length - sKeySuffix.length()), i18nKey), tokenKey, dictionary, skipDefaultDictionary, originalDictionary, true, skipMessageLoading); if (tokenValue != null && isAssignableFrom(tokenKey.expectedClass(), String.class)) - tokenValue = "" + tokenValue + getDictionaryTokenValueImpl(suffix, tokenKey, dictionary, skipDefaultDictionary, true, skipMessageLoading); + tokenValueSuffix = "" + getDictionaryTokenValueImpl(sKeySuffix, tokenKey, dictionary, skipDefaultDictionary, originalDictionary, true, skipMessageLoading); } } } - } - if (tokenValue instanceof String || tokenValue == null && messageKey instanceof String) { - String sToken = (String) (tokenValue == null ? messageKey : tokenValue); - int i1 = sToken.indexOf('['); - if (i1 >= 0) { - int i2 = i1 == 0 && sToken.endsWith("]") ? sToken.length() - 1 : sToken.indexOf(']', i1 + 1); - if (i2 > 0) { - Object resolvedValue = getDictionaryTokenValueImpl(new I18nSubKey(sToken.substring(i1 + 1, i2), i18nKey), tokenKey, dictionary, false, false, skipMessageLoading); - // If the bracket token has been resolved, we return it with the parts before and after the brackets - if (resolvedValue != null) - tokenValue = (i1 == 0 ? "" : sToken.substring(0, i1 - 1)) + resolvedValue + sToken.substring(i2 + 1); + // Case transformer keys + if (tokenValue == null && length > 1 && dictionary != null) { + // Second search but ignoring case + tokenValue = dictionary.getMessageTokenValue(messageKey, tokenKey, true); + if (tokenValue != null) { // Yes, we found a value this time! + String sValue = Strings.toString(tokenValue); + if (sKey.equals(sKey.toUpperCase())) // key was upper case ? => we upper case the value + tokenValue = sValue.toUpperCase(); + else if (sKey.equals(sKey.toLowerCase())) // key was lower case ? => we lower case the value + tokenValue = sValue.toLowerCase(); + else if (!sValue.isEmpty()) { + char firstCharKey = sKey.charAt(0); + char firstCharValue = sValue.charAt(0); + if (Character.isUpperCase(firstCharKey)) { // first letter uppercase ? => we upper case first letter in value + if (!Character.isUpperCase(firstCharValue)) + tokenValue = Character.toUpperCase(firstCharValue) + sValue.substring(1); + } else { // first letter lowercase ? => we lower case first letter in value + if (!Character.isLowerCase(firstCharValue)) + tokenValue = Character.toLowerCase(firstCharValue) + sValue.substring(1); + } + } } } } - if (tokenValue == null && !skipDefaultDictionary) { - Dictionary defaultDictionary = getDefaultDictionary(); - if (dictionary != defaultDictionary && defaultDictionary != null) - tokenValue = getDictionaryTokenValueImpl(i18nKey, tokenKey, defaultDictionary, true, skipMessageKeyInterpretation, skipMessageLoading); //getI18nPartValue(tokenSnapshot.i18nKey, part, defaultDictionary, skipPrefixOrSuffix); - if (tokenValue == null) { - if (!skipMessageLoading) - scheduleMessageLoading(i18nKey, true); - //if (tokenKey == DefaultTokenKey.TEXT) // Commented as we use it also for graphic in Modality after evaluating an expression that gives the path to the icon - tokenValue = messageKey; //;whatToReturnWhenI18nTextIsNotFound(tokenSnapshot.i18nKey, tokenSnapshot.tokenKey); + tokenValue = interpretBracketsAndDefaultInTokenValue(tokenValue, messageKey, i18nKey, tokenKey, dictionary, skipDefaultDictionary, originalDictionary, skipMessageLoading); + } + // Temporary code which is a workaround for the yaml parser not able to parse line feeds in strings. + if (tokenValue instanceof String) // TODO: remove this workaround once yaml parser is fixed + tokenValue = ((String) tokenValue).replace("\\n", "\n"); + if (tokenValuePrefix != null) + tokenValue = tokenValuePrefix + tokenValue; + if (tokenValueSuffix != null) + tokenValue = tokenValue + tokenValueSuffix; + return tokenValue; + } + + // public because called by AstDictionary to interpret token values within Ast objects as well + public & TokenKey> Object interpretBracketsAndDefaultInTokenValue(Object tokenValue, Object messageKey, Object i18nKey, TK tokenKey, Dictionary dictionary, boolean skipDefaultDictionary, Dictionary originalDictionary, boolean skipMessageLoading) { + // Token value bracket interpretation: if the value contains an i18n key in bracket, we interpret it + if (tokenValue instanceof String || tokenValue == null && messageKey instanceof String) { + String sToken = (String) (tokenValue == null ? messageKey : tokenValue); + int i1 = sToken.indexOf('['); + if (i1 >= 0) { + int i2 = i1 == 0 && sToken.endsWith("]") ? sToken.length() - 1 : sToken.indexOf(']', i1 + 1); + if (i2 > 0) { + // Note: we always use originalDictionary for the resolution, because even if that token value + // comes from the default dictionary (ex: EN), we still want the brackets to be interpreted in + // the original language (ex: FR). + Object resolvedValue = getDictionaryTokenValueImpl(new I18nSubKey(sToken.substring(i1 + 1, i2), i18nKey), tokenKey, originalDictionary, false, originalDictionary, false, skipMessageLoading); + // If the bracket token has been resolved, we return it with the parts before and after the brackets + if (resolvedValue != null) + tokenValue = (i1 == 0 ? "" : sToken.substring(0, i1)) + resolvedValue + sToken.substring(i2 + 1); } } } + if (tokenValue == null && !skipDefaultDictionary) { + Dictionary defaultDictionary = getDefaultDictionary(); + if (dictionary != defaultDictionary && defaultDictionary != null) + tokenValue = getDictionaryTokenValueImpl(i18nKey, tokenKey, defaultDictionary, true, originalDictionary, false, skipMessageLoading); //getI18nPartValue(tokenSnapshot.i18nKey, part, defaultDictionary, skipPrefixOrSuffix); + if (tokenValue == null) { + if (!skipMessageLoading) + scheduleMessageLoading(i18nKey, true); + if (tokenKey == DefaultTokenKey.TEXT || tokenKey == DefaultTokenKey.GRAPHIC) // we use it also for graphic in Modality after evaluating an expression that gives the path to the icon + tokenValue = messageKey; //;whatToReturnWhenI18nTextIsNotFound(tokenSnapshot.i18nKey, tokenSnapshot.tokenKey); + } + } return tokenValue; } @@ -265,7 +306,7 @@ private Object getFreshTokenValueFromSnapshot(TokenSnapshot tokenSnapshot) { private Object getFreshTokenValueFromSnapshot(TokenSnapshot tokenSnapshot, Dictionary dictionary) { Object i18nKey = tokenSnapshot.i18nKey; TokenKey tokenKey = tokenSnapshot.tokenKey; - return getDictionaryTokenValueImpl(i18nKey, (Enum & TokenKey) tokenKey, dictionary, false, false, true); + return getDictionaryTokenValueImpl(i18nKey, (Enum & TokenKey) tokenKey, dictionary, false, dictionary, false, true); } public boolean refreshMessageTokenProperties(Object freshI18nKey) { @@ -304,7 +345,10 @@ public void scheduleMessageLoading(Object i18nKey, boolean inDefaultLanguage) { .map(this::i18nKeyToDictionaryMessageKey).collect(Collectors.toSet()); // Asking the dictionary loader to load these messages in that language dictionaryLoader.loadDictionary(language, messageKeysToLoad) - .onFailure(Console::log) + .onFailure(e -> { + Console.log(e); + dictionaryProperty.set(null); // necessary to force default dictionary fallback and not keep previous language applied + }) .onSuccess(dictionary -> { // Once the dictionary is loaded, we take it as the current dictionary if it's in the current language if (language.equals(getLanguage())) // unless the load was a fallback to the default language @@ -312,6 +356,8 @@ public void scheduleMessageLoading(Object i18nKey, boolean inDefaultLanguage) { // Also taking it as the default dictionary if it's in the default language if (language.equals(getDefaultLanguage())) defaultDictionaryProperty.setValue(dictionary); + }) + .onComplete(ar -> { // Turning off dictionaryLoadRequired dictionaryLoadRequired = false; // Refreshing all loaded keys in the user interface @@ -326,10 +372,8 @@ public void scheduleMessageLoading(Object i18nKey, boolean inDefaultLanguage) { } if (unfoundKeys != null) { blacklistedKeys.addAll(unfoundKeys); - Console.log("⚠️ I18n keys not found (now blacklisted): " + Collections.toString(unfoundKeys, false, false)); + Console.log("⚠️ I18n keys not found (now blacklisted): " + Collections.toStringCommaSeparated(unfoundKeys)); } - }) - .onComplete(ar -> { // If the requested language has changed in the meantime, we might need to reload another dictionary! if (!language.equals(getLanguage())) { // We postpone the call to be sure that dictionaryLoadingScheduled will be finished diff --git a/webfx-stack-orm-entity-binding/pom.xml b/webfx-stack-orm-entity-binding/pom.xml new file mode 100644 index 000000000..351ea3a20 --- /dev/null +++ b/webfx-stack-orm-entity-binding/pom.xml @@ -0,0 +1,46 @@ + + + + 4.0.0 + + + dev.webfx + webfx-stack + 0.1.0-SNAPSHOT + + + webfx-stack-orm-entity-binding + + + + + org.openjfx + javafx-base + provided + + + + dev.webfx + webfx-platform-javatime-emul-j2cl + 0.1.0-SNAPSHOT + runtime + true + + + + dev.webfx + webfx-platform-util + 0.1.0-SNAPSHOT + + + + dev.webfx + webfx-stack-orm-entity + 0.1.0-SNAPSHOT + + + + + \ No newline at end of file diff --git a/webfx-stack-orm-entity-binding/src/main/java/dev/webfx/stack/orm/entity/binding/EntityBindings.java b/webfx-stack-orm-entity-binding/src/main/java/dev/webfx/stack/orm/entity/binding/EntityBindings.java new file mode 100644 index 000000000..cfa6ce6a6 --- /dev/null +++ b/webfx-stack-orm-entity-binding/src/main/java/dev/webfx/stack/orm/entity/binding/EntityBindings.java @@ -0,0 +1,97 @@ +package dev.webfx.stack.orm.entity.binding; + +import dev.webfx.platform.util.collection.Collections; +import dev.webfx.stack.orm.entity.Entity; +import dev.webfx.stack.orm.entity.EntityId; +import dev.webfx.stack.orm.entity.EntityStore; +import dev.webfx.stack.orm.entity.UpdateStore; +import dev.webfx.stack.orm.entity.impl.DynamicEntity; +import dev.webfx.stack.orm.entity.impl.UpdateStoreImpl; +import dev.webfx.stack.orm.entity.result.EntityChangesBuilder; +import dev.webfx.stack.orm.entity.result.EntityResult; +import javafx.beans.binding.BooleanExpression; +import javafx.beans.property.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +/** + * @author Bruno Salmon + */ +public final class EntityBindings { + + public static BooleanExpression hasChangesProperty(UpdateStore updateStore) { + UpdateStoreImpl updateStoreImpl = (UpdateStoreImpl) updateStore; + BooleanProperty hasChangesProperty = (BooleanProperty) updateStoreImpl.getHasChangesProperty(); + if (hasChangesProperty == null) { + EntityChangesBuilder changesBuilder = updateStoreImpl.getChangesBuilder(); + hasChangesProperty = new SimpleBooleanProperty(changesBuilder.hasChanges()); + changesBuilder.setHasChangesPropertyUpdater(hasChangesProperty::set); + } + return hasChangesProperty; + } + + public static BooleanProperty getBooleanFieldProperty(Entity entity, String fieldId) { + return (BooleanProperty) getFieldProperty(entity, fieldId, SimpleBooleanProperty::new); + } + + public static StringProperty getStringFieldProperty(Entity entity, String fieldId) { + return (StringProperty) getFieldProperty(entity, fieldId, SimpleStringProperty::new); + } + + public static IntegerProperty getIntegerFieldProperty(Entity entity, String fieldId) { + return (IntegerProperty) getFieldProperty(entity, fieldId, SimpleIntegerProperty::new); + } + + public static DoubleProperty getDoubleFieldProperty(Entity entity, String fieldId) { + return (DoubleProperty) getFieldProperty(entity, fieldId, SimpleDoubleProperty::new); + } + + private static Property getFieldProperty(Entity entity, String fieldId, Supplier propertyFactory) { + // Checking if that field property has already been instantiated + DynamicEntity dynamicEntity = (DynamicEntity) entity; + Property fieldProperty = (Property) dynamicEntity.getFieldProperty(fieldId); + if (fieldProperty == null) { // if not, we create it and initialize it + Property finalFieldProperty = fieldProperty = propertyFactory.get(); + // Setting its initial value + fieldProperty.setValue(entity.getFieldValue(fieldId)); + // Changes made on this property will be applied back to the entity + fieldProperty.addListener(observable -> entity.setFieldValue(fieldId, finalFieldProperty.getValue())); + // And changes to the entity will be sent back to the property + DynamicEntity.setFieldPropertyUpdater(EntityBindings::onEntityFieldValueChanged); + // Memorizing this new field property into the entity + dynamicEntity.setFieldProperty(fieldId, fieldProperty); + } + return fieldProperty; + } + + private static void onEntityFieldValueChanged(Object fieldProperty, Object value) { + ((Property) fieldProperty).setValue(value); + } + + private static final List STORES_LISTENING_ENTITY_CHANGES = new ArrayList<>(); + + public static void registerStoreForEntityChanges(EntityStore entityStore) { + Collections.addIfNotContains(entityStore, STORES_LISTENING_ENTITY_CHANGES); + } + + public static void unregisterStoreForEntityChanges(EntityStore entityStore) { + STORES_LISTENING_ENTITY_CHANGES.remove(entityStore); + } + + public static void applyEntityChangesToRegisteredStores(EntityResult entityChanges) { + for (EntityStore entityStore : STORES_LISTENING_ENTITY_CHANGES) { + for (EntityId entityId : entityChanges.getEntityIds()) { + Entity entity = entityStore.getEntity(entityId); + if (entity != null) { + for (Object fieldId : entityChanges.getFieldIds(entityId)) { + Object fieldValue = entityChanges.getFieldValue(entityId, fieldId); + entity.setFieldValue(fieldId, fieldValue); + } + } + } + } + } + +} diff --git a/webfx-stack-orm-entity-binding/src/main/java/module-info.java b/webfx-stack-orm-entity-binding/src/main/java/module-info.java new file mode 100644 index 000000000..a93a6f9c3 --- /dev/null +++ b/webfx-stack-orm-entity-binding/src/main/java/module-info.java @@ -0,0 +1,13 @@ +// File managed by WebFX (DO NOT EDIT MANUALLY) + +module webfx.stack.orm.entity.binding { + + // Direct dependencies modules + requires javafx.base; + requires webfx.platform.util; + requires webfx.stack.orm.entity; + + // Exported packages + exports dev.webfx.stack.orm.entity.binding; + +} \ No newline at end of file diff --git a/webfx-stack-orm-entity-binding/webfx.xml b/webfx-stack-orm-entity-binding/webfx.xml new file mode 100644 index 000000000..14bdc5177 --- /dev/null +++ b/webfx-stack-orm-entity-binding/webfx.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/webfx-stack-orm-entity-controls/src/main/java/dev/webfx/stack/orm/entity/controls/entity/selector/ButtonSelector.java b/webfx-stack-orm-entity-controls/src/main/java/dev/webfx/stack/orm/entity/controls/entity/selector/ButtonSelector.java index 9d13f2233..f2270dd7d 100644 --- a/webfx-stack-orm-entity-controls/src/main/java/dev/webfx/stack/orm/entity/controls/entity/selector/ButtonSelector.java +++ b/webfx-stack-orm-entity-controls/src/main/java/dev/webfx/stack/orm/entity/controls/entity/selector/ButtonSelector.java @@ -1,8 +1,8 @@ package dev.webfx.stack.orm.entity.controls.entity.selector; -import dev.webfx.extras.styles.materialdesign.textfield.MaterialTextFieldPane; import dev.webfx.extras.panes.MonoPane; import dev.webfx.extras.panes.ScalePane; +import dev.webfx.extras.styles.materialdesign.textfield.MaterialTextFieldPane; import dev.webfx.extras.util.layout.LayoutUtil; import dev.webfx.extras.util.scene.SceneUtil; import dev.webfx.kit.util.properties.FXProperties; @@ -62,13 +62,8 @@ public enum ShowMode { private ShowMode decidedShowMode; private final Property showModeProperty = new SimpleObjectProperty<>(ShowMode.AUTO); - private final Property selectedItemProperty = new SimpleObjectProperty<>() { - @Override - protected void invalidated() { - // Updating the content of the button when selected item changes - updateButtonContentFromSelectedItem(); - } - }; + // Updating the content of the button when selected item changes + private final Property selectedItemProperty = FXProperties.newObjectProperty(this::updateButtonContentFromSelectedItem); private final DoubleProperty dialogHeightProperty = new SimpleDoubleProperty(); public ButtonSelector(ButtonFactoryMixin buttonFactory, Callable parentGetter) { diff --git a/webfx-stack-orm-entity-controls/src/main/java/dev/webfx/stack/orm/entity/controls/entity/selector/EntityButtonSelector.java b/webfx-stack-orm-entity-controls/src/main/java/dev/webfx/stack/orm/entity/controls/entity/selector/EntityButtonSelector.java index cbbde4702..10c95cf30 100644 --- a/webfx-stack-orm-entity-controls/src/main/java/dev/webfx/stack/orm/entity/controls/entity/selector/EntityButtonSelector.java +++ b/webfx-stack-orm-entity-controls/src/main/java/dev/webfx/stack/orm/entity/controls/entity/selector/EntityButtonSelector.java @@ -7,6 +7,7 @@ import dev.webfx.extras.visual.VisualResult; import dev.webfx.extras.visual.controls.grid.SkinnedVisualGrid; import dev.webfx.extras.visual.controls.grid.VisualGrid; +import dev.webfx.kit.util.properties.FXProperties; import dev.webfx.platform.console.Console; import dev.webfx.platform.util.Arrays; import dev.webfx.platform.util.function.Callable; @@ -165,7 +166,8 @@ protected Region getOrCreateDialogContent() { dialogVisualGrid.setHeaderVisible(false); dialogVisualGrid.setCursor(Cursor.HAND); BorderPane.setAlignment(dialogVisualGrid, Pos.TOP_LEFT); - dialogVisualGrid.visualResultProperty().addListener((observable, oldValue, newValue) -> Platform.runLater(() -> deferredVisualResult.setValue(newValue))); + FXProperties.runOnPropertyChange(visualResult -> Platform.runLater(() -> deferredVisualResult.setValue(visualResult)) + , dialogVisualGrid.visualResultProperty()); EntityStore filterStore = loadingStore != null ? loadingStore : getSelectedItem() != null ? getSelectedItem().getStore() : null; entityDialogMapper = ReactiveVisualMapper.createReactiveChain() .always(jsonOrClass) @@ -201,13 +203,13 @@ protected Region getOrCreateDialogContent() { searchPane.setStretchWidth(true); // Actually shrinks the grid width back to fit again in the dialog searchPane.setScaleMode(ScaleMode.FIT_HEIGHT); // We will manually stretch the height to control the scale // We multiply the height by the same scale factor as the one applied on the visual grid to get the same scale - dialogVisualGrid.scaleYProperty().addListener((observable, oldValue, visualGridScaleY) -> { + FXProperties.runOnDoublePropertyChange(visualGridScaleY -> { // First we compute the searchPane normal height (with no scale). searchPane.setPrefHeight(Region.USE_COMPUTED_SIZE); // Necessary to force the computation double prefHeight = searchPane.prefHeight(searchPane.getWidth()); // Now we stretch the searchPane height with the visual grid scale factor - searchPane.setPrefHeight(prefHeight * visualGridScaleY.doubleValue()); // will scale the content (search text field + icon) - }); + searchPane.setPrefHeight(prefHeight * visualGridScaleY); // will scale the content (search text field + icon) + }, dialogVisualGrid.scaleYProperty()); } return scalePane; } diff --git a/webfx-stack-orm-entity-controls/src/main/java/dev/webfx/stack/orm/entity/controls/entity/sheet/EntityPropertiesSheet.java b/webfx-stack-orm-entity-controls/src/main/java/dev/webfx/stack/orm/entity/controls/entity/sheet/EntityPropertiesSheet.java index 51f475881..a852ee218 100644 --- a/webfx-stack-orm-entity-controls/src/main/java/dev/webfx/stack/orm/entity/controls/entity/sheet/EntityPropertiesSheet.java +++ b/webfx-stack-orm-entity-controls/src/main/java/dev/webfx/stack/orm/entity/controls/entity/sheet/EntityPropertiesSheet.java @@ -1,27 +1,29 @@ package dev.webfx.stack.orm.entity.controls.entity.sheet; -import javafx.scene.Node; -import javafx.scene.control.Label; -import javafx.scene.layout.Pane; -import javafx.scene.layout.VBox; import dev.webfx.extras.cell.renderer.ValueRenderer; import dev.webfx.extras.cell.renderer.ValueRenderingContext; import dev.webfx.extras.imagestore.ImageStore; +import dev.webfx.extras.type.PrimType; import dev.webfx.extras.type.Types; import dev.webfx.extras.visual.*; import dev.webfx.extras.visual.controls.grid.SkinnedVisualGrid; import dev.webfx.extras.visual.controls.grid.VisualGrid; import dev.webfx.extras.visual.impl.VisualColumnImpl; -import dev.webfx.stack.orm.reactive.entities.entities_to_grid.EntityColumn; -import dev.webfx.stack.orm.reactive.mapping.entities_to_visual.VisualEntityColumn; -import dev.webfx.stack.orm.reactive.mapping.entities_to_visual.VisualEntityColumnFactory; +import dev.webfx.kit.util.properties.FXProperties; +import dev.webfx.platform.uischeduler.UiScheduler; +import dev.webfx.platform.util.Arrays; import dev.webfx.stack.orm.domainmodel.DomainClass; import dev.webfx.stack.orm.domainmodel.formatter.ValueFormatter; import dev.webfx.stack.orm.domainmodel.formatter.ValueParser; import dev.webfx.stack.orm.entity.Entity; import dev.webfx.stack.orm.expression.Expression; -import dev.webfx.platform.uischeduler.UiScheduler; -import dev.webfx.platform.util.Arrays; +import dev.webfx.stack.orm.reactive.entities.entities_to_grid.EntityColumn; +import dev.webfx.stack.orm.reactive.mapping.entities_to_visual.VisualEntityColumn; +import dev.webfx.stack.orm.reactive.mapping.entities_to_visual.VisualEntityColumnFactory; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.layout.Pane; +import javafx.scene.layout.VBox; import java.util.ArrayList; import java.util.List; @@ -69,7 +71,7 @@ private ValueRenderingContext createValueRenderingContext(VisualEntityColumn // Returning a EntityRenderingContext otherwise (in case of a foreign entity) which will be used by the EntityRenderer else context = new EntityRenderingContext(entityColumn.isReadOnly(), labelKey, null, entityColumn, () -> entity.getStore(), () -> dialogParent, this); - context.getEditedValueProperty().addListener((observable, oldValue, newValue) -> applyUiChangeOnEntity(entityColumn, context)); + FXProperties.runOnPropertyChange(() -> applyUiChangeOnEntity(entityColumn, context), context.getEditedValueProperty()); return context; } @@ -184,6 +186,10 @@ private void applyUiChangeOnEntity(EntityColumn entityColumn, ValueRenderingCont if (Objects.equals(formatter.formatValue(value), formatter.formatValue(previousModelValue))) value = previousModelValue; } + // Empty strings are considered as null values for non-string expression + if ("".equals(value) && expression.getType() != PrimType.STRING) { + value = null; + } updateEntity.setExpressionValue(expression, value); updateOkButton(); syncUiFromModel(); diff --git a/webfx-stack-orm-entity-messaging/pom.xml b/webfx-stack-orm-entity-messaging/pom.xml new file mode 100644 index 000000000..5bcb289c4 --- /dev/null +++ b/webfx-stack-orm-entity-messaging/pom.xml @@ -0,0 +1,76 @@ + + + + 4.0.0 + + + dev.webfx + webfx-stack + 0.1.0-SNAPSHOT + + + webfx-stack-orm-entity-messaging + + + + + dev.webfx + webfx-platform-ast + 0.1.0-SNAPSHOT + + + + dev.webfx + webfx-platform-async + 0.1.0-SNAPSHOT + + + + dev.webfx + webfx-platform-javatime-emul-j2cl + 0.1.0-SNAPSHOT + runtime + true + + + + dev.webfx + webfx-platform-util + 0.1.0-SNAPSHOT + + + + dev.webfx + webfx-stack-com-bus + 0.1.0-SNAPSHOT + + + + dev.webfx + webfx-stack-com-serial + 0.1.0-SNAPSHOT + + + + dev.webfx + webfx-stack-orm-entity + 0.1.0-SNAPSHOT + + + + dev.webfx + webfx-stack-orm-entity-binding + 0.1.0-SNAPSHOT + + + + dev.webfx + webfx-stack-session-state-client-fx + 0.1.0-SNAPSHOT + + + + + \ No newline at end of file diff --git a/webfx-stack-orm-entity-messaging/src/main/java/dev/webfx/stack/orm/entity/messaging/EntityMessaging.java b/webfx-stack-orm-entity-messaging/src/main/java/dev/webfx/stack/orm/entity/messaging/EntityMessaging.java new file mode 100644 index 000000000..f02aea32c --- /dev/null +++ b/webfx-stack-orm-entity-messaging/src/main/java/dev/webfx/stack/orm/entity/messaging/EntityMessaging.java @@ -0,0 +1,83 @@ +package dev.webfx.stack.orm.entity.messaging; + +import dev.webfx.platform.async.Handler; +import dev.webfx.platform.util.Objects; +import dev.webfx.stack.com.bus.BusService; +import dev.webfx.stack.com.bus.Registration; +import dev.webfx.stack.com.serial.SerialCodecManager; +import dev.webfx.stack.orm.entity.EntityStore; +import dev.webfx.stack.orm.entity.binding.EntityBindings; +import dev.webfx.stack.orm.entity.result.EntityChanges; +import dev.webfx.stack.orm.entity.result.EntityResult; +import dev.webfx.stack.orm.entity.result.impl.EntityResultImpl; +import dev.webfx.stack.session.state.client.fx.FXConnected; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Bruno Salmon + */ +public final class EntityMessaging { + + private final String address; + private List> messageHandlers; + private Registration registration; + + public EntityMessaging(String address) { + this.address = address; + } + + // This is the method to use for the sender client (typically the back-office) to send a message to all front-office + // clients currently connected and listening (because they called addMessageBodyHandler() before). + public void publishMessage(Object messageBody) { + // We serialize the java object into json before publishing it on the event bus + Object encodedBody = SerialCodecManager.encodeToJson(messageBody); + // We publish the json encoded body to all front-office clients who are listening (because they called + // addFrontOfficeMessageBodyHandler() before). + BusService.bus().publish(address, encodedBody); + } + + // This is the method to use for the front-office client to listen messages published by the back-office. + public Registration addMessageBodyHandler(Handler messageBodyHandler) { + if (messageHandlers == null) { // Initialization on first call + messageHandlers = new ArrayList<>(); + // To make this work, the client bus call service must listen server calls! This takes place as soon as the + // connection to the server is ready, or each time we reconnect to the server: + FXConnected.runOnEachConnected(() -> + BusService.bus().register(address, message -> { + // Since the message body is encoded into json, we decode it into a java object + Object messageBody = SerialCodecManager.decodeFromJson(message.body()); + // Then we call each handler with that java message body + messageHandlers.forEach(h -> h.handle(messageBody)); + }) + ); + } + messageHandlers.add(messageBodyHandler); + return () -> messageHandlers.remove(messageBodyHandler); + } + + public Registration addMessageBodyHandler(Handler messageBodyHandler, Class messageClass) { + return addMessageBodyHandler(messageBody -> { + if (Objects.isInstanceOf(messageBody, messageClass)) { + messageBodyHandler.handle((T) messageBody); + } + }); + } + + public void publishEntityChanges(EntityChanges entityChanges) { + publishEntityChanges(entityChanges.getInsertedUpdatedEntityResult()); + } + + public void publishEntityChanges(EntityResult entityChanges) { + if (!entityChanges.getEntityIds().isEmpty()) + publishMessage(entityChanges); + } + + public void listenEntityChanges(EntityStore entityStore) { + EntityBindings.registerStoreForEntityChanges(entityStore); + if (registration == null) { + registration = addMessageBodyHandler(EntityBindings::applyEntityChangesToRegisteredStores, EntityResultImpl.class); + } + } +} diff --git a/webfx-stack-orm-entity-messaging/src/main/java/dev/webfx/stack/orm/entity/messaging/serial/EntityResultImplSerialCodec.java b/webfx-stack-orm-entity-messaging/src/main/java/dev/webfx/stack/orm/entity/messaging/serial/EntityResultImplSerialCodec.java new file mode 100644 index 000000000..6a245579c --- /dev/null +++ b/webfx-stack-orm-entity-messaging/src/main/java/dev/webfx/stack/orm/entity/messaging/serial/EntityResultImplSerialCodec.java @@ -0,0 +1,66 @@ +package dev.webfx.stack.orm.entity.messaging.serial; + +import dev.webfx.platform.ast.*; +import dev.webfx.stack.com.serial.spi.impl.SerialCodecBase; +import dev.webfx.stack.orm.entity.EntityId; +import dev.webfx.stack.orm.entity.result.impl.EntityResultImpl; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Bruno Salmon + */ +public final class EntityResultImplSerialCodec extends SerialCodecBase { + + private static final String CODEC_ID = "EntityResultImpl"; + + private static final String ENTITIES_KEY = "entities"; + private static final String CLASS_KEY = "class"; + private static final String PRIMARY_KEY = "pk"; + private static final String VALUES_KEY = "val"; + + public EntityResultImplSerialCodec() { + super(EntityResultImpl.class, CODEC_ID); + } + + @Override + public void encode(EntityResultImpl javaObject, AstObject serial) { + AstArray entities = AST.createArray(); + for (EntityId entityId : javaObject.getEntityIds()) { + AstObject entity = AST.createObject(); + entity.set(CLASS_KEY, entityId.getDomainClass().getId()); + entity.set(PRIMARY_KEY, entityId.getPrimaryKey()); + AstObject values = AST.createObject(); + for (Object fieldId : javaObject.getFieldIds(entityId)) { + values.set(fieldId.toString(), javaObject.getFieldValue(entityId, fieldId)); + } + entity.set(VALUES_KEY, values); + entities.push(entity); + } + serial.setArray(ENTITIES_KEY, entities); + } + + @Override + public EntityResultImpl decode(ReadOnlyAstObject serial) { + List entityIds = new ArrayList<>(); + List entityFieldsMaps = new ArrayList<>(); + ReadOnlyAstArray entities = serial.getArray(ENTITIES_KEY); + for (int i = 0; i < entities.size(); i++) { + ReadOnlyAstObject entity = entities.getObject(i); + Object classId = entity.get(CLASS_KEY); + Object primaryKey = entity.get(PRIMARY_KEY); + EntityId entityId = EntityId.create(classId, primaryKey); + Map fieldsMap = new HashMap(); + ReadOnlyAstObject values = entity.getObject(VALUES_KEY); + for (Object key : values.keys()) { + fieldsMap.put(key, values.get(key.toString())); + } + entityIds.add(entityId); + entityFieldsMaps.add(fieldsMap); + } + return new EntityResultImpl(entityIds, entityFieldsMaps); + } +} diff --git a/webfx-stack-orm-entity-messaging/src/main/java/module-info.java b/webfx-stack-orm-entity-messaging/src/main/java/module-info.java new file mode 100644 index 000000000..17195947c --- /dev/null +++ b/webfx-stack-orm-entity-messaging/src/main/java/module-info.java @@ -0,0 +1,22 @@ +// File managed by WebFX (DO NOT EDIT MANUALLY) + +module webfx.stack.orm.entity.messaging { + + // Direct dependencies modules + requires webfx.platform.ast; + requires webfx.platform.async; + requires webfx.platform.util; + requires webfx.stack.com.bus; + requires webfx.stack.com.serial; + requires webfx.stack.orm.entity; + requires webfx.stack.orm.entity.binding; + requires webfx.stack.session.state.client.fx; + + // Exported packages + exports dev.webfx.stack.orm.entity.messaging; + exports dev.webfx.stack.orm.entity.messaging.serial; + + // Provided services + provides dev.webfx.stack.com.serial.spi.SerialCodec with dev.webfx.stack.orm.entity.messaging.serial.EntityResultImplSerialCodec; + +} \ No newline at end of file diff --git a/webfx-stack-orm-entity-messaging/src/main/resources/META-INF/services/dev.webfx.stack.com.serial.spi.SerialCodec b/webfx-stack-orm-entity-messaging/src/main/resources/META-INF/services/dev.webfx.stack.com.serial.spi.SerialCodec new file mode 100644 index 000000000..114bbb071 --- /dev/null +++ b/webfx-stack-orm-entity-messaging/src/main/resources/META-INF/services/dev.webfx.stack.com.serial.spi.SerialCodec @@ -0,0 +1 @@ +dev.webfx.stack.orm.entity.messaging.serial.EntityResultImplSerialCodec diff --git a/webfx-stack-orm-entity-messaging/webfx.xml b/webfx-stack-orm-entity-messaging/webfx.xml new file mode 100644 index 000000000..15a50db64 --- /dev/null +++ b/webfx-stack-orm-entity-messaging/webfx.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + dev.webfx.stack.orm.entity.messaging.serial.EntityResultImplSerialCodec + + + \ No newline at end of file diff --git a/webfx-stack-orm-entity/pom.xml b/webfx-stack-orm-entity/pom.xml index 5854286f3..b5172f22b 100644 --- a/webfx-stack-orm-entity/pom.xml +++ b/webfx-stack-orm-entity/pom.xml @@ -15,12 +15,6 @@ - - org.openjfx - javafx-base - provided - - dev.webfx webfx-extras-type diff --git a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/Entities.java b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/Entities.java index 49db7f430..04ee8c6c1 100644 --- a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/Entities.java +++ b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/Entities.java @@ -1,5 +1,6 @@ package dev.webfx.stack.orm.entity; +import dev.webfx.platform.util.Arrays; import dev.webfx.stack.orm.expression.Expression; import dev.webfx.stack.orm.expression.terms.Select; import dev.webfx.stack.orm.expression.terms.function.Call; @@ -86,7 +87,15 @@ public static List select(List entityList, Select se public static List orderBy(List entityList, Expression... orderExpressions) { if (Collections.isEmpty(entityList)) - return Collections.emptyList(); - return Call.orderBy(entityList, new EntityDomainReader<>(entityList.get(0).getStore()), orderExpressions); + return entityList; + EntityStore store = entityList.get(0).getStore(); + return Call.orderBy(entityList, new EntityDomainReader<>(store), orderExpressions); + } + + public static List orderBy(List entityList, String... orderExpressions) { + if (Collections.isEmpty(entityList)) + return entityList; + DomainClass domainClass = entityList.get(0).getDomainClass(); + return orderBy(entityList, Arrays.map(orderExpressions, oe -> domainClass.parseExpression(oe.startsWith("order by") ? oe : "order by " + oe), Expression[]::new)); } -} +} \ No newline at end of file diff --git a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/EntityDomainClassIdRegistry.java b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/EntityDomainClassIdRegistry.java index 35e5dd843..2ac86ca54 100644 --- a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/EntityDomainClassIdRegistry.java +++ b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/EntityDomainClassIdRegistry.java @@ -24,21 +24,17 @@ public static Object getEntityDomainClassId(Class entityClass) // Utility methods - public static DomainClass getEntityDomainClass(Class entityClass) { - return getEntityDomainClass(entityClass, null); - } - - public static DomainClass getEntityDomainClass(Class entityClass, DomainModel domainModel) { - return getDomainClass(getEntityDomainClassId(entityClass), domainModel); - } - public static DomainClass getDomainClass(Object domainClassId) { return getDomainClass(domainClassId, null); } public static DomainClass getDomainClass(Object domainClassId, DomainModel domainModel) { + if (domainClassId instanceof DomainClass) + return (DomainClass) domainClassId; if (domainModel == null) domainModel = DataSourceModelService.getDefaultDataSourceModel().getDomainModel(); + if (domainClassId.getClass().equals(Class.class)) + domainClassId = getEntityDomainClassId((Class) domainClassId); return domainModel.getClass(domainClassId); } diff --git a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/EntityFactoryRegistry.java b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/EntityFactoryRegistry.java index 971568350..b374c87ad 100644 --- a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/EntityFactoryRegistry.java +++ b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/EntityFactoryRegistry.java @@ -32,7 +32,11 @@ public static void registerEntityFactory(EntityFactoryProvide public static void registerEntityFactory(Class entityClass, Object domainClassId, EntityFactory entityFactory) { //Logger.log("Registering " + domainClassId + " entity factory (creates " + entityFactory.createEntity(null, null).getClass().getName() + " instances for " + entityClass + ")"); EntityDomainClassIdRegistry.registerEntityDomainClassId(entityClass, domainClassId); - entityFactories.put(domainClassId, entityFactory); + EntityFactory existingEntityFactory = getEntityFactory(domainClassId); + if (existingEntityFactory != null) { // Happens with KBSX which overrides the Event entity class + Console.log("⚠️ Skipping '" + domainClassId + "' entity factory second registration (skipping " + entityFactory.getClass() + " and keeping " + existingEntityFactory.getClass() + ")"); + } else + entityFactories.put(domainClassId, entityFactory); } public static EntityFactory getEntityFactory(Class entityClass) { diff --git a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/EntityId.java b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/EntityId.java index ed2379ab0..607769019 100644 --- a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/EntityId.java +++ b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/EntityId.java @@ -31,19 +31,12 @@ default EntityId refactor(Object newPrimaryKey) { return EntityId.create(getDomainClass(), newPrimaryKey); } - static EntityId create(DomainClass domainClass, Object primaryKey) { - return EntityIdImpl.create(domainClass, primaryKey); + static EntityId create(Object domainClassId, Object primaryKey) { + return EntityIdImpl.create(domainClassId, primaryKey); } - static EntityId create(DomainClass domainClass) { - return EntityIdImpl.create(domainClass); + static EntityId create(Object domainClassId) { + return EntityIdImpl.create(domainClassId); } - static EntityId create(Class entityClass, Object primaryKey) { - return EntityIdImpl.create(EntityDomainClassIdRegistry.getEntityDomainClass(entityClass), primaryKey); - } - - static EntityId create(Class entityClass) { - return EntityIdImpl.create(EntityDomainClassIdRegistry.getEntityDomainClass(entityClass)); - } } diff --git a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/EntityList.java b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/EntityList.java index 3d6bf9dc2..5b3039777 100644 --- a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/EntityList.java +++ b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/EntityList.java @@ -1,13 +1,13 @@ package dev.webfx.stack.orm.entity; +import dev.webfx.platform.util.collection.Collections; +import dev.webfx.stack.orm.entity.impl.EntityListImpl; import dev.webfx.stack.orm.expression.Expression; import dev.webfx.stack.orm.expression.terms.Select; -import dev.webfx.stack.orm.entity.impl.EntityListImpl; - -import java.util.function.Predicate; import java.util.Collection; import java.util.List; +import java.util.function.Predicate; /** * @author Bruno Salmon @@ -16,8 +16,6 @@ public interface EntityList extends List, HasEntityStore { Object getListId(); - void orderBy(Expression... orderExpressions); - default List filter(String filterExpression) { return Entities.filter(this, filterExpression); } @@ -42,8 +40,16 @@ default List select(Select select) { return Entities.select(this, select); } + default List orderBy(Expression... orderExpressions) { + return Entities.orderBy(this, orderExpressions); + } + + default List orderBy(String... orderExpressions) { + return Entities.orderBy(this, orderExpressions); + } + - // static factoy methods + // static factory methods static EntityList create(Object listId, EntityStore store) { return new EntityListImpl<>(listId, store); @@ -51,8 +57,7 @@ static EntityList create(Object listId, EntityStore store) static EntityList create(Object listId, EntityStore store, Collection collections) { EntityList entities = create(listId, store); - for (E e : collections) - entities.add(e); + Collections.setAll(entities, collections); return entities; } } diff --git a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/EntityListMixin.java b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/EntityListMixin.java index 157e3b3fa..965e05128 100644 --- a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/EntityListMixin.java +++ b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/EntityListMixin.java @@ -1,7 +1,6 @@ package dev.webfx.stack.orm.entity; import dev.webfx.platform.util.collection.ListMixin; -import dev.webfx.stack.orm.expression.Expression; /** * @author Bruno Salmon @@ -24,8 +23,4 @@ default EntityStore getStore() { return getEntityList().getStore(); } - @Override - default void orderBy(Expression... orderExpressions) { - getEntityList().orderBy(orderExpressions); - } } diff --git a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/EntityStore.java b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/EntityStore.java index 88a937377..536cf1896 100644 --- a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/EntityStore.java +++ b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/EntityStore.java @@ -32,29 +32,18 @@ public interface EntityStore extends HasDataSourceModel { EntityDomainWriter getEntityDataWriter(); - default DomainClass getDomainClass(Object domainClassId) { - return domainClassId instanceof DomainClass ? (DomainClass) domainClassId : getDomainModel().getClass(domainClassId); - } + EntityStore getUnderlyingStore(); - default DomainClass getDomainClass(Class entityClass) { - return getDomainClass(getDomainClassId(entityClass)); - } - - default Object getDomainClassId(Class entityClass) { - return EntityDomainClassIdRegistry.getEntityDomainClassId(entityClass); + default DomainClass getDomainClass(Object domainClassId) { + return EntityDomainClassIdRegistry.getDomainClass(domainClassId, getDomainModel()); } - // EntityId management default EntityId getEntityId(Object domainClassId, Object primaryKey) { return EntityId.create(getDomainClass(domainClassId), primaryKey); } - default EntityId getEntityId(DomainClass domainClass, Object primaryKey) { - return EntityId.create(domainClass, primaryKey); - } - void applyEntityIdRefactor(EntityId oldId, EntityId newId); default void applyEntityIdRefactor(EntityId oldId, Object newPk) { @@ -68,57 +57,37 @@ default void applyEntityIdRefactor(EntityId oldId, Object newPk) { // Entity management default E createEntity(Class entityClass) { - return createEntity(getDomainClass(entityClass)); + return createEntity((Object) entityClass); } default E createEntity(Object domainClassId) { - return createEntity(getDomainClass(domainClassId)); - } - - default E createEntity(DomainClass domainClass) { - return createEntity(EntityId.create(domainClass)); - } - - default E createEntity(Class entityClass, Object primaryKey) { - return createEntity(getDomainClass(entityClass), primaryKey); + return createEntity(EntityId.create(getDomainClass(domainClassId))); } default E createEntity(Object domainClassId, Object primaryKey) { return primaryKey == null ? null : createEntity(getEntityId(domainClassId, primaryKey)); } - default E createEntity(DomainClass domainClass, Object primaryKey) { - return primaryKey == null ? null : createEntity(getEntityId(domainClass, primaryKey)); - } - E createEntity(EntityId id); default E getEntity(Class entityClass, Object primaryKey) { - return getEntity(getDomainClass(entityClass), primaryKey); + return getEntity((Object) entityClass, primaryKey); } default E getEntity(Object domainClassId, Object primaryKey) { return primaryKey == null ? null : getEntity(getEntityId(domainClassId, primaryKey)); } - default E getEntity(DomainClass domainClass, Object primaryKey) { - return primaryKey == null ? null : getEntity(getEntityId(domainClass, primaryKey)); - } - E getEntity(EntityId entityId); default E getOrCreateEntity(Class entityClass, Object primaryKey) { - return getOrCreateEntity(getDomainClass(entityClass), primaryKey); + return getOrCreateEntity((Object) entityClass, primaryKey); } default E getOrCreateEntity(Object domainClassId, Object primaryKey) { return primaryKey == null ? null : getOrCreateEntity(getEntityId(domainClassId, primaryKey)); } - default E getOrCreateEntity(DomainClass domainClass, Object primaryKey) { - return primaryKey == null ? null : getOrCreateEntity(getEntityId(domainClass, primaryKey)); - } - default E getOrCreateEntity(EntityId id) { if (id == null) return null; @@ -166,7 +135,7 @@ default T evaluateEntityExpression(Entity entity, String expression) { // Query methods default Future> executeQuery(String dqlQuery, Object... parameters) { - return executeListQuery(dqlQuery, dqlQuery, parameters); + return executeListQuery(null, dqlQuery, parameters); } default Future> executeCachedQuery(CacheEntry> cacheEntry, Consumer> cacheListConsumer, String dqlQuery, Object... parameters) { diff --git a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/EntityStoreQuery.java b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/EntityStoreQuery.java index 399d423a0..93f7ba839 100644 --- a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/EntityStoreQuery.java +++ b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/EntityStoreQuery.java @@ -18,7 +18,7 @@ public EntityStoreQuery(String select, Object listId) { } public EntityStoreQuery(String select, Object[] parameters) { - this(select, parameters, select); + this(select, parameters, null); // entity list will not be memorised in the entity store if not listId is provided } public EntityStoreQuery(String select, Object[] parameters, Object listId) { diff --git a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/UpdateStore.java b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/UpdateStore.java index b39cf583b..468f31f71 100644 --- a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/UpdateStore.java +++ b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/UpdateStore.java @@ -6,38 +6,20 @@ import dev.webfx.stack.db.submit.SubmitArgument; import dev.webfx.stack.db.submit.SubmitResult; import dev.webfx.stack.orm.domainmodel.DataSourceModel; -import dev.webfx.stack.orm.domainmodel.DomainClass; import dev.webfx.stack.orm.entity.impl.UpdateStoreImpl; import dev.webfx.stack.orm.entity.result.EntityChanges; -import javafx.beans.binding.BooleanExpression; /** * @author Bruno Salmon */ public interface UpdateStore extends EntityStore { - default E insertEntity(Class entityClass) { - return insertEntity(entityClass, null); - } - default E insertEntity(Object domainClassId) { return insertEntity(domainClassId, null); } - default E insertEntity(DomainClass domainClass) { - return insertEntity(domainClass, null); - } - - default E insertEntity(Class entityClass, Object primaryKey) { - return insertEntity(EntityDomainClassIdRegistry.getEntityDomainClassId(entityClass), primaryKey); - } - default E insertEntity(Object domainClassId, Object primaryKey) { - return insertEntity(getDomainClass(domainClassId), primaryKey); - } - - default E insertEntity(DomainClass domainClass, Object primaryKey) { - return insertEntity(EntityId.create(domainClass, primaryKey)); + return insertEntity(getEntityId(domainClassId, primaryKey)); } E insertEntity(EntityId entityId); @@ -49,42 +31,25 @@ default E updateEntity(E entity) { E updateEntity(EntityId entityId); - default E updateEntity(DomainClass domainClass, Object primaryKey) { - return updateEntity(EntityId.create(domainClass, primaryKey)); - } - default E updateEntity(Object domainClassId, Object primaryKey) { - return updateEntity(EntityId.create(getDomainClass(domainClassId), primaryKey)); - } - - default E updateEntity(Class entityClass, Object primaryKey) { - return updateEntity(EntityDomainClassIdRegistry.getEntityDomainClassId(entityClass), primaryKey); + return updateEntity(getEntityId(domainClassId, primaryKey)); } default void deleteEntity(Entity entity) { - deleteEntity(entity.getId()); + if (entity != null) + deleteEntity(entity.getId()); } void deleteEntity(EntityId entityId); - default void deleteEntity(DomainClass domainClass, Object primaryKey) { - deleteEntity(EntityId.create(domainClass, primaryKey)); - } - default void deleteEntity(Object domainClassId, Object primaryKey) { - deleteEntity(EntityId.create(getDomainClass(domainClassId), primaryKey)); - } - - default void deleteEntity(Class entityClass, Object primaryKey) { - deleteEntity(EntityDomainClassIdRegistry.getEntityDomainClassId(entityClass), primaryKey); + deleteEntity(getEntityId(domainClassId, primaryKey)); } EntityChanges getEntityChanges(); boolean hasChanges(); - BooleanExpression hasChangesProperty(); - void cancelChanges(); void markChangesAsCommitted(); diff --git a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/impl/DynamicEntity.java b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/impl/DynamicEntity.java index 2bcb60371..59a6ffcab 100644 --- a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/impl/DynamicEntity.java +++ b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/impl/DynamicEntity.java @@ -1,15 +1,16 @@ package dev.webfx.stack.orm.entity.impl; +import dev.webfx.platform.console.Console; import dev.webfx.stack.orm.entity.Entity; import dev.webfx.stack.orm.entity.EntityId; import dev.webfx.stack.orm.entity.EntityStore; import dev.webfx.stack.orm.entity.UpdateStore; -import dev.webfx.platform.console.Console; import java.util.HashMap; import java.util.Map; import java.util.Objects; +import java.util.function.BiConsumer; /** * @author Bruno Salmon @@ -18,11 +19,17 @@ public class DynamicEntity implements Entity { private EntityId id; private final EntityStore store; - private final Map fieldValues = new HashMap<>(); + private final Entity underlyingEntity; + private final Map fieldValues = new HashMap<>(); + // fields used by EntityBindings only: + private Map fieldProperties; // lazy instantiation + private static BiConsumer FIELD_PROPERTY_UPDATER; protected DynamicEntity(EntityId id, EntityStore store) { this.id = id; this.store = store; + EntityStore underlyingStore = store == null ? null : store.getUnderlyingStore(); + underlyingEntity = underlyingStore != null ? underlyingStore.getEntity(id) : null; } @Override @@ -45,12 +52,20 @@ public EntityStore getStore() { @Override public Object getFieldValue(Object domainFieldId) { - return fieldValues.get(domainFieldId); + Object value = fieldValues.get(domainFieldId); + if (value == null && underlyingEntity != null && !fieldValues.containsKey(domainFieldId)) { + return underlyingEntity.getFieldValue(domainFieldId); + } + return value; } @Override public boolean isFieldLoaded(Object domainFieldId) { - return fieldValues.containsKey(domainFieldId); + if (fieldValues.containsKey(domainFieldId)) + return true; + if (underlyingEntity != null) + return underlyingEntity.isFieldLoaded(domainFieldId); + return false; } @Override @@ -84,24 +99,17 @@ public EntityId getForeignEntityId(Object foreignFieldId) { @Override public void setFieldValue(Object domainFieldId, Object value) { + fieldValues.put(domainFieldId, value); if (store instanceof UpdateStore) { - Object previousValue = fieldValues.get(domainFieldId); - ((UpdateStoreImpl) this.store).updateEntity(id, domainFieldId, value, previousValue); + Object underlyingValue = underlyingEntity != null ? underlyingEntity.getFieldValue(domainFieldId) : null; + boolean isUnderlyingValueLoaded = underlyingValue != null || underlyingEntity != null && underlyingEntity.isFieldLoaded(domainFieldId); + ((UpdateStoreImpl) store).onInsertedOrUpdatedEntityFieldChange(id, domainFieldId, value, underlyingValue, isUnderlyingValueLoaded); + } + if (FIELD_PROPERTY_UPDATER != null) { + Object fieldProperty = getFieldProperty(domainFieldId); + if (fieldProperty != null) + FIELD_PROPERTY_UPDATER.accept(fieldProperty, value); } - fieldValues.put(domainFieldId, value); - } - - @Override - public String toString() { - return toString(new StringBuilder()).toString(); - } - - public StringBuilder toString(StringBuilder sb) { - sb.append(id.getDomainClass()).append("(pk: ").append(id.getPrimaryKey()); - for (Map.Entry entry : fieldValues.entrySet()) - sb.append(", ").append(entry.getKey()).append(": ").append(entry.getValue()); - sb.append(')'); - return sb; } public void copyAllFieldsFrom(Entity entity) { @@ -109,6 +117,10 @@ public void copyAllFieldsFrom(Entity entity) { fieldValues.putAll(dynamicEntity.fieldValues); } + public void clearAllFields() { + fieldValues.clear(); + } + // Implementing equals() and hashCode() -- Ex: entities are used as keys in GanttLayout parent/grandparent cache @Override @@ -123,4 +135,41 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(id); } + + @Override + public String toString() { + return toString(new StringBuilder(), true).toString(); + } + + private StringBuilder toString(StringBuilder sb, boolean pk) { + if (pk) + sb.append(id.getDomainClass()).append("(pk: ").append(id.getPrimaryKey()); + String separator = pk ? ", " : " | "; + for (Map.Entry entry : fieldValues.entrySet()) { + sb.append(separator).append(entry.getKey()).append(": ").append(entry.getValue()); + separator = ", "; + } + if (underlyingEntity instanceof DynamicEntity) { + ((DynamicEntity) underlyingEntity).toString(sb, false); + } + if (pk) + sb.append(')'); + return sb; + } + + // methods meant to be used by EntityBindings only + + public Object getFieldProperty(Object fieldId) { + return fieldProperties == null ? null : fieldProperties.get(fieldId); + } + + public void setFieldProperty(Object fieldId, Object fieldProperty) { + if (fieldProperties == null) + fieldProperties = new HashMap<>(); + fieldProperties.put(fieldId, fieldProperty); + } + + public static void setFieldPropertyUpdater(BiConsumer fieldPropertyUpdater) { + FIELD_PROPERTY_UPDATER = fieldPropertyUpdater; + } } diff --git a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/impl/EntityIdImpl.java b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/impl/EntityIdImpl.java index fe08a494c..4d50ddeaa 100644 --- a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/impl/EntityIdImpl.java +++ b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/impl/EntityIdImpl.java @@ -1,15 +1,16 @@ package dev.webfx.stack.orm.entity.impl; import dev.webfx.platform.util.Numbers; -import dev.webfx.stack.orm.entity.EntityId; import dev.webfx.stack.orm.domainmodel.DomainClass; +import dev.webfx.stack.orm.entity.EntityDomainClassIdRegistry; +import dev.webfx.stack.orm.entity.EntityId; import java.util.Objects; /** * @author Bruno Salmon */ -public final class EntityIdImpl implements EntityId { +public final class EntityIdImpl implements EntityId, Comparable { private final DomainClass domainClass; private final Object primaryKey; @@ -58,13 +59,23 @@ public int hashCode() { return result; } + @Override + public int compareTo(EntityId id2) { + if (id2 == null) + return -1; + int result = domainClass.getName().compareTo(id2.getDomainClass().getName()); + if (result == 0) + result = Numbers.compareObjectsOrNumbers(primaryKey, id2.getPrimaryKey()); + return result; + } + private static int newPk; - public static EntityIdImpl create(DomainClass domainClassId) { - return create(domainClassId, null); + public static EntityIdImpl create(Object domainClassId, Object primaryKey) { + return new EntityIdImpl(EntityDomainClassIdRegistry.getDomainClass(domainClassId), primaryKey != null ? primaryKey : --newPk); } - public static EntityIdImpl create(DomainClass domainClassId, Object primaryKey) { - return new EntityIdImpl(domainClassId, primaryKey != null ? primaryKey : --newPk); + public static EntityIdImpl create(Object domainClassId) { + return create(domainClassId, null); } } diff --git a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/impl/EntityListImpl.java b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/impl/EntityListImpl.java index b14f67f41..ccc0e1fe1 100644 --- a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/impl/EntityListImpl.java +++ b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/impl/EntityListImpl.java @@ -1,11 +1,9 @@ package dev.webfx.stack.orm.entity.impl; -import dev.webfx.stack.orm.entity.Entities; +import dev.webfx.platform.util.collection.Collections; import dev.webfx.stack.orm.entity.Entity; import dev.webfx.stack.orm.entity.EntityList; import dev.webfx.stack.orm.entity.EntityStore; -import dev.webfx.platform.util.collection.Collections; -import dev.webfx.stack.orm.expression.Expression; import java.util.ArrayList; @@ -32,13 +30,8 @@ public EntityStore getStore() { return store; } - @Override - public void orderBy(Expression... orderExpressions) { - Entities.orderBy(this, orderExpressions); - } - @Override public String toString() { - return Collections.toStringWithLineFeeds(this); + return Collections.toStringCommaSeparatedWithBracketsAndLineFeeds(this); } } diff --git a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/impl/EntityStoreImpl.java b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/impl/EntityStoreImpl.java index 643ee84d7..3e47c07cc 100644 --- a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/impl/EntityStoreImpl.java +++ b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/impl/EntityStoreImpl.java @@ -16,7 +16,7 @@ */ public class EntityStoreImpl implements EntityStore { - protected final DataSourceModel dataSourceModel; + private final DataSourceModel dataSourceModel; private final EntityStore underlyingStore; protected final Map entities = new HashMap<>(); private final Map entityLists = new HashMap<>(); @@ -47,6 +47,11 @@ public EntityDomainWriter getEntityDataWriter() { return entityDataWriter; } + @Override + public EntityStore getUnderlyingStore() { + return underlyingStore; + } + @Override public void applyEntityIdRefactor(EntityId oldId, EntityId newId) { Entity entity = entities.get(oldId); // entities.remove(oldId); @@ -87,8 +92,11 @@ public EntityList getEntityList(Object listId) { @Override public EntityList getOrCreateEntityList(Object listId) { EntityList entityList = getEntityList(listId); - if (entityList == null) - entityLists.put(listId, entityList = EntityList.create(listId, this)); + if (entityList == null) { + entityList = EntityList.create(listId, this); + if (listId != null) + entityLists.put(listId, entityList); + } return entityList; } diff --git a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/impl/UpdateStoreImpl.java b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/impl/UpdateStoreImpl.java index c2f36d99c..71639bf64 100644 --- a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/impl/UpdateStoreImpl.java +++ b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/impl/UpdateStoreImpl.java @@ -4,7 +4,7 @@ import dev.webfx.platform.async.Future; import dev.webfx.platform.console.Console; import dev.webfx.platform.util.Arrays; -import dev.webfx.platform.util.Objects; +import dev.webfx.platform.util.Numbers; import dev.webfx.stack.db.datascope.DataScope; import dev.webfx.stack.db.submit.SubmitArgument; import dev.webfx.stack.db.submit.SubmitResult; @@ -14,8 +14,10 @@ import dev.webfx.stack.orm.entity.EntityId; import dev.webfx.stack.orm.entity.EntityStore; import dev.webfx.stack.orm.entity.UpdateStore; -import dev.webfx.stack.orm.entity.result.*; -import javafx.beans.binding.BooleanExpression; +import dev.webfx.stack.orm.entity.result.EntityChanges; +import dev.webfx.stack.orm.entity.result.EntityChangesBuilder; +import dev.webfx.stack.orm.entity.result.EntityChangesToSubmitBatchGenerator; +import dev.webfx.stack.orm.entity.result.EntityResult; /** * @author Bruno Salmon @@ -23,8 +25,8 @@ public final class UpdateStoreImpl extends EntityStoreImpl implements UpdateStore { private final EntityChangesBuilder changesBuilder = EntityChangesBuilder.create(); - private EntityResultBuilder previousValues; private DataScope submitScope; + private Object hasChangesProperty; // managed by EntityBindings public UpdateStoreImpl(DataSourceModel dataSourceModel) { super(dataSourceModel); @@ -54,40 +56,12 @@ public E updateEntity(EntityId entityId) { return getOrCreateEntity(entityId); } - boolean updateEntity(EntityId id, Object domainFieldId, Object value, Object previousValue) { - if (!Objects.areEquals(value, previousValue) && changesBuilder.hasEntityId(id)) { - if (previousValues != null && Objects.areEquals(value, previousValues.getFieldValue(id, domainFieldId))) { - changesBuilder.removeFieldChange(id, domainFieldId); - return true; - } else { - boolean firstFieldChange = updateEntity(id, domainFieldId, value); - if (firstFieldChange) - rememberPreviousEntityFieldValue(id, domainFieldId, previousValue); - return firstFieldChange; - } - } - return false; - } - - boolean updateEntity(EntityId id, Object domainFieldId, Object value) { - return changesBuilder.addFieldChange(id, domainFieldId, value); - } - - void rememberPreviousEntityFieldValue(EntityId id, Object domainFieldId, Object value) { - if (previousValues == null) - previousValues = EntityResultBuilder.create(); - previousValues.setFieldValue(id, domainFieldId, value); - } - - void restorePreviousValues() { - if (previousValues != null) { - EntityResult rs = previousValues.build(); - for (EntityId id : rs.getEntityIds()) { - Entity entity = getEntity(id); - for (Object fieldId : rs.getFieldIds(id)) - entity.setFieldValue(fieldId, rs.getFieldValue(id, fieldId)); - } - previousValues = null; + void onInsertedOrUpdatedEntityFieldChange(EntityId id, Object domainFieldId, Object value, Object underlyingValue, boolean isUnderlyingValueLoaded) { + // If the user enters back the original value, we completely clear that field from the changes + if (isUnderlyingValueLoaded && Numbers.identicalObjectsOrNumberValues(value, underlyingValue)) { + changesBuilder.removeFieldChange(id, domainFieldId); + } else { + changesBuilder.addFieldChange(id, domainFieldId, value); } } @@ -99,10 +73,12 @@ public void setSubmitScope(DataScope submitScope) { @Override public Future> submitChanges(SubmitArgument... initialSubmits) { try { - EntityChangesToSubmitBatchGenerator.BatchGenerator updateBatchGenerator = EntityChangesToSubmitBatchGenerator.createSubmitBatchGenerator(getEntityChanges(), dataSourceModel, submitScope, initialSubmits); + EntityChangesToSubmitBatchGenerator.BatchGenerator updateBatchGenerator = EntityChangesToSubmitBatchGenerator. + createSubmitBatchGenerator(getEntityChanges(), getDataSourceModel(), submitScope, initialSubmits); Batch argBatch = updateBatchGenerator.generate(); Console.log("Executing submit batch " + Arrays.toStringWithLineFeeds(argBatch.getArray())); return SubmitService.executeSubmitBatch(argBatch).compose(resBatch -> { + // TODO: perf optimization: make these steps optional if not required by application code markChangesAsCommitted(); updateBatchGenerator.applyGeneratedKeys(resBatch, this); return Future.succeededFuture(resBatch); @@ -122,20 +98,69 @@ public boolean hasChanges() { return changesBuilder.hasChanges(); } - @Override - public BooleanExpression hasChangesProperty() { - return changesBuilder.hasChangesProperty(); - } - @Override public void cancelChanges() { + clearAllUpdatedValuesFromUpdateStore(); changesBuilder.clear(); - restorePreviousValues(); + } + + private void clearAllUpdatedValuesFromUpdateStore() { + EntityChanges changes = changesBuilder.build(); + EntityResult insertedUpdatedEntityResult = changes.getInsertedUpdatedEntityResult(); + if (insertedUpdatedEntityResult != null) { + for (EntityId entityId : insertedUpdatedEntityResult.getEntityIds()) { + clearAllUpdatedValuesFromUpdatedEntity(entityId); + } + } + } + + private void clearAllUpdatedValuesFromUpdatedEntity(EntityId entityId) { + if (!entityId.isNew()) { + Entity updateEntity = getEntity(entityId); + if (updateEntity instanceof DynamicEntity) { + ((DynamicEntity) updateEntity).clearAllFields(); + } + } } @Override public void markChangesAsCommitted() { - previousValues = null; + applyCommittedChangesToUnderlyingStore(); changesBuilder.clear(); } + + private void applyCommittedChangesToUnderlyingStore() { + EntityStore underlyingStore = getUnderlyingStore(); + if (underlyingStore != null) { + EntityChanges changes = changesBuilder.build(); + EntityResult insertedUpdatedEntityResult = changes.getInsertedUpdatedEntityResult(); + for (EntityId entityId : insertedUpdatedEntityResult.getEntityIds()) { + Entity underlyingEntity = underlyingStore.getEntity(entityId); + if (underlyingEntity != null) { + for (Object fieldId : insertedUpdatedEntityResult.getFieldIds(entityId)) { + if (fieldId != null) { + Object fieldValue = insertedUpdatedEntityResult.getFieldValue(entityId, fieldId); + underlyingEntity.setFieldValue(fieldId, fieldValue); + } + } + } + clearAllUpdatedValuesFromUpdatedEntity(entityId); + } + } + } + + // methods meant to be used by EntityBindings only + + + public EntityChangesBuilder getChangesBuilder() { + return changesBuilder; + } + + public void setHasChangesProperty(Object hasChangesProperty) { + this.hasChangesProperty = hasChangesProperty; + } + + public Object getHasChangesProperty() { + return hasChangesProperty; + } } diff --git a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/result/EntityChangesBuilder.java b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/result/EntityChangesBuilder.java index 37da5e2cc..b1e4a672c 100644 --- a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/result/EntityChangesBuilder.java +++ b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/result/EntityChangesBuilder.java @@ -1,13 +1,13 @@ package dev.webfx.stack.orm.entity.result; import dev.webfx.platform.util.collection.HashList; +import dev.webfx.stack.orm.domainmodel.DomainClass; +import dev.webfx.stack.orm.entity.EntityDomainClassIdRegistry; import dev.webfx.stack.orm.entity.EntityId; import dev.webfx.stack.orm.entity.result.impl.EntityChangesImpl; -import javafx.beans.binding.BooleanExpression; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; import java.util.Collection; +import java.util.function.Consumer; /** * @author Bruno Salmon @@ -16,11 +16,12 @@ public final class EntityChangesBuilder { private EntityResultBuilder rsb; private Collection deletedEntities; - private BooleanProperty hasChangesProperty; // lazy instantiation + private boolean hasChanges; + private Consumer hasChangesPropertyUpdater; // used by EntityBindings only private EntityChangesBuilder() {} - public void addDeletedEntityId(EntityId id) { + public EntityChangesBuilder addDeletedEntityId(EntityId id) { if (id.isNew()) { cancelEntityChanges(id); } else { @@ -28,19 +29,19 @@ public void addDeletedEntityId(EntityId id) { deletedEntities = new HashList<>(); // Hash for uniqueness and List for keeping sequence order deletedEntities.add(id); } - updateHasChangesProperty(); + return updateHasChanges(); } - public void addInsertedEntityId(EntityId id) { + public EntityChangesBuilder addInsertedEntityId(EntityId id) { if (id.isNew()) addFieldChange(id, null, null); - updateHasChangesProperty(); + return updateHasChanges(); } - public void addUpdatedEntityId(EntityId id) { + public EntityChangesBuilder addUpdatedEntityId(EntityId id) { if (!id.isNew()) addFieldChange(id, null, null); - updateHasChangesProperty(); + return updateHasChanges(); } public boolean hasEntityId(EntityId id) { @@ -49,42 +50,61 @@ public boolean hasEntityId(EntityId id) { public boolean addFieldChange(EntityId id, Object fieldId, Object fieldValue) { boolean fieldChanged = rsb().setFieldValue(id, fieldId, fieldValue); - updateHasChangesProperty(); + updateHasChanges(); return fieldChanged; } - public void removeFieldChange(EntityId id, Object fieldId) { + public EntityChangesBuilder removeFieldChange(EntityId id, Object fieldId) { if (rsb != null) rsb.unsetFieldValue(id, fieldId); - updateHasChangesProperty(); + return updateHasChanges(); } - public void cancelEntityChanges(EntityId id) { + public EntityChangesBuilder cancelEntityChanges(EntityId id) { if (deletedEntities != null) deletedEntities.remove(id); if (rsb != null) rsb.removeEntityId(id); - updateHasChangesProperty(); + return updateHasChanges(); } - public void clear() { + public EntityChangesBuilder addFilteredEntityChanges(EntityChanges ec, Object domainClassId, Object... fieldIds) { + return addFilteredEntityResult(ec.getInsertedUpdatedEntityResult(), domainClassId, fieldIds); + } + + public EntityChangesBuilder addFilteredEntityResult(EntityResult er, Object domainClassId, Object... fieldIds) { + if (er != null) { + DomainClass domainClass = EntityDomainClassIdRegistry.getDomainClass(domainClassId); + for (EntityId id : er.getEntityIds()) { + if (domainClass.equals(id.getDomainClass())) { + for (Object fieldId : fieldIds) { + Object fieldValue = er.getFieldValue(id, fieldId); + if (fieldValue != null || er.getFieldIds(id).contains(fieldId)) + addFieldChange(id, fieldId, fieldValue); + } + } + } + } + return this; + } + + public EntityChangesBuilder clear() { rsb = null; deletedEntities = null; + return updateHasChanges(); } public boolean hasChanges() { return deletedEntities != null && !deletedEntities.isEmpty() || rsb != null && !rsb.isEmpty(); } - public BooleanExpression hasChangesProperty() { - if (hasChangesProperty == null) - hasChangesProperty = new SimpleBooleanProperty(hasChanges()); - return hasChangesProperty; - } - - private void updateHasChangesProperty() { - if (hasChangesProperty != null) - hasChangesProperty.set(hasChanges()); + private EntityChangesBuilder updateHasChanges() { + if (hasChanges != hasChanges()) { + hasChanges = !hasChanges; + if (hasChangesPropertyUpdater != null) + hasChangesPropertyUpdater.accept(hasChanges); + } + return this; } private EntityResultBuilder rsb() { @@ -100,4 +120,10 @@ public EntityChanges build() { public static EntityChangesBuilder create() { return new EntityChangesBuilder(); } + + // method meant to be used by EntityBindings only + + public void setHasChangesPropertyUpdater(Consumer hasChangesPropertyUpdater) { + this.hasChangesPropertyUpdater = hasChangesPropertyUpdater; + } } diff --git a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/result/EntityChangesToSubmitBatchGenerator.java b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/result/EntityChangesToSubmitBatchGenerator.java index 6d2568c96..ba2c26e35 100644 --- a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/result/EntityChangesToSubmitBatchGenerator.java +++ b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/result/EntityChangesToSubmitBatchGenerator.java @@ -67,11 +67,15 @@ public static final class BatchGenerator { } public Batch generate() { - // First generating delete statements - generateDeletes(); - // Then insert and update statements. Statements parameters values may temporary contains EntityId objects, - // which will be replaced on next step while sorting statements. + // Generating insert and update statements. Statements parameters values may temporary contains EntityId + // objects, which will be replaced on next step while sorting statements. generateInsertUpdates(); + // Then generating delete statements. Better to execute after update statements, because some updates may + // clear the foreign keys pointing the rows we are about to delete. If deletes are executed before this + // clearing, it's very likely that the database will raise a reference constraint error. Note that these + // delete statements will be executed in the same order as called by the application code, which therefore + // is responsible for this order (in case there are references between deleted objects). + generateDeletes(); // Finally sorting the statements so that any statement (insert or update) that is referring to a new entity // will be executed after that entity has been inserted into the database. For such statements, the parameter // value referring to the new entity is replaced with a GeneratedKeyReference object that contains the index diff --git a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/result/EntityResultBuilder.java b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/result/EntityResultBuilder.java index 57611c946..9c93f5f11 100644 --- a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/result/EntityResultBuilder.java +++ b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/result/EntityResultBuilder.java @@ -41,14 +41,21 @@ public Object getFieldValue(EntityId id, Object fieldId) { return fieldMap.get(fieldId); } + public boolean hasFieldValue(EntityId id, Object fieldId) { + Map fieldMap = entityFieldMap(id); + return fieldMap != null && fieldMap.containsKey(fieldId); + } + void unsetFieldValue(EntityId id, Object fieldId) { Map fieldMap = entityFieldMap(id); - if (fieldId != null) - fieldMap.remove(fieldId); - else - fieldMap.clear(); - if (hasEntityNoChange(id, fieldMap)) - changedEntitiesCount--; + if (!hasEntityNoChange(id, fieldMap)) { + if (fieldId != null) + fieldMap.remove(fieldId); + else + fieldMap.clear(); + if (hasEntityNoChange(id, fieldMap)) + changedEntitiesCount--; + } } boolean isEmpty() { diff --git a/webfx-stack-orm-entity/src/main/java/module-info.java b/webfx-stack-orm-entity/src/main/java/module-info.java index c1b5809f0..ede370e62 100644 --- a/webfx-stack-orm-entity/src/main/java/module-info.java +++ b/webfx-stack-orm-entity/src/main/java/module-info.java @@ -3,7 +3,6 @@ module webfx.stack.orm.entity { // Direct dependencies modules - requires javafx.base; requires webfx.extras.type; requires transitive webfx.platform.async; requires webfx.platform.console; diff --git a/webfx-stack-orm-expression/src/main/java/dev/webfx/stack/orm/expression/terms/PrimitiveBinaryBooleanExpression.java b/webfx-stack-orm-expression/src/main/java/dev/webfx/stack/orm/expression/terms/PrimitiveBinaryBooleanExpression.java index c9aa2ebf8..96dd3569f 100644 --- a/webfx-stack-orm-expression/src/main/java/dev/webfx/stack/orm/expression/terms/PrimitiveBinaryBooleanExpression.java +++ b/webfx-stack-orm-expression/src/main/java/dev/webfx/stack/orm/expression/terms/PrimitiveBinaryBooleanExpression.java @@ -29,7 +29,7 @@ public Boolean evaluateCondition(Object leftValue, Object rightValue, DomainRead if (leftPrimType != null) { leftValue = domainReader.prepareValueBeforeTypeConversion(leftValue, leftPrimType); rightValue = domainReader.prepareValueBeforeTypeConversion(rightValue, leftPrimType); - if (leftValue != null && rightValue != null) + if (leftValue != null && rightValue != null) { switch (leftPrimType) { case BOOLEAN: return evaluateBoolean(Booleans.toBoolean(leftValue), Booleans.toBoolean(rightValue)); @@ -46,6 +46,7 @@ public Boolean evaluateCondition(Object leftValue, Object rightValue, DomainRead case DATE: return evaluateDate(Times.toInstant(leftValue), Times.toInstant(rightValue)); } + } } return evaluateObject(leftValue, rightValue); } diff --git a/webfx-stack-orm-expression/src/main/java/dev/webfx/stack/orm/expression/terms/PrimitiveBinaryExpression.java b/webfx-stack-orm-expression/src/main/java/dev/webfx/stack/orm/expression/terms/PrimitiveBinaryExpression.java index dd23a12d1..dba18fbda 100644 --- a/webfx-stack-orm-expression/src/main/java/dev/webfx/stack/orm/expression/terms/PrimitiveBinaryExpression.java +++ b/webfx-stack-orm-expression/src/main/java/dev/webfx/stack/orm/expression/terms/PrimitiveBinaryExpression.java @@ -29,7 +29,7 @@ public Object evaluate(Object leftValue, Object rightValue, DomainReader doma if (leftPrimType != null) { leftValue = domainReader.prepareValueBeforeTypeConversion(leftValue, leftPrimType); rightValue = domainReader.prepareValueBeforeTypeConversion(rightValue, leftPrimType); - if (leftValue != null && rightValue != null) + if (leftValue != null && rightValue != null) { switch (leftPrimType) { case BOOLEAN: return evaluateBoolean(Booleans.toBoolean(leftValue), Booleans.toBoolean(rightValue)); @@ -46,6 +46,7 @@ public Object evaluate(Object leftValue, Object rightValue, DomainReader doma case DATE: return evaluateDate(Times.toInstant(leftValue), Times.toInstant(rightValue)); } + } } return evaluateObject(leftValue, rightValue); } diff --git a/webfx-stack-orm-expression/src/main/java/dev/webfx/stack/orm/expression/terms/function/Call.java b/webfx-stack-orm-expression/src/main/java/dev/webfx/stack/orm/expression/terms/function/Call.java index 7f36017d9..e8ed176e2 100644 --- a/webfx-stack-orm-expression/src/main/java/dev/webfx/stack/orm/expression/terms/function/Call.java +++ b/webfx-stack-orm-expression/src/main/java/dev/webfx/stack/orm/expression/terms/function/Call.java @@ -1,6 +1,7 @@ package dev.webfx.stack.orm.expression.terms.function; import dev.webfx.extras.type.Type; +import dev.webfx.platform.util.Numbers; import dev.webfx.stack.orm.expression.CollectOptions; import dev.webfx.stack.orm.expression.Expression; import dev.webfx.stack.orm.expression.lci.DomainReader; @@ -130,18 +131,16 @@ public static List orderBy(List list, DomainReader domainReader, Ex for (Expression orderExpression : orderExpressions) { Object o1 = orderExpression.evaluate(e1, domainReader); Object o2 = orderExpression.evaluate(e2, domainReader); - Ordered ordered = orderExpression instanceof Ordered ? (Ordered) orderExpression : null; - int result; if (o1 == o2) - result = 0; - else if (!(o1 instanceof Comparable)) - result = ordered == null || ordered.isNullsLast() ? 1 : -1; - else if (!(o2 instanceof Comparable)) - result = ordered == null || ordered.isNullsLast() ? -1 : 1; - else { - result = ((Comparable) o1).compareTo(o2); - if (ordered != null && ordered.isDescending()) - result = -result; + return 0; + Ordered ordered = orderExpression instanceof Ordered ? (Ordered) orderExpression : null; + if (o1 == null) + return ordered != null && ordered.isNullsFirst() ? -1 : 1; + if (o2 == null) + return ordered != null && ordered.isNullsFirst() ? 1 : -1; + int result = Numbers.compareObjectsOrNumbers(o1, o2); + if (result != 0 && ordered != null && ordered.isDescending()) { + result = -result; } if (result != 0) return result; diff --git a/webfx-stack-orm-reactive-call/pom.xml b/webfx-stack-orm-reactive-call/pom.xml index fd550f07d..0e72276ff 100644 --- a/webfx-stack-orm-reactive-call/pom.xml +++ b/webfx-stack-orm-reactive-call/pom.xml @@ -21,6 +21,12 @@ provided + + dev.webfx + webfx-kit-util + 0.1.0-SNAPSHOT + + dev.webfx webfx-platform-async diff --git a/webfx-stack-orm-reactive-call/src/main/java/dev/webfx/stack/orm/reactive/call/ReactiveCall.java b/webfx-stack-orm-reactive-call/src/main/java/dev/webfx/stack/orm/reactive/call/ReactiveCall.java index b5a94ccae..92ea00402 100644 --- a/webfx-stack-orm-reactive-call/src/main/java/dev/webfx/stack/orm/reactive/call/ReactiveCall.java +++ b/webfx-stack-orm-reactive-call/src/main/java/dev/webfx/stack/orm/reactive/call/ReactiveCall.java @@ -1,5 +1,6 @@ package dev.webfx.stack.orm.reactive.call; +import dev.webfx.kit.util.properties.FXProperties; import dev.webfx.platform.console.Console; import dev.webfx.platform.util.tuples.Pair; import dev.webfx.stack.cache.CacheEntry; @@ -26,36 +27,18 @@ public class ReactiveCall { private CacheEntry> resultCacheEntry; private Scheduled fireCallNowIfRequiredScheduled; - private final BooleanProperty startedProperty = new SimpleBooleanProperty() { - @Override - protected void invalidated() { - if (get()) - onStarted(); - else - onStopped(); - } - }; + private final BooleanProperty startedProperty = FXProperties.newBooleanProperty(started -> { + if (started) + onStarted(); + else + onStopped(); + }); - private final BooleanProperty activeProperty = new SimpleBooleanProperty(true) { - @Override - protected void invalidated() { - onActiveChanged(); - } - }; + private final BooleanProperty activeProperty = FXProperties.newBooleanProperty(true, this::onActiveChanged); - private final ObjectProperty argumentProperty = new SimpleObjectProperty() { - @Override - protected void invalidated() { - onArgumentChanged(); - } - }; + private final ObjectProperty argumentProperty = FXProperties.newObjectProperty(this::onArgumentChanged); - protected final ObjectProperty resultProperty = new SimpleObjectProperty() { - @Override - protected void invalidated() { - onResultChanged(); - } - }; + protected final ObjectProperty resultProperty = FXProperties.newObjectProperty(this::onResultChanged); public ReactiveCall(AsyncFunction asyncFunction) { this.asyncFunction = asyncFunction; @@ -329,6 +312,7 @@ public void setAutoRefreshDelay(long autoRefreshDelay) { } private Scheduled autoRefreshScheduled; + private void rescheduleAutoRefresh() { stopAutoRefresh(); long autoRefreshDelay = getAutoRefreshDelay(); diff --git a/webfx-stack-orm-reactive-call/src/main/java/dev/webfx/stack/orm/reactive/call/query/push/ReactiveQueryOptionalPush.java b/webfx-stack-orm-reactive-call/src/main/java/dev/webfx/stack/orm/reactive/call/query/push/ReactiveQueryOptionalPush.java index 9788012be..a2eff1f8a 100644 --- a/webfx-stack-orm-reactive-call/src/main/java/dev/webfx/stack/orm/reactive/call/query/push/ReactiveQueryOptionalPush.java +++ b/webfx-stack-orm-reactive-call/src/main/java/dev/webfx/stack/orm/reactive/call/query/push/ReactiveQueryOptionalPush.java @@ -1,11 +1,11 @@ package dev.webfx.stack.orm.reactive.call.query.push; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; -import dev.webfx.stack.orm.reactive.call.query.ReactiveQueryCall; -import dev.webfx.stack.orm.reactive.call.SwitchableReactiveCall; +import dev.webfx.kit.util.properties.FXProperties; import dev.webfx.stack.db.query.QueryArgument; import dev.webfx.stack.db.query.QueryResult; +import dev.webfx.stack.orm.reactive.call.SwitchableReactiveCall; +import dev.webfx.stack.orm.reactive.call.query.ReactiveQueryCall; +import javafx.beans.property.BooleanProperty; /** * @author Bruno Salmon @@ -28,12 +28,7 @@ public ReactiveQueryPushCall getReactiveQueryPush() { return reactiveQueryPush; } - private final BooleanProperty pushProperty = new SimpleBooleanProperty(false) { - @Override - protected void invalidated() { - updateDelegate(); - } - }; + private final BooleanProperty pushProperty = FXProperties.newBooleanProperty(false, this::updateDelegate); public BooleanProperty pushProperty() { return pushProperty; diff --git a/webfx-stack-orm-reactive-call/src/main/java/dev/webfx/stack/orm/reactive/call/query/push/ReactiveQueryPushCall.java b/webfx-stack-orm-reactive-call/src/main/java/dev/webfx/stack/orm/reactive/call/query/push/ReactiveQueryPushCall.java index d4783d87f..08725252c 100644 --- a/webfx-stack-orm-reactive-call/src/main/java/dev/webfx/stack/orm/reactive/call/query/push/ReactiveQueryPushCall.java +++ b/webfx-stack-orm-reactive-call/src/main/java/dev/webfx/stack/orm/reactive/call/query/push/ReactiveQueryPushCall.java @@ -1,5 +1,6 @@ package dev.webfx.stack.orm.reactive.call.query.push; +import dev.webfx.kit.util.properties.FXProperties; import dev.webfx.stack.db.query.QueryArgument; import dev.webfx.stack.db.query.QueryResult; import dev.webfx.stack.db.querypush.QueryPushArgument; @@ -11,7 +12,6 @@ import dev.webfx.stack.session.state.client.fx.FXReconnected; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import java.util.ArrayList; @@ -36,22 +36,16 @@ public ReactiveQueryPushCall() { // Property used to memorize that a lost connection occurred - bound to FXConnectionLost in onStarted() - private final BooleanProperty lostConnectionProperty = new SimpleBooleanProperty() { - @Override - protected void invalidated() { - if (get()) // Means that we just lost connection to the server - lostConnection = true; // memorising this event for further decisions - } - }; + private final BooleanProperty lostConnectionProperty = FXProperties.newBooleanProperty(lost -> { + if (lost) // Means that we just lost connection to the server + lostConnection = true; // memorising this event for further decisions + }); // Property used to react to reconnections - bound to FXReconnected in onStarted() - private final BooleanProperty reconnectedProperty = new SimpleBooleanProperty() { - @Override - protected void invalidated() { - if (get()) // Means that the connection to the server is just back now - scheduleFireCallNowIfRequired(); // We schedule a server call if required to refresh the data - } - }; + private final BooleanProperty reconnectedProperty = FXProperties.newBooleanProperty(reconnected -> { + if (reconnected) // Means that the connection to the server is just back now + scheduleFireCallNowIfRequired(); // We schedule a server call if required to refresh the data + }); private final ObjectProperty activeParentProperty = new SimpleObjectProperty() { private ReactiveQueryPushCall previousActiveParent; @@ -82,12 +76,12 @@ protected boolean isFireCallRequiredNow() { if (waitingQueryStreamId) // If we already wait the queryStreamId, we won't make a new call now (we can't update the stream without its id) queryHasChangeWhileWaitingQueryStreamId |= hasArgumentChangedSinceLastCall(); // but we mark this flag in order to update the stream (if modified) when receiving its id ReactiveQueryPushCall parent = getActiveParent(); - return isStarted() - && getArgument() != null - && !waitingQueryStreamId - // Skipping new stream not yet active (waiting it becomes active before calling the server) - && (queryStreamId != null || isActive()) - && (parent == null || parent.queryStreamId != null && !parent.lostConnection); + return isStarted() + && getArgument() != null + && !waitingQueryStreamId + // Skipping new stream not yet active (waiting it becomes active before calling the server) + && (queryStreamId != null || isActive()) + && (parent == null || parent.queryStreamId != null && !parent.lostConnection); } @Override @@ -114,19 +108,19 @@ protected void callAsyncFunction() { Object parentQueryStreamId = parent == null ? null : parent.queryStreamId; waitingQueryStreamId = queryStreamId == null; // Setting the waitingQueryStreamId flag to true when queryStreamId is not yet known QueryPushService.executeQueryPush(QueryPushArgument.builder() - .setQueryStreamId(queryStreamId) - .setParentQueryStreamId(parentQueryStreamId) - .setQueryArgument(transmittedQueryArgument) - .setActive(isActive()) - .setResend(resend) - // This consumer will be called each time the server will push a change notification on the result - .setQueryPushResultConsumer(queryPushResult -> onCallResult(computeQueryResult(queryPushResult), null)) - .build() + .setQueryStreamId(queryStreamId) + .setParentQueryStreamId(parentQueryStreamId) + .setQueryArgument(transmittedQueryArgument) + .setActive(isActive()) + .setResend(resend) + // This consumer will be called each time the server will push a change notification on the result + .setQueryPushResultConsumer(queryPushResult -> onCallResult(computeQueryResult(queryPushResult), null)) + .build() ).onComplete(ar -> { // This handler is called only once when the query push service call returns boolean refreshChildren = false; // Cases where we need to trigger a new query push service call: if (ar.failed() // 1) on failure (this may happen if queryStreamId is not registered on the server anymore, for ex after server restart with a non-persistent query push provider such as the in-memory default one) - || queryHasChangeWhileWaitingQueryStreamId) { // 2) when the query has changed while we were waiting for the query stream id + || queryHasChangeWhileWaitingQueryStreamId) { // 2) when the query has changed while we were waiting for the query stream id log((isActive() ? "Refreshing queryStreamId=" + queryStreamId : "queryStreamId=" + queryStreamId + " will be refreshed when active") + (queryHasChangeWhileWaitingQueryStreamId ? " because the query has changed while waiting the queryStreamId" : " because a failure occurred while updating the query (may be an unrecognized queryStreamId after server restart)")); queryHasChangeWhileWaitingQueryStreamId = false; // Resetting the flag fireCallWhenReady(); // This will trigger a new pass (when active) leading to a new call to the query push service @@ -202,6 +196,7 @@ protected void onStopped() { private static int SEQ; private final int seq = ++SEQ; + @Override protected void log(String message) { super.log("ReactiveQueryPushCall-" + seq + "[queryStreamId=" + queryStreamId + "]: " + message); diff --git a/webfx-stack-orm-reactive-call/src/main/java/module-info.java b/webfx-stack-orm-reactive-call/src/main/java/module-info.java index e5f17e744..2c693894f 100644 --- a/webfx-stack-orm-reactive-call/src/main/java/module-info.java +++ b/webfx-stack-orm-reactive-call/src/main/java/module-info.java @@ -4,6 +4,7 @@ // Direct dependencies modules requires javafx.base; + requires webfx.kit.util; requires webfx.platform.async; requires webfx.platform.console; requires webfx.platform.scheduler; diff --git a/webfx-stack-orm-reactive-dql/src/main/java/dev/webfx/stack/orm/reactive/dql/query/ReactiveDqlQuery.java b/webfx-stack-orm-reactive-dql/src/main/java/dev/webfx/stack/orm/reactive/dql/query/ReactiveDqlQuery.java index c200ce718..dc87ab3ef 100644 --- a/webfx-stack-orm-reactive-dql/src/main/java/dev/webfx/stack/orm/reactive/dql/query/ReactiveDqlQuery.java +++ b/webfx-stack-orm-reactive-dql/src/main/java/dev/webfx/stack/orm/reactive/dql/query/ReactiveDqlQuery.java @@ -47,8 +47,7 @@ public ReactiveDqlQuery(ReactiveDqlStatement reactiveDqlStatement) { public ReactiveDqlQuery(ReactiveDqlStatement reactiveDqlStatement, ReactiveCall reactiveQueryCall) { this.reactiveDqlStatement = reactiveDqlStatement; this.reactiveQueryCall = reactiveQueryCall; - resultingDqlStatementProperty().addListener((observableValue, oldDqlStatement, newDqlStatement) - -> updateQueryArgument(newDqlStatement)); + FXProperties.runOnPropertyChange(this::updateQueryArgument, resultingDqlStatementProperty()); } @Override @@ -79,7 +78,7 @@ public DataSourceModel getDataSourceModel() { @Override public ReactiveDqlQuery setAggregateScope(ObservableValue property, Converter toAggregateScopeConverter) { - FXProperties.runNowAndOnPropertiesChange(() -> aggregateScope = toAggregateScopeConverter.convert(property.getValue()) , property); + FXProperties.runNowAndOnPropertyChange(value -> aggregateScope = toAggregateScopeConverter.convert(value), property); return this; } diff --git a/webfx-stack-orm-reactive-dql/src/main/java/dev/webfx/stack/orm/reactive/dql/statement/ReactiveDqlStatement.java b/webfx-stack-orm-reactive-dql/src/main/java/dev/webfx/stack/orm/reactive/dql/statement/ReactiveDqlStatement.java index 1f14d89fd..3b21339bc 100644 --- a/webfx-stack-orm-reactive-dql/src/main/java/dev/webfx/stack/orm/reactive/dql/statement/ReactiveDqlStatement.java +++ b/webfx-stack-orm-reactive-dql/src/main/java/dev/webfx/stack/orm/reactive/dql/statement/ReactiveDqlStatement.java @@ -129,7 +129,7 @@ private DqlStatement mergeDqlStatements() { ================================================================================================================*/ public ReactiveDqlStatement always(ObservableValue dqlStatementProperty) { - FXProperties.runOnPropertiesChange(this::markDqlStatementsAsChanged, dqlStatementProperty); + FXProperties.runOnPropertyChange(this::markDqlStatementsAsChanged, dqlStatementProperty); return addWithoutListening(dqlStatementProperty); } diff --git a/webfx-stack-orm-reactive-entities/src/main/java/dev/webfx/stack/orm/reactive/entities/dql_to_entities/ReactiveEntitiesMapper.java b/webfx-stack-orm-reactive-entities/src/main/java/dev/webfx/stack/orm/reactive/entities/dql_to_entities/ReactiveEntitiesMapper.java index 940bd4ce7..12cc3d425 100644 --- a/webfx-stack-orm-reactive-entities/src/main/java/dev/webfx/stack/orm/reactive/entities/dql_to_entities/ReactiveEntitiesMapper.java +++ b/webfx-stack-orm-reactive-entities/src/main/java/dev/webfx/stack/orm/reactive/entities/dql_to_entities/ReactiveEntitiesMapper.java @@ -1,17 +1,16 @@ package dev.webfx.stack.orm.reactive.entities.dql_to_entities; import dev.webfx.extras.util.OptimizedObservableListWrapper; +import dev.webfx.kit.util.properties.FXProperties; import dev.webfx.kit.util.properties.ObservableLists; -import dev.webfx.stack.orm.reactive.dql.query.ReactiveDqlQuery; -import dev.webfx.stack.orm.reactive.dql.querypush.ReactiveDqlQueryPush; +import dev.webfx.stack.db.query.QueryResult; import dev.webfx.stack.orm.dql.sqlcompiler.sql.SqlCompiled; import dev.webfx.stack.orm.entity.*; import dev.webfx.stack.orm.entity.query_result_to_entities.QueryResultToEntitiesMapper; -import dev.webfx.kit.util.properties.FXProperties; -import dev.webfx.stack.db.query.QueryResult; +import dev.webfx.stack.orm.reactive.dql.query.ReactiveDqlQuery; +import dev.webfx.stack.orm.reactive.dql.querypush.ReactiveDqlQueryPush; import javafx.application.Platform; import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleObjectProperty; import javafx.collections.ObservableList; import java.util.ArrayList; @@ -24,12 +23,7 @@ public final class ReactiveEntitiesMapper implements HasEntityStore, ReactiveEntitiesMapperAPI> { private final ReactiveDqlQuery reactiveDqlQuery; - private final ObjectProperty> entitiesProperty = new SimpleObjectProperty/*GWT*/>() { - @Override - protected void invalidated() { - scheduleOnEntitiesChanged(); - } - }; + private final ObjectProperty> entitiesProperty = FXProperties.newObjectProperty(this::scheduleOnEntitiesChanged); private ObservableList observableEntities; private List restrictedFilterList; private EntityStore store; @@ -41,7 +35,7 @@ protected void invalidated() { public ReactiveEntitiesMapper(ReactiveDqlQuery reactiveDqlQuery) { this.reactiveDqlQuery = reactiveDqlQuery; - FXProperties.runOnPropertiesChange(p -> onNewQueryResult((QueryResult) p.getValue()), reactiveDqlQuery.resultProperty()); + FXProperties.runOnPropertyChange(this::onNewQueryResult, reactiveDqlQuery.resultProperty()); } @Override diff --git a/webfx-stack-orm-reactive-entities/src/main/java/dev/webfx/stack/orm/reactive/entities/entities_to_grid/ReactiveGridMapper.java b/webfx-stack-orm-reactive-entities/src/main/java/dev/webfx/stack/orm/reactive/entities/entities_to_grid/ReactiveGridMapper.java index 6fb0dae88..4d378d658 100644 --- a/webfx-stack-orm-reactive-entities/src/main/java/dev/webfx/stack/orm/reactive/entities/entities_to_grid/ReactiveGridMapper.java +++ b/webfx-stack-orm-reactive-entities/src/main/java/dev/webfx/stack/orm/reactive/entities/entities_to_grid/ReactiveGridMapper.java @@ -53,7 +53,7 @@ public ReactiveGridMapper(ReactiveEntitiesMapper reactiveEntitiesMapper) { return dqlStatement; }); // Reacting to the change of i18n dictionary TODO: make this configurable without forcing i18n dependency - FXProperties.runOnPropertiesChange(() -> onEntityListChanged(getCurrentEntities()), I18n.dictionaryProperty()); + FXProperties.runOnPropertyChange(() -> onEntityListChanged(getCurrentEntities()), I18n.dictionaryProperty()); //reactiveDqlStatement.combine(persistentFieldsDqlStatementProperty); } diff --git a/webfx-stack-orm-reactive-visual/src/main/java/dev/webfx/stack/orm/reactive/mapping/entities_to_visual/ReactiveVisualMapper.java b/webfx-stack-orm-reactive-visual/src/main/java/dev/webfx/stack/orm/reactive/mapping/entities_to_visual/ReactiveVisualMapper.java index 6c0389be8..fdd153a39 100644 --- a/webfx-stack-orm-reactive-visual/src/main/java/dev/webfx/stack/orm/reactive/mapping/entities_to_visual/ReactiveVisualMapper.java +++ b/webfx-stack-orm-reactive-visual/src/main/java/dev/webfx/stack/orm/reactive/mapping/entities_to_visual/ReactiveVisualMapper.java @@ -17,7 +17,6 @@ import javafx.beans.InvalidationListener; import javafx.beans.property.ObjectProperty; import javafx.beans.property.Property; -import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -58,57 +57,50 @@ public final class ReactiveVisualMapper extends ReactiveGridMa // set selectedEntities (and consequently the visual selection), and if the application code changes selectedEntities // (or if the user changes the visual selection -> which changes selectedEntities), this will also set // selectedEntityProperty. - private final ObjectProperty selectedEntityProperty = new SimpleObjectProperty<>() { - @Override - protected void invalidated() { - // System.out.println("selectedEntity = " + get()); - // Preventing reentrant calls from internal operations - if (syncingFromSelectedEntity) - return; - - syncingFromSelectedEntity = true; - E selectedEntity = getSelectedEntity(); // Getting the selected entity asked by the application code - // Managing the sync selectedEntityProperty -> selectedEntities - if (!syncingFromSelectedEntities) { // already done if syncing from selectedEntities - // As this change is coming from the application code - which preferred to work with selectedEntity rather - // than selectedEntities - we need to update selectedEntities to this single selection. - if (selectedEntity == null) // If it's null, we clear selectedEntities - selectedEntities.clear(); - else // If it's not null, we apply it as a single element to selectedEntities - selectedEntities.setAll(selectedEntity); - // Note: this code above has triggered the selectedEntities listener that did the sync in the opposite - // direction (selectedEntities -> selectedEntityProperty) but the reentrant call will be prevented. - // However, it's possible that the value of selectedEntityProperty has changed if the entity asked by the - // application was not valid (i.e. not present in the loaded entities). So we refresh the value. - selectedEntity = getSelectedEntity(); - } + private final ObjectProperty selectedEntityProperty = FXProperties.newObjectProperty(selectedEntity -> { + // System.out.println("selectedEntity = " + selectedEntity); + // Preventing reentrant calls from internal operations + if (syncingFromSelectedEntity) + return; - // If the change doesn't come from requestedSelectedEntity but is a subsequent change from the application - // code or from the user selection, we do the back sync selectedEntity -> requestedSelectedEntity - if (!syncingFromRequestedSelectedEntity) - requestSelectedEntity(selectedEntity); // reentrant call will be prevented by syncingFromSelectedEntity + syncingFromSelectedEntity = true; + // Managing the sync selectedEntityProperty -> selectedEntities + if (!syncingFromSelectedEntities) { // already done if syncing from selectedEntities + // As this change is coming from the application code - which preferred to work with selectedEntity rather + // than selectedEntities - we need to update selectedEntities to this single selection. + if (selectedEntity == null) // If it's null, we clear selectedEntities + selectedEntities.clear(); + else // If it's not null, we apply it as a single element to selectedEntities + selectedEntities.setAll(selectedEntity); + // Note: this code above has triggered the selectedEntities listener that did the sync in the opposite + // direction (selectedEntities -> selectedEntityProperty) but the reentrant call will be prevented. + // However, it's possible that the value of selectedEntityProperty has changed if the entity asked by the + // application was not valid (i.e. not present in the loaded entities). So we refresh the value. + selectedEntity = getSelectedEntity(); + } - // Finally, we call the selectedEntityHandler (if set) - if (selectedEntityHandler != null) { - //System.out.println("Calling selectedEntityHandler"); - selectedEntityHandler.accept(selectedEntity); - } + // If the change doesn't come from requestedSelectedEntity but is a subsequent change from the application + // code or from the user selection, we do the back sync selectedEntity -> requestedSelectedEntity + if (!syncingFromRequestedSelectedEntity) + requestSelectedEntity(selectedEntity); // reentrant call will be prevented by syncingFromSelectedEntity - syncingFromSelectedEntity = false; - } - }; - - private final ObjectProperty requestedSelectedEntityProperty = new SimpleObjectProperty<>() { - @Override - protected void invalidated() { - //System.out.println("requestedSelectedEntity = " + get()); - // Preventing reentrant calls from internal operations - if (syncingFromSelectedEntity) - return; - // Syncing selected entity from the requested selected entity (if appropriate at this time) - syncFromRequestedSelectedEntity(); + // Finally, we call the selectedEntityHandler (if set) + if (selectedEntityHandler != null) { + //System.out.println("Calling selectedEntityHandler"); + selectedEntityHandler.accept(selectedEntity); } - }; + + syncingFromSelectedEntity = false; + }); + + private final ObjectProperty requestedSelectedEntityProperty = FXProperties.newObjectProperty(() -> { + //System.out.println("requestedSelectedEntity = " + get()); + // Preventing reentrant calls from internal operations + if (syncingFromSelectedEntity) + return; + // Syncing selected entity from the requested selected entity (if appropriate at this time) + syncFromRequestedSelectedEntity(); + }); private void syncFromRequestedSelectedEntity() { // We don't do anything if the reactive visual mapper is not active (which happens when its associated activity @@ -131,7 +123,7 @@ private void syncFromRequestedSelectedEntity() { if (entityIndex < 0) requestedSelectedEntity = null; else // if yes, we get that loaded entity (which can be a different instance if requestedSelectedEntity - // come from another entity store. + // come from another entity store. requestedSelectedEntity = getEntityAt(entityIndex); } // We apply the new selection @@ -141,33 +133,24 @@ private void syncFromRequestedSelectedEntity() { } } - private final ObjectProperty visualResultProperty = new SimpleObjectProperty() { - @Override - protected void invalidated() { - // When the whole visual result has changed, we need to update the selection. We try to keep the selection - // unchanged, which happens when the selected entities are still in that new result (for example when this - // comes from a server push where just some fields have changed but the entity list remains the same). - // Otherwise, we reduce the selected entities to those that are still present in the new result. - if (autoSelectSingleRow && get() != null && get().getRowCount() == 1) { - setVisualSelection(VisualSelection.createSingleRowSelection(0)); - clearAutoSelectSingleRowOnNextResultSet = true; - } else if (clearAutoSelectSingleRowOnNextResultSet) { - setVisualSelection(null); - } else if (selectedEntities.isEmpty() && getRequestedSelectedEntity() != null) { - syncFromRequestedSelectedEntity(); - } else { - syncFromSelectedEntities(); - } + private final ObjectProperty visualResultProperty = FXProperties.newObjectProperty(visualResult -> { + // When the whole visual result has changed, we need to update the selection. We try to keep the selection + // unchanged, which happens when the selected entities are still in that new result (for example when this + // comes from a server push where just some fields have changed but the entity list remains the same). + // Otherwise, we reduce the selected entities to those that are still present in the new result. + if (autoSelectSingleRow && visualResult != null &&visualResult.getRowCount() == 1) { + setVisualSelection(VisualSelection.createSingleRowSelection(0)); + clearAutoSelectSingleRowOnNextResultSet = true; + } else if (clearAutoSelectSingleRowOnNextResultSet) { + setVisualSelection(null); + } else if (selectedEntities.isEmpty() && getRequestedSelectedEntity() != null) { + syncFromRequestedSelectedEntity(); + } else { + syncFromSelectedEntities(); } - }; + }); - private final ObjectProperty visualSelectionProperty = new SimpleObjectProperty<>() { - @Override - protected void invalidated() { - //System.out.println("visualSelection = " + (get() == null ? "null" : get().getSelectedRow())); - syncFromVisualSelection(); - } - }; + private final ObjectProperty visualSelectionProperty = FXProperties.newObjectProperty(this::syncFromVisualSelection); private void syncFromVisualSelection() { // Preventing reentrant calls from internal operations @@ -191,9 +174,9 @@ public ReactiveVisualMapper(ReactiveEntitiesMapper reactiveEntitiesMapper) { super(reactiveEntitiesMapper); // Calling syncFromSelectedEntities() on selected entities changes selectedEntities.addListener((InvalidationListener) observable -> - syncFromSelectedEntities()); + syncFromSelectedEntities()); // Calling syncFromRequestedSelectedEntity() on active changes (to possibly show the confirm dialog on activity resume) - FXProperties.runOnPropertiesChange(this::syncFromRequestedSelectedEntity, activeProperty()); + FXProperties.runOnPropertyChange(this::syncFromRequestedSelectedEntity, activeProperty()); } // Exposing selectedEntities and selectedEntityProperty in public methods @@ -460,7 +443,7 @@ public static ReactiveVisualMapper createPushReactiveChain } // Master - + public static ReactiveVisualMapper createMasterReactiveChain(Object pm) { return createMaster(pm, ReactiveEntitiesMapper.createMasterReactiveChain(pm)); } @@ -478,7 +461,7 @@ public static ReactiveVisualMapper createMasterPushReactiv } // Group - + public static ReactiveVisualMapper createGroupReactiveChain(Object pm) { return createGroup(pm, ReactiveEntitiesMapper.createGroupReactiveChain(pm)); } diff --git a/webfx-stack-routing-router-client/src/main/java/dev/webfx/stack/routing/router/auth/RedirectAuthHandler.java b/webfx-stack-routing-router-client/src/main/java/dev/webfx/stack/routing/router/auth/RedirectAuthHandler.java index cb235fcff..f352fd717 100644 --- a/webfx-stack-routing-router-client/src/main/java/dev/webfx/stack/routing/router/auth/RedirectAuthHandler.java +++ b/webfx-stack-routing-router-client/src/main/java/dev/webfx/stack/routing/router/auth/RedirectAuthHandler.java @@ -9,7 +9,7 @@ */ public interface RedirectAuthHandler extends Handler { - static RedirectAuthHandler create(String loginPath, String unauthorizedPath) { - return new RedirectAuthHandlerImpl(loginPath, unauthorizedPath); + static RedirectAuthHandler create(String loginPath, String unauthorizedPath, Runnable onUnnecessaryLoginHandler) { + return new RedirectAuthHandlerImpl(loginPath, unauthorizedPath, onUnnecessaryLoginHandler); } } diff --git a/webfx-stack-routing-router-client/src/main/java/dev/webfx/stack/routing/router/auth/impl/RedirectAuthHandlerImpl.java b/webfx-stack-routing-router-client/src/main/java/dev/webfx/stack/routing/router/auth/impl/RedirectAuthHandlerImpl.java index 9dfcf8501..ba17ceb3c 100644 --- a/webfx-stack-routing-router-client/src/main/java/dev/webfx/stack/routing/router/auth/impl/RedirectAuthHandlerImpl.java +++ b/webfx-stack-routing-router-client/src/main/java/dev/webfx/stack/routing/router/auth/impl/RedirectAuthHandlerImpl.java @@ -5,6 +5,7 @@ import dev.webfx.stack.routing.router.auth.RedirectAuthHandler; import dev.webfx.stack.routing.router.auth.authz.RouteRequest; import dev.webfx.stack.routing.router.spi.impl.client.ClientRoutingContextBase; +import dev.webfx.stack.session.state.client.fx.FXLoggedIn; import dev.webfx.stack.session.state.client.fx.FXLoggedOut; /** @@ -14,23 +15,31 @@ public final class RedirectAuthHandlerImpl implements RedirectAuthHandler { private final String loginPath; private final String unauthorizedPath; + private final Runnable onUnnecessaryLoginHandler; - public RedirectAuthHandlerImpl(String loginPath, String unauthorizedPath) { + public RedirectAuthHandlerImpl(String loginPath, String unauthorizedPath, Runnable onUnnecessaryLoginHandler) { this.loginPath = loginPath; this.unauthorizedPath = unauthorizedPath; + this.onUnnecessaryLoginHandler = onUnnecessaryLoginHandler; } @Override public void handle(RoutingContext context) { String requestedPath = context.path(); - if (requestedPath.equals(loginPath) || requestedPath.equals(unauthorizedPath)) + boolean isLoginPath = requestedPath.equals(loginPath); + boolean isUnauthorizedPath = requestedPath.equals(unauthorizedPath); + boolean isRedirected = context instanceof ClientRoutingContextBase && ((ClientRoutingContextBase) context).isRedirected(); + if (isLoginPath && !isRedirected && onUnnecessaryLoginHandler != null && FXLoggedIn.isLoggedIn()) { // Should it be !LoggedOut.isLoggedOut()? + onUnnecessaryLoginHandler.run(); + } else if (isLoginPath || isUnauthorizedPath) { context.next(); // Always accepting login and unauthorized paths - else // Otherwise continuing the route only if the user is authorized, otherwise redirecting to auth page (login or unauthorized) + } else { // Otherwise continuing the route only if the user is authorized, otherwise redirecting to auth page (login or unauthorized) new AuthorizationClientRequest<>() - .setOperationRequest(new RouteRequest(requestedPath)) - .onAuthorizedExecute(context::next) - .onUnauthorizedExecute(() -> redirectToAuth(context)) - .executeAsync(); + .setOperationRequest(new RouteRequest(requestedPath)) + .onAuthorizedExecute(context::next) + .onUnauthorizedExecute(() -> redirectToAuth(context)) + .executeAsync(); + } } private void redirectToAuth(RoutingContext context) { diff --git a/webfx-stack-routing-router-client/src/main/java/dev/webfx/stack/routing/router/spi/impl/client/ClientRoutingContextBase.java b/webfx-stack-routing-router-client/src/main/java/dev/webfx/stack/routing/router/spi/impl/client/ClientRoutingContextBase.java index 4d5877aa9..168ea6b37 100644 --- a/webfx-stack-routing-router-client/src/main/java/dev/webfx/stack/routing/router/spi/impl/client/ClientRoutingContextBase.java +++ b/webfx-stack-routing-router-client/src/main/java/dev/webfx/stack/routing/router/spi/impl/client/ClientRoutingContextBase.java @@ -17,16 +17,22 @@ public abstract class ClientRoutingContextBase implements RoutingContext { protected final String mountPoint; protected final String path; protected final Collection routes; + private final boolean redirected; protected Iterator iter; protected Route currentRoute; private AstObject params; - ClientRoutingContextBase(String mountPoint, String path, Collection routes, Object state) { + ClientRoutingContextBase(String mountPoint, String path, Collection routes, Object state, boolean redirected) { this.mountPoint = mountPoint; this.path = path; this.routes = routes; this.params = (AstObject) state; // Is merging state and params the right thing to do? iter = routes.iterator(); + this.redirected = redirected; + } + + public boolean isRedirected() { + return redirected; } @Override @@ -112,12 +118,12 @@ public AstObject getParams() { public static RoutingContext newRedirectedContext(RoutingContext context, String redirectPath) { if (context instanceof ClientRoutingContextImpl) { ClientRoutingContextImpl ctx = (ClientRoutingContextImpl) context; - return new ClientRoutingContextImpl(ctx.mountPoint(), ctx.router(), redirectPath, ctx.routes, ctx.getParams()); + return new ClientRoutingContextImpl(ctx.mountPoint(), ctx.router(), redirectPath, ctx.routes, ctx.getParams(), true); } if (context instanceof SubClientRoutingContext) { SubClientRoutingContext ctx = (SubClientRoutingContext) context; return new SubClientRoutingContext(null, redirectPath, ctx.routes, // trying first to find the login page in the sub-router - new SubClientRoutingContext(ctx.mountPoint(), redirectPath, ctx.routes, ctx.inner) // otherwise in the parent router + new SubClientRoutingContext(ctx.mountPoint(), redirectPath, ctx.routes, ctx.inner, true), true // otherwise in the parent router ); } return null; // Shouldn't happen diff --git a/webfx-stack-routing-router-client/src/main/java/dev/webfx/stack/routing/router/spi/impl/client/ClientRoutingContextImpl.java b/webfx-stack-routing-router-client/src/main/java/dev/webfx/stack/routing/router/spi/impl/client/ClientRoutingContextImpl.java index 22d7c5107..47b3c6294 100644 --- a/webfx-stack-routing-router-client/src/main/java/dev/webfx/stack/routing/router/spi/impl/client/ClientRoutingContextImpl.java +++ b/webfx-stack-routing-router-client/src/main/java/dev/webfx/stack/routing/router/spi/impl/client/ClientRoutingContextImpl.java @@ -15,7 +15,11 @@ final class ClientRoutingContextImpl extends ClientRoutingContextBase { private int statusCode = -1; ClientRoutingContextImpl(String mountPoint, ClientRouter router, String path, Collection routes, Object state) { - super(mountPoint, path, routes, state); + this(mountPoint, router, path, routes, state, false); + } + + ClientRoutingContextImpl(String mountPoint, ClientRouter router, String path, Collection routes, Object state, boolean redirected) { + super(mountPoint, path, routes, state, redirected); this.router = router; } diff --git a/webfx-stack-routing-router-client/src/main/java/dev/webfx/stack/routing/router/spi/impl/client/SubClientRoutingContext.java b/webfx-stack-routing-router-client/src/main/java/dev/webfx/stack/routing/router/spi/impl/client/SubClientRoutingContext.java index b95f7d1cb..062226e5d 100644 --- a/webfx-stack-routing-router-client/src/main/java/dev/webfx/stack/routing/router/spi/impl/client/SubClientRoutingContext.java +++ b/webfx-stack-routing-router-client/src/main/java/dev/webfx/stack/routing/router/spi/impl/client/SubClientRoutingContext.java @@ -15,7 +15,11 @@ final class SubClientRoutingContext extends ClientRoutingContextBase { private final String mountPoint; SubClientRoutingContext(String mountPoint, String path, Collection routes, RoutingContext inner) { - super(mountPoint, path, routes, null); + this(mountPoint, path, routes, inner, false); + } + + SubClientRoutingContext(String mountPoint, String path, Collection routes, RoutingContext inner, boolean redirected) { + super(mountPoint, path, routes, null, redirected); this.inner = inner; if (mountPoint == null) this.mountPoint = null; diff --git a/webfx-stack-routing-uirouter/pom.xml b/webfx-stack-routing-uirouter/pom.xml index 7e69d82df..cf8ce165d 100644 --- a/webfx-stack-routing-uirouter/pom.xml +++ b/webfx-stack-routing-uirouter/pom.xml @@ -91,6 +91,12 @@ 0.1.0-SNAPSHOT + + dev.webfx + webfx-stack-i18n + 0.1.0-SNAPSHOT + + dev.webfx webfx-stack-routing-activity diff --git a/webfx-stack-routing-uirouter/src/main/java/dev/webfx/stack/routing/uirouter/HistoryRouter.java b/webfx-stack-routing-uirouter/src/main/java/dev/webfx/stack/routing/uirouter/HistoryRouter.java index 624c7a804..1173998c1 100644 --- a/webfx-stack-routing-uirouter/src/main/java/dev/webfx/stack/routing/uirouter/HistoryRouter.java +++ b/webfx-stack-routing-uirouter/src/main/java/dev/webfx/stack/routing/uirouter/HistoryRouter.java @@ -53,7 +53,7 @@ public void setDefaultInitialHistoryPath(String defaultInitialHistoryPath) { this.defaultInitialHistoryPath = defaultInitialHistoryPath; } - private void replaceCurrentHistoryWithInitialDefaultPath() { + protected void replaceCurrentHistoryWithInitialDefaultPath() { if (defaultInitialHistoryPath != null) history.replace(defaultInitialHistoryPath); } @@ -68,16 +68,25 @@ public void refresh() { } protected void onNewHistoryLocation(BrowsingHistoryLocation browsingHistoryLocation) { - String path = null; - Object state = null; + String path; + Object state; // On first call, browsingHistoryLocation might be null when not running in the browser - if (browsingHistoryLocation != null) { + if (browsingHistoryLocation == null) { // in-memory history not yet initialised + path = defaultInitialHistoryPath; + state = null; + history.push(path); // initialising in-memory history to the default initial path + } else { // general case (browser history or in-memory history but initialised) path = history.getPath(browsingHistoryLocation); state = browsingHistoryLocation.getState(); } // Also on first call, the path might be empty in the browser if the location is just the domain name - if (path == null || path.isEmpty()) // In both cases, we route to the initial default path + if (path == null || path.isEmpty()) { // In that case, we route to the initial default path path = defaultInitialHistoryPath; + } + // Submitting the new path & state to the router, and this even if the path & state didn't change, as the router + // may behave differently in dependence on other factors (ex: it may show a login window on first attempt, then + // the actual requested page on second attend (if logged in and authorized) or the unauthorized page (if logged + // in but not authorized). router.accept(path, state); } diff --git a/webfx-stack-routing-uirouter/src/main/java/dev/webfx/stack/routing/uirouter/UiRouter.java b/webfx-stack-routing-uirouter/src/main/java/dev/webfx/stack/routing/uirouter/UiRouter.java index 0f800c6e9..c2338f0d7 100644 --- a/webfx-stack-routing-uirouter/src/main/java/dev/webfx/stack/routing/uirouter/UiRouter.java +++ b/webfx-stack-routing-uirouter/src/main/java/dev/webfx/stack/routing/uirouter/UiRouter.java @@ -27,6 +27,8 @@ import dev.webfx.stack.routing.uirouter.activity.uiroute.impl.UiRouteActivityContextBase; import dev.webfx.stack.routing.uirouter.activity.view.HasMountNodeProperty; import dev.webfx.stack.routing.uirouter.activity.view.HasNodeProperty; +import javafx.beans.property.Property; +import javafx.scene.Node; import java.util.Collection; import java.util.HashMap; @@ -88,6 +90,13 @@ private UiRouter(Router router, BrowsingHistory browsingHistory, UiRouteActivity hostingUiRouterActivityContext.setUiRouter(this); } + protected void replaceCurrentHistoryWithInitialDefaultPath() { + if (mountParentRouter == null) + super.replaceCurrentHistoryWithInitialDefaultPath(); + else + mountParentRouter.replaceCurrentHistoryWithInitialDefaultPath(); + } + @Override public void refresh() { if (mountParentRouter == null) @@ -97,7 +106,7 @@ public void refresh() { } public UiRouter setRedirectAuthHandler(String loginPath, String unauthorizedPath) { - return setRedirectAuthHandler(RedirectAuthHandler.create(loginPath, unauthorizedPath)); + return setRedirectAuthHandler(RedirectAuthHandler.create(loginPath, unauthorizedPath, this::replaceCurrentHistoryWithInitialDefaultPath)); } public UiRouter setRedirectAuthHandler(RedirectAuthHandler redirectAuthHandler) { @@ -249,33 +258,38 @@ public void handle(RoutingContext routingContext) { activityManager = activityManagerFactory.create(); // So we create the activity manager (and its associated activity) activityManager.create(activityContext); // and we transit the activity into the created state and pass the context } - // Now that the new requested activity is displayed, we pause the previous activity - if (previousActivityManager != null) // if there was a previous activity + // Before displaying the activity, we pause the previous one (if there is one) + if (previousActivityManager != null) previousActivityManager.pause(); - // Now we transit the current activity (which was either paused or newly created) into the resume state and - // once done we display the activity node by binding it with the hosting context (done in the UI tread) - activityManager.resume().onComplete(event -> { - if (hostingContext instanceof HasNodeProperty && activityContext instanceof HasNodeProperty) - UiScheduler.runInUiThread(() -> - ((HasNodeProperty) hostingContext).nodeProperty().bind(((HasNodeProperty) activityContext).nodeProperty()) - ); - }); + // Now we transit the current activity (which was either paused or newly created) into the resume state + activityManager.resume() + .onComplete(event -> { // Then we display the activity node by binding it with the hosting context + if (hostingContext instanceof HasNodeProperty && activityContext instanceof HasNodeProperty) { + UiScheduler.runInUiThread(() -> { // done in the UI tread (JavaFX thread in OpenJFX) + Property hostingNodeProperty = ((HasNodeProperty) hostingContext).nodeProperty(); + Property activityNodeProperty = ((HasNodeProperty) activityContext).nodeProperty(); + hostingNodeProperty.bind(activityNodeProperty); + }); + } + }); /*--- Sub routing management ---*/ // When the activity is a mount child activity coming from sub routing, we make sure the mount parent activity is displayed if (mountParentRouter != null) { // Indicates it is a child sub router mountParentRouter.mountChildSubRouter = UiRouter.this; // Setting the parent router child pointer // Calling the parent router on the mount point will cause the parent activity to be displayed (if not already done) - if (mountParentRouter.mountParentRouter == null) // only on root router (to prevent route "/" not found exception in BookEventActivity router - not sure it's good code) + if (mountParentRouter.mountParentRouter == null) // only on root router (to prevent route "/" not found exception in BookEventActivity router - not sure if it's good code) mountParentRouter.router.accept(Strings.toSafeString(routingContext.mountPoint()) + "/", routingContext.getParams()); } // When the activity is a mount parent activity, we make the trick so the child activity is displayed within the parent activity if (mountChildSubRouter != null) { // Indicates it is a mount parent activity // The trick is to bind the mount node of the parent activity to the child activity node if (activityContext instanceof HasMountNodeProperty && mountChildSubRouter.hostingContext instanceof HasNodeProperty) { - UiScheduler.runInUiThread(() -> - // This should display the child activity because a mount parent activity is supposed to bind its context mount node to the UI - ((HasMountNodeProperty) activityContext).mountNodeProperty().bind(((HasNodeProperty) mountChildSubRouter.hostingContext).nodeProperty()) // Using the hosting context node which is bound to the child activity node - ); + UiScheduler.runInUiThread(() -> { + // This should display the child activity because a mount parent activity is supposed to bind its context mount node to the UI + Property parentMountNodeProperty = ((HasMountNodeProperty) activityContext).mountNodeProperty(); + Property childHostingNodeProperty = ((HasNodeProperty) mountChildSubRouter.hostingContext).nodeProperty(); + parentMountNodeProperty.bind(childHostingNodeProperty); + }); } } } diff --git a/webfx-stack-routing-uirouter/src/main/java/dev/webfx/stack/routing/uirouter/operations/RouteBackwardRequest.java b/webfx-stack-routing-uirouter/src/main/java/dev/webfx/stack/routing/uirouter/operations/RouteBackwardRequest.java index 9a3e0775b..3609b6d9e 100644 --- a/webfx-stack-routing-uirouter/src/main/java/dev/webfx/stack/routing/uirouter/operations/RouteBackwardRequest.java +++ b/webfx-stack-routing-uirouter/src/main/java/dev/webfx/stack/routing/uirouter/operations/RouteBackwardRequest.java @@ -1,15 +1,16 @@ package dev.webfx.stack.routing.uirouter.operations; -import dev.webfx.stack.ui.operation.HasOperationCode; -import dev.webfx.platform.windowhistory.spi.BrowsingHistory; import dev.webfx.platform.async.AsyncFunction; +import dev.webfx.platform.windowhistory.spi.BrowsingHistory; +import dev.webfx.stack.i18n.HasI18nKey; +import dev.webfx.stack.ui.operation.HasOperationCode; /** * @author Bruno Salmon */ public final class RouteBackwardRequest extends RouteRequestBase - implements HasOperationCode { + implements HasOperationCode, HasI18nKey { private static final String OPERATION_CODE = "RouteBackward"; @@ -22,6 +23,11 @@ public Object getOperationCode() { return OPERATION_CODE; } + @Override + public Object getI18nKey() { + return RouteI18nKeys.RouteBackward; + } + @Override public AsyncFunction getOperationExecutor() { return RouteBackwardExecutor::executeRequest; diff --git a/webfx-stack-routing-uirouter/src/main/java/dev/webfx/stack/routing/uirouter/operations/RouteForwardRequest.java b/webfx-stack-routing-uirouter/src/main/java/dev/webfx/stack/routing/uirouter/operations/RouteForwardRequest.java index 4b64a1e8f..418d6624b 100644 --- a/webfx-stack-routing-uirouter/src/main/java/dev/webfx/stack/routing/uirouter/operations/RouteForwardRequest.java +++ b/webfx-stack-routing-uirouter/src/main/java/dev/webfx/stack/routing/uirouter/operations/RouteForwardRequest.java @@ -1,5 +1,6 @@ package dev.webfx.stack.routing.uirouter.operations; +import dev.webfx.stack.i18n.HasI18nKey; import dev.webfx.stack.ui.operation.HasOperationCode; import dev.webfx.platform.windowhistory.spi.BrowsingHistory; import dev.webfx.platform.async.AsyncFunction; @@ -9,7 +10,7 @@ */ public final class RouteForwardRequest extends RouteRequestBase - implements HasOperationCode { + implements HasOperationCode, HasI18nKey { private static final String OPERATION_CODE = "RouteForward"; @@ -22,6 +23,11 @@ public Object getOperationCode() { return OPERATION_CODE; } + @Override + public Object getI18nKey() { + return RouteI18nKeys.RouteForward; + } + @Override public AsyncFunction getOperationExecutor() { return RouteForwardExecutor::executeRequest; diff --git a/webfx-stack-routing-uirouter/src/main/java/dev/webfx/stack/routing/uirouter/operations/RouteI18nKeys.java b/webfx-stack-routing-uirouter/src/main/java/dev/webfx/stack/routing/uirouter/operations/RouteI18nKeys.java new file mode 100644 index 000000000..dfa49462f --- /dev/null +++ b/webfx-stack-routing-uirouter/src/main/java/dev/webfx/stack/routing/uirouter/operations/RouteI18nKeys.java @@ -0,0 +1,9 @@ +// File managed by WebFX (DO NOT EDIT MANUALLY) +package dev.webfx.stack.routing.uirouter.operations; + +public interface RouteI18nKeys { + + String RouteBackward = "RouteBackward"; + String RouteForward = "RouteForward"; + +} \ No newline at end of file diff --git a/webfx-stack-routing-uirouter/src/main/java/module-info.java b/webfx-stack-routing-uirouter/src/main/java/module-info.java index a8f9f6713..368bf7304 100644 --- a/webfx-stack-routing-uirouter/src/main/java/module-info.java +++ b/webfx-stack-routing-uirouter/src/main/java/module-info.java @@ -13,6 +13,7 @@ requires webfx.platform.uischeduler; requires transitive webfx.platform.util; requires transitive webfx.platform.windowhistory; + requires webfx.stack.i18n; requires transitive webfx.stack.routing.activity; requires webfx.stack.routing.router; requires transitive webfx.stack.routing.router.client; diff --git a/webfx-stack-routing-uirouter/src/main/webfx/i18n/webfx-stack-uirouter@en.yaml b/webfx-stack-routing-uirouter/src/main/webfx/i18n/webfx-stack-uirouter@en.yaml new file mode 100644 index 000000000..d7e3494bc --- /dev/null +++ b/webfx-stack-routing-uirouter/src/main/webfx/i18n/webfx-stack-uirouter@en.yaml @@ -0,0 +1,15 @@ +RouteBackward: + text: "" + graphic: + stroke: "gray" + strokeWidth: 2 + strokeLineCap: "round" + svgPath: "M 0 8 L 16 8 M 0 8 L 6.4 1.6 M 0 8 L 6.4 14.4" + +RouteForward: + text: "" + graphic: + stroke: "gray" + strokeWidth: 2 + strokeLineCap: "round" + svgPath: "M 16 8 L 0 8 M 16 8 L 9.6 1.6 M 16 8 L 9.6 14.4" diff --git a/webfx-stack-routing-uirouter/webfx.xml b/webfx-stack-routing-uirouter/webfx.xml index c542c818f..92105c289 100644 --- a/webfx-stack-routing-uirouter/webfx.xml +++ b/webfx-stack-routing-uirouter/webfx.xml @@ -14,4 +14,6 @@ + + \ No newline at end of file diff --git a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXAuthorizationsChanged.java b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXAuthorizationsChanged.java index 5da29247e..2dd1168f8 100644 --- a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXAuthorizationsChanged.java +++ b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXAuthorizationsChanged.java @@ -4,19 +4,15 @@ import dev.webfx.platform.uischeduler.UiScheduler; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ReadOnlyBooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; /** * @author Bruno Salmon */ public final class FXAuthorizationsChanged { - private final static BooleanProperty authorizationsChangedProperty = new SimpleBooleanProperty(false) { - @Override - protected void invalidated() { - //Console.log("FXAuthorizationsChanged = " + get()); - } - }; + private final static BooleanProperty authorizationsChangedProperty = FXProperties.newBooleanProperty(changed -> { + //Console.log("FXAuthorizationsChanged = " + changed); + }); private static boolean fireScheduled; public static ReadOnlyBooleanProperty authorizationsChangedProperty() { @@ -32,7 +28,7 @@ public static boolean hasAuthorizationsChanged() { /** * "If we haven't already scheduled a call to fire the authorizationsChangedProperty, then schedule a call to fire the * authorizationsChangedProperty." - * + *

* The authorizationsChangedProperty is a JavaFX property that is used to notify the UI that the authorizations have * changed */ @@ -49,10 +45,14 @@ public static void fireAuthorizationsChanged() { } public static void runOnAuthorizationsChanged(Runnable runnable) { - FXProperties.runOnPropertiesChange(() -> { - if (hasAuthorizationsChanged()) // Only on false -> true transition + FXProperties.runOnPropertyChange(changed -> { + if (changed) // Only on false -> true transition runnable.run(); }, authorizationsChangedProperty()); } + static { // All FXClass in this package should call FXInit.init() in their static initializer + FXInit.init(); // See FXInit comments to understand why + } + } diff --git a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXAuthorizationsReceived.java b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXAuthorizationsReceived.java index 8abeb3d41..597527b4d 100644 --- a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXAuthorizationsReceived.java +++ b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXAuthorizationsReceived.java @@ -4,7 +4,6 @@ import dev.webfx.platform.console.Console; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ReadOnlyBooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; import java.util.Objects; @@ -13,12 +12,9 @@ */ public final class FXAuthorizationsReceived { - private final static BooleanProperty authorizationsReceivedProperty = new SimpleBooleanProperty(false) { - @Override - protected void invalidated() { - Console.log("FXAuthorizationsReceived = " + get()); - } - }; + private final static BooleanProperty authorizationsReceivedProperty = FXProperties.newBooleanProperty(received -> + Console.log("FXAuthorizationsReceived = " + received) + ); public static ReadOnlyBooleanProperty authorizationsReceivedProperty() { return authorizationsReceivedProperty; @@ -39,8 +35,12 @@ else if (FXAuthorizationsChanged.hasAuthorizationsChanged()) setAuthorizationsReceived(true); } - static { - FXProperties.runNowAndOnPropertiesChange(FXAuthorizationsReceived::updateAuthorizationsReceived, FXLoggedOut.loggedOutProperty()); + static { // All FXClass in this package should call FXInit.init() in their static initializer + FXInit.init(); // See FXInit comments to understand why + } + + static void init() { // Called back (only once) by FXInit in a controlled overall sequence + FXProperties.runNowAndOnPropertyChange(FXAuthorizationsReceived::updateAuthorizationsReceived, FXLoggedOut.loggedOutProperty()); FXAuthorizationsChanged.runOnAuthorizationsChanged(FXAuthorizationsReceived::updateAuthorizationsReceived); } diff --git a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXAuthorizationsWaiting.java b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXAuthorizationsWaiting.java index 47e94611a..b15acac8c 100644 --- a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXAuthorizationsWaiting.java +++ b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXAuthorizationsWaiting.java @@ -4,7 +4,6 @@ import dev.webfx.platform.console.Console; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ReadOnlyBooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; import java.util.Objects; @@ -13,12 +12,9 @@ */ public final class FXAuthorizationsWaiting { - private final static BooleanProperty authorizationsWaitingProperty = new SimpleBooleanProperty(false) { - @Override - protected void invalidated() { - Console.log("FXAuthorizationsWaiting = " + get()); - } - }; + private final static BooleanProperty authorizationsWaitingProperty = FXProperties.newBooleanProperty(waiting -> + Console.log("FXAuthorizationsWaiting = " + waiting) + ); public static ReadOnlyBooleanProperty authorizationsWaitingProperty() { return authorizationsWaitingProperty; @@ -38,18 +34,18 @@ private static void updateAuthorizationsWaiting() { public static void runOnAuthorizationsChangedOrWaiting(Runnable runnable) { FXAuthorizationsChanged.runOnAuthorizationsChanged(runnable); - FXProperties.runOnPropertiesChange(() -> { - if (isAuthorizationsWaiting() || FXLoggedOut.isLoggedOut()) + FXProperties.runOnPropertyChange(loggedOut -> { + if (isAuthorizationsWaiting() || loggedOut) runnable.run(); }, FXLoggedOut.loggedOutProperty()); } - public static void init() { - // The first call will trigger the static initializer below, and subsequent calls won't do anything + static { // All FXClass in this package should call FXInit.init() in their static initializer + FXInit.init(); // See FXInit comments to understand why } - static { - FXProperties.runNowAndOnPropertiesChange(FXAuthorizationsWaiting::updateAuthorizationsWaiting, FXLoggedOut.loggedOutProperty()); + static void init() { // Called back (only once) by FXInit in a controlled overall sequence + FXProperties.runNowAndOnPropertyChange(FXAuthorizationsWaiting::updateAuthorizationsWaiting, FXLoggedOut.loggedOutProperty()); FXAuthorizationsChanged.runOnAuthorizationsChanged(FXAuthorizationsWaiting::updateAuthorizationsWaiting); } diff --git a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXConnected.java b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXConnected.java index 2c6e72191..77b2db74e 100644 --- a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXConnected.java +++ b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXConnected.java @@ -5,21 +5,17 @@ import dev.webfx.platform.console.Console; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ReadOnlyBooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; /** * @author Bruno Salmon */ public final class FXConnected { - private final static BooleanProperty connectedProperty = new SimpleBooleanProperty(false) { - @Override - protected void invalidated() { - Console.log("FXConnected = " + get()); - FXConnectionLost.refreshValue(); - FXReconnected.refreshValue(); - } - }; + private final static BooleanProperty connectedProperty = FXProperties.newBooleanProperty(connected -> { + Console.log("FXConnected = " + connected); + FXConnectionLost.refreshValue(); + FXReconnected.refreshValue(); + }); public static ReadOnlyBooleanProperty connectedProperty() { return connectedProperty; @@ -30,19 +26,18 @@ public static boolean isConnected() { } static void setConnected(boolean connected) { - connectedProperty.set(connected); + connectedProperty.set(connected); } public static Unregisterable runOnEachConnected(Runnable runnable) { - return FXProperties.runNowAndOnPropertiesChange(() -> { - if (isConnected()) // Only on false -> true transition + return FXProperties.runNowAndOnPropertyChange(connected -> { + if (connected) // Only on false -> true transition runnable.run(); }, connectedProperty()); } - static { - FXInit.init(); - FXConnectionSequence.init(); + static { // All FXClass in this package should call FXInit.init() in their static initializer + FXInit.init(); // See FXInit comments to understand why } } diff --git a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXConnectionLost.java b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXConnectionLost.java index d05e75db3..b9764a665 100644 --- a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXConnectionLost.java +++ b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXConnectionLost.java @@ -1,21 +1,18 @@ package dev.webfx.stack.session.state.client.fx; +import dev.webfx.kit.util.properties.FXProperties; import dev.webfx.platform.console.Console; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ReadOnlyBooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; /** * @author Bruno Salmon */ public final class FXConnectionLost { - private final static BooleanProperty connectionLostProperty = new SimpleBooleanProperty(false) { - @Override - protected void invalidated() { - Console.log("FXConnectionLost = " + get()); - } - }; + private final static BooleanProperty connectionLostProperty = FXProperties.newBooleanProperty(lost -> + Console.log("FXConnectionLost = " + lost) + ); public static ReadOnlyBooleanProperty connectionLostProperty() { return connectionLostProperty; @@ -36,4 +33,8 @@ private static void setConnectionLost(boolean connectionLost) { connectionLostProperty.set(connectionLost); } + static { // All FXClass in this package should call FXInit.init() in their static initializer + FXInit.init(); // See FXInit comments to understand why + } + } diff --git a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXConnectionSequence.java b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXConnectionSequence.java index 08e86521c..7d7dd655f 100644 --- a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXConnectionSequence.java +++ b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXConnectionSequence.java @@ -1,23 +1,20 @@ package dev.webfx.stack.session.state.client.fx; +import dev.webfx.kit.util.properties.FXProperties; import dev.webfx.platform.console.Console; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ReadOnlyIntegerProperty; -import javafx.beans.property.SimpleIntegerProperty; /** * @author Bruno Salmon */ public final class FXConnectionSequence { - private final static IntegerProperty connectionSequence = new SimpleIntegerProperty(FXConnected.isConnected() ? 1 : 0) { - @Override - protected void invalidated() { - Console.log("FxConnectionSequence = " + get()); - FXConnectionLost.refreshValue(); - FXReconnected.refreshValue(); - } - }; + private final static IntegerProperty connectionSequence = FXProperties.newIntegerProperty(FXConnected.isConnected() ? 1 : 0, seq -> { + Console.log("FxConnectionSequence = " + seq); + FXConnectionLost.refreshValue(); + FXReconnected.refreshValue(); + }); public static int getConnectionSequence() { return connectionSequence.get(); @@ -31,10 +28,14 @@ private static void setConnectionSequence(int connectionSequence) { FXConnectionSequence.connectionSequence.set(connectionSequence); } - static void init() { - FXConnected.connectedProperty().addListener((observable, oldValue, connected) -> { + static { // All FXClass in this package should call FXInit.init() in their static initializer + FXInit.init(); // See FXInit comments to understand why + } + + static void init() { // Called back (only once) by FXInit in a controlled overall sequence + FXProperties.runOnPropertyChange(connected -> { if (connected) setConnectionSequence(getConnectionSequence() + 1); - }); + }, FXConnected.connectedProperty()); } } diff --git a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXInit.java b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXInit.java index d6562e579..1762700f2 100644 --- a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXInit.java +++ b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXInit.java @@ -5,11 +5,24 @@ import dev.webfx.stack.session.state.client.ClientSideStateSessionListener; /** + * It's important to call FXInit.init() first whatever FXClass the application code starts using. FXInit will install + * all necessary listeners over all FXClasses present in this package, so that the additional listeners possibly set by + * the application code are positioned after. This ensures that when they are called, all FX classes have already + * transitioned to a final, coherent & stable state. + * * @author Bruno Salmon */ final class FXInit { + static void init() { } // Only the first call will trigger the static initializer below, not subsequent calls + static { + FXLoggedOut.init(); + FXAuthorizationsWaiting.init(); + FXAuthorizationsReceived.init(); + FXLoggedIn.init(); + FXConnectionSequence.init(); + ClientSideStateSession.getInstance().setClientSideStateSessionHolder(new ClientSideStateSessionListener() { @Override @@ -39,7 +52,4 @@ public void onConnectedChanged(boolean connected) { }); } - static void init() { - } - } diff --git a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXLoggedIn.java b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXLoggedIn.java index 6bc6a7343..7e81cab1d 100644 --- a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXLoggedIn.java +++ b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXLoggedIn.java @@ -4,19 +4,15 @@ import dev.webfx.platform.console.Console; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ReadOnlyBooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; /** * @author Bruno Salmon */ public final class FXLoggedIn { - private final static BooleanProperty loggedInProperty = new SimpleBooleanProperty(false) { - @Override - protected void invalidated() { - Console.log("FXLoggedIn = " + get()); - } - }; + private final static BooleanProperty loggedInProperty = FXProperties.newBooleanProperty(loggedIn -> + Console.log("FXLoggedIn = " + loggedIn) + ); public static ReadOnlyBooleanProperty loggedInProperty() { return loggedInProperty; @@ -38,8 +34,12 @@ private static void updateLoggedIn() { setLoggedIn(FXAuthorizationsReceived.isAuthorizationsReceived()); } - static { - FXProperties.runNowAndOnPropertiesChange(FXLoggedIn::updateLoggedIn, FXAuthorizationsReceived.authorizationsReceivedProperty()); + static { // All FXClass in this package should call FXInit.init() in their static initializer + FXInit.init(); // See FXInit comments to understand why + } + + static void init() { // Called back (only once) by FXInit in a controlled overall sequence + FXProperties.runNowAndOnPropertyChange(FXLoggedIn::updateLoggedIn, FXAuthorizationsReceived.authorizationsReceivedProperty()); } } diff --git a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXLoggedOut.java b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXLoggedOut.java index efc29b279..47fb63148 100644 --- a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXLoggedOut.java +++ b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXLoggedOut.java @@ -5,19 +5,15 @@ import dev.webfx.stack.session.state.LogoutUserId; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ReadOnlyBooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; /** * @author Bruno Salmon */ public final class FXLoggedOut { - private final static BooleanProperty loggedOutProperty = new SimpleBooleanProperty() { - @Override - protected void invalidated() { - Console.log("FXLoggedOut = " + get()); - } - }; + private final static BooleanProperty loggedOutProperty = FXProperties.newBooleanProperty(loggedOut -> + Console.log("FXLoggedOut = " + loggedOut) + ); public static ReadOnlyBooleanProperty loggedOutProperty() { return loggedOutProperty; @@ -42,8 +38,12 @@ private static void updateLoggedOut() { setLoggedOut(LogoutUserId.isLogoutUserIdOrNull(userId)); } - static { - FXProperties.runNowAndOnPropertiesChange(FXLoggedOut::updateLoggedOut, FXUserId.userIdProperty()); + static { // All FXClass in this package should call FXInit.init() in their static initializer + FXInit.init(); // See FXInit comments to understand why + } + + static void init() { // Called back (only once) by FXInit in a controlled overall sequence + FXProperties.runNowAndOnPropertyChange(FXLoggedOut::updateLoggedOut, FXUserId.userIdProperty()); } } diff --git a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXReconnected.java b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXReconnected.java index 4446fad5d..af2259ec5 100644 --- a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXReconnected.java +++ b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXReconnected.java @@ -1,21 +1,18 @@ package dev.webfx.stack.session.state.client.fx; +import dev.webfx.kit.util.properties.FXProperties; import dev.webfx.platform.console.Console; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ReadOnlyBooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; /** * @author Bruno Salmon */ public final class FXReconnected { - private final static BooleanProperty reconnectedProperty = new SimpleBooleanProperty(false) { - @Override - protected void invalidated() { - Console.log("FXReconnected = " + get()); - } - }; + private final static BooleanProperty reconnectedProperty = FXProperties.newBooleanProperty(reconnected -> + Console.log("FXReconnected = " + reconnected) + ); public static ReadOnlyBooleanProperty reconnectedProperty() { return reconnectedProperty; @@ -35,4 +32,8 @@ static void refreshValue() { setReconnected(FXConnected.isConnected() && FXConnectionSequence.getConnectionSequence() >= 2); } + static { // All FXClass in this package should call FXInit.init() in their static initializer + FXInit.init(); // See FXInit comments to understand why + } + } diff --git a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXRunId.java b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXRunId.java index 43de3f216..2c040e987 100644 --- a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXRunId.java +++ b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXRunId.java @@ -5,20 +5,16 @@ import dev.webfx.stack.session.state.client.ClientSideStateSession; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.beans.property.SimpleObjectProperty; /** * @author Bruno Salmon */ public final class FXRunId { - private final static ObjectProperty runIdProperty = new SimpleObjectProperty<>() { - @Override - protected void invalidated() { - Console.log("FxRunId = " + get()); - ClientSideStateSession.getInstance().changeRunId(get().toString(), true, false); - } - }; + private final static ObjectProperty runIdProperty = FXProperties.newObjectProperty(runId -> { + Console.log("FxRunId = " +runId); + ClientSideStateSession.getInstance().changeRunId(runId.toString(), true, false); + }); public static ReadOnlyObjectProperty runIdProperty() { return runIdProperty; @@ -33,8 +29,8 @@ public static void setRunId(Object runId) { FXProperties.setIfNotEquals(runIdProperty, runId); } - static { - FXInit.init(); + static { // All FXClass in this package should call FXInit.init() in their static initializer + FXInit.init(); // See FXInit comments to understand why } } diff --git a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXServerSessionId.java b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXServerSessionId.java index cdb6b5a4c..0cfc53edc 100644 --- a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXServerSessionId.java +++ b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXServerSessionId.java @@ -5,21 +5,16 @@ import dev.webfx.stack.session.state.client.ClientSideStateSession; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.beans.property.SimpleObjectProperty; /** * @author Bruno Salmon */ public final class FXServerSessionId { - private final static ObjectProperty serverSessionIdProperty = new SimpleObjectProperty<>() { - @Override - protected void invalidated() { - Object serverSessionId = get(); - Console.log("FxServerSessionId = " + serverSessionId); - ClientSideStateSession.getInstance().changeServerSessionId(serverSessionId.toString(), true, false); - } - }; + private final static ObjectProperty serverSessionIdProperty = FXProperties.newObjectProperty(serverSessionId -> { + Console.log("FxServerSessionId = " + serverSessionId); + ClientSideStateSession.getInstance().changeServerSessionId(serverSessionId.toString(), true, false); + }); public static ReadOnlyObjectProperty serverSessionIdProperty() { return serverSessionIdProperty; @@ -33,8 +28,8 @@ static void setServerSessionId(Object serverSessionId) { FXProperties.setIfNotEquals(serverSessionIdProperty, serverSessionId); } - static { - FXInit.init(); + static { // All FXClass in this package should call FXInit.init() in their static initializer + FXInit.init(); // See FXInit comments to understand why } } diff --git a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXSession.java b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXSession.java index c93940eb1..235d6b979 100644 --- a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXSession.java +++ b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXSession.java @@ -1,26 +1,22 @@ package dev.webfx.stack.session.state.client.fx; +import dev.webfx.kit.util.properties.FXProperties; import dev.webfx.platform.console.Console; import dev.webfx.stack.session.Session; import dev.webfx.stack.session.state.client.ClientSideStateSession; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.beans.property.SimpleObjectProperty; /** * @author Bruno Salmon */ public final class FXSession { - private final static ObjectProperty sessionProperty = new SimpleObjectProperty<>() { - @Override - protected void invalidated() { - Session session = get(); - String localSessionId = session == null ? null : session.id(); - Console.log("FxSession: localId = " + localSessionId); - ClientSideStateSession.getInstance().setClientSession(session); - } - }; + private final static ObjectProperty sessionProperty = FXProperties.newObjectProperty(session -> { + String localSessionId = session == null ? null : session.id(); + Console.log("FxSession: localId = " + localSessionId); + ClientSideStateSession.getInstance().setClientSession(session); + }); public static ReadOnlyObjectProperty sessionProperty() { return sessionProperty; @@ -34,8 +30,8 @@ static void setSession(Session session) { sessionProperty.set(session); } - static { - FXInit.init(); + static { // All FXClass in this package should call FXInit.init() in their static initializer + FXInit.init(); // See FXInit comments to understand why } } diff --git a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXUserClaims.java b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXUserClaims.java index 47250ca22..b02d8acba 100644 --- a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXUserClaims.java +++ b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXUserClaims.java @@ -27,12 +27,16 @@ private static void setUserClaims(UserClaims userClaims) { FXProperties.setIfNotEquals(userClaimsProperty, userClaims); } + static { // All FXClass in this package should call FXInit.init() in their static initializer + FXInit.init(); // See FXInit comments to understand why + } + static { - FXProperties.runNowAndOnPropertiesChange(() -> { + FXProperties.runNowAndOnPropertyChange(() -> { // Forgetting the previous user claims on user change setUserClaims(null); // Asking the new user claim if the user is logged in (ignored if the user just logged out) - if (FXLoggedIn.isLoggedIn()) { + if (FXUserId.getUserId() != null) { AuthenticationService.getUserClaims() .onFailure(Console::log) .onSuccess(userClaims -> UiScheduler.runInUiThread(() -> setUserClaims(userClaims))); diff --git a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXUserId.java b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXUserId.java index fe5449839..62ec9ca99 100644 --- a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXUserId.java +++ b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXUserId.java @@ -4,21 +4,16 @@ import dev.webfx.platform.console.Console; import dev.webfx.stack.session.state.client.ClientSideStateSession; import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleObjectProperty; /** * @author Bruno Salmon */ public final class FXUserId { - private final static ObjectProperty userIdProperty = new SimpleObjectProperty<>() { - @Override - protected void invalidated() { - Object userId = get(); - Console.log("FxUserId = " + userId); - ClientSideStateSession.getInstance().changeUserId(userId, false, false); - } - }; + private final static ObjectProperty userIdProperty = FXProperties.newObjectProperty(userId -> { + Console.log("FxUserId = " + userId); + ClientSideStateSession.getInstance().changeUserId(userId, false, false); + }); public static ObjectProperty userIdProperty() { return userIdProperty; @@ -32,8 +27,8 @@ public static void setUserId(Object userId) { FXProperties.setIfNotEquals(userIdProperty, userId); } - static { - FXInit.init(); + static { // All FXClass in this package should call FXInit.init() in their static initializer + FXInit.init(); // See FXInit comments to understand why } } diff --git a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXUserPrincipal.java b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXUserPrincipal.java index b4bc79263..147860353 100644 --- a/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXUserPrincipal.java +++ b/webfx-stack-session-state-client-fx/src/main/java/dev/webfx/stack/session/state/client/fx/FXUserPrincipal.java @@ -24,4 +24,8 @@ public static Object getUserPrincipal() { return userPrincipalProperty().getValue(); } + static { // All FXClass in this package should call FXInit.init() in their static initializer + FXInit.init(); // See FXInit comments to understand why + } + } diff --git a/webfx-stack-session-state-client-fx/src/main/java/module-info.java b/webfx-stack-session-state-client-fx/src/main/java/module-info.java index 18792a9c1..dff9600b7 100644 --- a/webfx-stack-session-state-client-fx/src/main/java/module-info.java +++ b/webfx-stack-session-state-client-fx/src/main/java/module-info.java @@ -7,7 +7,7 @@ requires webfx.kit.util; requires webfx.platform.console; requires webfx.platform.uischeduler; - requires webfx.stack.authn; + requires transitive webfx.stack.authn; requires webfx.stack.session; requires webfx.stack.session.state; requires webfx.stack.session.state.client; diff --git a/webfx-stack-session-state-client-fx/webfx.xml b/webfx-stack-session-state-client-fx/webfx.xml index 9e8facde4..d9fec248e 100644 --- a/webfx-stack-session-state-client-fx/webfx.xml +++ b/webfx-stack-session-state-client-fx/webfx.xml @@ -6,7 +6,9 @@ - + + webfx-stack-authn + \ No newline at end of file diff --git a/webfx-stack-session-state-server/src/main/java/dev/webfx/stack/session/state/server/ServerSideStateSessionSyncer.java b/webfx-stack-session-state-server/src/main/java/dev/webfx/stack/session/state/server/ServerSideStateSessionSyncer.java index 6989a19fe..cef928632 100644 --- a/webfx-stack-session-state-server/src/main/java/dev/webfx/stack/session/state/server/ServerSideStateSessionSyncer.java +++ b/webfx-stack-session-state-server/src/main/java/dev/webfx/stack/session/state/server/ServerSideStateSessionSyncer.java @@ -99,7 +99,7 @@ private static Future syncFixedServerSessionFromIncomingClientState(Ses } private static Future storeServerSession(Session serverSession) { - return SessionService.getSessionStore().put(serverSession) + return serverSession.store() .onFailure(Console::log) .map(x -> serverSession); } diff --git a/webfx-stack-session/src/main/java/dev/webfx/stack/session/Session.java b/webfx-stack-session/src/main/java/dev/webfx/stack/session/Session.java index 87b9a6b36..c2c94d4f5 100644 --- a/webfx-stack-session/src/main/java/dev/webfx/stack/session/Session.java +++ b/webfx-stack-session/src/main/java/dev/webfx/stack/session/Session.java @@ -1,5 +1,7 @@ package dev.webfx.stack.session; +import dev.webfx.platform.async.Future; + /** * @author Bruno Salmon */ @@ -36,4 +38,8 @@ public interface Session { T remove(String key); boolean isEmpty(); + + default Future store() { + return SessionService.getSessionStore().put(this); + } } diff --git a/webfx-stack-ui-action-tuner/pom.xml b/webfx-stack-ui-action-tuner/pom.xml new file mode 100644 index 000000000..6572056ea --- /dev/null +++ b/webfx-stack-ui-action-tuner/pom.xml @@ -0,0 +1,46 @@ + + + + 4.0.0 + + + dev.webfx + webfx-stack + 0.1.0-SNAPSHOT + + + webfx-stack-ui-action-tuner + + + + + org.openjfx + javafx-base + provided + + + + dev.webfx + webfx-platform-javatime-emul-j2cl + 0.1.0-SNAPSHOT + runtime + true + + + + dev.webfx + webfx-stack-session-state-client-fx + 0.1.0-SNAPSHOT + + + + dev.webfx + webfx-stack-ui-action + 0.1.0-SNAPSHOT + + + + + \ No newline at end of file diff --git a/webfx-stack-ui-action-tuner/src/main/java/dev/webfx/stack/ui/action/tuner/ActionTuner.java b/webfx-stack-ui-action-tuner/src/main/java/dev/webfx/stack/ui/action/tuner/ActionTuner.java new file mode 100644 index 000000000..26f04f145 --- /dev/null +++ b/webfx-stack-ui-action-tuner/src/main/java/dev/webfx/stack/ui/action/tuner/ActionTuner.java @@ -0,0 +1,12 @@ +package dev.webfx.stack.ui.action.tuner; + +import dev.webfx.stack.ui.action.Action; + +/** + * @author Bruno Salmon + */ +public interface ActionTuner { + + Action tuneAction(Action action); + +} diff --git a/webfx-stack-ui-action-tuner/src/main/java/dev/webfx/stack/ui/action/tuner/logout/LogoutOnlyActionTuner.java b/webfx-stack-ui-action-tuner/src/main/java/dev/webfx/stack/ui/action/tuner/logout/LogoutOnlyActionTuner.java new file mode 100644 index 000000000..0ac74161d --- /dev/null +++ b/webfx-stack-ui-action-tuner/src/main/java/dev/webfx/stack/ui/action/tuner/logout/LogoutOnlyActionTuner.java @@ -0,0 +1,23 @@ +package dev.webfx.stack.ui.action.tuner.logout; + +import dev.webfx.stack.session.state.client.fx.FXLoggedOut; +import dev.webfx.stack.ui.action.Action; +import dev.webfx.stack.ui.action.ActionBuilder; +import dev.webfx.stack.ui.action.tuner.ActionTuner; +import javafx.beans.property.ReadOnlyBooleanProperty; + +/** + * @author Bruno Salmon + */ +public interface LogoutOnlyActionTuner extends ActionTuner { + + @Override + default Action tuneAction(Action action) { + ReadOnlyBooleanProperty loggedOutProperty = FXLoggedOut.loggedOutProperty(); + return new ActionBuilder(action) + .setVisibleProperty(loggedOutProperty) + .setHiddenWhenDisabled(true) // TODO: see if we can get this information from the underlying action instead of forcing it + .build(); + } + +} diff --git a/webfx-stack-ui-action-tuner/src/main/java/module-info.java b/webfx-stack-ui-action-tuner/src/main/java/module-info.java new file mode 100644 index 000000000..7cdb60bf0 --- /dev/null +++ b/webfx-stack-ui-action-tuner/src/main/java/module-info.java @@ -0,0 +1,14 @@ +// File managed by WebFX (DO NOT EDIT MANUALLY) + +module webfx.stack.ui.action.tuner { + + // Direct dependencies modules + requires javafx.base; + requires webfx.stack.session.state.client.fx; + requires webfx.stack.ui.action; + + // Exported packages + exports dev.webfx.stack.ui.action.tuner; + exports dev.webfx.stack.ui.action.tuner.logout; + +} \ No newline at end of file diff --git a/webfx-stack-ui-action-tuner/webfx.xml b/webfx-stack-ui-action-tuner/webfx.xml new file mode 100644 index 000000000..14bdc5177 --- /dev/null +++ b/webfx-stack-ui-action-tuner/webfx.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/Action.java b/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/Action.java index 2ad444993..84ba75952 100644 --- a/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/Action.java +++ b/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/Action.java @@ -2,12 +2,14 @@ import dev.webfx.stack.ui.action.impl.ReadOnlyAction; import javafx.beans.value.ObservableBooleanValue; -import javafx.beans.value.ObservableObjectValue; import javafx.beans.value.ObservableStringValue; +import javafx.beans.value.ObservableValue; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.scene.Node; +import java.util.function.Supplier; + /** * An action compatible with standard JavaFX API (ex: can be passed to Button.setOnAction()) but enriched with graphical * properties (ie text, graphic, disabled and visible properties). The ActionBinder utility class can be used to help @@ -22,9 +24,14 @@ default String getText() { return textProperty().get(); } - ObservableObjectValue graphicProperty(); - default Node getGraphic() { - return graphicProperty().get(); + ObservableValue> graphicFactoryProperty(); // TODO: should it be rather Supplier>? + default Supplier getGraphicFactory() { + return graphicFactoryProperty().getValue(); + } + + default Node createGraphic() { + Supplier graphicFactory = getGraphicFactory(); + return graphicFactory == null ? null : graphicFactory.get(); } ObservableBooleanValue disabledProperty(); @@ -41,7 +48,7 @@ default boolean isVisible() { Object getUserData(); - static Action create(ObservableStringValue textProperty, ObservableObjectValue graphicProperty, ObservableBooleanValue disabledProperty, ObservableBooleanValue visibleProperty, EventHandler actionHandler) { - return new ReadOnlyAction(textProperty, graphicProperty, disabledProperty, visibleProperty, actionHandler); + static Action create(ObservableStringValue textProperty, ObservableValue> graphicFactoryProperty, ObservableBooleanValue disabledProperty, ObservableBooleanValue visibleProperty, EventHandler actionHandler) { + return new ReadOnlyAction(textProperty, graphicFactoryProperty, disabledProperty, visibleProperty, actionHandler); } } diff --git a/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/ActionBinder.java b/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/ActionBinder.java index 120221221..ed25b0563 100644 --- a/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/ActionBinder.java +++ b/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/ActionBinder.java @@ -2,26 +2,37 @@ import dev.webfx.kit.util.properties.FXProperties; import dev.webfx.kit.util.properties.ObservableLists; +import dev.webfx.kit.util.properties.Unregisterable; import dev.webfx.platform.util.function.Converter; import dev.webfx.stack.ui.action.impl.WritableAction; import javafx.beans.property.ObjectProperty; -import javafx.beans.value.ObservableObjectValue; +import javafx.beans.value.ObservableValue; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.scene.Node; -import javafx.scene.control.ButtonBase; -import javafx.scene.control.Labeled; -import javafx.scene.control.MenuItem; -import javafx.scene.image.ImageView; +import javafx.scene.control.*; import javafx.scene.layout.Pane; import java.util.Collection; +import java.util.function.Supplier; /** * @author Bruno Salmon */ public final class ActionBinder { + public static Button newActionButton(Action action) { + return bindButtonToAction(new Button(), action); + } + + public static Hyperlink newActionHyperlink(Action action) { + return bindButtonToAction(new Hyperlink(), action); + } + + public static MenuItem newActionMenuItem(Action action) { + return bindMenuItemToAction(new MenuItem(), action); + } + public static T bindButtonToAction(T button, Action action) { bindLabeledToAction(button, action); button.setOnAction(action); @@ -30,7 +41,7 @@ public static T bindButtonToAction(T button, Action actio public static T bindMenuItemToAction(T menuItem, Action action) { menuItem.textProperty().bind(action.textProperty()); - bindGraphicProperties(menuItem.graphicProperty(), action.graphicProperty()); + bindGraphicProperties(menuItem.graphicProperty(), action.graphicFactoryProperty()); menuItem.disableProperty().bind(action.disabledProperty()); menuItem.visibleProperty().bind(action.visibleProperty()); menuItem.setOnAction(action); @@ -39,31 +50,24 @@ public static T bindMenuItemToAction(T menuItem, Action act private static T bindLabeledToAction(T labeled, Action action) { labeled.textProperty().bind(action.textProperty()); - bindGraphicProperties(labeled.graphicProperty(), action.graphicProperty()); + bindGraphicProperties(labeled.graphicProperty(), action.graphicFactoryProperty()); bindNodeToAction(labeled, action, false); return labeled; } - private static void bindGraphicProperties(ObjectProperty dstGraphicProperty, ObservableObjectValue srcGraphicProperty) { + private static void bindGraphicProperties(ObjectProperty dstGraphicProperty, ObservableValue> srcGraphicFactoryProperty) { // Needs to make a copy of the graphic in case it is used in several places (JavaFX nodes must be unique instances in the scene graph) - FXProperties.runNowAndOnPropertiesChange(p -> dstGraphicProperty.setValue(copyGraphic(srcGraphicProperty.getValue())), srcGraphicProperty); + FXProperties.runNowAndOnPropertyChange(srcGraphicFactory -> + dstGraphicProperty.setValue(createGraphic(srcGraphicFactory)) + , srcGraphicFactoryProperty); } - private static Node copyGraphic(Node graphic) { - // Handling only ImageView for now - if (graphic instanceof ImageView) { - ImageView imageView = (ImageView) graphic; - ImageView copy = new ImageView(); - copy.imageProperty().bind(imageView.imageProperty()); - copy.fitWidthProperty().bind(imageView.fitWidthProperty()); - copy.fitHeightProperty().bind(imageView.fitHeightProperty()); - return copy; - } - return graphic; + private static Node createGraphic(Supplier graphicFactory) { + return graphicFactory == null ? null : graphicFactory.get(); } public static Node getAndBindActionIcon(Action action) { - return bindNodeToAction(action.getGraphic(), action, true); + return bindNodeToAction(createGraphic(action.getGraphicFactory()), action, true); } private static T bindNodeToAction(T node, Action action, boolean setOnMouseClicked) { @@ -78,23 +82,22 @@ private static T bindNodeToAction(T node, Action action, boolea public static void bindWritableActionToAction(WritableAction writableAction, Action action) { writableAction.writableTextProperty().bind(action.textProperty()); - writableAction.writableGraphicProperty().bind(action.graphicProperty()); + writableAction.writableGraphicFactoryProperty().bind(action.graphicFactoryProperty()); writableAction.writableDisabledProperty().bind(action.disabledProperty()); writableAction.writableVisibleProperty().bind(action.visibleProperty()); } - public static

P bindChildrenToVisibleActions(P parent, Collection actions, Converter nodeFactory) { + + public static

Unregisterable bindChildrenToVisibleActions(P parent, Collection actions, Converter nodeFactory) { ActionGroup actionGroup = new ActionGroupBuilder().setActions(actions).build(); return bindChildrenToActionGroup(parent, actionGroup, nodeFactory); } - public static

P bindChildrenToActionGroup(P parent, ActionGroup actionGroup, Converter nodeFactory) { - bindChildrenToActionGroup(parent.getChildren(), actionGroup, nodeFactory); - return parent; + public static

Unregisterable bindChildrenToActionGroup(P parent, ActionGroup actionGroup, Converter nodeFactory) { + return bindChildrenToActionGroup(parent.getChildren(), actionGroup, nodeFactory); } - public static ObservableList bindChildrenToActionGroup(ObservableList children, ActionGroup actionGroup, Converter nodeFactory) { - ObservableLists.bindConverted(children, actionGroup.getVisibleActions(), nodeFactory); - return children; + public static Unregisterable bindChildrenToActionGroup(ObservableList children, ActionGroup actionGroup, Converter nodeFactory) { + return ObservableLists.bindConverted(children, actionGroup.getVisibleActions(), nodeFactory); } } diff --git a/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/ActionBuilder.java b/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/ActionBuilder.java index 9575ff494..904e1eb90 100644 --- a/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/ActionBuilder.java +++ b/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/ActionBuilder.java @@ -1,17 +1,20 @@ package dev.webfx.stack.ui.action; +import dev.webfx.kit.util.properties.FXProperties; +import dev.webfx.stack.i18n.I18n; +import dev.webfx.stack.ui.json.JsonImageView; import javafx.beans.binding.BooleanExpression; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableBooleanValue; -import javafx.beans.value.ObservableObjectValue; import javafx.beans.value.ObservableStringValue; +import javafx.beans.value.ObservableValue; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.scene.Node; -import dev.webfx.stack.i18n.I18n; -import dev.webfx.stack.ui.json.JsonImageView; + +import java.util.function.Supplier; /** * @author Bruno Salmon @@ -24,8 +27,8 @@ public class ActionBuilder { private String text; private Object i18nKey; - private ObservableObjectValue graphicProperty; - private Node graphic; + private ObservableValue> graphicFactoryProperty; + private Supplier graphicFactory; private Object graphicUrlOrJson; private ObservableBooleanValue disabledProperty; @@ -39,6 +42,8 @@ public class ActionBuilder { private EventHandler actionHandler; + private Object userData; + private ActionBuilderRegistry registry; public ActionBuilder() { @@ -48,6 +53,18 @@ public ActionBuilder(Object actionKey) { this.actionKey = actionKey; } + public ActionBuilder(Action action) { + setTextProperty(action.textProperty()); + setText(action.getText()); + setGraphicFactoryProperty(action.graphicFactoryProperty()); + setGraphicFactory(action.getGraphicFactory()); + setDisabledProperty(action.disabledProperty()); + setVisibleProperty(action.visibleProperty()); + setActionHandler(action); + setUserData(action.getUserData()); + //setHiddenWhenDisabled(visibleProperty == disabledProperty || visibleProperty instanceof BooleanBinding && ((BooleanBinding) visibleProperty).getDependencies().contains(disabledProperty)); + } + public Object getActionKey() { return actionKey; } @@ -84,21 +101,21 @@ public ActionBuilder setI18nKey(Object i18nKey) { return this; } - public ObservableObjectValue getGraphicProperty() { - return graphicProperty; + public ObservableValue> getGraphicFactoryProperty() { + return graphicFactoryProperty; } - public ActionBuilder setGraphicProperty(ObservableObjectValue graphicProperty) { - this.graphicProperty = graphicProperty; + public ActionBuilder setGraphicFactoryProperty(ObservableValue> graphicFactoryProperty) { + this.graphicFactoryProperty = graphicFactoryProperty; return this; } - public Node getGraphic() { - return graphic; + public Supplier getGraphicFactory() { + return graphicFactory; } - public ActionBuilder setGraphic(Node graphic) { - this.graphic = graphic; + public ActionBuilder setGraphicFactory(Supplier graphicFactory) { + this.graphicFactory = graphicFactory; return this; } @@ -169,6 +186,15 @@ public ActionBuilder setActionHandler(Runnable actionHandler) { return setActionHandler(e -> actionHandler.run()); } + public Object getUserData() { + return userData; + } + + public ActionBuilder setUserData(Object userData) { + this.userData = userData; + return this; + } + public ActionBuilder setRegistry(ActionBuilderRegistry registry) { this.registry = registry; return this; @@ -184,8 +210,8 @@ public ActionBuilder duplicate() { .setTextProperty(textProperty) .setText(text) .setI18nKey(i18nKey) - .setGraphicProperty(graphicProperty) - .setGraphic(graphic) + .setGraphicFactoryProperty(graphicFactoryProperty) + .setGraphicFactory(graphicFactory) .setGraphicUrlOrJson(graphicUrlOrJson) .setDisabledProperty(disabledProperty) .setVisibleProperty(visibleProperty) @@ -209,7 +235,9 @@ public ActionBuilder removeText() { public Action build() { completePropertiesForBuild(); - return Action.create(textProperty, graphicProperty, disabledProperty, visibleProperty, actionHandler); + Action action = Action.create(textProperty, graphicFactoryProperty, disabledProperty, visibleProperty, actionHandler); + action.setUserData(userData); + return action; } void completePropertiesForBuild() { @@ -229,13 +257,14 @@ private void completeTextProperty() { } private void completeGraphicProperty() { - if (graphicProperty == null) { - if (graphic == null && graphicUrlOrJson != null) - graphic = JsonImageView.createImageView(graphicUrlOrJson); - if (graphic != null || i18nKey == null) - graphicProperty = new SimpleObjectProperty<>(graphic); + if (graphicFactoryProperty == null) { + if (graphicFactory == null && graphicUrlOrJson != null) + graphicFactory = () -> JsonImageView.createImageView(graphicUrlOrJson); + if (graphicFactory != null || i18nKey == null) + graphicFactoryProperty = new SimpleObjectProperty<>(graphicFactory); else - graphicProperty = I18n.i18nGraphicProperty(i18nKey); + graphicFactoryProperty = FXProperties.compute(I18n.dictionaryProperty(), dictionary -> + () -> I18n.getI18nGraphic(i18nKey)); } } diff --git a/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/ActionGroup.java b/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/ActionGroup.java index 438acab5a..71838eb4a 100644 --- a/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/ActionGroup.java +++ b/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/ActionGroup.java @@ -2,14 +2,15 @@ import dev.webfx.stack.ui.action.impl.ActionGroupImpl; import javafx.beans.value.ObservableBooleanValue; -import javafx.beans.value.ObservableObjectValue; import javafx.beans.value.ObservableStringValue; +import javafx.beans.value.ObservableValue; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.scene.Node; import java.util.Collection; +import java.util.function.Supplier; /** * @author Bruno Salmon @@ -22,8 +23,8 @@ public interface ActionGroup extends Action { boolean hasSeparators(); - static ActionGroup create(Collection actions, ObservableStringValue textProperty, ObservableObjectValue graphicProperty, ObservableBooleanValue disabledProperty, ObservableBooleanValue visibleProperty, boolean hasSeparators, EventHandler actionHandler) { - return new ActionGroupImpl(actions, textProperty, graphicProperty, disabledProperty, visibleProperty, hasSeparators, actionHandler); + static ActionGroup create(Collection actions, ObservableStringValue textProperty, ObservableValue> graphicFactoryProperty, ObservableBooleanValue disabledProperty, ObservableBooleanValue visibleProperty, boolean hasSeparators, EventHandler actionHandler) { + return new ActionGroupImpl(actions, textProperty, graphicFactoryProperty, disabledProperty, visibleProperty, hasSeparators, actionHandler); } } diff --git a/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/ActionGroupBuilder.java b/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/ActionGroupBuilder.java index a0185e70a..ea2fd6bfa 100644 --- a/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/ActionGroupBuilder.java +++ b/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/ActionGroupBuilder.java @@ -1,14 +1,15 @@ package dev.webfx.stack.ui.action; +import dev.webfx.platform.util.collection.Collections; import javafx.beans.value.ObservableBooleanValue; -import javafx.beans.value.ObservableObjectValue; import javafx.beans.value.ObservableStringValue; +import javafx.beans.value.ObservableValue; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.scene.Node; -import dev.webfx.platform.util.collection.Collections; import java.util.Collection; +import java.util.function.Supplier; /** * @author Bruno Salmon @@ -43,7 +44,7 @@ public ActionGroupBuilder setHasSeparators(boolean hasSeparators) { @Override public ActionGroup build() { completePropertiesForBuild(); - return ActionGroup.create(actions, getTextProperty(), getGraphicProperty(), getDisabledProperty(), getVisibleProperty(), hasSeparators, getActionHandler()); + return ActionGroup.create(actions, getTextProperty(), getGraphicFactoryProperty(), getDisabledProperty(), getVisibleProperty(), hasSeparators, getActionHandler()); } // --- Overriding fluent API methods to return ActionGroupBuilder instead of ActionBuilder --- @@ -69,13 +70,13 @@ public ActionGroupBuilder setI18nKey(Object i18nKey) { } @Override - public ActionGroupBuilder setGraphicProperty(ObservableObjectValue graphicProperty) { - return (ActionGroupBuilder) super.setGraphicProperty(graphicProperty); + public ActionGroupBuilder setGraphicFactoryProperty(ObservableValue> graphicFactoryProperty) { + return (ActionGroupBuilder) super.setGraphicFactoryProperty(graphicFactoryProperty); } @Override - public ActionGroupBuilder setGraphic(Node graphic) { - return (ActionGroupBuilder) super.setGraphic(graphic); + public ActionGroupBuilder setGraphicFactory(Supplier graphicFactory) { + return (ActionGroupBuilder) super.setGraphicFactory(graphicFactory); } @Override diff --git a/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/impl/ActionGroupImpl.java b/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/impl/ActionGroupImpl.java index e331342e5..d37f5620e 100644 --- a/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/impl/ActionGroupImpl.java +++ b/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/impl/ActionGroupImpl.java @@ -1,22 +1,23 @@ package dev.webfx.stack.ui.action.impl; +import dev.webfx.kit.util.properties.FXProperties; +import dev.webfx.platform.util.collection.Collections; +import dev.webfx.stack.ui.action.Action; +import dev.webfx.stack.ui.action.ActionGroup; import javafx.beans.property.Property; import javafx.beans.value.ObservableBooleanValue; -import javafx.beans.value.ObservableObjectValue; import javafx.beans.value.ObservableStringValue; +import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.scene.Node; -import dev.webfx.stack.ui.action.Action; -import dev.webfx.stack.ui.action.ActionGroup; -import dev.webfx.kit.util.properties.FXProperties; -import dev.webfx.platform.util.collection.Collections; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.function.Supplier; /** * @author Bruno Salmon @@ -27,8 +28,8 @@ public final class ActionGroupImpl extends ReadOnlyAction implements ActionGroup private final ObservableList visibleActions = FXCollections.observableArrayList(); private final boolean hasSeparators; - public ActionGroupImpl(Collection actions, ObservableStringValue textProperty, ObservableObjectValue graphicProperty, ObservableBooleanValue disabledProperty, ObservableBooleanValue visibleProperty, boolean hasSeparators, EventHandler actionHandler) { - super(textProperty, graphicProperty, disabledProperty, visibleProperty, actionHandler); + public ActionGroupImpl(Collection actions, ObservableStringValue textProperty, ObservableValue> graphicFactoryProperty, ObservableBooleanValue disabledProperty, ObservableBooleanValue visibleProperty, boolean hasSeparators, EventHandler actionHandler) { + super(textProperty, graphicFactoryProperty, disabledProperty, visibleProperty, actionHandler); this.actions = actions; this.hasSeparators = hasSeparators; FXProperties.runNowAndOnPropertiesChange(this::updateVisibleActions, Collections.map(actions, Action::visibleProperty)); @@ -40,7 +41,7 @@ private void updateVisibleActions() { for (Action action : this.actions) { if (action.isVisible()) { int n = actions.size(); - if (action.getText() == null && action.getGraphic() == null && action instanceof ActionGroup) { + if (action.getText() == null && action.getGraphicFactory() == null && action instanceof ActionGroup) { ActionGroup actionGroup = (ActionGroup) action; actions.addAll(actionGroup.getVisibleActions()); if (actions.size() > n) { diff --git a/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/impl/ReadOnlyAction.java b/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/impl/ReadOnlyAction.java index 17d90a782..c0843f2b0 100644 --- a/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/impl/ReadOnlyAction.java +++ b/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/impl/ReadOnlyAction.java @@ -1,12 +1,14 @@ package dev.webfx.stack.ui.action.impl; +import dev.webfx.stack.ui.action.Action; import javafx.beans.value.ObservableBooleanValue; -import javafx.beans.value.ObservableObjectValue; import javafx.beans.value.ObservableStringValue; +import javafx.beans.value.ObservableValue; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.scene.Node; -import dev.webfx.stack.ui.action.Action; + +import java.util.function.Supplier; /** * A read-only action where properties are observable values. @@ -15,9 +17,9 @@ */ public class ReadOnlyAction implements Action { - public ReadOnlyAction(ObservableStringValue textProperty, ObservableObjectValue graphicProperty, ObservableBooleanValue disabledProperty, ObservableBooleanValue visibleProperty, EventHandler actionHandler) { + public ReadOnlyAction(ObservableStringValue textProperty, ObservableValue> graphicFactoryProperty, ObservableBooleanValue disabledProperty, ObservableBooleanValue visibleProperty, EventHandler actionHandler) { this.textProperty = textProperty; - this.graphicProperty = graphicProperty; + this.graphicFactoryProperty = graphicFactoryProperty; this.disabledProperty = disabledProperty; this.visibleProperty = visibleProperty; this.actionHandler = actionHandler; @@ -29,10 +31,10 @@ public ObservableStringValue textProperty() { return textProperty; } - private final ObservableObjectValue graphicProperty; + private final ObservableValue> graphicFactoryProperty; @Override - public ObservableObjectValue graphicProperty() { - return graphicProperty; + public ObservableValue> graphicFactoryProperty() { + return graphicFactoryProperty; } private final ObservableBooleanValue disabledProperty; diff --git a/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/impl/WritableAction.java b/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/impl/WritableAction.java index 849ef13ca..8f42b2cee 100644 --- a/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/impl/WritableAction.java +++ b/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/impl/WritableAction.java @@ -1,14 +1,16 @@ package dev.webfx.stack.ui.action.impl; +import dev.webfx.platform.util.Arrays; +import dev.webfx.stack.ui.action.Action; import javafx.beans.property.*; import javafx.beans.value.ObservableBooleanValue; -import javafx.beans.value.ObservableObjectValue; import javafx.beans.value.ObservableStringValue; +import javafx.beans.value.ObservableValue; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.scene.Node; -import dev.webfx.stack.ui.action.Action; -import dev.webfx.platform.util.Arrays; + +import java.util.function.Supplier; /** * A writable action where properties (text, graphic, disabled, visible) can be set later (ie after constructor call) @@ -19,15 +21,15 @@ public class WritableAction extends ReadOnlyAction { public WritableAction(Action action, String... writablePropertyNames) { - this(createStringProperty(action.textProperty(), "text", writablePropertyNames), createObjectProperty(action.graphicProperty(), "graphic", writablePropertyNames), createBooleanProperty(action.disabledProperty(), "disabled", writablePropertyNames), createBooleanProperty(action.visibleProperty(), "visible", writablePropertyNames), action); + this(createStringProperty(action.textProperty(), "text", writablePropertyNames), createObjectProperty(action.graphicFactoryProperty(), "graphicFactory", writablePropertyNames), createBooleanProperty(action.disabledProperty(), "disabled", writablePropertyNames), createBooleanProperty(action.visibleProperty(), "visible", writablePropertyNames), action); } public WritableAction(EventHandler actionHandler) { this(new SimpleStringProperty(), new SimpleObjectProperty<>(), new SimpleBooleanProperty(true /* disabled until it is bound */), new SimpleBooleanProperty(false /* invisible until it is bound */), actionHandler); } - public WritableAction(ObservableStringValue textProperty, ObservableObjectValue graphicProperty, ObservableBooleanValue disabledProperty, ObservableBooleanValue visibleProperty, EventHandler actionHandler) { - super(textProperty, graphicProperty, disabledProperty, visibleProperty, actionHandler); + public WritableAction(ObservableStringValue textProperty, ObservableValue> graphicFactoryProperty, ObservableBooleanValue disabledProperty, ObservableBooleanValue visibleProperty, EventHandler actionHandler) { + super(textProperty, graphicFactoryProperty, disabledProperty, visibleProperty, actionHandler); } public StringProperty writableTextProperty() { @@ -38,12 +40,12 @@ public void setText(String text) { writableTextProperty().set(text); } - public ObjectProperty writableGraphicProperty() { - return (ObjectProperty) graphicProperty(); + public ObjectProperty> writableGraphicFactoryProperty() { + return (ObjectProperty>) graphicFactoryProperty(); } - public void setGraphic(Node graphic) { - writableGraphicProperty().set(graphic); + public void setGraphicFactory(Supplier graphicFactory) { + writableGraphicFactoryProperty().set(graphicFactory); } public BooleanProperty writableDisabledProperty() { @@ -76,10 +78,10 @@ public void set(String newValue) { return writableProperty; } - private static ObjectProperty createObjectProperty(ObservableObjectValue readOnlyProperty, String propertyName, String... writablePropertyNames) { + private static ObjectProperty createObjectProperty(ObservableValue readOnlyProperty, String propertyName, String... writablePropertyNames) { if (readOnlyProperty instanceof ObjectProperty) return (ObjectProperty) readOnlyProperty; - SimpleObjectProperty writableProperty = new SimpleObjectProperty() { + SimpleObjectProperty writableProperty = new SimpleObjectProperty<>() { @Override public void set(T newValue) { unbindPropertyIfWritable(this, propertyName, writablePropertyNames); diff --git a/webfx-stack-ui-controls/src/main/java/dev/webfx/stack/ui/controls/ControlFactoryMixin.java b/webfx-stack-ui-controls/src/main/java/dev/webfx/stack/ui/controls/ControlFactoryMixin.java index dcdeb5c0e..061e99672 100644 --- a/webfx-stack-ui-controls/src/main/java/dev/webfx/stack/ui/controls/ControlFactoryMixin.java +++ b/webfx-stack-ui-controls/src/main/java/dev/webfx/stack/ui/controls/ControlFactoryMixin.java @@ -110,11 +110,11 @@ default Button styleButton(Button button) { } default CheckBox newCheckBox(Object i18nKey) { - return I18nControls.bindI18nProperties(new CheckBox(), i18nKey); + return I18nControls.newCheckBox(i18nKey); } default RadioButton newRadioButton(Object i18nKey) { - return I18nControls.bindI18nProperties(new RadioButton(), i18nKey); + return I18nControls.newRadioButton(i18nKey); } default RadioButton newRadioButton(Object i18nKey, ToggleGroup toggleGroup) { @@ -124,7 +124,7 @@ default RadioButton newRadioButton(Object i18nKey, ToggleGroup toggleGroup) { } default Label newLabel(Object i18nKey) { - return I18nControls.bindI18nProperties(new Label(), i18nKey); + return I18nControls.newLabel(i18nKey); } default TextField newTextField() { @@ -144,7 +144,7 @@ default Hyperlink newHyperlink() { } default Hyperlink newHyperlink(Object i18nKey) { - return I18nControls.bindI18nProperties(newHyperlink(), i18nKey); + return I18nControls.newHyperlink(i18nKey); } default Hyperlink newHyperlink(Object i18nKey, EventHandler onAction) { @@ -158,7 +158,7 @@ default TextArea newTextArea(Object i18nKey) { } default Text newText(Object i18nKey) { - return I18n.bindI18nProperties(new Text(), i18nKey); + return I18n.newText(i18nKey); } } diff --git a/webfx-stack-ui-controls/src/main/java/dev/webfx/stack/ui/controls/button/ButtonFactory.java b/webfx-stack-ui-controls/src/main/java/dev/webfx/stack/ui/controls/button/ButtonFactory.java index bcd8188e9..b52cd46d5 100644 --- a/webfx-stack-ui-controls/src/main/java/dev/webfx/stack/ui/controls/button/ButtonFactory.java +++ b/webfx-stack-ui-controls/src/main/java/dev/webfx/stack/ui/controls/button/ButtonFactory.java @@ -62,7 +62,7 @@ public static Button decorateButtonWithDropDownArrow(Button button) { downArrow.setFill(null); downArrow.setContent("M1 1.22998L6.325 6.55499L11.65 1.22998"); GraphicDecoration dropDownArrowDecoration = new GraphicDecoration(downArrow, Pos.CENTER_RIGHT, 0, 0, -1, 0); - FXProperties.runNowAndOnPropertiesChange(() -> Platform.runLater(() -> + FXProperties.runNowAndOnPropertyChange(() -> Platform.runLater(() -> Controls.onSkinReady(button, () -> dropDownArrowDecoration.applyDecoration(button)) ), button.graphicProperty()); // Code to clip the content before the down arrow diff --git a/webfx-stack-ui-controls/src/main/java/dev/webfx/stack/ui/controls/dialog/DialogContent.java b/webfx-stack-ui-controls/src/main/java/dev/webfx/stack/ui/controls/dialog/DialogContent.java index 73739f4ee..c0b7e8c82 100644 --- a/webfx-stack-ui-controls/src/main/java/dev/webfx/stack/ui/controls/dialog/DialogContent.java +++ b/webfx-stack-ui-controls/src/main/java/dev/webfx/stack/ui/controls/dialog/DialogContent.java @@ -72,6 +72,12 @@ public DialogContent setYesNo() { return this; } + public DialogContent setOk() { + primaryButtonText = "Ok"; + secondaryButton.setManaged(false); + return this; + } + public Button getPrimaryButton() { return primaryButton; } diff --git a/webfx-stack-ui-json/src/main/java/dev/webfx/stack/ui/json/JsonSVGPath.java b/webfx-stack-ui-json/src/main/java/dev/webfx/stack/ui/json/JsonSVGPath.java index 8d2154fd0..3bd74ee98 100644 --- a/webfx-stack-ui-json/src/main/java/dev/webfx/stack/ui/json/JsonSVGPath.java +++ b/webfx-stack-ui-json/src/main/java/dev/webfx/stack/ui/json/JsonSVGPath.java @@ -7,6 +7,8 @@ import javafx.scene.paint.Paint; import javafx.scene.shape.FillRule; import javafx.scene.shape.SVGPath; +import javafx.scene.shape.StrokeLineCap; +import javafx.scene.shape.StrokeLineJoin; /** * @author Bruno Salmon @@ -21,6 +23,9 @@ public static SVGPath createSVGPath(ReadOnlyAstObject json) { svgPath.setStroke(toPaint(json.getString("stroke"), null)); svgPath.setStrokeWidth(json.getDouble("strokeWidth", 1d)); svgPath.setFillRule(toFillRule(json.getString("fillRule"))); + svgPath.setStrokeLineCap(toStrokeLineCap(json.getString("strokeLineCap"))); + svgPath.setStrokeLineJoin(toStrokeLineJoin(json.getString("strokeLineJoin"))); + svgPath.setStrokeMiterLimit(json.getDouble("strokeMiterLimit", 10d)); return svgPath; } @@ -40,11 +45,36 @@ private static Paint toPaint(String paintText, Paint defaultPaint) { } private static FillRule toFillRule(String ruleText) { - if (ruleText != null) + if (ruleText != null) { switch (ruleText.trim().toLowerCase()) { - case "evenodd" : return FillRule.EVEN_ODD; - case "nonzero" : return FillRule.NON_ZERO; + case "evenodd": + return FillRule.EVEN_ODD; + case "nonzero": + return FillRule.NON_ZERO; } + } return FillRule.NON_ZERO; } + + private static StrokeLineCap toStrokeLineCap(String strokeLineCapText) { + if (strokeLineCapText != null) { + switch (strokeLineCapText.trim().toLowerCase()) { + case "square" : return StrokeLineCap.SQUARE; + case "butt" : return StrokeLineCap.BUTT; + case "round" : return StrokeLineCap.ROUND; + } + } + return StrokeLineCap.SQUARE; + } + + private static StrokeLineJoin toStrokeLineJoin(String StrokeLineJoinText) { + if (StrokeLineJoinText != null) { + switch (StrokeLineJoinText.trim().toLowerCase()) { + case "miter": return StrokeLineJoin.MITER; + case "bevel": return StrokeLineJoin.BEVEL; + case "round": return StrokeLineJoin.ROUND; + } + } + return StrokeLineJoin.MITER; + } } diff --git a/webfx-stack-ui-operation-action/pom.xml b/webfx-stack-ui-operation-action/pom.xml index b65dbafb7..d7388c4a9 100644 --- a/webfx-stack-ui-operation-action/pom.xml +++ b/webfx-stack-ui-operation-action/pom.xml @@ -45,6 +45,14 @@ 0.1.0-SNAPSHOT + + dev.webfx + webfx-platform-javabase-emul-j2cl + 0.1.0-SNAPSHOT + runtime + true + + dev.webfx webfx-platform-javatime-emul-j2cl @@ -53,6 +61,12 @@ true + + dev.webfx + webfx-platform-scheduler + 0.1.0-SNAPSHOT + + dev.webfx webfx-platform-uischeduler @@ -77,6 +91,12 @@ 0.1.0-SNAPSHOT + + dev.webfx + webfx-stack-ui-action-tuner + 0.1.0-SNAPSHOT + + dev.webfx webfx-stack-ui-exceptions diff --git a/webfx-stack-ui-operation-action/src/main/java/dev/webfx/stack/ui/operation/action/OperationAction.java b/webfx-stack-ui-operation-action/src/main/java/dev/webfx/stack/ui/operation/action/OperationAction.java index cb8aaf170..de427612b 100644 --- a/webfx-stack-ui-operation-action/src/main/java/dev/webfx/stack/ui/operation/action/OperationAction.java +++ b/webfx-stack-ui-operation-action/src/main/java/dev/webfx/stack/ui/operation/action/OperationAction.java @@ -58,13 +58,9 @@ private OperationAction(OperationAction[] me, Function me[0] = this; this.operationRequestFactory = operationRequestFactory; OperationActionRegistry registry = getOperationActionRegistry(); - registry.bindOperationActionGraphicalProperties(this); - // Also, if some graphical dependencies are passed, we update the graphical properties when they change - if (graphicalDependencies.length > 0) - FXProperties.runNowAndOnPropertiesChange(() -> { - registry.updateOperationActionGraphicalProperties(this); - registry.bindOperationActionGraphicalProperties(this); - }, graphicalDependencies); + FXProperties.runNowAndOnPropertiesChange(() -> + registry.bindOperationActionGraphicalProperties(this) + , graphicalDependencies); // Also updating the graphical properties when graphical dependencies change } public OperationActionRegistry getOperationActionRegistry() { @@ -86,9 +82,8 @@ private void startShowingActionAsExecuting(Object operationRequest) { // If in addition an icon has been provided to graphically indicate the execution is in progress, if (actionExecutingIconFactory != null) { // we apply it to the graphic property Node executingIcon = actionExecutingIconFactory.apply(operationRequest); - if (executingIcon != null) { - FXProperties.setEvenIfBound(writableGraphicProperty(), executingIcon); - } + if (executingIcon != null) // For some operations such as routing operation, there is no executing icon + FXProperties.setEvenIfBound(writableGraphicFactoryProperty(), () -> executingIcon); } } @@ -100,9 +95,8 @@ private void stopShowingActionAsExecuting(Object operationRequest, Throwable exc // If in addition an icon has been provided to graphically indicate the execution has ended, if (actionExecutedIconFactory != null) { // we apply it to the graphic property for 2s Node executedIcon = actionExecutedIconFactory.apply(operationRequest, exception); - if (executedIcon != null) { - FXProperties.setEvenIfBound(writableGraphicProperty(), executedIcon); - } + if (executedIcon != null) // For some operations such as routing operation, there is no executed icon + FXProperties.setEvenIfBound(writableGraphicFactoryProperty(), () -> executedIcon); UiScheduler.scheduleDelay(2000, () -> { // After 2 seconds, we reestablish the original action icon, unless it's already executing again if (!executing) { // if executing again, we keep the possible executing icon instead diff --git a/webfx-stack-ui-operation-action/src/main/java/dev/webfx/stack/ui/operation/action/OperationActionRegistry.java b/webfx-stack-ui-operation-action/src/main/java/dev/webfx/stack/ui/operation/action/OperationActionRegistry.java index 90e74f406..0691fe3da 100644 --- a/webfx-stack-ui-operation-action/src/main/java/dev/webfx/stack/ui/operation/action/OperationActionRegistry.java +++ b/webfx-stack-ui-operation-action/src/main/java/dev/webfx/stack/ui/operation/action/OperationActionRegistry.java @@ -1,11 +1,17 @@ package dev.webfx.stack.ui.operation.action; +import dev.webfx.kit.util.properties.FXProperties; import dev.webfx.platform.async.AsyncFunction; import dev.webfx.platform.async.Future; +import dev.webfx.platform.console.Console; +import dev.webfx.platform.scheduler.Scheduled; import dev.webfx.platform.uischeduler.UiScheduler; +import dev.webfx.platform.util.collection.Collections; import dev.webfx.stack.authz.client.factory.AuthorizationUtil; import dev.webfx.stack.ui.action.Action; import dev.webfx.stack.ui.action.ActionBinder; +import dev.webfx.stack.ui.action.impl.WritableAction; +import dev.webfx.stack.ui.action.tuner.ActionTuner; import dev.webfx.stack.ui.operation.HasOperationCode; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; @@ -13,10 +19,8 @@ import javafx.beans.value.ObservableValue; import javafx.event.ActionEvent; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; +import java.lang.ref.WeakReference; +import java.util.*; import java.util.function.Consumer; import java.util.function.Function; @@ -41,17 +45,32 @@ */ public final class OperationActionRegistry { - private static OperationActionRegistry INSTANCE; + private static final boolean LOG_DEBUG = false; - private final Map graphicalActions = new HashMap<>(); - private Collection notYetBoundOperationActions; - private Map> operationActionProperties; - private Runnable pendingBindRunnable; + private static final OperationActionRegistry INSTANCE = new OperationActionRegistry(); + + // Holding the actions that have been registered by the application code through registerOperationGraphicalAction(). + // These actions usually hold only the graphical properties, they are not executable (the event handler doesn't do + // anything). They are used to bind the graphical properties of the executable operation actions - through + // bindOperationActionGraphicalProperties(). + private final Map registeredGraphicalActions = new HashMap<>(); + + // Holding the executable operation actions instantiated by the application code (probably bound to a UI control + // such as a button) which have been asked to be bound to a registered graphical action, through + // bindOperationActionGraphicalProperties(). + private final Map>> registeredOperationActions = new HashMap<>(); + // Keeping a list of operation actions whose graphical action are not yet registered => the binding is deferred + // until the graphical action is registered. + private final List notYetBoundExecutableOperationActions = new ArrayList<>(); + + // When the application code wants to be notified when an executable operation action has been bound to its graphical + // properties, it can get an observable which will transit from null to the operation action at that time. + private final Map> executableOperationActionNotifyingProperties = new HashMap<>(); + + private Scheduled bindScheduled; private Consumer operationActionGraphicalPropertiesUpdater; public static OperationActionRegistry getInstance() { - if (INSTANCE == null) - INSTANCE = new OperationActionRegistry(); return INSTANCE; } @@ -80,103 +99,120 @@ public Future apply(Object request) { return AuthorizationUtil.authorizedOperationProperty( operationRequestFactory , embedAuthorizationFunction - , operationActionProperty(operationCode) // reactive property (will change when operation action will be registered, causing a new authorization evaluation) + , executableOperationActionNotifyingProperty(operationCode) // reactive property (will change when operation action will be registered, causing a new authorization evaluation) ); } - public OperationActionRegistry registerOperationGraphicalAction(Class operationRequestClass, Action graphicalAction) { - synchronized (graphicalActions) { - graphicalActions.put(operationRequestClass, graphicalAction); - return checkPendingOperationActionGraphicalBindings(); + private void processRegisteredOperationActions(Object operationCodeOrRequestClass, Consumer processor) { + List> operationActions = registeredOperationActions.get(operationCodeOrRequestClass); + if (operationActions != null) { + for (Iterator> it = operationActions.iterator(); it.hasNext(); ) { + OperationAction oa = it.next().get(); + if (oa == null) { + logDebug(operationCodeOrRequestClass + " operation action was garbage-collected"); + it.remove(); + } else { + processor.accept(oa); + } + } } } - public OperationActionRegistry registerOperationGraphicalAction(Object operationCode, Action graphicalAction) { - synchronized (graphicalActions) { - graphicalActions.put(operationCode, graphicalAction); + public OperationActionRegistry registerOperationGraphicalAction(Object operationCodeOrRequestClass, Action graphicalAction) { + synchronized (registeredGraphicalActions) { + logDebug("Registering " + operationCodeOrRequestClass + " graphical action (" + (registeredGraphicalActions.containsKey(operationCodeOrRequestClass) ? "not" : "") + " first time)"); + registeredGraphicalActions.put(operationCodeOrRequestClass, graphicalAction); + processRegisteredOperationActions(operationCodeOrRequestClass, oa -> { + logDebug(operationCodeOrRequestClass + " operation action will be rebound to new graphical action"); + Collections.addIfNotContains(oa, notYetBoundExecutableOperationActions); + }); return checkPendingOperationActionGraphicalBindings(); } } + private OperationActionRegistry registerOperationAction(Object operationCodeOrRequestClass, OperationAction operationAction) { + synchronized (registeredOperationActions) { + boolean[] alreadyRegistered = { false }; + processRegisteredOperationActions(operationCodeOrRequestClass, oa -> { + if (oa == operationAction) + alreadyRegistered[0] = true; + }); + if (!alreadyRegistered[0]) { + List> operationActions = registeredOperationActions.computeIfAbsent(operationCodeOrRequestClass, k -> new ArrayList<>()); + operationActions.add(new WeakReference<>(operationAction)); + logDebug("Registering " + operationCodeOrRequestClass + " operation action -> n°" + operationActions.size()); + } + return this; + } + } + private OperationActionRegistry checkPendingOperationActionGraphicalBindings() { - if (notYetBoundOperationActions != null && pendingBindRunnable == null) { - UiScheduler.scheduleDeferred(pendingBindRunnable = () -> { - Collection operationActionsToBind = notYetBoundOperationActions; - pendingBindRunnable = null; - notYetBoundOperationActions = null; - for (OperationAction operationAction : operationActionsToBind) - bindOperationActionGraphicalProperties(operationAction); + if (!notYetBoundExecutableOperationActions.isEmpty() && (bindScheduled == null || bindScheduled.isFinished())) { + bindScheduled = UiScheduler.scheduleDeferred(() -> { + notYetBoundExecutableOperationActions.forEach(this::bindOperationActionGraphicalProperties); + notYetBoundExecutableOperationActions.clear(); }); } return this; } - void bindOperationActionGraphicalProperties(OperationAction operationAction) { - if (bindOperationActionGraphicalPropertiesNow(operationAction)) + void bindOperationActionGraphicalProperties(OperationAction executableOperationAction) { + if (bindOperationActionGraphicalPropertiesNow(executableOperationAction)) return; - if (notYetBoundOperationActions == null) - notYetBoundOperationActions = new ArrayList<>(); - notYetBoundOperationActions.add(operationAction); + notYetBoundExecutableOperationActions.add(executableOperationAction); } - private boolean bindOperationActionGraphicalPropertiesNow(OperationAction operationAction) { + private boolean bindOperationActionGraphicalPropertiesNow(OperationAction executableOperationAction) { // The binding is possible only if a graphical action has been registered for that operation // Instantiating an operation request just to have the request class or operation code - A operationRequest = newOperationActionRequest(operationAction); + A operationRequest = newOperationActionRequest(executableOperationAction); + + // Registering the operation action (should it be done only once?) + Class operationRequestClass = operationRequest.getClass(); + registerOperationAction(operationRequestClass, executableOperationAction); + Object operationCode = operationRequest instanceof HasOperationCode ? ((HasOperationCode) operationRequest).getOperationCode() : null; + if (operationCode != null) + registerOperationAction(operationCode, executableOperationAction); + // Then getting the graphical action from it Action graphicalAction = getGraphicalActionFromOperationRequest(operationRequest); // If this is not the case, we return false (can't do the binding now) if (graphicalAction == null) return false; - Map> properties = operationActionProperties; - if (properties != null) { - Object code = getOperationCodeFromGraphicalAction(graphicalAction); - if (code != null) { - ObjectProperty operationActionProperty = properties.remove(code); + // if we reach this point, we can do the binding. + updateOperationActionGraphicalProperties(executableOperationAction); + ActionBinder.bindWritableActionToAction(executableOperationAction, graphicalAction); + // We also notify the application code that we now have an executable operation action associated + if (!executableOperationActionNotifyingProperties.isEmpty()) { + if (operationCode != null) { + ObjectProperty operationActionProperty = executableOperationActionNotifyingProperties.remove(operationCode); if (operationActionProperty != null) - operationActionProperty.set(operationAction); - if (properties.isEmpty()) - operationActionProperties = null; + operationActionProperty.set(executableOperationAction); } } - ActionBinder.bindWritableActionToAction(operationAction, graphicalAction); return true; } - public Action getGraphicalActionFromOperationAction(OperationAction operationAction) { - // Instantiating an operation request just to have the request class or operation code - A operationRequest = newOperationActionRequest(operationAction); - // Then getting the graphical action from it - return getGraphicalActionFromOperationRequest(operationRequest); - } - + // Important: the code calling this method should not store the value, but request it again each time it needs it, + // because the graphical action can change (ex: cache value on application start & then refreshed value from database) public Action getGraphicalActionFromOperationRequest(Object operationRequest) { // Trying to get the operation action registered with the operation request class or code. Action graphicalAction = getGraphicalActionFromOperationRequestClass(operationRequest.getClass()); if (graphicalAction == null && operationRequest instanceof HasOperationCode) graphicalAction = getGraphicalActionFromOperationCode(((HasOperationCode) operationRequest).getOperationCode()); + if (graphicalAction != null && operationRequest instanceof ActionTuner) { + graphicalAction = ((ActionTuner) operationRequest).tuneAction(graphicalAction); + } return graphicalAction; } private Action getGraphicalActionFromOperationRequestClass(Class operationRequestClass) { - synchronized (graphicalActions) { - return graphicalActions.get(operationRequestClass); - } + return getGraphicalActionFromOperationCode(operationRequestClass); // because they share the same map } private Action getGraphicalActionFromOperationCode(Object operationCode) { - synchronized (graphicalActions) { - return graphicalActions.get(operationCode); - } - } - - private Object getOperationCodeFromGraphicalAction(Action graphicalAction) { - synchronized (graphicalActions) { - for (Map.Entry entry : graphicalActions.entrySet()) { - if (entry.getValue() == graphicalAction) - return entry.getKey(); - } - return null; + synchronized (registeredGraphicalActions) { + return registeredGraphicalActions.get(operationCode); } } @@ -189,12 +225,15 @@ void updateOperationActionGraphicalProperties(OperationAction opera operationActionGraphicalPropertiesUpdater.accept(operationAction); } - private ObservableValue operationActionProperty(Object operationCode) { - ObjectProperty operationActionProperty = new SimpleObjectProperty<>(); - if (operationActionProperties == null) - operationActionProperties = new HashMap<>(); - operationActionProperties.put(operationCode, operationActionProperty); - return operationActionProperty; + private ObservableValue executableOperationActionNotifyingProperty(Object operationCode) { + ObjectProperty property = executableOperationActionNotifyingProperties.computeIfAbsent(operationCode, k -> new SimpleObjectProperty<>()); + if (property.get() == null) { + processRegisteredOperationActions(operationCode, oa -> { + if (property.get() == null) + property.set(oa); + }); + } + return property; } public A newOperationActionRequest(OperationAction operationAction) { @@ -206,4 +245,26 @@ public A newOperationActionRequest(OperationAction operationAction) return null; } + public Action getOrWaitOperationAction(Object operationCode) { + // Waiting to be notified + ObservableValue operationActionProperty = executableOperationActionNotifyingProperty(operationCode); + // For now, we create a wrapper action that delegates the execution to the operation action (if set) + WritableAction wrapperAction = new WritableAction(e -> { // Invisible & disabled at this stage + OperationAction operationAction = operationActionProperty.getValue(); + if (operationAction != null) { + operationAction.handle(e); + } + }); + FXProperties.onPropertySet(operationActionProperty, operationAction -> + ActionBinder.bindWritableActionToAction(wrapperAction, operationAction) + ); + return wrapperAction; + } + + private static void logDebug(String message) { + if (LOG_DEBUG) { + Console.log("[OperationActionRegistry] - " + message); + } + } + } diff --git a/webfx-stack-ui-operation-action/src/main/java/module-info.java b/webfx-stack-ui-operation-action/src/main/java/module-info.java index d434dda29..d94c95d13 100644 --- a/webfx-stack-ui-operation-action/src/main/java/module-info.java +++ b/webfx-stack-ui-operation-action/src/main/java/module-info.java @@ -8,10 +8,12 @@ requires webfx.kit.util; requires webfx.platform.async; requires webfx.platform.console; + requires webfx.platform.scheduler; requires webfx.platform.uischeduler; requires webfx.platform.util; requires webfx.stack.authz.client; requires transitive webfx.stack.ui.action; + requires webfx.stack.ui.action.tuner; requires webfx.stack.ui.exceptions; requires webfx.stack.ui.operation; diff --git a/webfx-stack-ui-operation/pom.xml b/webfx-stack-ui-operation/pom.xml index 4d52619b7..719d5fd24 100644 --- a/webfx-stack-ui-operation/pom.xml +++ b/webfx-stack-ui-operation/pom.xml @@ -27,6 +27,12 @@ provided + + dev.webfx + webfx-extras-util-control + 0.1.0-SNAPSHOT + + dev.webfx webfx-kit-util diff --git a/webfx-stack-ui-operation/src/main/java/dev/webfx/stack/ui/operation/OperationUtil.java b/webfx-stack-ui-operation/src/main/java/dev/webfx/stack/ui/operation/OperationUtil.java index 0deb82f94..d2beb5692 100644 --- a/webfx-stack-ui-operation/src/main/java/dev/webfx/stack/ui/operation/OperationUtil.java +++ b/webfx-stack-ui-operation/src/main/java/dev/webfx/stack/ui/operation/OperationUtil.java @@ -1,12 +1,12 @@ package dev.webfx.stack.ui.operation; +import dev.webfx.extras.util.control.ControlUtil; import dev.webfx.kit.util.properties.FXProperties; import dev.webfx.platform.async.AsyncFunction; import dev.webfx.platform.async.Future; import dev.webfx.platform.uischeduler.UiScheduler; import javafx.scene.Node; import javafx.scene.control.Labeled; -import javafx.scene.control.ProgressIndicator; /** * @author Bruno Salmon @@ -48,9 +48,7 @@ private static void setWaitMode(boolean on, Labeled... buttons) { Node graphic = null; if (button == buttons[0]) { if (on) { - ProgressIndicator progressIndicator = new ProgressIndicator(); - progressIndicator.setPrefSize(20, 20); // Note setMaxSize() doesn't work with WebFX but setPrefSize() does - graphic = progressIndicator; + graphic = ControlUtil.createProgressIndicator(20); // Memorising the previous graphic before changing it button.getProperties().put("webfx-operation-util-graphic", button.getGraphic()); } else { diff --git a/webfx-stack-ui-operation/src/main/java/module-info.java b/webfx-stack-ui-operation/src/main/java/module-info.java index ce97e7869..367c34a3b 100644 --- a/webfx-stack-ui-operation/src/main/java/module-info.java +++ b/webfx-stack-ui-operation/src/main/java/module-info.java @@ -5,6 +5,7 @@ // Direct dependencies modules requires javafx.controls; requires javafx.graphics; + requires webfx.extras.util.control; requires webfx.kit.util; requires webfx.platform.async; requires webfx.platform.uischeduler; diff --git a/webfx-stack-ui-validation/pom.xml b/webfx-stack-ui-validation/pom.xml index 9d625dd07..1014ed9ea 100644 --- a/webfx-stack-ui-validation/pom.xml +++ b/webfx-stack-ui-validation/pom.xml @@ -33,6 +33,44 @@ provided + + dev.webfx + webfx-extras-imagestore + 0.1.0-SNAPSHOT + + + + dev.webfx + webfx-extras-util-background + 0.1.0-SNAPSHOT + + + + dev.webfx + webfx-extras-util-border + 0.1.0-SNAPSHOT + + + + dev.webfx + webfx-extras-util-scene + 0.1.0-SNAPSHOT + + + + dev.webfx + webfx-kit-util + 0.1.0-SNAPSHOT + + + + dev.webfx + webfx-platform-javabase-emul-j2cl + 0.1.0-SNAPSHOT + runtime + true + + dev.webfx webfx-platform-javatime-emul-j2cl @@ -41,6 +79,18 @@ true + + dev.webfx + webfx-platform-uischeduler + 0.1.0-SNAPSHOT + + + + dev.webfx + webfx-platform-util + 0.1.0-SNAPSHOT + + \ No newline at end of file diff --git a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/ValidationIcons.java b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/ValidationIcons.java new file mode 100644 index 000000000..5ef3b7991 --- /dev/null +++ b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/ValidationIcons.java @@ -0,0 +1,11 @@ +package dev.webfx.stack.ui.validation; + +/** + * @author Bruno Salmon + */ +final class ValidationIcons { + + public final static String validationErrorIcon16Url = "dev/webfx/stack/ui/validation/controlsfx/images/decoration-error.png"; + public final static String validationRequiredIcon16Url = "dev/webfx/stack/ui/validation/controlsfx/images/required-indicator.png"; + +} diff --git a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/ValidationSupport.java b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/ValidationSupport.java new file mode 100644 index 000000000..bb2f74b8b --- /dev/null +++ b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/ValidationSupport.java @@ -0,0 +1,558 @@ +package dev.webfx.stack.ui.validation; + +import dev.webfx.extras.imagestore.ImageStore; +import dev.webfx.extras.util.background.BackgroundFactory; +import dev.webfx.extras.util.border.BorderFactory; +import dev.webfx.extras.util.scene.SceneUtil; +import dev.webfx.kit.util.properties.FXProperties; +import dev.webfx.platform.uischeduler.UiScheduler; +import dev.webfx.platform.util.collection.Collections; +import dev.webfx.stack.ui.validation.controlsfx.control.decoration.Decoration; +import dev.webfx.stack.ui.validation.controlsfx.control.decoration.GraphicDecoration; +import dev.webfx.stack.ui.validation.controlsfx.validation.decoration.GraphicValidationDecoration; +import dev.webfx.stack.ui.validation.mvvmfx.ObservableRuleBasedValidator; +import dev.webfx.stack.ui.validation.mvvmfx.ValidationMessage; +import dev.webfx.stack.ui.validation.mvvmfx.Validator; +import dev.webfx.stack.ui.validation.mvvmfx.visualization.ControlsFxVisualizer; +import javafx.application.Platform; +import javafx.beans.binding.Bindings; +import javafx.beans.property.*; +import javafx.beans.value.ObservableBooleanValue; +import javafx.beans.value.ObservableStringValue; +import javafx.beans.value.ObservableValue; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Group; +import javafx.scene.Node; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.scene.shape.Rectangle; +import javafx.scene.text.Font; +import javafx.scene.transform.Rotate; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.regex.Pattern; + +/** + * @author Bruno Salmon + */ +public final class ValidationSupport { + + private static final ObservableStringValue DEFAULT_REQUIRED_MESSAGE = new SimpleStringProperty("This field is required"); + + private final List validators = new ArrayList<>(); + private final List validatorErrorDecorationNodes = new ArrayList<>(); + private final BooleanProperty validatingProperty = new SimpleBooleanProperty(); + private Node popOverContentNode; + private Node popOverOwnerNode; + + public boolean isValid() { + validatingProperty.setValue(false); + validatingProperty.setValue(true); + Validator firstInvalidValidator = firstInvalidValidator(); + if (firstInvalidValidator != null) + Platform.runLater(() -> { + popUpOverAutoScroll = true; + showValidatorErrorPopOver(firstInvalidValidator); + }); + return firstInvalidValidator == null; + } + + public void reset() { + // This will hide the possible validation error popup and other warning icons + validatingProperty.setValue(false); + } + + public void clear() { + reset(); // hiding possible validation messages + validatorErrorDecorationNodes.forEach(this::uninstallNodeDecorator); + validatorErrorDecorationNodes.clear(); + validators.forEach(validator -> { + if (validator instanceof ObservableRuleBasedValidator) { + ((ObservableRuleBasedValidator) validator).clear(); + } + }); + validators.clear(); + } + + public boolean isEmpty() { + return validators.isEmpty(); + } + + private Validator firstInvalidValidator() { + return Collections.findFirst(validators, validator -> !validator.getValidationStatus().isValid()); + } + + public void addRequiredInputs(TextInputControl... textInputControls) { + for (TextInputControl textInputControl : textInputControls) + addRequiredInput(textInputControl); + } + + public void addRequiredInput(TextInputControl textInputControl) { + addRequiredInput(textInputControl, DEFAULT_REQUIRED_MESSAGE); + } + + public void addRequiredInput(TextInputControl textInputControl, ObservableStringValue errorMessage) { + addRequiredInput(textInputControl.textProperty(), textInputControl, errorMessage); + } + + public void addRequiredInput(ObservableValue valueProperty, Node inputNode) { + addRequiredInput(valueProperty, inputNode, DEFAULT_REQUIRED_MESSAGE); + } + + public void addRequiredInput(ObservableValue valueProperty, Node inputNode, ObservableStringValue errorMessage) { + addValidationRule(Bindings.createBooleanBinding(() -> testNotEmpty(valueProperty.getValue()), valueProperty), inputNode, errorMessage, true); + } + + private static boolean testNotEmpty(Object value) { + return value != null && (!(value instanceof String) || !((String) value).trim().isEmpty()); + } + + public ObservableBooleanValue addValidationRule(ObservableValue validProperty, Node node, ObservableStringValue errorMessage) { + return addValidationRule(validProperty, node, errorMessage, false); + } + + public ObservableBooleanValue addValidationRule(ObservableValue validProperty, Node node, ObservableStringValue errorMessageProperty, boolean required) { + ObservableRuleBasedValidator validator = new ObservableRuleBasedValidator(); + ObservableBooleanValue finalValidProperty = // when true, no decorations are displayed on the node + Bindings.createBooleanBinding(() -> + !validatingProperty.get() // no decoration displayed if not validation + || validProperty.getValue() // no decoration displayed if the node is valid + || !isShowing(node) // no decoration displayed if the node is not showing + , validProperty, validatingProperty); + ValidationMessage errorValidationMessage = ValidationMessage.error(errorMessageProperty); + validator.addRule(finalValidProperty, errorValidationMessage); + validators.add(validator); + validatorErrorDecorationNodes.add(node); + installNodeDecorator(node, validator, required); + // The following code is to remove the error message after being displayed and the user is re-typing + if (node instanceof TextInputControl) { + FXProperties.runOnPropertiesChange( + () -> { + validator.validateBooleanRule(true, errorValidationMessage); + uninstallNodeDecorator(node); + } + , ((TextInputControl) node).textProperty()); // ex of dependencies: textField.textProperty() + } + return finalValidProperty; + } + + private void installNodeDecorator(Node node, ObservableRuleBasedValidator validator, boolean required) { + if (node instanceof Control) { + Control control = (Control) node; + ControlsFxVisualizer validationVisualizer = new ControlsFxVisualizer(); + validationVisualizer.setDecoration(new GraphicValidationDecoration() { + @Override + protected Node createErrorNode() { + return ImageStore.createImageView(ValidationIcons.validationErrorIcon16Url); + } + + @Override + protected Collection createValidationDecorations(dev.webfx.stack.ui.validation.controlsfx.validation.ValidationMessage message) { + boolean isTextInput = node instanceof TextInputControl; + boolean isButton = node instanceof Button; + // isInside flag will determine if we position the decoration inside the node or not (ie outside) + boolean isInside; + if (isTextInput) // inside for text inputs + isInside = true; + else { // for others, will be generally outside unless it is stretched to full width by its container + Parent parent = node.getParent(); + while (parent instanceof Pane && !(parent instanceof VBox) && !(parent instanceof HBox)) + parent = parent.getParent(); + isInside = parent instanceof VBox && ((VBox) parent).isFillWidth(); + } + double xRelativeOffset = isInside ? -1 : 1; // positioning the decoration inside the control for button and text input + double xOffset = isInside && isButton ? -20 : 0; // moving the decoration before the drop down arrow + return java.util.Collections.singletonList( + new GraphicDecoration(createDecorationNode(message), + Pos.CENTER_RIGHT, + xOffset, + 0, + xRelativeOffset, + 0) + ); + } + + @Override + protected Collection createRequiredDecorations(Control target) { + return java.util.Collections.singletonList( + new GraphicDecoration(ImageStore.createImageView(ValidationIcons.validationRequiredIcon16Url), + Pos.CENTER_LEFT, + -10, + 0)); + } + }); + validationVisualizer.initVisualization(validator.getValidationStatus(), control, required); + node.getProperties().put("validationVisualizer", validationVisualizer); + } + } + + private void uninstallNodeDecorator(Node node) { + if (node instanceof Control) { + Control control = (Control) node; + ControlsFxVisualizer validationVisualizer = (ControlsFxVisualizer) node.getProperties().get("validationVisualizer"); + if (validationVisualizer != null) + validationVisualizer.removeDecorations(control); + } + } + + private void showValidatorErrorPopOver(Validator validator) { + int index = validators.indexOf(validator); + if (index >= 0) { + Node decorationNode = validatorErrorDecorationNodes.get(index); + if (decorationNode != null) + showValidatorErrorPopOver(validator, decorationNode); + } + } + + private void showValidatorErrorPopOver(Validator validator, Node errorDecorationNode) { + ValidationMessage errorMessage = Collections.first(validator.getValidationStatus().getErrorMessages()); + if (errorMessage != null) { + Label label = new Label(); + label.textProperty().bind(errorMessage.messageProperty()); + label.setPadding(new Insets(8)); + label.setFont(Font.font("Verdana", 11.5)); + label.setTextFill(Color.WHITE); + label.setBackground(BackgroundFactory.newBackground(Color.RED, 5, 2)); + label.setBorder(BorderFactory.newBorder(Color.WHITE, 5, 2)); + Rectangle diamond = new Rectangle(10, 10, Color.RED); + diamond.getTransforms().add(new Rotate(45, 5, 5)); + diamond.layoutYProperty().bind(FXProperties.compute(label.heightProperty(), n -> n.doubleValue() - 7)); + diamond.setLayoutX(20d); + popOverContentNode = new Group(label, diamond); + //popOverContentNode.setOpacity(0.75); + //popOverContentNode.setEffect(new DropShadow()); + showPopOver(errorDecorationNode); + // Removing the error pop over when the status is valid again + FXProperties.runOrUnregisterOnPropertyChange((thisListener, oldValue, valid) -> { + if (valid) { + thisListener.unregister(); + popOverOwnerNode = null; + hidePopOver(); + } + }, validator.getValidationStatus().validProperty()); + } + } + + private void showPopOver(Node node) { + popOverOwnerNode = node; + showPopOverNow(); + if (!node.getProperties().containsKey("popOverListen")) { + node.getProperties().put("popOverListen", true); + node.sceneProperty().addListener(observable -> { + if (popOverOwnerNode == node) { + showPopOverNow(); + } + }); + node.parentProperty().addListener(observable -> { + if (popOverOwnerNode == node) { + showPopOverNow(); + } + }); + } + } + + public void addEmailValidation(TextField emailInput, Node where, ObservableStringValue errorMessage) { + // Define the email pattern + String emailPattern = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}$"; + Pattern pattern = Pattern.compile(emailPattern); + // Create the validation rule + addValidationRule( + Bindings.createBooleanBinding( + () -> pattern.matcher(emailInput.getText()).matches(), + emailInput.textProperty() + ), + where, + errorMessage + ); + } + + public void addEmailNotEqualValidation(TextField emailInput, String forbiddenValue, Node where, ObservableStringValue errorMessage) { + // Create the validation rule + addValidationRule( + Bindings.createBooleanBinding( + () -> !emailInput.getText().equalsIgnoreCase(forbiddenValue), + emailInput.textProperty() + ), + where, + errorMessage + ); + } + + public void addUrlValidation(TextField urlInput, Node where, ObservableStringValue errorMessage) { + // Define the URL pattern (basic) + String urlPattern = "^(https?://)(www\\.)?[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}(/.*)?$"; + Pattern pattern = Pattern.compile(urlPattern); + + addValidationRule( + Bindings.createBooleanBinding( + () -> { + String input = urlInput.getText(); + return input!=null && pattern.matcher(input).matches(); // Accept empty or valid URL + }, + urlInput.textProperty() + ), + where, + errorMessage + ); + } + + public void addMinimumDurationValidation(TextField timeInput, Node where, ObservableStringValue errorMessage) { + addValidationRule( + Bindings.createBooleanBinding( + () -> { + try { + String input = timeInput.getText(); + if (input == null || input.isEmpty()) return true; // Allow empty input + int value = Integer.parseInt(input); // Try to parse the input to an integer + return value >= 60; // Validate if the integer is at least 60 + } catch (NumberFormatException e) { + return false; // Invalid if input is not a valid integer + } + }, + timeInput.textProperty() + ), + where, + errorMessage + ); + } + + + public void addUrlOrEmptyValidation(TextField urlInput, ObservableStringValue errorMessage) { + // Define the URL pattern (basic) + String urlPattern = "^(https?|srt|rtmp|rtsp)://[\\w.-]+(:\\d+)?(/[\\w./-]*)?(\\?[\\w=&%.-]*)?(#[\\w!:.=&,-]*)?$"; + Pattern pattern = Pattern.compile(urlPattern); + + // Create the validation rule + addValidationRule( + Bindings.createBooleanBinding( + () -> { + String input = urlInput.getText(); + return input.isEmpty() || pattern.matcher(input).matches(); // Accept empty or valid URL + }, + urlInput.textProperty() + ), + urlInput, + errorMessage + ); + } + + public void addNonEmptyValidation(TextField textField, Node where, ObservableStringValue errorMessage) { + // Create the validation rule + addValidationRule( + Bindings.createBooleanBinding( + () -> !textField.getText().trim().isEmpty(), + textField.textProperty() + ), + where, + errorMessage); + } + + public void addDateValidation(TextField textField,String dateFormat, Node where, ObservableStringValue errorMessage) { + DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(dateFormat); + // Create the validation rule + addValidationRule( + Bindings.createBooleanBinding(() -> { + try { + dateFormatter.parse(textField.getText().trim()); + return true; + } catch (DateTimeParseException e) { + return false; + }}, textField.textProperty()), + where, + errorMessage + ); + } + + public void addDateOrEmptyValidation(TextField textField, String dateFormat, Node where, ObservableStringValue errorMessage) { + DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(dateFormat); + + // Create the validation rule + addValidationRule( + Bindings.createBooleanBinding(() -> { + String input = textField.getText().trim(); + // Allow empty input to be valid + if (input.isEmpty()) { + return true; + } + try { + dateFormatter.parse(input); + return true; // Input is valid if it can be parsed + } catch (DateTimeParseException e) { + return false; // Invalid date format + } + }, textField.textProperty()), + where, + errorMessage + ); + } + + public void addIntegerValidation(TextField textField, Node where, ObservableStringValue errorMessage) { + // Create the validation rule + addValidationRule( + Bindings.createBooleanBinding(() -> { + try { + // Try to parse the text as an integer + Integer.parseInt(textField.getText().trim()); + return true; + } catch (NumberFormatException e) { + return false; + } + }, textField.textProperty()), + where, + errorMessage + ); + } + + public void addLegalAgeValidation(TextField textField, String dateFormat, int legalAge, Node where, ObservableStringValue errorMessage) { + DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(dateFormat); + // Create the validation rule + addValidationRule( + Bindings.createBooleanBinding(() -> { + try { + LocalDate birthDate = LocalDate.parse(textField.getText().trim(), dateFormatter); + LocalDate now = LocalDate.now(); + return birthDate.plusYears(legalAge).isBefore(now) || birthDate.plusYears(legalAge).isEqual(now); + } catch (DateTimeParseException e) { + return false; + } + }, textField.textProperty()), + where, + errorMessage + ); + } + + public void addPasswordMatchValidation(TextField passwordField, TextField repeatPasswordField, ObservableStringValue errorMessageProperty) { + addValidationRule( + Bindings.createBooleanBinding( + () -> passwordField.getText().equals(repeatPasswordField.getText()), + passwordField.textProperty(), + repeatPasswordField.textProperty() + ), + repeatPasswordField, + errorMessageProperty, + true + ); + } + + public void addPasswordStrengthValidation(TextField passwordField, ObservableStringValue errorMessage) { + addValidationRule( + Bindings.createBooleanBinding( + () -> checkPasswordStrength(passwordField.getText()), + passwordField.textProperty() + ), + passwordField, + errorMessage, + true + ); + } + /** + * Checks if a password meets strength requirements. + * @param password the password to validate. + * @return true if the password meets the requirements, false otherwise. + */ + private boolean checkPasswordStrength(String password) { + if (password == null || password.isEmpty()) { + return false; + } + // Minimum length + if (password.length() < 8) { + return false; + } + // Contains at least one uppercase letter + if (!password.matches(".*[A-Z].*")) { + return false; + } + // Contains at least one lowercase letter + if (!password.matches(".*[a-z].*")) { + return false; + } + // Contains at least one digit + if (!password.matches(".*\\d.*")) { + return false; + } + // Contains at least one special character + if (!password.matches(".*[@#$%^&+=!().,*?-].*")) { + return false; + } + return true; + } + +/* + private PopOver popOver; + private void showPopOverNow() { + if (popOver != null && popOver.getOwnerNode() != popOverOwnerNode) { + popOver.hide(); + popOver = null; + } + if (popOver == null && isShowing(popOverOwnerNode)) { + popOver = new PopOver(); + popOver.setContentNode(popOverContentNode); + popOver.setArrowLocation(PopOver.ArrowLocation.BOTTOM_LEFT); + //Platform.runLater(() -> { + popOver.show(popOverOwnerNode, -(popOverOwnerNode instanceof ImageView ? ((ImageView) popOverOwnerNode).getImage().getHeight() : 0) + 4); + //}); + } + } +*/ + + private GraphicDecoration popOverDecoration; + private Node popOverDecorationTarget; + private boolean popUpOverAutoScroll; + + private void showPopOverNow() { + Platform.runLater(() -> { + hidePopOver(); + if (isShowing(popOverOwnerNode)) { + popOverDecorationTarget = popOverOwnerNode; + popOverDecoration = new GraphicDecoration(popOverContentNode, 0, -1, 0, -1); + popOverDecoration.applyDecoration(popOverDecorationTarget); + if (popUpOverAutoScroll) { + SceneUtil.scrollNodeToBeVerticallyVisibleOnScene(popOverDecorationTarget); + SceneUtil.autoFocusIfEnabled(popOverOwnerNode); + popUpOverAutoScroll = false; + } + } + }); + } + + private void hidePopOver() { + UiScheduler.runInUiThread(() -> { + if (popOverDecoration != null) { + popOverDecoration.removeDecoration(popOverDecorationTarget); + popOverDecoration = null; + } + }); + } + + private static boolean isShowing(Node node) { + if (node == null || !node.isVisible()) + return false; + Parent parent = node.getParent(); + if (parent != null) + return isShowing(parent); + Scene scene = node.getScene(); + return scene != null && scene.getRoot() == node; + } + + public void addPasswordValidation(TextField passwordInput, Label passwordLabel, ObservableStringValue errorMessage) { + addValidationRule( + Bindings.createBooleanBinding( + () -> passwordInput.getText().length() >= 8, + passwordInput.textProperty() + ), + passwordLabel, + errorMessage + ); + } +} diff --git a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/controlsfx/validation/ValidationSupport.java b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/controlsfx/validation/ControlsFxValidationSupport.java similarity index 99% rename from webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/controlsfx/validation/ValidationSupport.java rename to webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/controlsfx/validation/ControlsFxValidationSupport.java index e6c9d2396..ac68b6876 100644 --- a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/controlsfx/validation/ValidationSupport.java +++ b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/controlsfx/validation/ControlsFxValidationSupport.java @@ -76,7 +76,7 @@ * * */ -public class ValidationSupport { +public class ControlsFxValidationSupport { private static final String CTRL_REQUIRED_FLAG = "$org.controlsfx.validation.required$"; //$NON-NLS-1$ @@ -111,7 +111,7 @@ public static boolean isRequired( Control c ) { * Creates validation support instance.
* If initial decoration is desired invoke {@link #initInitialDecoration()}. */ - public ValidationSupport() { + public ControlsFxValidationSupport() { validationResultProperty().addListener( (o, oldValue, validationResult) -> { invalidProperty.set(!validationResult.getErrors().isEmpty()); diff --git a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/controlsfx/validation/ValidationMessage.java b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/controlsfx/validation/ValidationMessage.java index 45b699d6f..3ad4a0edb 100644 --- a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/controlsfx/validation/ValidationMessage.java +++ b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/controlsfx/validation/ValidationMessage.java @@ -35,28 +35,24 @@ */ public interface ValidationMessage extends Comparable{ - public static final Comparator COMPARATOR = new Comparator() { - - @Override - public int compare(ValidationMessage vm1, ValidationMessage vm2) { - if ( vm1 == vm2 ) return 0; - if ( vm1 == null) return 1; - if ( vm2 == null) return -1; - return vm1.compareTo(vm2); - } - }; + Comparator COMPARATOR = (vm1, vm2) -> { + if ( vm1 == vm2 ) return 0; + if ( vm1 == null) return 1; + if ( vm2 == null) return -1; + return vm1.compareTo(vm2); + }; /** * Message text * @return message text */ - public String getText(); + String getText(); /** * Message {@link Severity} * @return message severity */ - public Severity getSeverity(); + Severity getSeverity(); /** @@ -71,7 +67,7 @@ public int compare(ValidationMessage vm1, ValidationMessage vm2) { * @param text message text * @return error message */ - public static ValidationMessage error(Control target, String text) { + static ValidationMessage error(Control target, String text) { return new SimpleValidationMessage(target, text, Severity.ERROR); } @@ -81,11 +77,11 @@ public static ValidationMessage error(Control target, String text) { * @param text message text * @return warning message */ - public static ValidationMessage warning(Control target, String text) { + static ValidationMessage warning(Control target, String text) { return new SimpleValidationMessage(target, text, Severity.WARNING); } - @Override default public int compareTo(ValidationMessage msg) { + @Override default int compareTo(ValidationMessage msg) { return msg == null || getTarget() != msg.getTarget() ? -1: getSeverity().compareTo(msg.getSeverity()); } } diff --git a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/controlsfx/validation/decoration/AbstractValidationDecoration.java b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/controlsfx/validation/decoration/AbstractValidationDecoration.java index a2011dd5d..8a9685232 100644 --- a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/controlsfx/validation/decoration/AbstractValidationDecoration.java +++ b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/controlsfx/validation/decoration/AbstractValidationDecoration.java @@ -1,18 +1,18 @@ /** * Copyright (c) 2014, 2015, ControlsFX * All rights reserved. - * + *

* Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * * Redistributions of source code must retain the above copyright + * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above copyright + * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. - * * Neither the name of ControlsFX, any associated website, nor the + * * Neither the name of ControlsFX, any associated website, nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. - * + *

* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -30,7 +30,7 @@ import dev.webfx.stack.ui.validation.controlsfx.control.decoration.Decorator; import dev.webfx.stack.ui.validation.controlsfx.validation.ValidationMessage; import javafx.scene.control.Control; -import dev.webfx.stack.ui.validation.controlsfx.validation.ValidationSupport; +import dev.webfx.stack.ui.validation.controlsfx.validation.ControlsFxValidationSupport; import java.util.Collection; import java.util.List; @@ -42,67 +42,69 @@ * how 'validation' and 'required' decorations should be created *
* See {@link GraphicValidationDecoration} for examples of such implementations. - * + * */ public abstract class AbstractValidationDecoration implements ValidationDecoration { - - private static final String VALIDATION_DECORATION = "$org.controlsfx.decoration.vaidation$"; //$NON-NLS-1$ - - private static boolean isValidationDecoration( Decoration decoration) { + + private static final String VALIDATION_DECORATION = "$org.controlsfx.decoration.validation$"; //$NON-NLS-1$ + + private static boolean isValidationDecoration(Decoration decoration) { return decoration != null && decoration.getProperties().get(VALIDATION_DECORATION) == Boolean.TRUE; } - private static void setValidationDecoration( Decoration decoration ) { - if ( decoration != null ) { + private static void setValidationDecoration(Decoration decoration) { + if (decoration != null) { decoration.getProperties().put(VALIDATION_DECORATION, Boolean.TRUE); } } - protected abstract Collection createValidationDecorations(ValidationMessage message); - protected abstract Collection createRequiredDecorations(Control target); - - /** - * Removes all validation related decorations from the target - * @param target control - */ - @Override - public void removeDecorations(Control target) { - List decorations = Decorator.getDecorations(target); - if ( decorations != null ) { - // conversion to array is a trick to prevent concurrent modification exception - for ( Decoration d: Decorator.getDecorations(target).toArray(new Decoration[0]) ) { - if (isValidationDecoration(d)) Decorator.removeDecoration(target, d); + protected abstract Collection createValidationDecorations(ValidationMessage message); + + protected abstract Collection createRequiredDecorations(Control target); + + /** + * Removes all validation related decorations from the target + * @param target control + */ + @Override + public void removeDecorations(Control target) { + List decorations = Decorator.getDecorations(target); + if (decorations != null) { + // conversion to array is a trick to prevent concurrent modification exception + for (Decoration d : Decorator.getDecorations(target).toArray(new Decoration[0])) { + if (isValidationDecoration(d)) + Decorator.removeDecoration(target, d); } } - } - - /* - * (non-Javadoc) - * @see org.controlsfx.validation.decorator.ValidationDecorator#applyValidationDecoration(org.controlsfx.validation.ValidationMessage) - */ - @Override - public void applyValidationDecoration(ValidationMessage message) { - //createValidationDecorations(message).stream().forEach( d -> decorate( message.getTarget(), d )); // stream don't work on Android - for (Decoration decoration : createValidationDecorations(message)) - decorate(message.getTarget(), decoration); - } + } + + /* + * (non-Javadoc) + * @see org.controlsfx.validation.decorator.ValidationDecorator#applyValidationDecoration(org.controlsfx.validation.ValidationMessage) + */ + @Override + public void applyValidationDecoration(ValidationMessage message) { + //createValidationDecorations(message).stream().forEach( d -> decorate( message.getTarget(), d )); // stream don't work on Android + for (Decoration decoration : createValidationDecorations(message)) + decorate(message.getTarget(), decoration); + } - /* - * (non-Javadoc) - * @see org.controlsfx.validation.decorator.ValidationDecorator#applyRequiredDecoration(javafx.scene.control.Control) - */ - @Override - public void applyRequiredDecoration(Control target) { - if ( ValidationSupport.isRequired(target)) { - // createRequiredDecorations(target).stream().forEach( d -> decorate( target, d )); // stream don't work on Android - for (Decoration decoration : createRequiredDecorations(target)) - decorate(target, decoration); - } - } - - private void decorate( Control target, Decoration d ) { - setValidationDecoration(d); // mark as validation specific decoration + /* + * (non-Javadoc) + * @see org.controlsfx.validation.decorator.ValidationDecorator#applyRequiredDecoration(javafx.scene.control.Control) + */ + @Override + public void applyRequiredDecoration(Control target) { + if (ControlsFxValidationSupport.isRequired(target)) { + // createRequiredDecorations(target).stream().forEach( d -> decorate( target, d )); // stream don't work on Android + for (Decoration decoration : createRequiredDecorations(target)) + decorate(target, decoration); + } + } + + private void decorate(Control target, Decoration d) { + setValidationDecoration(d); // mark as validation specific decoration Decorator.addDecoration(target, d); - } + } } diff --git a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/ObservableRuleBasedValidator.java b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/ObservableRuleBasedValidator.java index 8b5cb2aae..e72274e28 100644 --- a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/ObservableRuleBasedValidator.java +++ b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/ObservableRuleBasedValidator.java @@ -15,6 +15,8 @@ ******************************************************************************/ package dev.webfx.stack.ui.validation.mvvmfx; +import dev.webfx.kit.util.properties.FXProperties; +import dev.webfx.kit.util.properties.Unregisterable; import javafx.beans.value.ObservableValue; import java.util.ArrayList; @@ -32,7 +34,7 @@ * * *

Boolean Rules of type ObservableValue<Boolean>

- * + *

* The first variant uses an {@link ObservableValue} together with a static message. If this observable * has a value of false the validation status will be "invalid" and the given message will be present in the {@link ValidationStatus} * of this validator. @@ -40,35 +42,27 @@ * * *

Complex Rules of type ObservableValue<ValidationMessage>

- * + *

* The second variant allows more complex rules. It uses a {@link ObservableValue} as rule. * If this observable has a value other then null it is considered to be invalid. The {@link ValidationMessage} * value will be present in the {@link ValidationStatus} of this validator. * *

- * + *

* You can add multiple rules via the {@link #addRule(ObservableValue, ValidationMessage)} and {@link #addRule(ObservableValue)} method. * If multiple rules are violated, each message will be present. */ public class ObservableRuleBasedValidator implements Validator { - /* - These lists are used to store the rules that this validator is working with. - The reason for this is to prevent problems with garbage collection. - If the validator wouldn't keep references to all given rules it would be possible that they are - removed by the garbage collector which would result in wrong validation results. - To prevent this we store references to all given rules here. - Don't get confused by the fact that no values are only added to these lists but not obtained. - */ - private List> booleanRules = new ArrayList<>(); - private List> complexRules = new ArrayList<>(); - - private ValidationStatus validationStatus = new ValidationStatus(); + private final List unregisterableRules = new ArrayList<>(); + + private final ValidationStatus validationStatus = new ValidationStatus(); /** * Creates an instance of the Validator without any rules predefined. */ - public ObservableRuleBasedValidator() {} + public ObservableRuleBasedValidator() { + } /** * Creates an instance of the Validator with the given rule predefined. @@ -86,50 +80,49 @@ public ObservableRuleBasedValidator(ObservableValue rule, ValidationMes * Creates an instance of the Validator with the given complex rules predefined. * It's a shortcut for creating an empty validator and * adding one or multiple complex rules with {@link #addRule(ObservableValue)}. + * * @param rules */ - public ObservableRuleBasedValidator(ObservableValue ... rules) { + public ObservableRuleBasedValidator(ObservableValue... rules) { for (ObservableValue rule : rules) { addRule(rule); } } - /** - * Add a rule for this validator. - *

- * The rule defines a condition that has to be fulfilled. - *

- * A rule is defined by an observable boolean value. If the rule has a value of true the rule is - * "fulfilled". If the rule has a value of false the rule is violated. In this case the given message - * object will be added to the status of this validator. - *

- * There are some predefined rules for common use cases in the {@link ObservableRules} class that can be used. - * - * @param rule - * @param message - */ - public void addRule(ObservableValue rule, ValidationMessage message) { - booleanRules.add(rule); - - rule.addListener((observable, oldValue, newValue) -> { - validateBooleanRule(newValue, message); - }); - - validateBooleanRule(rule.getValue(), message); - } - - private void validateBooleanRule(boolean isValid, ValidationMessage message) { - if (isValid) { - validationStatus.removeMessage(message); - } else { - validationStatus.addMessage(message); - } - } - - @Override - public ValidationStatus getValidationStatus() { - return validationStatus; - } + /** + * Add a rule for this validator. + *

+ * The rule defines a condition that has to be fulfilled. + *

+ * A rule is defined by an observable boolean value. If the rule has a value of true the rule is + * "fulfilled". If the rule has a value of false the rule is violated. In this case the given message + * object will be added to the status of this validator. + *

+ * There are some predefined rules for common use cases in the {@link ObservableRules} class that can be used. + * + * @param validProperty + * @param message + */ + public void addRule(ObservableValue validProperty, ValidationMessage message) { + unregisterableRules.add( + FXProperties.runNowAndOnPropertyChange((observable, oldValue, newValue) -> + validateBooleanRule(newValue, message) + , validProperty) + ); + } + + public void validateBooleanRule(boolean isValid, ValidationMessage message) { + if (isValid) { + hideMessage(message); + } else { + showMessage(message); + } + } + + @Override + public ValidationStatus getValidationStatus() { + return validationStatus; + } /** * Add a complex rule for this validator. @@ -140,26 +133,37 @@ public ValidationStatus getValidationStatus() { * If the observable doesn't contain a value (in other words it contains null) the rule is considered to * be fulfilled and the validation status of this validator will be valid (given that no other rule is violated). *

- * - * This method allows some more complex rules compared to {@link #addRule(ObservableValue, ValidationMessage)}. - * This way you can define rules that have different messages for specific cases. + *

+ * This method allows some more complex rules compared to {@link #addRule(ObservableValue, ValidationMessage)}. + * This way you can define rules that have different messages for specific cases. * * @param rule */ public void addRule(ObservableValue rule) { - complexRules.add(rule); - - rule.addListener((observable, oldValue, newValue) -> { - if(oldValue != null) { - validationStatus.removeMessage(oldValue); - } - if(newValue != null) { - validationStatus.addMessage(newValue); - } - }); - - if(rule.getValue() != null) { - validationStatus.addMessage(rule.getValue()); + unregisterableRules.add( + FXProperties.runNowAndOnPropertyChange((observable, oldValue, newValue) -> { + hideMessage(oldValue); // does nothing if oldValue is null + showMessage(newValue); // does nothing if newValue is null + }, rule) + ); + } + + private void showMessage(ValidationMessage message) { + if (message != null) { + validationStatus.addMessage(message); + } + } + + private void hideMessage(ValidationMessage message) { + if (message != null) { + validationStatus.removeMessage(message); } } + + public void clear() { + unregisterableRules.forEach(Unregisterable::unregister); + unregisterableRules.clear(); + validationStatus.clearMessages(); + } + } diff --git a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/ValidationMessage.java b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/ValidationMessage.java index 57c3b90e3..9ece55d7b 100644 --- a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/ValidationMessage.java +++ b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/ValidationMessage.java @@ -15,6 +15,8 @@ ******************************************************************************/ package dev.webfx.stack.ui.validation.mvvmfx; +import javafx.beans.value.ObservableStringValue; + import java.util.Objects; /** @@ -25,26 +27,30 @@ */ public class ValidationMessage { - private final String message; + private final ObservableStringValue messageProperty; private final Severity severity; - public ValidationMessage(Severity severity, String message) { + public ValidationMessage(Severity severity, ObservableStringValue messageProperty) { this.severity = Objects.requireNonNull(severity); - this.message = Objects.requireNonNull(message); + this.messageProperty = Objects.requireNonNull(messageProperty); } - public static ValidationMessage warning(String message) { - return new ValidationMessage(Severity.WARNING, message); + public static ValidationMessage warning(ObservableStringValue messageProperty) { + return new ValidationMessage(Severity.WARNING, messageProperty); } - public static ValidationMessage error(String message) { - return new ValidationMessage(Severity.ERROR, message); + public static ValidationMessage error(ObservableStringValue messageProperty) { + return new ValidationMessage(Severity.ERROR, messageProperty); } + public ObservableStringValue messageProperty() { + return messageProperty; + } + public String getMessage() { - return message; + return messageProperty.get(); } public Severity getSeverity() { @@ -54,27 +60,27 @@ public Severity getSeverity() { @Override public String toString() { return "ValidationMessage{" + - "message='" + message + '\'' + - ", severity=" + severity + - '}'; + "messageProperty='" + messageProperty + '\'' + + ", severity=" + severity + + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || !(o instanceof ValidationMessage)) + if (!(o instanceof ValidationMessage)) return false; ValidationMessage that = (ValidationMessage) o; - return message.equals(that.message) && severity == that.severity; + return messageProperty.equals(that.messageProperty) && severity == that.severity; } @Override public int hashCode() { - int result = message.hashCode(); + int result = messageProperty.hashCode(); result = 31 * result + severity.hashCode(); return result; } diff --git a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/ValidationStatus.java b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/ValidationStatus.java index d1f82602e..82d4062fb 100644 --- a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/ValidationStatus.java +++ b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/ValidationStatus.java @@ -37,11 +37,11 @@ */ public class ValidationStatus { - private ListProperty messages = new SimpleListProperty<>(FXCollections.observableArrayList()); + private final ListProperty messages = new SimpleListProperty<>(FXCollections.observableArrayList()); - private ObservableList unmodifiableMessages = FXCollections.unmodifiableObservableList(messages); - private ObservableList errorMessages = new FilteredList<>(unmodifiableMessages, message -> message.getSeverity().equals(Severity.ERROR)); - private ObservableList warningMessages = new FilteredList<>(unmodifiableMessages, message -> message.getSeverity().equals(Severity.WARNING)); + private final ObservableList unmodifiableMessages = FXCollections.unmodifiableObservableList(messages); + private final ObservableList errorMessages = new FilteredList<>(unmodifiableMessages, message -> message.getSeverity().equals(Severity.ERROR)); + private final ObservableList warningMessages = new FilteredList<>(unmodifiableMessages, message -> message.getSeverity().equals(Severity.WARNING)); /** * Intended for subclasses used by special validator implementations. diff --git a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/visualization/ControlsFxVisualizer.java b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/visualization/ControlsFxVisualizer.java index a3e093bd9..6ede92357 100644 --- a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/visualization/ControlsFxVisualizer.java +++ b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/visualization/ControlsFxVisualizer.java @@ -17,7 +17,7 @@ import dev.webfx.stack.ui.validation.mvvmfx.Severity; import dev.webfx.stack.ui.validation.mvvmfx.ValidationMessage; -import dev.webfx.stack.ui.validation.controlsfx.validation.ValidationSupport; +import dev.webfx.stack.ui.validation.controlsfx.validation.ControlsFxValidationSupport; import dev.webfx.stack.ui.validation.controlsfx.validation.decoration.GraphicValidationDecoration; import dev.webfx.stack.ui.validation.controlsfx.validation.decoration.ValidationDecoration; import javafx.scene.control.Control; @@ -52,7 +52,7 @@ public void setDecoration(ValidationDecoration decoration) { @Override void applyRequiredVisualization(Control control, boolean required) { - ValidationSupport.setRequired(control, required); + ControlsFxValidationSupport.setRequired(control, required); if (required) { decoration.applyRequiredDecoration(control); } @@ -74,12 +74,14 @@ void applyVisualization(Control control, Optional messageOpti } } else { - decoration.removeDecorations(control); - } - - if (required) { - decoration.applyRequiredDecoration(control); + removeDecorations(control); } + + applyRequiredVisualization(control, required); + } + + @Override + public void removeDecorations(Control control) { + decoration.removeDecorations(control); } - } diff --git a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/visualization/ValidationVisualizerBase.java b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/visualization/ValidationVisualizerBase.java index 4b6d33d41..447baca68 100644 --- a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/visualization/ValidationVisualizerBase.java +++ b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/visualization/ValidationVisualizerBase.java @@ -15,11 +15,12 @@ ******************************************************************************/ package dev.webfx.stack.ui.validation.mvvmfx.visualization; +import dev.webfx.kit.util.properties.FXProperties; +import dev.webfx.kit.util.properties.ObservableLists; +import dev.webfx.platform.uischeduler.UiScheduler; import dev.webfx.stack.ui.validation.mvvmfx.Severity; import dev.webfx.stack.ui.validation.mvvmfx.ValidationMessage; import dev.webfx.stack.ui.validation.mvvmfx.ValidationStatus; -import javafx.application.Platform; -import javafx.collections.ListChangeListener; import javafx.scene.control.Control; import java.util.Optional; @@ -42,20 +43,13 @@ public abstract class ValidationVisualizerBase implements ValidationVisualizer { @Override public void initVisualization(final ValidationStatus result, final Control control, boolean required) { if (control.getSkin() == null) { - control.skinProperty().addListener((observable, oldValue, newValue) -> initVisualization(result, control, required)); + FXProperties.onPropertySet(control.skinProperty(), skin -> initVisualization(result, control, required)); return; } - if (required) { - applyRequiredVisualization(control, required); - } - - applyVisualization(control, result.getHighestMessage(), required); - - result.getMessages().addListener((ListChangeListener) c -> { - while (c.next()) { - Platform.runLater(() -> applyVisualization(control, result.getHighestMessage(), required)); - } - }); + + ObservableLists.runNowAndOnListChange(c -> UiScheduler.runInUiThread(() -> + applyVisualization(control, result.getHighestMessage(), required) + ), result.getMessages()); } /** @@ -93,5 +87,7 @@ public void initVisualization(final ValidationStatus result, final Control contr * a boolean flag indicating whether this control is mandatory or not. */ abstract void applyVisualization(Control control, Optional messageOptional, boolean required); + + public abstract void removeDecorations(Control control); } diff --git a/webfx-stack-ui-validation/src/main/java/module-info.java b/webfx-stack-ui-validation/src/main/java/module-info.java index 9aca87547..b37e66d92 100644 --- a/webfx-stack-ui-validation/src/main/java/module-info.java +++ b/webfx-stack-ui-validation/src/main/java/module-info.java @@ -6,8 +6,16 @@ requires javafx.base; requires javafx.controls; requires javafx.graphics; + requires webfx.extras.imagestore; + requires webfx.extras.util.background; + requires webfx.extras.util.border; + requires webfx.extras.util.scene; + requires webfx.kit.util; + requires webfx.platform.uischeduler; + requires webfx.platform.util; // Exported packages + exports dev.webfx.stack.ui.validation; exports dev.webfx.stack.ui.validation.controlsfx.control.decoration; exports dev.webfx.stack.ui.validation.controlsfx.impl; exports dev.webfx.stack.ui.validation.controlsfx.impl.skin;