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
similarity index 100%
rename from webfx-stack-authn-login-ui-gateway-magiclink-plugin/src/main/webfx/i18n/webfx-magiclink@en.properties
rename to webfx-stack-authn-login-ui-gateway-magiclink-plugin/src/main/webfx/i18n/webfx-magiclink_en.properties
diff --git a/webfx-stack-authn-logout-client/src/main/webfx/i18n/webfx-stack-logout@fr.properties b/webfx-stack-authn-logout-client/src/main/webfx/i18n/webfx-stack-logout@fr.properties
deleted file mode 100644
index 12c74f222..000000000
--- a/webfx-stack-authn-logout-client/src/main/webfx/i18n/webfx-stack-logout@fr.properties
+++ /dev/null
@@ -1 +0,0 @@
-Logout = Se déconnecter
\ No newline at end of file
diff --git a/webfx-stack-cloud-image-client/pom.xml b/webfx-stack-cloud-image-client/pom.xml
index 7bb530d05..8b6a10514 100644
--- a/webfx-stack-cloud-image-client/pom.xml
+++ b/webfx-stack-cloud-image-client/pom.xml
@@ -23,25 +23,25 @@
dev.webfx
- webfx-platform-conf
+ webfx-platform-blob
0.1.0-SNAPSHOT
dev.webfx
- webfx-platform-console
+ webfx-platform-conf
0.1.0-SNAPSHOT
dev.webfx
- webfx-platform-fetch
+ webfx-platform-console
0.1.0-SNAPSHOT
dev.webfx
- webfx-platform-file
+ webfx-platform-fetch
0.1.0-SNAPSHOT
diff --git a/webfx-stack-cloud-image-client/src/main/java/dev/webfx/stack/cloud/image/impl/client/ClientImageService.java b/webfx-stack-cloud-image-client/src/main/java/dev/webfx/stack/cloud/image/impl/client/ClientImageService.java
index 85a707cf7..cbe8f9873 100644
--- a/webfx-stack-cloud-image-client/src/main/java/dev/webfx/stack/cloud/image/impl/client/ClientImageService.java
+++ b/webfx-stack-cloud-image-client/src/main/java/dev/webfx/stack/cloud/image/impl/client/ClientImageService.java
@@ -1,13 +1,14 @@
package dev.webfx.stack.cloud.image.impl.client;
import dev.webfx.platform.async.Future;
+import dev.webfx.platform.async.Promise;
+import dev.webfx.platform.blob.Blob;
import dev.webfx.platform.conf.ConfigLoader;
import dev.webfx.platform.console.Console;
import dev.webfx.platform.fetch.CorsMode;
import dev.webfx.platform.fetch.Fetch;
import dev.webfx.platform.fetch.FetchOptions;
import dev.webfx.platform.fetch.FormData;
-import dev.webfx.platform.file.File;
import dev.webfx.platform.util.http.HttpMethod;
import dev.webfx.platform.util.http.HttpResponseStatus;
import dev.webfx.stack.cloud.image.CloudImageService;
@@ -23,8 +24,11 @@ public class ClientImageService implements CloudImageService {
private String uploadUrl;
private String deleteUrl;
private String urlPattern;
+ private final Future urlPatternFuture;
public ClientImageService() {
+ Promise urlPatternPromise = Promise.promise();
+ urlPatternFuture = urlPatternPromise.future();
ConfigLoader.onConfigLoaded(CONFIG_PATH, config -> {
existsUrl = config.getString("existsUrl");
uploadUrl = config.getString("uploadUrl");
@@ -33,45 +37,52 @@ public ClientImageService() {
Fetch.fetchText(urlPatternUrl, new FetchOptions()
.setMethod(HttpMethod.GET)
.setMode(CorsMode.NO_CORS)
- )
- .onFailure(Console::log)
- .onSuccess(text -> urlPattern = text);
+ )
+ .onFailure(Console::log)
+ .onSuccess(text -> urlPattern = text)
+ .onComplete(ar -> urlPatternPromise.complete());
});
}
+ // Helper method to ensure that we return the future only when urlPattern is loaded (because the client may need to
+ // call CloudImageService.url() method - which requires urlPattern to be loaded - after returning the future).
+ private Future whenUrlPatternLoaded(Future future) {
+ return urlPatternFuture.compose(v -> future);
+ }
+
public Future exists(String id) {
- return Fetch.fetch(existsUrl, new FetchOptions()
- .setMethod(HttpMethod.POST)
- .setMode(CorsMode.NO_CORS)
- .setBody(new FormData().append("id", id))
+ return whenUrlPatternLoaded(Fetch.fetch(existsUrl, new FetchOptions()
+ .setMethod(HttpMethod.POST)
+ .setMode(CorsMode.NO_CORS)
+ .setBody(new FormData().append("id", id))
).compose(response -> {
if (response.ok())
return Future.succeededFuture(response.status() == HttpResponseStatus.OK_200); // OK_200 = exists, NO_CONTENT_204 = doesn't exist
return Future.failedFuture("Failed to call " + existsUrl + ", status = " + response.statusText());
- });
+ }));
}
- public Future upload(File file, String id, boolean overwrite) {
- return Fetch.fetch(uploadUrl, new FetchOptions()
- .setMethod(HttpMethod.POST)
- .setMode(CorsMode.NO_CORS)
- .setBody(new FormData()
- .append("id", id)
- .append("overwrite", overwrite)
- .append("file", file, id)
- )
- ).map(response -> null);
+ public Future upload(Blob blob, String id, boolean overwrite) {
+ return whenUrlPatternLoaded(Fetch.fetch(uploadUrl, new FetchOptions()
+ .setMethod(HttpMethod.POST)
+ .setMode(CorsMode.NO_CORS)
+ .setBody(new FormData()
+ .append("id", id)
+ .append("overwrite", overwrite)
+ .append("file", blob, id)
+ )
+ ).map(response -> null));
}
public Future delete(String id, boolean invalidate) {
- return Fetch.fetch(deleteUrl, new FetchOptions()
- .setMethod(HttpMethod.POST)
- .setMode(CorsMode.NO_CORS)
- .setBody(new FormData()
- .append("id", id)
- .append("invalidate", invalidate)
- )
- ).map(response -> null);
+ return whenUrlPatternLoaded(Fetch.fetch(deleteUrl, new FetchOptions()
+ .setMethod(HttpMethod.POST)
+ .setMode(CorsMode.NO_CORS)
+ .setBody(new FormData()
+ .append("id", id)
+ .append("invalidate", invalidate)
+ )
+ ).map(response -> null));
}
@Override
diff --git a/webfx-stack-cloud-image-client/src/main/java/module-info.java b/webfx-stack-cloud-image-client/src/main/java/module-info.java
index 0e1b4f61f..ab730d6b5 100644
--- a/webfx-stack-cloud-image-client/src/main/java/module-info.java
+++ b/webfx-stack-cloud-image-client/src/main/java/module-info.java
@@ -4,10 +4,10 @@
// Direct dependencies modules
requires webfx.platform.async;
+ requires webfx.platform.blob;
requires webfx.platform.conf;
requires webfx.platform.console;
requires webfx.platform.fetch;
- requires webfx.platform.file;
requires webfx.platform.util.http;
requires webfx.stack.cloud.image;
diff --git a/webfx-stack-cloud-image-cloudinary/pom.xml b/webfx-stack-cloud-image-cloudinary/pom.xml
index 89d97eb06..c40c514ae 100644
--- a/webfx-stack-cloud-image-cloudinary/pom.xml
+++ b/webfx-stack-cloud-image-cloudinary/pom.xml
@@ -15,12 +15,24 @@
+
+ dev.webfx
+ webfx-platform-ast
+ 0.1.0-SNAPSHOT
+
+
dev.webfx
webfx-platform-async
0.1.0-SNAPSHOT
+
+ dev.webfx
+ webfx-platform-blob
+ 0.1.0-SNAPSHOT
+
+
dev.webfx
webfx-platform-conf
@@ -29,13 +41,13 @@
dev.webfx
- webfx-platform-fetch
+ webfx-platform-console
0.1.0-SNAPSHOT
dev.webfx
- webfx-platform-file
+ webfx-platform-fetch
0.1.0-SNAPSHOT
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 88b44513c..0527a3382 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
@@ -1,23 +1,30 @@
package dev.webfx.stack.cloud.image.impl.cloudinary;
+import dev.webfx.platform.ast.AST;
+import dev.webfx.platform.ast.ReadOnlyAstObject;
import dev.webfx.platform.async.Future;
+import dev.webfx.platform.blob.Blob;
import dev.webfx.platform.conf.ConfigLoader;
+import dev.webfx.platform.console.Console;
import dev.webfx.platform.fetch.*;
-import dev.webfx.platform.file.File;
-import dev.webfx.platform.util.http.HttpHeaders;
-import dev.webfx.platform.util.http.HttpMethod;
import dev.webfx.platform.util.Strings;
import dev.webfx.platform.util.collection.Collections;
+import dev.webfx.platform.util.http.HttpHeaders;
+import dev.webfx.platform.util.http.HttpMethod;
import dev.webfx.stack.cloud.image.impl.fetchbased.FetchBasedCloudImageService;
import dev.webfx.stack.hash.sha1.Sha1;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
/**
* @author Bruno Salmon
*/
public class Cloudinary extends FetchBasedCloudImageService {
+ private static final boolean LOG_JSON_REPLY = true;
private static final String CONFIG_PATH = "webfx.stack.cloud.image.cloudinary";
private String cloudName;
@@ -43,7 +50,7 @@ public Future exists(String id) {
.map(Response::ok);
}
- public Future upload(File file, String id, boolean overwrite) {
+ public Future upload(Blob blob, String id, boolean overwrite) {
return fetchJsonObject(
"https://api.cloudinary.com/v1_1/" + cloudName + "/image/upload",
HttpMethod.POST,
@@ -51,9 +58,10 @@ public Future upload(File file, String id, boolean overwrite) {
signFormData(new FormData()
.append("public_id", id)
.append("overwrite", overwrite)
- ).append("file", file, id)
+ .append("invalidate", true) // Otherwise the new image might not be displayed immediately after upload
+ ).append("file", blob, id)
)
- ).map(json -> null);
+ ).map(json -> logJsonReply("upload", json));
}
public Future delete(String id, boolean invalidate) {
@@ -66,7 +74,14 @@ public Future delete(String id, boolean invalidate) {
.append("invalidate", invalidate)
)
)
- ).map(json -> null);
+ ).map(json -> logJsonReply("delete", json));
+ }
+
+ private static Void logJsonReply(String operation, ReadOnlyAstObject jsonReply) {
+ if (LOG_JSON_REPLY) {
+ Console.log("[CLOUDINARY] - " + operation + " - json reply = " + AST.formatObject(jsonReply, "json"));
+ }
+ return null;
}
@Override
diff --git a/webfx-stack-cloud-image-cloudinary/src/main/java/module-info.java b/webfx-stack-cloud-image-cloudinary/src/main/java/module-info.java
index 517de7810..dc0bebe26 100644
--- a/webfx-stack-cloud-image-cloudinary/src/main/java/module-info.java
+++ b/webfx-stack-cloud-image-cloudinary/src/main/java/module-info.java
@@ -3,10 +3,12 @@
module webfx.stack.cloud.image.cloudinary {
// Direct dependencies modules
+ requires webfx.platform.ast;
requires webfx.platform.async;
+ requires webfx.platform.blob;
requires webfx.platform.conf;
+ requires webfx.platform.console;
requires webfx.platform.fetch;
- requires webfx.platform.file;
requires webfx.platform.util;
requires webfx.platform.util.http;
requires webfx.stack.cloud.image;
diff --git a/webfx-stack-cloud-image/pom.xml b/webfx-stack-cloud-image/pom.xml
index 87803014d..bb98529e6 100644
--- a/webfx-stack-cloud-image/pom.xml
+++ b/webfx-stack-cloud-image/pom.xml
@@ -29,19 +29,19 @@
dev.webfx
- webfx-platform-fetch
+ webfx-platform-blob
0.1.0-SNAPSHOT
dev.webfx
- webfx-platform-fetch-ast-json
+ webfx-platform-fetch
0.1.0-SNAPSHOT
dev.webfx
- webfx-platform-file
+ webfx-platform-fetch-ast-json
0.1.0-SNAPSHOT
diff --git a/webfx-stack-cloud-image/src/main/java/dev/webfx/stack/cloud/image/CloudImageService.java b/webfx-stack-cloud-image/src/main/java/dev/webfx/stack/cloud/image/CloudImageService.java
index 8c8ad1d74..95d4889aa 100644
--- a/webfx-stack-cloud-image/src/main/java/dev/webfx/stack/cloud/image/CloudImageService.java
+++ b/webfx-stack-cloud-image/src/main/java/dev/webfx/stack/cloud/image/CloudImageService.java
@@ -1,8 +1,7 @@
package dev.webfx.stack.cloud.image;
-import dev.webfx.platform.ast.ReadOnlyAstObject;
import dev.webfx.platform.async.Future;
-import dev.webfx.platform.file.File;
+import dev.webfx.platform.blob.Blob;
/**
* @author Bruno Salmon
@@ -11,12 +10,15 @@ public interface CloudImageService {
Future exists(String id);
- Future upload(File file, String id, boolean overwrite);
+ Future upload(Blob blob, String id, boolean overwrite);
Future delete(String id, boolean invalidate);
default String url(String source, int width, int height) {
- String url = urlPattern().replace(":source", source);
+ String urlPattern = urlPattern();
+ if (urlPattern == null)
+ throw new IllegalStateException("[CloudImageService] urlPattern is null");
+ String url = urlPattern.replace(":source", source);
if (width > 0)
url = url.replace(":width", "" + width);
else
@@ -25,6 +27,9 @@ default String url(String source, int width, int height) {
url = url.replace(":height", "" + height);
else
url = url.replace("/h_:height", ""); // temporary
+
+ //We add a random parameter to prevent the cache to display an old image
+ url = url + "?t=" + System.currentTimeMillis();
return url;
}
diff --git a/webfx-stack-cloud-image/src/main/java/module-info.java b/webfx-stack-cloud-image/src/main/java/module-info.java
index 51e812655..894fb9a4d 100644
--- a/webfx-stack-cloud-image/src/main/java/module-info.java
+++ b/webfx-stack-cloud-image/src/main/java/module-info.java
@@ -5,9 +5,9 @@
// Direct dependencies modules
requires transitive webfx.platform.ast;
requires webfx.platform.async;
+ requires webfx.platform.blob;
requires webfx.platform.fetch;
requires webfx.platform.fetch.ast.json;
- requires webfx.platform.file;
requires webfx.platform.util.http;
// Exported packages
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 01b2fdf44..448d0803e 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
@@ -89,4 +89,8 @@ public static CheckBox newCheckBox(Object i18nKey, Object... args) {
return bindI18nProperties(new CheckBox(), i18nKey, args);
}
+ public static ToggleButton newToggleButton(Object i18nKey, Object... args) {
+ return bindI18nProperties(new ToggleButton(), i18nKey, args);
+ }
+
}
diff --git a/webfx-stack-i18n-time-plugin/src/main/webfx/i18n/webfx-stack-time@en.properties b/webfx-stack-i18n-time-plugin/src/main/webfx/i18n/webfx-stack-time_en.properties
similarity index 100%
rename from webfx-stack-i18n-time-plugin/src/main/webfx/i18n/webfx-stack-time@en.properties
rename to webfx-stack-i18n-time-plugin/src/main/webfx/i18n/webfx-stack-time_en.properties
diff --git a/webfx-stack-i18n-time-plugin/src/main/webfx/i18n/webfx-stack-time@fr.properties b/webfx-stack-i18n-time-plugin/src/main/webfx/i18n/webfx-stack-time_fr.properties
similarity index 100%
rename from webfx-stack-i18n-time-plugin/src/main/webfx/i18n/webfx-stack-time@fr.properties
rename to webfx-stack-i18n-time-plugin/src/main/webfx/i18n/webfx-stack-time_fr.properties
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 cf28245d9..f348b5f6f 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
@@ -235,8 +235,12 @@ public & TokenKey> Object interpretBracketsAndDefaultInToken
// 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 (resolvedValue != null) {
+ if (i1 == 0 && i2 == sToken.length() - 1) // except if there are no parts before and after the brackets
+ tokenValue = resolvedValue; // in which case we return the resolved object as is (possibly not a String)
+ else
+ tokenValue = (i1 == 0 ? "" : sToken.substring(0, i1)) + resolvedValue + sToken.substring(i2 + 1);
+ }
}
}
}
@@ -338,7 +342,7 @@ public void scheduleMessageLoading(Object i18nKey, boolean inDefaultLanguage) {
// (presumably in the same animation frame) we do the actual load of these keys.
dictionaryLoadingScheduled = UiScheduler.scheduleDeferred(() -> {
// Making a copy of the keys before clearing it for the next possible schedule
- Set
+
+ org.openjfx
+ javafx-graphics
+ provided
+
+
dev.webfx
webfx-platform-javatime-emul-j2cl
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
index cfa6ce6a6..2667c47a0 100644
--- 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
@@ -11,6 +11,7 @@
import dev.webfx.stack.orm.entity.result.EntityResult;
import javafx.beans.binding.BooleanExpression;
import javafx.beans.property.*;
+import javafx.scene.Node;
import java.util.ArrayList;
import java.util.List;
@@ -26,12 +27,29 @@ public static BooleanExpression hasChangesProperty(UpdateStore updateStore) {
BooleanProperty hasChangesProperty = (BooleanProperty) updateStoreImpl.getHasChangesProperty();
if (hasChangesProperty == null) {
EntityChangesBuilder changesBuilder = updateStoreImpl.getChangesBuilder();
- hasChangesProperty = new SimpleBooleanProperty(changesBuilder.hasChanges());
+ hasChangesProperty = new SimpleBooleanProperty(changesBuilder.hasChanges()) {
+ @Override
+ protected void invalidated() {
+ get(); // For some reason, it's necessary to call get() here, otherwise the bindings depending on
+ // this property might not be updated 🤷
+ }
+ };
changesBuilder.setHasChangesPropertyUpdater(hasChangesProperty::set);
}
return hasChangesProperty;
}
+ public static BooleanExpression hasNoChangesProperty(UpdateStore updateStore) {
+ return hasChangesProperty(updateStore).not();
+ }
+
+ public static void disableNodesWhenUpdateStoreHasNoChanges(UpdateStore updateStore, Node... nodes) {
+ BooleanExpression hasNoChanges = hasNoChangesProperty(updateStore);
+ for (Node node : nodes)
+ node.disableProperty().bind(hasNoChanges);
+
+ }
+
public static BooleanProperty getBooleanFieldProperty(Entity entity, String fieldId) {
return (BooleanProperty) getFieldProperty(entity, fieldId, SimpleBooleanProperty::new);
}
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
index a93a6f9c3..fd55c82b3 100644
--- a/webfx-stack-orm-entity-binding/src/main/java/module-info.java
+++ b/webfx-stack-orm-entity-binding/src/main/java/module-info.java
@@ -4,6 +4,7 @@
// Direct dependencies modules
requires javafx.base;
+ requires javafx.graphics;
requires webfx.platform.util;
requires webfx.stack.orm.entity;
diff --git a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/Entity.java b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/Entity.java
index 7223cb18d..cc88d35f3 100644
--- a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/Entity.java
+++ b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/Entity.java
@@ -1,15 +1,15 @@
package dev.webfx.stack.orm.entity;
+import dev.webfx.platform.async.Future;
+import dev.webfx.platform.util.Booleans;
+import dev.webfx.platform.util.Numbers;
+import dev.webfx.platform.util.Strings;
+import dev.webfx.platform.util.time.Times;
import dev.webfx.stack.orm.domainmodel.DomainClass;
import dev.webfx.stack.orm.domainmodel.DomainField;
import dev.webfx.stack.orm.expression.Expression;
import dev.webfx.stack.orm.expression.terms.Dot;
import dev.webfx.stack.orm.expression.terms.ExpressionArray;
-import dev.webfx.platform.util.Booleans;
-import dev.webfx.platform.util.time.Times;
-import dev.webfx.platform.util.Numbers;
-import dev.webfx.platform.util.Strings;
-import dev.webfx.platform.async.Future;
import java.time.Instant;
import java.time.LocalDate;
@@ -67,6 +67,8 @@ default boolean isNew() {
boolean isFieldLoaded(Object domainFieldId);
+ Collection getLoadedFields();
+
/**
* Return the field value as a boolean. If the type is not a boolean, this can result in runtime errors.
*/
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 536cf1896..d94d18a5a 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
@@ -71,27 +71,51 @@ default E createEntity(Object domainClassId, Object primaryKe
E createEntity(EntityId id);
default E getEntity(Class entityClass, Object primaryKey) {
- return getEntity((Object) entityClass, primaryKey);
+ return getEntity(entityClass, primaryKey, false);
}
default E getEntity(Object domainClassId, Object primaryKey) {
- return primaryKey == null ? null : getEntity(getEntityId(domainClassId, primaryKey));
+ return getEntity(domainClassId, primaryKey, false);
}
- E getEntity(EntityId entityId);
+ default E getEntity(EntityId entityId) {
+ return getEntity(entityId, false);
+ }
+
+ default E getEntity(Class entityClass, Object primaryKey, boolean includeUnderlyingStore) {
+ return getEntity((Object) entityClass, primaryKey, includeUnderlyingStore);
+ }
+
+ default E getEntity(Object domainClassId, Object primaryKey, boolean includeUnderlyingStore) {
+ return primaryKey == null ? null : getEntity(getEntityId(domainClassId, primaryKey), includeUnderlyingStore);
+ }
+
+ E getEntity(EntityId entityId, boolean includeUnderlyingStore);
default E getOrCreateEntity(Class entityClass, Object primaryKey) {
- return getOrCreateEntity((Object) entityClass, primaryKey);
+ return getOrCreateEntity(entityClass, primaryKey, false);
}
default E getOrCreateEntity(Object domainClassId, Object primaryKey) {
- return primaryKey == null ? null : getOrCreateEntity(getEntityId(domainClassId, primaryKey));
+ return getOrCreateEntity(domainClassId, primaryKey, false);
}
default E getOrCreateEntity(EntityId id) {
+ return getOrCreateEntity(id, false);
+ }
+
+ default E getOrCreateEntity(Class entityClass, Object primaryKey, boolean includeUnderlyingStore) {
+ return getOrCreateEntity((Object) entityClass, primaryKey, includeUnderlyingStore);
+ }
+
+ default E getOrCreateEntity(Object domainClassId, Object primaryKey, boolean includeUnderlyingStore) {
+ return primaryKey == null ? null : getOrCreateEntity(getEntityId(domainClassId, primaryKey), includeUnderlyingStore);
+ }
+
+ default E getOrCreateEntity(EntityId id, boolean includeUnderlyingStore) {
if (id == null)
return null;
- E entity = getEntity(id);
+ E entity = getEntity(id, includeUnderlyingStore);
if (entity == null)
entity = createEntity(id);
return entity;
@@ -100,9 +124,7 @@ default E getOrCreateEntity(EntityId id) {
default E copyEntity(E entity) {
if (entity == null)
return null;
- E copy = getOrCreateEntity(entity.getId());
- if (copy.getStore() != this) // Ensuring the copy is in this store
- copy = createEntity(entity.getId());
+ E copy = getOrCreateEntity(entity.getId(), false); // Ensuring the copy is in this store
if (copy != entity)
((DynamicEntity) copy).copyAllFieldsFrom(entity);
return copy;
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 468f31f71..f5b0391b7 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,6 +6,7 @@
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.entity.impl.DynamicEntity;
import dev.webfx.stack.orm.entity.impl.UpdateStoreImpl;
import dev.webfx.stack.orm.entity.result.EntityChanges;
@@ -25,8 +26,12 @@ default E insertEntity(Object domainClassId, Object primaryKe
E insertEntity(EntityId entityId);
default E updateEntity(E entity) {
- updateEntity(entity.getId());
- return copyEntity(entity);
+ E updatedEntity = updateEntity(entity.getId());
+ if (updatedEntity instanceof DynamicEntity && entity != updatedEntity) {
+ DynamicEntity dynamicEntity = (DynamicEntity) updatedEntity;
+ dynamicEntity.setUnderlyingEntity(entity);
+ }
+ return updatedEntity;
}
E updateEntity(EntityId entityId);
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 59a6ffcab..99090a58f 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
@@ -7,9 +7,7 @@
import dev.webfx.stack.orm.entity.EntityStore;
import dev.webfx.stack.orm.entity.UpdateStore;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
+import java.util.*;
import java.util.function.BiConsumer;
/**
@@ -19,7 +17,7 @@ public class DynamicEntity implements Entity {
private EntityId id;
private final EntityStore store;
- private final Entity underlyingEntity;
+ private Entity underlyingEntity;
private final Map fieldValues = new HashMap<>();
// fields used by EntityBindings only:
private Map fieldProperties; // lazy instantiation
@@ -32,6 +30,10 @@ protected DynamicEntity(EntityId id, EntityStore store) {
underlyingEntity = underlyingStore != null ? underlyingStore.getEntity(id) : null;
}
+ public void setUnderlyingEntity(Entity underlyingEntity) { // meant to be called by UpdateStore.updateEntity() only
+ this.underlyingEntity = underlyingEntity;
+ }
+
@Override
public EntityId getId() {
return id;
@@ -68,6 +70,16 @@ public boolean isFieldLoaded(Object domainFieldId) {
return false;
}
+ @Override
+ public Collection getLoadedFields() {
+ Set loadFields = fieldValues.keySet();
+ if (underlyingEntity != null) {
+ loadFields = new HashSet<>(loadFields); // because ketSet() returns an immutable set
+ loadFields.addAll(underlyingEntity.getLoadedFields());
+ }
+ return loadFields;
+ }
+
@Override
public void setForeignField(Object foreignFieldId, Object foreignFieldValue) {
EntityId foreignEntityId;
@@ -98,9 +110,17 @@ public EntityId getForeignEntityId(Object foreignFieldId) {
}
@Override
+ public E getForeignEntity(Object foreignFieldId) {
+ E foreignEntity = Entity.super.getForeignEntity(foreignFieldId);
+ if (foreignEntity == null && underlyingEntity != null)
+ foreignEntity = underlyingEntity.getForeignEntity(foreignFieldId);
+ return foreignEntity;
+ }
+
public void setFieldValue(Object domainFieldId, Object value) {
- fieldValues.put(domainFieldId, value);
- if (store instanceof UpdateStore) {
+ boolean loadedValue = ThreadLocalEntityLoadingContext.isThreadLocalEntityLoading();
+ fieldValues.put(domainFieldId, value); // TODO: what if it's a loaded value and previous value was not?
+ if (!loadedValue && store instanceof UpdateStore) {
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);
@@ -112,6 +132,7 @@ public void setFieldValue(Object domainFieldId, Object value) {
}
}
+
public void copyAllFieldsFrom(Entity entity) {
DynamicEntity dynamicEntity = (DynamicEntity) entity;
fieldValues.putAll(dynamicEntity.fieldValues);
@@ -128,7 +149,11 @@ public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DynamicEntity that = (DynamicEntity) o;
- return id.equals(that.id);
+ if (!id.equals(that.id))
+ return false;
+// if (!fieldValues.equals(that.fieldValues))
+// return false;
+ return underlyingEntity == null || that.underlyingEntity == null || Objects.equals(underlyingEntity, that.underlyingEntity);
}
@Override
@@ -157,7 +182,7 @@ private StringBuilder toString(StringBuilder sb, boolean pk) {
return sb;
}
- // methods meant to be used by EntityBindings only
+ // methods are public but meant to be used by EntityBindings only
public Object getFieldProperty(Object fieldId) {
return fieldProperties == null ? null : fieldProperties.get(fieldId);
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 3e47c07cc..90b31f8a6 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
@@ -64,9 +64,9 @@ public void applyEntityIdRefactor(EntityId oldId, EntityId newId) {
// Entity management
@Override
- public E getEntity(EntityId entityId) {
+ public E getEntity(EntityId entityId, boolean includeUnderlyingStore) {
E entity = (E) entities.get(entityId);
- if (entity == null && underlyingStore != null)
+ if (entity == null && underlyingStore != null && includeUnderlyingStore)
entity = underlyingStore.getEntity(entityId);
return entity;
}
diff --git a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/impl/ThreadLocalEntityLoadingContext.java b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/impl/ThreadLocalEntityLoadingContext.java
new file mode 100644
index 000000000..14ff25373
--- /dev/null
+++ b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/impl/ThreadLocalEntityLoadingContext.java
@@ -0,0 +1,36 @@
+package dev.webfx.stack.orm.entity.impl;
+
+/**
+ * Usage:
+ *
+ * try (var context = ThreadLocalEntityLoadingContext.open(true)) {
+ * ...
+ * any call to DynamicEntity.setFieldValue() won't record this as modification in an UpdateStore
+ * ...
+ * }
+ *
+ * @author Bruno Salmon
+ */
+public final class ThreadLocalEntityLoadingContext implements AutoCloseable {
+
+ private static final ThreadLocal entityLoadingThreadLocal = new ThreadLocal<>();
+
+ private final Boolean previousEntityLoading = entityLoadingThreadLocal.get();
+
+ private ThreadLocalEntityLoadingContext(Boolean entityLoading) {
+ entityLoadingThreadLocal.set(entityLoading);
+ }
+
+ @Override
+ public void close() {
+ entityLoadingThreadLocal.set(previousEntityLoading);
+ }
+
+ public static ThreadLocalEntityLoadingContext open(Boolean entityLoading) {
+ return entityLoading == null ? null : new ThreadLocalEntityLoadingContext(entityLoading);
+ }
+
+ public static boolean isThreadLocalEntityLoading() {
+ return Boolean.TRUE.equals(entityLoadingThreadLocal.get());
+ }
+}
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 71639bf64..2979d2260 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
@@ -24,9 +24,10 @@
*/
public final class UpdateStoreImpl extends EntityStoreImpl implements UpdateStore {
- private final EntityChangesBuilder changesBuilder = EntityChangesBuilder.create();
+ private final EntityChangesBuilder changesBuilder = EntityChangesBuilder.create().setUpdateStore(this);
private DataScope submitScope;
private Object hasChangesProperty; // managed by EntityBindings
+ private boolean submitting;
public UpdateStoreImpl(DataSourceModel dataSourceModel) {
super(dataSourceModel);
@@ -45,6 +46,7 @@ public EntityChanges getEntityChanges() {
public E insertEntity(EntityId entityId) {
if (!entityId.isNew())
throw new IllegalArgumentException("entityId must be new");
+ logWarningIfChangesDuringSubmit();
E entity = createEntity(entityId);
changesBuilder.addInsertedEntityId(entityId);
return entity;
@@ -52,11 +54,13 @@ public E insertEntity(EntityId entityId) {
@Override
public E updateEntity(EntityId entityId) {
+ logWarningIfChangesDuringSubmit();
changesBuilder.addUpdatedEntityId(entityId);
return getOrCreateEntity(entityId);
}
void onInsertedOrUpdatedEntityFieldChange(EntityId id, Object domainFieldId, Object value, Object underlyingValue, boolean isUnderlyingValueLoaded) {
+ logWarningIfChangesDuringSubmit();
// 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);
@@ -77,19 +81,23 @@ public Future> submitChanges(SubmitArgument... initialSubmit
createSubmitBatchGenerator(getEntityChanges(), getDataSourceModel(), submitScope, initialSubmits);
Batch argBatch = updateBatchGenerator.generate();
Console.log("Executing submit batch " + Arrays.toStringWithLineFeeds(argBatch.getArray()));
+ submitting = true;
return SubmitService.executeSubmitBatch(argBatch).compose(resBatch -> {
// TODO: perf optimization: make these steps optional if not required by application code
markChangesAsCommitted();
updateBatchGenerator.applyGeneratedKeys(resBatch, this);
+ submitting = false;
return Future.succeededFuture(resBatch);
});
} catch (Exception e) {
+ submitting = false;
return Future.failedFuture(e);
}
}
@Override
public void deleteEntity(EntityId entityId) {
+ logWarningIfChangesDuringSubmit();
changesBuilder.addDeletedEntityId(entityId);
}
@@ -134,21 +142,28 @@ private void applyCommittedChangesToUnderlyingStore() {
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);
+ if (insertedUpdatedEntityResult != null) {
+ 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);
}
- clearAllUpdatedValuesFromUpdatedEntity(entityId);
}
}
}
+ private void logWarningIfChangesDuringSubmit() {
+ if (submitting)
+ Console.log("[UpdateStore][WARNING] ⚠️ Making changes during submitChanges() is not yet supported, and leads to inconsistent UpdateStore state.", new Exception("Please use this exception stacktrace to identify the faulty call"));
+ }
+
// methods meant to be used by EntityBindings only
diff --git a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/query_result_to_entities/QueryResultToEntitiesMapper.java b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/query_result_to_entities/QueryResultToEntitiesMapper.java
index 5af68def5..00c783ac2 100644
--- a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/query_result_to_entities/QueryResultToEntitiesMapper.java
+++ b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/query_result_to_entities/QueryResultToEntitiesMapper.java
@@ -9,6 +9,7 @@
import dev.webfx.stack.orm.entity.Entity;
import dev.webfx.stack.orm.entity.EntityList;
import dev.webfx.stack.orm.entity.EntityStore;
+import dev.webfx.stack.orm.entity.impl.ThreadLocalEntityLoadingContext;
import java.time.LocalDate;
import java.time.LocalDateTime;
@@ -23,56 +24,59 @@ public static EntityList mapQueryResultToEntities(QueryRes
EntityList entities = store.getOrCreateEntityList(listId);
entities.clear();
// Now iterating along the query result to create one entity per record
- if (rs != null)
- for (int rowIndex = 0, rowCount = rs.getRowCount(); rowIndex < rowCount; rowIndex++) {
- // Retrieving the primary key of this record
- Object primaryKey = rs.getValue(rowIndex, rowMapping.getPrimaryKeyColumnIndex());
- // Creating the entity (empty for now)
- E entity = store.getOrCreateEntity(rowMapping.getDomainClassId(), primaryKey);
- // Now populating the entity values by iterating along the other column indexes (though column mappings)
- for (QueryColumnToEntityFieldMapping columnMapping : rowMapping.getColumnMappings()) {
- // The target entity (to affect the column value to) is normally the current entity
- Entity targetEntity = entity;
- // However if this column index is associated with a join, it actually refers to a foreign entity, so let's check this
- QueryColumnToEntityFieldMapping joinMapping = columnMapping.getForeignIdColumnMapping();
- if (joinMapping != null) { // Yes it is a join
- // So let's first get the row id of the database foreign record
- Object foreignKey = rs.getValue(rowIndex, joinMapping.getColumnIndex());
- // If it is null, there is nothing to do (finally no target entity)
- if (foreignKey == null)
- continue;
- // And creating the foreign entity (or getting the same instance if already created)
- targetEntity = store.getOrCreateEntity(joinMapping.getForeignClassId(), foreignKey); // And finally using is as the target entity
- }
- // Now that we have the target entity, getting the value for the column index
- Object value = rs.getValue(rowIndex, columnMapping.getColumnIndex());
- // If this is a foreign key (when foreignClassId is filled), we transform the value into a link to the foreign entity
- if (value != null && columnMapping.getForeignClassId() != null)
- value = store.getOrCreateEntity(columnMapping.getForeignClassId(), value).getId();
- // Now everything is ready to set the field on the target entity
- Object fieldId = columnMapping.getDomainFieldId();
- // Some conversion to do if it is a domain field
- if (fieldId instanceof DomainField) {
- DomainField domainField = (DomainField) fieldId;
- // First, getting the field id
- fieldId = domainField.getId();
- // And second, converting the dates possibly returned as String by the QueryService into LocalDate or LocalDateTime objects
- if (value != null && domainField.getType() == PrimType.DATE && value instanceof String) {
- LocalDateTime localDateTime = Times.toLocalDateTime((String) value);
- if (localDateTime != null)
- value = localDateTime;
- else {
- LocalDate localDate = Times.toLocalDate((String) value);
- if (localDate != null)
- value = localDate;
+ if (rs != null) {
+ try (var context = ThreadLocalEntityLoadingContext.open(true)) {
+ for (int rowIndex = 0, rowCount = rs.getRowCount(); rowIndex < rowCount; rowIndex++) {
+ // Retrieving the primary key of this record
+ Object primaryKey = rs.getValue(rowIndex, rowMapping.getPrimaryKeyColumnIndex());
+ // Creating the entity (empty for now)
+ E entity = store.getOrCreateEntity(rowMapping.getDomainClassId(), primaryKey);
+ // Now populating the entity values by iterating along the other column indexes (though column mappings)
+ for (QueryColumnToEntityFieldMapping columnMapping : rowMapping.getColumnMappings()) {
+ // The target entity (to affect the column value to) is normally the current entity
+ Entity targetEntity = entity;
+ // However if this column index is associated with a join, it actually refers to a foreign entity, so let's check this
+ QueryColumnToEntityFieldMapping joinMapping = columnMapping.getForeignIdColumnMapping();
+ if (joinMapping != null) { // Yes it is a join
+ // So let's first get the row id of the database foreign record
+ Object foreignKey = rs.getValue(rowIndex, joinMapping.getColumnIndex());
+ // If it is null, there is nothing to do (finally no target entity)
+ if (foreignKey == null)
+ continue;
+ // And creating the foreign entity (or getting the same instance if already created)
+ targetEntity = store.getOrCreateEntity(joinMapping.getForeignClassId(), foreignKey); // And finally using is as the target entity
+ }
+ // Now that we have the target entity, getting the value for the column index
+ Object value = rs.getValue(rowIndex, columnMapping.getColumnIndex());
+ // If this is a foreign key (when foreignClassId is filled), we transform the value into a link to the foreign entity
+ if (value != null && columnMapping.getForeignClassId() != null)
+ value = store.getOrCreateEntity(columnMapping.getForeignClassId(), value).getId();
+ // Now everything is ready to set the field on the target entity
+ Object fieldId = columnMapping.getDomainFieldId();
+ // Some conversion to do if it is a domain field
+ if (fieldId instanceof DomainField) {
+ DomainField domainField = (DomainField) fieldId;
+ // First, getting the field id
+ fieldId = domainField.getId();
+ // And second, converting the dates possibly returned as String by the QueryService into LocalDate or LocalDateTime objects
+ if (value != null && domainField.getType() == PrimType.DATE && value instanceof String) {
+ LocalDateTime localDateTime = Times.toLocalDateTime((String) value);
+ if (localDateTime != null)
+ value = localDateTime;
+ else {
+ LocalDate localDate = Times.toLocalDate((String) value);
+ if (localDate != null)
+ value = localDate;
+ }
+ }
}
+ targetEntity.setFieldValue(fieldId, value);
+ //System.out.println(targetEntity.getId().toString() + '.' + columnMapping.getDomainFieldId() + " = " + value);
}
+ // And finally adding this entity to the list
+ entities.add(entity);
}
- targetEntity.setFieldValue(fieldId, value);
- //System.out.println(targetEntity.getId().toString() + '.' + columnMapping.getDomainFieldId() + " = " + value);
}
- // And finally adding this entity to the list
- entities.add(entity);
}
//Logger.log("Ok : " + entities);
return entities;
diff --git a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/result/EntityChanges.java b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/result/EntityChanges.java
index a3db84460..73bcbfcea 100644
--- a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/result/EntityChanges.java
+++ b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/result/EntityChanges.java
@@ -1,6 +1,7 @@
package dev.webfx.stack.orm.entity.result;
import dev.webfx.stack.orm.entity.EntityId;
+import dev.webfx.stack.orm.entity.UpdateStore;
import java.util.Collection;
@@ -13,4 +14,6 @@ public interface EntityChanges {
Collection getDeletedEntityIds();
+ UpdateStore getUpdateStore();
+
}
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 b1e4a672c..b8dca81cd 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
@@ -4,6 +4,7 @@
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.UpdateStore;
import dev.webfx.stack.orm.entity.result.impl.EntityChangesImpl;
import java.util.Collection;
@@ -18,6 +19,7 @@ public final class EntityChangesBuilder {
private Collection deletedEntities;
private boolean hasChanges;
private Consumer hasChangesPropertyUpdater; // used by EntityBindings only
+ private UpdateStore updateStore; // Optional, used to sort the deleted entities when provided
private EntityChangesBuilder() {}
@@ -107,6 +109,11 @@ private EntityChangesBuilder updateHasChanges() {
return this;
}
+ public EntityChangesBuilder setUpdateStore(UpdateStore updateStore) {
+ this.updateStore = updateStore;
+ return this;
+ }
+
private EntityResultBuilder rsb() {
if (rsb == null)
rsb = EntityResultBuilder.create();
@@ -114,14 +121,14 @@ private EntityResultBuilder rsb() {
}
public EntityChanges build() {
- return new EntityChangesImpl(rsb == null ? null : rsb.build(), deletedEntities);
+ return new EntityChangesImpl(rsb == null ? null : rsb.build(), deletedEntities, updateStore);
}
public static EntityChangesBuilder create() {
return new EntityChangesBuilder();
}
- // method meant to be used by EntityBindings only
+ // method is public but 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 ba2c26e35..ddff64b82 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
@@ -12,8 +12,10 @@
import dev.webfx.stack.orm.dql.sqlcompiler.lci.CompilerDomainModelReader;
import dev.webfx.stack.orm.dql.sqlcompiler.sql.SqlCompiled;
import dev.webfx.stack.orm.dql.sqlcompiler.sql.dbms.DbmsSqlSyntax;
+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.expression.Expression;
import dev.webfx.stack.orm.expression.parser.lci.ParserDomainModelReader;
import dev.webfx.stack.orm.expression.terms.*;
@@ -272,11 +274,10 @@ private int getGeneratedKeyIndexShift(int batchIndex, List batchIndexTr
void generateDeletes() {
Collection deletedEntities = changes.getDeletedEntityIds();
if (deletedEntities != null && !deletedEntities.isEmpty()) {
- /* Commented delete sort (not working), so for now the application code is responsible for sequencing deletes
- List deletedList = new ArrayList<>(deletedEntities);
- // Sorting according to classes references
- deletedList.sort(comparing(id -> id.getDomainClass().getName()));
- */
+ UpdateStore updateStore = changes.getUpdateStore();
+ if (updateStore != null) {
+ deletedEntities = new TopologicalSort(deletedEntities, updateStore).sort();
+ }
deletedEntities.forEach(this::generateDelete);
}
}
@@ -333,12 +334,54 @@ void addToBatch(String language, String statement, Object... parameters) {
SubmitArgument newSubmitArgument(String language, String statement, Object... parameters) {
return SubmitArgument.builder()
- .setDataSourceId(dataSourceId)
- .setDataScope(dataScope)
- .setLanguage(language)
- .setStatement(statement)
- .setParameters(parameters)
- .build();
+ .setDataSourceId(dataSourceId)
+ .setDataScope(dataScope)
+ .setLanguage(language)
+ .setStatement(statement)
+ .setParameters(parameters)
+ .build();
+ }
+ }
+
+ private static class TopologicalSort {
+ private final Collection entityIds;
+ private final UpdateStore updateStore;
+ private final Set visited = new HashSet<>();
+ private final List sorted = new ArrayList<>();
+
+ public TopologicalSort(Collection entityIds, UpdateStore updateStore) {
+ this.entityIds = entityIds;
+ this.updateStore = updateStore;
+ }
+
+ public List sort() {
+ // Doing a deep first search, which will sort the independent entities first, and then the entities referring
+ // to them.
+ for (EntityId entityId : entityIds) {
+ if (!visited.contains(entityId)) {
+ deepFirstSearch(entityId);
+ }
+ }
+ // We reverse the previous sort, because if we delete first the independent entities whose other entities
+ // refer to (meaning that there are foreign keys pointing to them), then the database will raise a constraint
+ // exception. We need to proceed the deletes in the exact opposite order (deleting first the entities
+ // referring to other entities).
+ Collections.reverse(sorted);
+ return sorted;
+ }
+
+ private void deepFirstSearch(EntityId entityId) {
+ visited.add(entityId);
+ Entity entity = updateStore.getEntity(entityId, true);
+ if (entity != null) {
+ for (Object loadedField : entity.getLoadedFields()) {
+ Object fieldValue = entity.getFieldValue(loadedField);
+ if (fieldValue instanceof EntityId && entityIds.contains(fieldValue) && !visited.contains(fieldValue)) {
+ deepFirstSearch((EntityId) fieldValue);
+ }
+ }
+ }
+ sorted.add(entityId);
}
}
}
diff --git a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/result/impl/EntityChangesImpl.java b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/result/impl/EntityChangesImpl.java
index 58ca34336..95be4042d 100644
--- a/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/result/impl/EntityChangesImpl.java
+++ b/webfx-stack-orm-entity/src/main/java/dev/webfx/stack/orm/entity/result/impl/EntityChangesImpl.java
@@ -1,6 +1,7 @@
package dev.webfx.stack.orm.entity.result.impl;
import dev.webfx.stack.orm.entity.EntityId;
+import dev.webfx.stack.orm.entity.UpdateStore;
import dev.webfx.stack.orm.entity.result.EntityChanges;
import dev.webfx.stack.orm.entity.result.EntityResult;
@@ -13,10 +14,12 @@ public final class EntityChangesImpl implements EntityChanges {
private final EntityResult insertedUpdatedEntities;
private final Collection deletedEntities;
+ private final UpdateStore updateStore; // Optional, used to sort delete entities if provided
- public EntityChangesImpl(EntityResult insertedUpdatedEntities, Collection deletedEntities) {
+ public EntityChangesImpl(EntityResult insertedUpdatedEntities, Collection deletedEntities, UpdateStore updateStore) {
this.insertedUpdatedEntities = insertedUpdatedEntities;
this.deletedEntities = deletedEntities;
+ this.updateStore = updateStore;
}
@Override
@@ -28,4 +31,9 @@ public EntityResult getInsertedUpdatedEntityResult() {
public Collection getDeletedEntityIds() {
return deletedEntities;
}
+
+ @Override
+ public UpdateStore getUpdateStore() {
+ return updateStore;
+ }
}
diff --git a/webfx-stack-ui-controls/src/main/java/dev/webfx/stack/ui/controls/dialog/SimpleDialogBuilder.java b/webfx-stack-ui-controls/src/main/java/dev/webfx/stack/ui/controls/dialog/SimpleDialogBuilder.java
new file mode 100644
index 000000000..eca658603
--- /dev/null
+++ b/webfx-stack-ui-controls/src/main/java/dev/webfx/stack/ui/controls/dialog/SimpleDialogBuilder.java
@@ -0,0 +1,32 @@
+package dev.webfx.stack.ui.controls.dialog;
+
+import dev.webfx.stack.ui.dialog.DialogCallback;
+import javafx.scene.layout.Region;
+
+/**
+ * @author Bruno Salmon
+ */
+public class SimpleDialogBuilder implements DialogBuilder {
+
+ private final Region content;
+ private DialogCallback dialogCallback;
+
+ public SimpleDialogBuilder(Region content) {
+ this.content = content;
+ }
+
+ @Override
+ public Region build() {
+ return content;
+ }
+
+ @Override
+ public void setDialogCallback(DialogCallback dialogCallback) {
+ this.dialogCallback = dialogCallback;
+ }
+
+ @Override
+ public DialogCallback getDialogCallback() {
+ return dialogCallback;
+ }
+}
diff --git a/webfx-stack-ui-dialog/src/main/java/dev/webfx/stack/ui/dialog/DialogUtil.java b/webfx-stack-ui-dialog/src/main/java/dev/webfx/stack/ui/dialog/DialogUtil.java
index 0e2a11d78..748af233c 100644
--- a/webfx-stack-ui-dialog/src/main/java/dev/webfx/stack/ui/dialog/DialogUtil.java
+++ b/webfx-stack-ui-dialog/src/main/java/dev/webfx/stack/ui/dialog/DialogUtil.java
@@ -12,7 +12,6 @@
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.HPos;
-import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.geometry.VPos;
import javafx.scene.Node;
@@ -42,9 +41,10 @@ public static DialogCallback showModalNodeInGoldLayout(Region modalNode, Pane pa
}
public static DialogCallback showModalNodeInGoldLayout(Region modalNode, Pane parent, double percentageWidth, double percentageHeight) {
- Insets padding = modalNode.getPadding();
+ //Insets padding = modalNode.getPadding();
return showModalNode(LayoutUtil.createGoldLayout(decorate(modalNode), percentageWidth, percentageHeight), parent)
- .addCloseHook(() -> modalNode.setPadding(padding));
+ //.addCloseHook(() -> modalNode.setPadding(padding))
+ ;
}
public static DialogCallback showModalNode(Region modalNode, Pane parent) {
@@ -67,10 +67,14 @@ private static void setUpModalNodeResizeRelocate(Region modalNode, Pane parent,
}
public static BorderPane decorate(Node content) {
+ /* Commented out because the content may set its own max width/height (ex: Festival creator dialog)
+ // TODO: completely remove if no side effect, or set the max size to pref size only if the content has no max size set
// Setting max width/height to pref width/height (otherwise the grid pane takes all space with cells in top left corner)
- if (content instanceof Region)
- LayoutUtil.setMaxSizeToPref(LayoutUtil.createPadding((Region) content, 10));
- BorderPane decorator = new BorderPane(content);
+ if (content instanceof Region) {
+ Region region = (Region) content;
+ LayoutUtil.setMaxSizeToPref(region);
+ }*/
+ BorderPane decorator = LayoutUtil.createPadding(new BorderPane(content), 0);
decorator.backgroundProperty().bind(dialogBackgroundProperty());
decorator.borderProperty().bind(dialogBorderProperty());
decorator.setMinHeight(0d);
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 0691fe3da..84dc96e07 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
@@ -166,6 +166,8 @@ private boolean bindOperationActionGraphicalPropertiesNow(OperationAction
// 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(executableOperationAction);
+ if (operationRequest == null)
+ return false;
// Registering the operation action (should it be done only once?)
Class> operationRequestClass = operationRequest.getClass();
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
index bb2f74b8b..1c733d396 100644
--- 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
@@ -291,7 +291,7 @@ public void addEmailNotEqualValidation(TextField emailInput, String forbiddenVal
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}(/.*)?$";
+ String urlPattern = "^(https?://).+\\..+$";
Pattern pattern = Pattern.compile(urlPattern);
addValidationRule(
@@ -330,7 +330,7 @@ public void addMinimumDurationValidation(TextField timeInput, Node where, Observ
public void addUrlOrEmptyValidation(TextField urlInput, ObservableStringValue errorMessage) {
// Define the URL pattern (basic)
- String urlPattern = "^(https?|srt|rtmp|rtsp)://[\\w.-]+(:\\d+)?(/[\\w./-]*)?(\\?[\\w=&%.-]*)?(#[\\w!:.=&,-]*)?$";
+ String urlPattern = "^(https|srt|rtmp|rtsp)://(www\\.)?[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}(/[a-zA-Z0-9%._/-]*)$";
Pattern pattern = Pattern.compile(urlPattern);
// Create the validation rule