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 loadingKeys = new HashSet<>(keysToLoad); + Set loadingKeys = new HashSet<>(keysToLoad); // ConcurrentModificationException observed keysToLoad.clear(); // Extracting the message keys to load from them (in case they are different) Set messageKeysToLoad = loadingKeys.stream() diff --git a/webfx-stack-orm-entity-binding/pom.xml b/webfx-stack-orm-entity-binding/pom.xml index 351ea3a20..44a312908 100644 --- a/webfx-stack-orm-entity-binding/pom.xml +++ b/webfx-stack-orm-entity-binding/pom.xml @@ -21,6 +21,12 @@ provided + + 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