From 34ce40447900372337964ec1daa34d13578d748a Mon Sep 17 00:00:00 2001 From: smetanka Date: Wed, 27 Aug 2025 13:12:32 +0200 Subject: [PATCH 1/5] [NAE-2184] Nullable object as serializable alternative to Optional - Add Nullable utility class to handle nullable arguments in Plugins --- .../logic/action/ActionDelegate.groovy | 1 + .../engine/objects/utils/Nullable.java | 307 ++++++++++++++++++ 2 files changed, 308 insertions(+) create mode 100644 nae-object-library/src/main/java/com/netgrif/application/engine/objects/utils/Nullable.java diff --git a/application-engine/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy b/application-engine/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy index 4fa10efcc1a..69902059a98 100644 --- a/application-engine/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy +++ b/application-engine/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy @@ -90,6 +90,7 @@ import org.springframework.core.io.FileSystemResource import org.springframework.data.domain.Page import org.springframework.data.domain.PageRequest import org.springframework.data.domain.Pageable +import com.netgrif.application.engine.objects.utils.Nullable; import java.time.ZoneId import java.util.stream.Collectors diff --git a/nae-object-library/src/main/java/com/netgrif/application/engine/objects/utils/Nullable.java b/nae-object-library/src/main/java/com/netgrif/application/engine/objects/utils/Nullable.java new file mode 100644 index 00000000000..f448f7550f6 --- /dev/null +++ b/nae-object-library/src/main/java/com/netgrif/application/engine/objects/utils/Nullable.java @@ -0,0 +1,307 @@ +package com.netgrif.application.engine.objects.utils; + +import java.io.Serializable; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Stream; + + +/** + * A utility class representing a nullable container for a value of type {@code T}. It provides methods to handle + * optionality in a fluent and functional programming style. This class is inspired by {@link Optional} + * but differs in implementation and naming conventions. + * + * @param The type of the value contained in this {@code Nullable}. + */ +public final class Nullable implements Serializable { + + private static final Nullable EMPTY = new Nullable<>(null); + + private final T value; + + private Nullable(T value) { + this.value = value; + } + + /** + * Returns the value held by this instance. + * + * @return the value contained in this instance, or {@code null} if the instance is empty + */ + public T get() { + return value; + } + + /** + * Creates a new {@code Nullable} instance containing the given value. + * + * @param value the value to wrap in a {@code Nullable} instance, can be {@code null} + * @return a {@code Nullable} instance wrapping the provided value + */ + public static Nullable of(T value) { + return new Nullable<>(value); + } + + /** + * Returns an empty {@code Nullable} instance holding no value. + * + * @param the type of the value that can be held by this {@code Nullable} instance + * @return an empty {@code Nullable} instance + */ + public static Nullable empty() { + @SuppressWarnings("unchecked") + Nullable EMPTY = (Nullable) Nullable.EMPTY; + return EMPTY; + } + + /** + * Checks if a value is present in this instance. + * + * @return {@code true} if the value is not {@code null}, otherwise {@code false} + */ + public boolean isPresent() { + return value != null; + } + + /** + * Checks if this instance holds no value. + * + * @return {@code true} if the value is {@code null}, otherwise {@code false} + */ + public boolean isEmpty() { + return value == null; + } + + /** + * If a value is present in this instance, performs the given action with the value. + * + * @param action the action to be performed if a value is present; must be non-null + */ + public void ifPresent(Consumer action) { + if (value != null) { + action.accept(value); + } + } + + /** + * Performs the given action with the value if it is present, otherwise executes the provided empty action. + * + * @param action the {@code Consumer} to be executed if the value is present; must not be null + * @param emptyAction the {@code Runnable} to be executed if the value is not present; must not be null + */ + public void ifPresentOrElse(Consumer action, Runnable emptyAction) { + if (value != null) { + action.accept(value); + } else { + emptyAction.run(); + } + } + + /** + * Converts the value held by this instance into an {@code Optional}. + * + * @return an {@code Optional} containing the value if it is present, or an empty {@code Optional} if the value is {@code null} + */ + public Optional toOptional() { + return Optional.ofNullable(value); + } + + /** + * Filters the value contained in this {@code Nullable} instance based on the provided predicate. + * If the value is present and satisfies the predicate, this instance is returned. + * If the value does not satisfy the predicate or if the instance is empty, an empty {@code Nullable} is returned. + * + * @param predicate the predicate used to evaluate the contained value; must be non-null + * @return this {@code Nullable} instance if the value satisfies the predicate, + * or an empty {@code Nullable} instance otherwise + */ + public Nullable filter(Predicate predicate) { + Objects.requireNonNull(predicate); + if (isEmpty()) { + return this; + } else { + return predicate.test(value) ? this : empty(); + } + } + + /** + * Transforms the value contained in this {@code Nullable} instance using the provided mapping function. + * If this {@code Nullable} instance is empty, an empty {@code Nullable} instance is returned. + * + * @param the type of the value produced by the mapping function + * @param mapper the function to apply to the value; must not be null + * @return a {@code Nullable} instance containing the value produced by applying the mapping function, + * or an empty {@code Nullable} instance if this instance is empty + */ + public Nullable map(Function mapper) { + Objects.requireNonNull(mapper); + if (isEmpty()) { + return empty(); + } else { + return Nullable.of(mapper.apply(value)); + } + } + + /** + * Applies the provided mapping function to the value contained in this {@code Nullable} instance, + * and returns the {@code Nullable} instance produced by the mapping function. If this instance is empty, + * an empty {@code Nullable} is returned. + * + * @param the type of the value contained in the resulting {@code Nullable} instance + * @param mapper the mapping function to apply to the value if it is present; must not be null + * @return a {@code Nullable} instance produced by applying the mapping function to the value, + * or an empty {@code Nullable} if this instance is empty + */ + public Nullable flatMap(Function> mapper) { + Objects.requireNonNull(mapper); + if (isEmpty()) { + return empty(); + } else { + @SuppressWarnings("unchecked") + Nullable r = (Nullable) mapper.apply(value); + return Objects.requireNonNull(r); + } + } + + /** + * Returns this {@code Nullable} instance if a value is present, otherwise returns the result + * of invoking the provided {@code Supplier}. + * + * @param supplier the {@code Supplier} providing an alternative {@code Nullable} instance + * if this instance is empty; must not be {@code null} + * @return this {@code Nullable} instance if it contains a value, or the {@code Nullable} instance + * provided by the supplier if this instance is empty + * @throws NullPointerException if the supplier is {@code null} or the {@code Nullable} instance + * provided by the supplier is {@code null} + */ + public Nullable or(Supplier> supplier) { + Objects.requireNonNull(supplier); + if (isPresent()) { + return this; + } else { + @SuppressWarnings("unchecked") + Nullable r = (Nullable) supplier.get(); + return Objects.requireNonNull(r); + } + } + + /** + * Creates a sequential {@code Stream} containing the value held by this {@code Nullable} instance, + * if a value is present. If this instance is empty, returns an empty {@code Stream}. + * + * @return a {@code Stream} containing the value if it is present, or an empty {@code Stream} otherwise + */ + public Stream stream() { + if (isEmpty()) { + return Stream.empty(); + } else { + return Stream.of(value); + } + } + + /** + * Returns the value held by this instance if it is non-null; otherwise, returns the specified default value. + * + * @param other the value to be returned if the current value is null + * @return the value held by this instance if it is non-null, or the specified default value if the value is null + */ + public T orElse(T other) { + return value != null ? value : other; + } + + /** + * Returns the value held by this instance if it is present; otherwise, + * returns the result produced by the provided {@code Supplier}. + * + * @param supplier a {@code Supplier} to provide an alternative value + * if the current instance holds no value; must not be {@code null} + * @return the value contained in this instance if present, or the value + * obtained from the supplied {@code Supplier} if the instance is empty + * @throws NullPointerException if the provided supplier is {@code null} + */ + public T orElseGet(Supplier supplier) { + return value != null ? value : supplier.get(); + } + + /** + * Returns the value held by this instance if it is present; otherwise, + * throws a {@link NoSuchElementException}. + * + * @return the value contained in this instance + * @throws NoSuchElementException if no value is present in this instance + */ + public T orElseThrow() { + if (value == null) { + throw new NoSuchElementException("No value present"); + } + return value; + } + + /** + * Returns the value held by this instance if it is present, otherwise throws an exception + * supplied by the provided {@code Supplier}. + * + * @param the type of the exception to be thrown + * @param exceptionSupplier the supplier that provides the exception to be thrown if no value is present; must not be null + * @return the value held by this instance if it is present + * @throws X if no value is present + * @throws NullPointerException if the {@code exceptionSupplier} is null + */ + public T orElseThrow(Supplier exceptionSupplier) throws X { + if (value != null) { + return value; + } else { + throw exceptionSupplier.get(); + } + } + + /** + * Compares this {@code Nullable} instance with the specified object for equality. + * Two {@code Nullable} instances are considered equal if they both contain the + * same value or are both empty. + * + * @param obj the object to be compared for equality with this {@code Nullable} instance + * @return {@code true} if the specified object is equal to this {@code Nullable} instance, + * {@code false} otherwise + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + return obj instanceof Nullable other + && Objects.equals(value, other.value); + } + + /** + * Computes the hash code for this instance based on its value field. + * + * @return the hash code as an integer for this instance, + * calculated using the value field or default if the field is null. + */ + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + /** + * Returns a string representation of the object. + * If the encapsulated value is non-null, the string will include the value. + * If the encapsulated value is null, a representation indicating emptiness will be returned. + * + * @return a string representation of the object, indicating the encapsulated value or an empty state + */ + @Override + public String toString() { + return value != null + ? ("Nullable[" + value + "]") + : "Nullable.empty"; + } + +} From da9c0e8522dded1b362e95ed98c7050490d447f3 Mon Sep 17 00:00:00 2001 From: smetanka Date: Wed, 27 Aug 2025 14:18:51 +0200 Subject: [PATCH 2/5] [NAE-2184] Nullable object as serializable alternative to Optional - Added Nullable to action imports --- application-engine/src/main/resources/application.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application-engine/src/main/resources/application.yaml b/application-engine/src/main/resources/application.yaml index 4e2bb303c18..876ae43853c 100644 --- a/application-engine/src/main/resources/application.yaml +++ b/application-engine/src/main/resources/application.yaml @@ -159,4 +159,4 @@ netgrif: RoleActionsRunner: debug CaseFieldsExpressionRunner: debug actions: - imports: com.netgrif.application.engine.objects.workflow.domain.menu.MenuItemBody,com.netgrif.application.engine.objects.petrinet.domain.I18nString,com.netgrif.application.engine.objects.workflow.domain.menu.dashboard.DashboardManagementBody,com.netgrif.application.engine.objects.workflow.domain.menu.dashboard.DashboardItemBody + imports: com.netgrif.application.engine.objects.workflow.domain.menu.MenuItemBody,com.netgrif.application.engine.objects.petrinet.domain.I18nString,com.netgrif.application.engine.objects.workflow.domain.menu.dashboard.DashboardManagementBody,com.netgrif.application.engine.objects.workflow.domain.menu.dashboard.DashboardItemBody,com.netgrif.application.engine.objects.utils.Nullable From 25e998041c5f2eaa0b6fbb954a5e4583603601b5 Mon Sep 17 00:00:00 2001 From: smetanka Date: Wed, 27 Aug 2025 14:28:30 +0200 Subject: [PATCH 3/5] [NAE-2184] Nullable object as serializable alternative to Optional - Added serialVersionUID --- .../netgrif/application/engine/objects/utils/Nullable.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nae-object-library/src/main/java/com/netgrif/application/engine/objects/utils/Nullable.java b/nae-object-library/src/main/java/com/netgrif/application/engine/objects/utils/Nullable.java index f448f7550f6..9dd3a323b6f 100644 --- a/nae-object-library/src/main/java/com/netgrif/application/engine/objects/utils/Nullable.java +++ b/nae-object-library/src/main/java/com/netgrif/application/engine/objects/utils/Nullable.java @@ -1,5 +1,6 @@ package com.netgrif.application.engine.objects.utils; +import java.io.Serial; import java.io.Serializable; import java.util.NoSuchElementException; import java.util.Objects; @@ -20,6 +21,9 @@ */ public final class Nullable implements Serializable { + @Serial + private static final long serialVersionUID = 8683452581122892189L; + private static final Nullable EMPTY = new Nullable<>(null); private final T value; From 8d8b1ceeee2f34db8c8b2a9d08db35d484a41c0d Mon Sep 17 00:00:00 2001 From: Milan Mladoniczky <6153201+tuplle@users.noreply.github.com> Date: Wed, 27 Aug 2025 18:27:27 +0200 Subject: [PATCH 4/5] Enhance GroovyShell configuration and introduce utility methods - Added default engine imports in `GroovyShellConfiguration` for better functionality. - Introduced a generic `nullable` method in `ActionDelegate` to handle optional values. --- .../dataset/logic/action/ActionDelegate.groovy | 14 +++++++++----- .../groovy/GroovyShellConfiguration.java | 13 +++++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/application-engine/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy b/application-engine/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy index 69902059a98..f0275b57fbf 100644 --- a/application-engine/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy +++ b/application-engine/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy @@ -90,7 +90,7 @@ import org.springframework.core.io.FileSystemResource import org.springframework.data.domain.Page import org.springframework.data.domain.PageRequest import org.springframework.data.domain.Pageable -import com.netgrif.application.engine.objects.utils.Nullable; +import com.netgrif.application.engine.objects.utils.Nullable import java.time.ZoneId import java.util.stream.Collectors @@ -908,6 +908,10 @@ class ActionDelegate { return find?.call(ico) } + Nullable nullable(T value) { + return Nullable.of(value) + } + Object get(String key) { map[key] } void set(String key, Object value) { map[key] = value } @@ -2764,12 +2768,12 @@ class ActionDelegate { String resolveStoragePath(Case aCase, String fileFieldId, String fileName) { Optional> storageFieldOptional = aCase.getPetriNet().getField(fileFieldId) if (storageFieldOptional.isEmpty()) { - throw new IllegalArgumentException("Field with id [%s] does not exist on Petri Net [%s]".formatted(fileFieldId, aCase.getPetriNetId())) - } + throw new IllegalArgumentException("Field with id [%s] does not exist on Petri Net [%s]".formatted(fileFieldId, aCase.getPetriNetId())) + } Field field = storageFieldOptional.get() if (!(field instanceof StorageField)) { - throw new IllegalArgumentException("Field with id [%s] is not a StorageField on Petri Net [%s]".formatted(fileFieldId, aCase.getPetriNetId())) - } + throw new IllegalArgumentException("Field with id [%s] is not a StorageField on Petri Net [%s]".formatted(fileFieldId, aCase.getPetriNetId())) + } StorageField storageField = (StorageField) field IStorageService storageService = storageResolverService.resolve(storageField.storageType) return storageService.getPath(aCase.stringId, fileFieldId, fileName) diff --git a/application-engine/src/main/java/com/netgrif/application/engine/configuration/groovy/GroovyShellConfiguration.java b/application-engine/src/main/java/com/netgrif/application/engine/configuration/groovy/GroovyShellConfiguration.java index 6ea43c0efd5..919ae67a91e 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/configuration/groovy/GroovyShellConfiguration.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/configuration/groovy/GroovyShellConfiguration.java @@ -8,6 +8,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import java.util.List; + @Configuration public class GroovyShellConfiguration { @@ -18,6 +20,7 @@ public class GroovyShellConfiguration { @ConditionalOnMissingBean public ImportCustomizer importCustomizer() { ImportCustomizer importCustomizer = new ImportCustomizer(); + importCustomizer.addStarImports(getDefaultEngineImports()); importCustomizer.addImports(actionsProperties.getImports().toArray(new String[0])); importCustomizer.addStarImports(actionsProperties.getStarImports().toArray(new String[0])); importCustomizer.addStaticStars(actionsProperties.getStaticStarImports().toArray(new String[0])); @@ -32,4 +35,14 @@ public CompilerConfiguration compilerConfiguration() { return configuration; } + + protected String[] getDefaultEngineImports() { + return new String[]{ + "com.netgrif.application.engine.objects", + "com.netgrif.application.engine.adapter.spring", + "java.time" + }; + } + + } From 3f8aa8c0ddc04ccf975fc6b69e579935df25d508 Mon Sep 17 00:00:00 2001 From: Milan Mladoniczky <6153201+tuplle@users.noreply.github.com> Date: Wed, 27 Aug 2025 22:29:19 +0200 Subject: [PATCH 5/5] Refactor Nullable implementation and update JavaDocs - Removed unnecessary static EMPTY field to simplify code and avoid redundancy. - Simplified `toString` method to return an empty string when no value is present. --- .../engine/objects/utils/Nullable.java | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/nae-object-library/src/main/java/com/netgrif/application/engine/objects/utils/Nullable.java b/nae-object-library/src/main/java/com/netgrif/application/engine/objects/utils/Nullable.java index 9dd3a323b6f..df50295a4ab 100644 --- a/nae-object-library/src/main/java/com/netgrif/application/engine/objects/utils/Nullable.java +++ b/nae-object-library/src/main/java/com/netgrif/application/engine/objects/utils/Nullable.java @@ -23,8 +23,6 @@ public final class Nullable implements Serializable { @Serial private static final long serialVersionUID = 8683452581122892189L; - - private static final Nullable EMPTY = new Nullable<>(null); private final T value; @@ -58,9 +56,7 @@ public static Nullable of(T value) { * @return an empty {@code Nullable} instance */ public static Nullable empty() { - @SuppressWarnings("unchecked") - Nullable EMPTY = (Nullable) Nullable.EMPTY; - return EMPTY; + return new Nullable<>(null); } /** @@ -95,7 +91,7 @@ public void ifPresent(Consumer action) { /** * Performs the given action with the value if it is present, otherwise executes the provided empty action. * - * @param action the {@code Consumer} to be executed if the value is present; must not be null + * @param action the {@code Consumer} to be executed if the value is present; must not be null * @param emptyAction the {@code Runnable} to be executed if the value is not present; must not be null */ public void ifPresentOrElse(Consumer action, Runnable emptyAction) { @@ -122,7 +118,7 @@ public Optional toOptional() { * * @param predicate the predicate used to evaluate the contained value; must be non-null * @return this {@code Nullable} instance if the value satisfies the predicate, - * or an empty {@code Nullable} instance otherwise + * or an empty {@code Nullable} instance otherwise */ public Nullable filter(Predicate predicate) { Objects.requireNonNull(predicate); @@ -137,10 +133,10 @@ public Nullable filter(Predicate predicate) { * Transforms the value contained in this {@code Nullable} instance using the provided mapping function. * If this {@code Nullable} instance is empty, an empty {@code Nullable} instance is returned. * - * @param the type of the value produced by the mapping function + * @param the type of the value produced by the mapping function * @param mapper the function to apply to the value; must not be null * @return a {@code Nullable} instance containing the value produced by applying the mapping function, - * or an empty {@code Nullable} instance if this instance is empty + * or an empty {@code Nullable} instance if this instance is empty */ public Nullable map(Function mapper) { Objects.requireNonNull(mapper); @@ -156,7 +152,7 @@ public Nullable map(Function mapper) { * and returns the {@code Nullable} instance produced by the mapping function. If this instance is empty, * an empty {@code Nullable} is returned. * - * @param the type of the value contained in the resulting {@code Nullable} instance + * @param the type of the value contained in the resulting {@code Nullable} instance * @param mapper the mapping function to apply to the value if it is present; must not be null * @return a {@code Nullable} instance produced by applying the mapping function to the value, * or an empty {@code Nullable} if this instance is empty @@ -179,7 +175,7 @@ public Nullable flatMap(Function supplier) { @@ -250,10 +246,10 @@ public T orElseThrow() { * Returns the value held by this instance if it is present, otherwise throws an exception * supplied by the provided {@code Supplier}. * - * @param the type of the exception to be thrown + * @param the type of the exception to be thrown * @param exceptionSupplier the supplier that provides the exception to be thrown if no value is present; must not be null * @return the value held by this instance if it is present - * @throws X if no value is present + * @throws X if no value is present * @throws NullPointerException if the {@code exceptionSupplier} is null */ public T orElseThrow(Supplier exceptionSupplier) throws X { @@ -271,7 +267,7 @@ public T orElseThrow(Supplier exceptionSuppli * * @param obj the object to be compared for equality with this {@code Nullable} instance * @return {@code true} if the specified object is equal to this {@code Nullable} instance, - * {@code false} otherwise + * {@code false} otherwise */ @Override public boolean equals(Object obj) { @@ -303,9 +299,7 @@ public int hashCode() { */ @Override public String toString() { - return value != null - ? ("Nullable[" + value + "]") - : "Nullable.empty"; + return value != null ? value.toString() : ""; } }