diff --git a/actions/pom.xml b/actions/pom.xml index 7f76bef9..683451ba 100644 --- a/actions/pom.xml +++ b/actions/pom.xml @@ -23,11 +23,11 @@ tools.dynamia tools.dynamia.parent - 5.4.11 + 5.4.12 tools.dynamia.actions - 5.4.11 + 5.4.12 DynamiaTools - Actions https://dynamia.tools/docs/actions @@ -65,12 +65,12 @@ tools.dynamia tools.dynamia.integration - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.commons - 5.4.11 + 5.4.12 diff --git a/app/pom.xml b/app/pom.xml index b137b7cb..9eca2298 100644 --- a/app/pom.xml +++ b/app/pom.xml @@ -23,11 +23,11 @@ tools.dynamia tools.dynamia.parent - 5.4.11 + 5.4.12 tools.dynamia.app - 5.4.11 + 5.4.12 DynamiaTools - App https://dynamia.tools/docs/app @@ -74,58 +74,58 @@ tools.dynamia tools.dynamia.actions - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.commons - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.crud - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.domain - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.integration - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.io - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.navigation - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.reports - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.templates - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.viewers - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.web - 5.4.11 + 5.4.12 org.springframework.data @@ -208,7 +208,7 @@ tools.dynamia tools.dynamia.domain.jpa - 5.4.11 + 5.4.12 test diff --git a/app/src/main/java/tools/dynamia/app/controllers/CrudServiceRestController.java b/app/src/main/java/tools/dynamia/app/controllers/CrudServiceRestController.java index e8fc626e..f608ee14 100644 --- a/app/src/main/java/tools/dynamia/app/controllers/CrudServiceRestController.java +++ b/app/src/main/java/tools/dynamia/app/controllers/CrudServiceRestController.java @@ -1,7 +1,7 @@ package tools.dynamia.app.controllers; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -43,7 +43,7 @@ public class CrudServiceRestController { /** * JSON object mapper for entity serialization/deserialization. */ - private final ObjectMapper mapper = StringPojoParser.createJsonMapper(); + private final JsonMapper mapper = StringPojoParser.createJsonMapper(); /** * Constructs a new {@code CrudServiceRestController} with the given CRUD service. diff --git a/commons/pom.xml b/commons/pom.xml index 12fbec5d..bdaff51b 100644 --- a/commons/pom.xml +++ b/commons/pom.xml @@ -22,11 +22,10 @@ tools.dynamia.commons jar - 5.4.11 tools.dynamia tools.dynamia.parent - 5.4.11 + 5.4.12 DynamiaTools - Commons https://dynamia.tools/docs/common diff --git a/commons/src/main/java/tools/dynamia/commons/StringPojoParser.java b/commons/src/main/java/tools/dynamia/commons/StringPojoParser.java index 425d2c73..da55d078 100644 --- a/commons/src/main/java/tools/dynamia/commons/StringPojoParser.java +++ b/commons/src/main/java/tools/dynamia/commons/StringPojoParser.java @@ -20,7 +20,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.dataformat.xml.XmlMapper; @@ -53,7 +52,7 @@ public static String convertMapToJson(Map map) { if (map == null || map.isEmpty()) { return ""; } - ObjectMapper jsonMapper = createJsonMapper(); + var jsonMapper = createJsonMapper(); return jsonMapper.writeValueAsString(map); } catch (JsonProcessingException e) { throw new JsonParsingException(e); @@ -61,11 +60,11 @@ public static String convertMapToJson(Map map) { } /** - * Creates a configured JSON {@link ObjectMapper} with indentation, disabled empty beans, and JavaTimeModule support. + * Creates a configured JSON {@link JsonMapper} with indentation, disabled empty beans, and JavaTimeModule support. * * @return the configured JSON ObjectMapper */ - public static ObjectMapper createJsonMapper() { + public static JsonMapper createJsonMapper() { return JsonMapper.builder() .enable(SerializationFeature.INDENT_OUTPUT) .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) @@ -85,7 +84,7 @@ public static String convertPojoToJson(Object pojo) { if (pojo == null) { return ""; } - ObjectMapper jsonMapper = createJsonMapper(); + var jsonMapper = createJsonMapper(); return jsonMapper.writeValueAsString(pojo); } catch (JsonProcessingException e) { throw new JsonParsingException(e); @@ -141,7 +140,7 @@ public static T parseJsonToPojo(String json, Class pojoType) { return null; } - ObjectMapper jsonMapper = createJsonMapper(); + var jsonMapper = createJsonMapper(); return jsonMapper.readerFor(pojoType).readValue(json); } catch (IOException e) { throw new JsonParsingException(e); @@ -161,7 +160,7 @@ public static T parseJsonToPojo(Map map, Class pojoType) { return null; } - ObjectMapper jsonMapper = createJsonMapper(); + var jsonMapper = createJsonMapper(); return jsonMapper.convertValue(map, pojoType); } catch (IllegalArgumentException e) { throw new JsonParsingException(e); @@ -184,12 +183,12 @@ public static String convertPojoToXml(Object pojo) { } /** - * Create a xml {@link ObjectMapper} with enable IDENT_OUTPUT and disabled FAIL_ON_EMPTY_BEANS. Also add support + * Create a xml {@link XmlMapper} with enable IDENT_OUTPUT and disabled FAIL_ON_EMPTY_BEANS. Also add support * to {@link JavaTimeModule} from JSR310 dependency * - * @return xml ObjectMapper + * @return xml mapper */ - public static ObjectMapper createXmlMapper() { + public static XmlMapper createXmlMapper() { return XmlMapper.builder() .enable(SerializationFeature.INDENT_OUTPUT) .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) @@ -244,7 +243,7 @@ public static String convertListToJson(List list) { if (list == null || list.isEmpty()) { return ""; } - ObjectMapper jsonMapper = createJsonMapper(); + var jsonMapper = createJsonMapper(); return jsonMapper.writeValueAsString(list); } catch (JsonProcessingException e) { throw new JsonParsingException(e); diff --git a/commons/src/main/java/tools/dynamia/commons/math/Randoms.java b/commons/src/main/java/tools/dynamia/commons/math/Randoms.java new file mode 100644 index 00000000..0b8ef3dd --- /dev/null +++ b/commons/src/main/java/tools/dynamia/commons/math/Randoms.java @@ -0,0 +1,297 @@ +package tools.dynamia.commons.math; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.concurrent.ThreadLocalRandom; + +/** + * Utility class for generating random numbers and strings. + * Uses {@link ThreadLocalRandom} for better performance in concurrent environments. + */ +public class Randoms { + + /** + * Returns a random integer between the specified origin (inclusive) and the specified bound (exclusive). + * + * @param min the least value returned + * @param max the upper bound (exclusive) + * @return a random integer between {@code min} (inclusive) and {@code max} (exclusive) + */ + public static int nextInt(int min, int max) { + return ThreadLocalRandom.current().nextInt(min, max); + } + + /** + * Returns a random integer between 0 (inclusive) and the specified bound (exclusive). + * + * @param max the upper bound (exclusive) + * @return a random integer between 0 (inclusive) and {@code max} (exclusive) + */ + public static int nextInt(int max) { + return ThreadLocalRandom.current().nextInt(max); + } + + /** + * Returns a random integer. + * + * @return a random integer + */ + public static int nextInt() { + return ThreadLocalRandom.current().nextInt(0, Integer.MAX_VALUE); + } + + /** + * Returns a random long between the specified origin (inclusive) and the specified bound (exclusive). + * + * @param min the least value returned + * @param max the upper bound (exclusive) + * @return a random long between {@code min} (inclusive) and {@code max} (exclusive) + */ + public static long nextLong(long min, long max) { + return ThreadLocalRandom.current().nextLong(min, max); + } + + /** + * Returns a random float between the specified origin (inclusive) and the specified bound (exclusive). + * + * @param min the least value returned + * @param max the upper bound (exclusive) + * @return a random float between {@code min} (inclusive) and {@code max} (exclusive) + */ + public static float nextFloat(float min, float max) { + return min + ThreadLocalRandom.current().nextFloat() * (max - min); + } + + /** + * Returns a random double between the specified origin (inclusive) and the specified bound (exclusive). + * + * @param min the least value returned + * @param max the upper bound (exclusive) + * @return a random double between {@code min} (inclusive) and {@code max} (exclusive) + */ + public static double nextDouble(double min, double max) { + return ThreadLocalRandom.current().nextDouble(min, max); + } + + /** + * Generates a random string of the specified length using alphanumeric characters. + * + * @param length the length of the random string + * @return a random string of alphanumeric characters + */ + public static String nextString(int length) { + StringBuilder sb = new StringBuilder(); + String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for (int i = 0; i < length; i++) { + int index = nextInt(chars.length()); + sb.append(chars.charAt(index)); + } + return sb.toString(); + } + + /** + * Generates a random string of the specified length using numeric characters. + * + * @param length the length of the random string + * @return a random string of numeric characters + */ + public static String nextNumericString(int length) { + StringBuilder sb = new StringBuilder(); + String chars = "0123456789"; + for (int i = 0; i < length; i++) { + int index = nextInt(chars.length()); + sb.append(chars.charAt(index)); + } + return sb.toString(); + } + + /** + * Generates a random string of the specified length using alphabetic characters. + * + * @param length the length of the random string + * @return a random string of alphabetic characters + */ + public static String nextAlphabeticString(int length) { + StringBuilder sb = new StringBuilder(); + String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + for (int i = 0; i < length; i++) { + int index = nextInt(chars.length()); + sb.append(chars.charAt(index)); + } + return sb.toString(); + } + + /** + * Generates a random string of the specified length using hexadecimal characters. + * + * @param length the length of the random string + * @return a random string of hexadecimal characters + */ + public static String nextHexString(int length) { + StringBuilder sb = new StringBuilder(); + String chars = "0123456789ABCDEF"; + for (int i = 0; i < length; i++) { + int index = nextInt(chars.length()); + sb.append(chars.charAt(index)); + } + return sb.toString(); + } + + /** + * Generates a random hex color string (e.g., #FF0000). + * + * @return a random hex color string + */ + public static String nextHexColor() { + return "#" + nextHexString(6); + } + + /** + * Generates a random UUID string. + * + * @return a random UUID string + */ + public static String nextUUID() { + return java.util.UUID.randomUUID().toString(); + } + + /** + * Returns a random name from a predefined list of names. + * + * @return a random name + */ + public static String nextName() { + String[] names = {"Alice", "Bob", "Charlie", "David", "Eve", "Frank", "Grace", "Hannah", "Ivy", "Jack", + "Kathy", "Liam", "Mia", "Noah", "Olivia", "Paul", "Quinn", "Rachel", "Sam", "Tina", + "Uma", "Victor", "Wendy", "Xander", "Yara", "Zane", "Aaron", "Bella", "Carter", "Diana", "Ethan", "Fiona", + "Gavin", "Hailey", "Ian", "Jasmine", "Kevin", "Luna", "Mason", "Nora", "Owen", "Piper", "Quincy", "Ruby", "Sean", "Tara", + "Mario", "Nina", "Leo", "Eva", "Jake", "Lily", "Cindy", "Derek", "Elena", "Felix", "Gloria"}; + return names[nextInt(names.length)]; + } + + /** + * Returns a random element from the specified array. + * + * @param the type of the elements + * @param array the array to choose from + * @return a random element from the array, or {@code null} if the array is null or empty + */ + public static T chooseRandom(T[] array) { + if (array == null || array.length == 0) { + return null; + } + int index = nextInt(array.length); + return array[index]; + } + + /** + * Returns a random element from the specified array, or the default value if the array is null or empty. + * + * @param the type of the elements + * @param array the array to choose from + * @param defaultValue the value to return if the array is null or empty + * @return a random element from the array, or {@code defaultValue} + */ + public static T chooseRandom(T[] array, T defaultValue) { + if (array == null || array.length == 0) { + return defaultValue; + } + + int index = nextInt(array.length); + return array[index]; + } + + /** + * Returns a random element from the specified list. + * + * @param the type of the elements + * @param list the list to choose from + * @return a random element from the list, or {@code null} if the list is null or empty + */ + public static T chooseRandom(java.util.List list) { + if (list == null || list.isEmpty()) { + return null; + } + int index = nextInt(list.size()); + return list.get(index); + } + + /** + * Returns a random date between the specified start (inclusive) and end (exclusive) dates. + * + * @param startInclusive the start date (inclusive) + * @param endExclusive the end date (exclusive) + * @return a random date between {@code startInclusive} and {@code endExclusive} + */ + public static LocalDate nextDate(LocalDate startInclusive, LocalDate endExclusive) { + long startEpochDay = startInclusive.toEpochDay(); + long endEpochDay = endExclusive.toEpochDay(); + long randomDay = nextLong(startEpochDay, endEpochDay); + return LocalDate.ofEpochDay(randomDay); + } + + /** + * Returns a random date in the past, up to the specified maximum number of days. + * + * @param maxDaysInPast the maximum number of days in the past + * @return a random date in the past + */ + public static LocalDate nextPastDate(int maxDaysInPast) { + LocalDate today = LocalDate.now(); + long randomDays = nextLong(1, maxDaysInPast + 1); + return today.minusDays(randomDays); + } + + /** + * Returns a random date in the future, up to the specified maximum number of days. + * + * @param maxDaysInFuture the maximum number of days in the future + * @return a random date in the future + */ + public static LocalDate nextFutureDate(int maxDaysInFuture) { + LocalDate today = LocalDate.now(); + long randomDays = nextLong(1, maxDaysInFuture + 1); + return today.plusDays(randomDays); + } + + /** + * Returns a random date-time between the specified start (inclusive) and end (exclusive) date-times. + * + * @param startInclusive the start date-time (inclusive) + * @param endExclusive the end date-time (exclusive) + * @return a random date-time between {@code startInclusive} and {@code endExclusive} + */ + public static LocalDateTime nextDateTime(LocalDateTime startInclusive, LocalDateTime endExclusive) { + long startEpochSecond = startInclusive.toEpochSecond(java.time.ZoneOffset.UTC); + long endEpochSecond = endExclusive.toEpochSecond(java.time.ZoneOffset.UTC); + long randomSecond = nextLong(startEpochSecond, endEpochSecond); + return LocalDateTime.ofEpochSecond(randomSecond, 0, java.time.ZoneOffset.UTC); + } + + /** + * Returns a random time of day. + * + * @return a random time + */ + public static LocalTime nextTime() { + int hour = nextInt(0, 24); + int minute = nextInt(0, 60); + int second = nextInt(0, 60); + return LocalTime.of(hour, minute, second); + } + + /** + * Returns a random time between the specified minimum hour (inclusive) and maximum hour (exclusive). + * + * @param minHour the minimum hour (inclusive) + * @param maxHour the maximum hour (exclusive) + * @return a random time between {@code minHour} and {@code maxHour} + */ + public static LocalTime nextTime(int minHour, int maxHour) { + int hour = nextInt(minHour, maxHour); + int minute = nextInt(0, 60); + int second = nextInt(0, 60); + return LocalTime.of(hour, minute, second); + } +} diff --git a/crud/pom.xml b/crud/pom.xml index 7ff6c992..607eff86 100644 --- a/crud/pom.xml +++ b/crud/pom.xml @@ -23,11 +23,11 @@ tools.dynamia tools.dynamia.parent - 5.4.11 + 5.4.12 tools.dynamia.crud - 5.4.11 + 5.4.12 DynamiaTools - CRUD https://dynamia.tools/docs/crud @@ -62,23 +62,23 @@ tools.dynamia tools.dynamia.actions - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.viewers - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.navigation - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.domain.jpa - 5.4.11 + 5.4.12 test diff --git a/domain-jpa/pom.xml b/domain-jpa/pom.xml index 15b8fd3b..10f1a9a8 100644 --- a/domain-jpa/pom.xml +++ b/domain-jpa/pom.xml @@ -23,13 +23,13 @@ tools.dynamia tools.dynamia.parent - 5.4.11 + 5.4.12 DynamiaTools - Domain JPA https://dynamia.tools/docs/domain tools.dynamia.domain.jpa - 5.4.11 + 5.4.12 jar diff --git a/domain/pom.xml b/domain/pom.xml index ee105d0e..b80debd0 100644 --- a/domain/pom.xml +++ b/domain/pom.xml @@ -21,13 +21,13 @@ 4.0.0 tools.dynamia.domain - 5.4.11 + 5.4.12 jar tools.dynamia tools.dynamia.parent - 5.4.11 + 5.4.12 DynamiaTools - Domain https://dynamia.tools/docs/domain diff --git a/integration/pom.xml b/integration/pom.xml index 9be0885f..c0fbc78c 100644 --- a/integration/pom.xml +++ b/integration/pom.xml @@ -27,12 +27,12 @@ tools.dynamia tools.dynamia.parent - 5.4.11 + 5.4.12 DynamiaTools - Integration A set of classes and interfaces that help integrate modules - 5.4.11 + 5.4.12 https://dynamia.tools/docs/integration diff --git a/integration/src/main/java/tools/dynamia/integration/scheduling/SchedulerUtil.java b/integration/src/main/java/tools/dynamia/integration/scheduling/SchedulerUtil.java index 2c5de048..9c1985ba 100644 --- a/integration/src/main/java/tools/dynamia/integration/scheduling/SchedulerUtil.java +++ b/integration/src/main/java/tools/dynamia/integration/scheduling/SchedulerUtil.java @@ -53,29 +53,22 @@ public class SchedulerUtil { * Execute a task asynchronously using a Virtual Thread executor from {@link VT} helper class. * * @param task the runnable + * @return a completable future */ - public static void run(Runnable task) { + public static CompletableFuture run(Runnable task) { Runnable runnableWithContext = getWithContext(task); - VT.executor().execute(runnableWithContext); + return CompletableFuture.runAsync(runnableWithContext, VT.executor()); } /** * Execute a task using a Virtual Thread executor from {@link VT} helper class and wait for its completion - * or timeout. + * or timeout. This is a shorthand for run(task).get(timeout). * * @param task the runnable * @param timeout the timeout */ public static void runAndWait(Runnable task, Duration timeout) { - CompletableFuture future = new CompletableFuture<>(); - run(() -> { - try { - task.run(); - future.complete(null); - } catch (Exception e) { - future.completeExceptionally(e); - } - }); + CompletableFuture future = run(task); try { future.get(timeout.toMillis(), TimeUnit.MILLISECONDS); @@ -90,6 +83,7 @@ public static void runAndWait(Runnable task, Duration timeout) { * * @param firstTask the first task * @param others the tasks + * @return the completable future */ public static CompletableFuture run(Runnable firstTask, Runnable... others) { diff --git a/io/pom.xml b/io/pom.xml index 237b518b..bf7c79cb 100644 --- a/io/pom.xml +++ b/io/pom.xml @@ -28,11 +28,11 @@ tools.dynamia tools.dynamia.parent - 5.4.11 + 5.4.12 DynamiaTools - IO - 5.4.11 + 5.4.12 A set of classes and interfaces that help in any kind io task https://dynamia.tools/docs/io diff --git a/navigation/pom.xml b/navigation/pom.xml index 752b97b3..c56be039 100644 --- a/navigation/pom.xml +++ b/navigation/pom.xml @@ -23,11 +23,11 @@ tools.dynamia tools.dynamia.parent - 5.4.11 + 5.4.12 tools.dynamia.navigation - 5.4.11 + 5.4.12 DynamiaTools - Navigation https://dynamia.tools/docs/navigation @@ -63,17 +63,17 @@ tools.dynamia tools.dynamia.commons - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.integration - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.actions - 5.4.11 + 5.4.12 diff --git a/navigation/src/main/java/tools/dynamia/navigation/BaseNavigationManager.java b/navigation/src/main/java/tools/dynamia/navigation/BaseNavigationManager.java index 3c76dc80..3817b891 100644 --- a/navigation/src/main/java/tools/dynamia/navigation/BaseNavigationManager.java +++ b/navigation/src/main/java/tools/dynamia/navigation/BaseNavigationManager.java @@ -243,11 +243,16 @@ public void navigateTo(String path) { */ @Override public void navigateTo(String path, Map params) { - if (!path.contains("/")) { - setActiveModule(container.getModuleById(path)); - } else { - Page page = findPage(path); - setCurrentPage(page, params); + if(path!=null) { + path = path.trim(); + if (!path.contains("/")) { + setActiveModule(container.getModuleById(path)); + } else { + Page page = findPage(path); + setCurrentPage(page, params); + } + }else{ + logger.warn("path is null, cannot navigate"); } } diff --git a/pom.xml b/pom.xml index 870304ff..3fbe8d80 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ 4.0.0 tools.dynamia tools.dynamia.parent - 5.4.11 + 5.4.12 pom Dynamia Soluciones IT SAS @@ -80,7 +80,7 @@ ${project.baseUri} - 3.5.8 + 3.5.9 2.2.38 1 diff --git a/reports/pom.xml b/reports/pom.xml index 0766b79e..3fee21b6 100644 --- a/reports/pom.xml +++ b/reports/pom.xml @@ -26,11 +26,11 @@ tools.dynamia tools.dynamia.parent - 5.4.11 + 5.4.12 DynamiaTools - Reports - 5.4.11 + 5.4.12 A set of classes and interfaces that help building Reports https://dynamia.tools/docs/reports diff --git a/starter/pom.xml b/starter/pom.xml index 1c1fd00b..27ce261a 100644 --- a/starter/pom.xml +++ b/starter/pom.xml @@ -4,7 +4,7 @@ tools.dynamia tools.dynamia.parent - 5.4.11 + 5.4.12 dynamia-tools-starter DynamiaTools - Starter @@ -26,17 +26,22 @@ tools.dynamia tools.dynamia.app - 5.4.11 + 5.4.12 + + + tools.dynamia + tools.dynamia.commons + 5.4.12 tools.dynamia tools.dynamia.zk - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.domain.jpa - 5.4.11 + 5.4.12 org.hibernate.validator @@ -46,7 +51,7 @@ org.springdoc springdoc-openapi-starter-webmvc-api - 2.8.4 + 2.8.12 @@ -59,7 +64,7 @@ org.springframework.boot spring-boot-maven-plugin - 3.4.4 + ${springboot.version} -Dspring.application.admin.enabled=true diff --git a/templates/pom.xml b/templates/pom.xml index 83632e47..9fbfead2 100644 --- a/templates/pom.xml +++ b/templates/pom.xml @@ -23,12 +23,12 @@ tools.dynamia.parent tools.dynamia - 5.4.11 + 5.4.12 tools.dynamia.templates - 5.4.11 + 5.4.12 DynamiaTools - Templates https://dynamia.tools/docs/templates @@ -64,12 +64,12 @@ tools.dynamia tools.dynamia.integration - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.commons - 5.4.11 + 5.4.12 diff --git a/ui/pom.xml b/ui/pom.xml index c744d8d4..63b4b300 100644 --- a/ui/pom.xml +++ b/ui/pom.xml @@ -23,11 +23,11 @@ tools.dynamia tools.dynamia.parent - 5.4.11 + 5.4.12 tools.dynamia.ui - 5.4.11 + 5.4.12 DynamiaTools - UI https://dynamia.tools/docs/ui Helper classes for module integrations and messages @@ -64,17 +64,17 @@ tools.dynamia tools.dynamia.integration - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.commons - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.io - 5.4.11 + 5.4.12 diff --git a/viewers/pom.xml b/viewers/pom.xml index 75fca7b8..cbb57442 100644 --- a/viewers/pom.xml +++ b/viewers/pom.xml @@ -23,13 +23,13 @@ tools.dynamia.viewers jar - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.parent - 5.4.11 + 5.4.12 DynamiaTools - Viewers @@ -67,27 +67,27 @@ tools.dynamia tools.dynamia.commons - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.integration - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.io - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.domain - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.actions - 5.4.11 + 5.4.12 org.yaml diff --git a/viewers/src/main/java/tools/dynamia/viewers/JsonView.java b/viewers/src/main/java/tools/dynamia/viewers/JsonView.java index 8155d72c..279e9141 100644 --- a/viewers/src/main/java/tools/dynamia/viewers/JsonView.java +++ b/viewers/src/main/java/tools/dynamia/viewers/JsonView.java @@ -18,9 +18,9 @@ package tools.dynamia.viewers; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.module.SimpleModule; +import tools.dynamia.commons.StringPojoParser; import java.io.IOException; @@ -31,7 +31,7 @@ public class JsonView implements View { private T value; private View parentView; private ViewDescriptor viewDescriptor; - private ObjectMapper mapper; + private JsonMapper mapper; public JsonView() { @@ -80,7 +80,7 @@ public void setViewDescriptor(ViewDescriptor viewDescriptor) { public String renderJson() { try { - ObjectMapper mapper = getObjectMapper(); + var mapper = getJsonMapper(); return mapper.writeValueAsString(value); } catch (JsonProcessingException e) { throw new ViewRendererException("Exception rendering json view of " + value + " with descriptor " + viewDescriptor, e); @@ -94,18 +94,16 @@ public void parse(String json) { } try { //noinspection unchecked - value = (T) getObjectMapper().readValue(json, viewDescriptor.getBeanClass()); + value = (T) getJsonMapper().readValue(json, viewDescriptor.getBeanClass()); } catch (IOException e) { throw new ViewRendererException("Error parsing json to object", e); } } - private ObjectMapper getObjectMapper() { + private JsonMapper getJsonMapper() { if (mapper == null) { - mapper = new ObjectMapper(); - mapper.enable(SerializationFeature.INDENT_OUTPUT); - mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); + mapper = StringPojoParser.createJsonMapper(); SimpleModule module = new SimpleModule(); module.addSerializer(viewDescriptor.getBeanClass(), new JsonViewDescriptorSerializer(viewDescriptor)); //noinspection unchecked diff --git a/web/pom.xml b/web/pom.xml index 1decdc7c..5dd2c2bc 100644 --- a/web/pom.xml +++ b/web/pom.xml @@ -23,14 +23,14 @@ tools.dynamia.web jar - 5.4.11 + 5.4.12 A set of common classes and interfaces for web application development tools.dynamia tools.dynamia.parent - 5.4.11 + 5.4.12 DynamiaTools - Web @@ -88,27 +88,27 @@ tools.dynamia tools.dynamia.commons - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.integration - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.navigation - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.viewers - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.crud - 5.4.11 + 5.4.12 org.springframework diff --git a/web/src/main/java/tools/dynamia/web/navigation/RestNavigationController.java b/web/src/main/java/tools/dynamia/web/navigation/RestNavigationController.java index fe58c604..8d20b16e 100644 --- a/web/src/main/java/tools/dynamia/web/navigation/RestNavigationController.java +++ b/web/src/main/java/tools/dynamia/web/navigation/RestNavigationController.java @@ -19,8 +19,6 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; import jakarta.servlet.http.HttpServletRequest; import org.springframework.core.annotation.Order; import org.springframework.http.HttpHeaders; @@ -276,7 +274,7 @@ private ResponseEntity update(String path, Long id, String jsonData, Htt } ViewDescriptor descriptor = getJsonFormDescriptor(entityClass, true); - ObjectMapper mapper = new ObjectMapper(); + var mapper = StringPojoParser.createJsonMapper(); try { final ViewDescriptor desc = descriptor; JsonNode node = mapper.readTree(jsonData); @@ -338,10 +336,7 @@ public static int getParameterNumber(HttpServletRequest request, String name) { public static ResponseEntity getMetadata(HttpServletRequest request, ViewDescriptor viewDescriptor) { if (viewDescriptor != null && request.getParameter("_metadata") != null) { - ObjectMapper mapper = new ObjectMapper(); - mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); - + var mapper = StringPojoParser.createJsonMapper(); try { return new ResponseEntity<>(mapper.writeValueAsString(viewDescriptor), HttpStatus.OK); diff --git a/zk/pom.xml b/zk/pom.xml index 245107c0..b3c25174 100644 --- a/zk/pom.xml +++ b/zk/pom.xml @@ -21,12 +21,12 @@ tools.dynamia.parent tools.dynamia - 5.4.11 + 5.4.12 4.0.0 tools.dynamia.zk - 5.4.11 + 5.4.12 jar DynamiaTools - ZK https://dynamia.tools/docs/zk @@ -99,31 +99,31 @@ tools.dynamia tools.dynamia.web - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.navigation - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.ui - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.domain - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.viewers - 5.4.11 + 5.4.12 org.yaml @@ -134,19 +134,19 @@ tools.dynamia tools.dynamia.crud - 5.4.11 + 5.4.12 tools.dynamia tools.dynamia.reports - 5.4.11 + 5.4.12 compile tools.dynamia tools.dynamia.templates - 5.4.11 + 5.4.12 compile @@ -220,6 +220,12 @@ spring-boot-autoconfigure + + com.google.guava + failureaccess + 1.0.3 + + diff --git a/zk/src/main/java/tools/dynamia/zk/ui/LongOperationMonitorWindow.java b/zk/src/main/java/tools/dynamia/zk/ui/LongOperationMonitorWindow.java index b1e9797a..33fd4e47 100644 --- a/zk/src/main/java/tools/dynamia/zk/ui/LongOperationMonitorWindow.java +++ b/zk/src/main/java/tools/dynamia/zk/ui/LongOperationMonitorWindow.java @@ -17,141 +17,142 @@ package tools.dynamia.zk.ui; import org.zkoss.zk.ui.event.Events; -import org.zkoss.zul.Button; -import org.zkoss.zul.Caption; -import org.zkoss.zul.Div; -import org.zkoss.zul.Hlayout; -import org.zkoss.zul.Label; -import org.zkoss.zul.Progressmeter; -import org.zkoss.zul.Vlayout; -import org.zkoss.zul.Window; -import tools.dynamia.commons.Callback; +import org.zkoss.zul.*; import tools.dynamia.commons.ClassMessages; +import tools.dynamia.commons.DateTimeUtils; import tools.dynamia.commons.Messages; -import tools.dynamia.commons.StopWatch; import tools.dynamia.commons.logger.LoggingService; +import tools.dynamia.integration.ProgressEvent; import tools.dynamia.integration.ProgressMonitor; +import tools.dynamia.ui.MessageType; import tools.dynamia.ui.UIMessages; import tools.dynamia.zk.util.LongOperation; import tools.dynamia.zk.util.ZKUtil; -import java.util.function.Consumer; +import java.util.Date; public class LongOperationMonitorWindow extends Window { - /** - * - */ - private static final long serialVersionUID = -2630380982547205553L; - public static final int DEFAULT_REFRESH_RATE = 2000; + private static final long serialVersionUID = 1L; + private final ClassMessages messages = ClassMessages.get(LongOperationMonitorWindow.class); private final ProgressMonitor monitor; + private final LongOperation longOperation; private Progressmeter progress; - private String messageTemplate = messages.get("DefaultProgressMessage"); - private int refreshRate; - private final LongOperation longOperation; private Caption titleCaption; private Label messageLabel; - public LongOperationMonitorWindow(LongOperation longOperation, ProgressMonitor monitor) { - this(longOperation, monitor, DEFAULT_REFRESH_RATE); - } + private String messageTemplate = messages.get("DefaultProgressMessage"); + private Listbox logListbox; + private boolean showLog; - public LongOperationMonitorWindow(LongOperation longOperation, ProgressMonitor monitor, int refreshRate) { + public LongOperationMonitorWindow(LongOperation longOperation, ProgressMonitor monitor) { this.longOperation = longOperation; this.monitor = monitor; - this.refreshRate = refreshRate; initUI(); - initMonitor(); + bindEventListeners(); setPage(ZKUtil.getFirstPage()); } - public static LongOperationMonitorWindow show(String title, LongOperation longOperation, ProgressMonitor monitor) { - return show(title, longOperation, monitor, DEFAULT_REFRESH_RATE); + public static LongOperationMonitorWindow show(String title, + LongOperation longOperation, + ProgressMonitor monitor) { + LongOperationMonitorWindow win = new LongOperationMonitorWindow(longOperation, monitor); + win.setTitle(title); + win.setPosition("center"); + win.doModal(); + longOperation.onException(e -> win.detach()); + return win; } - public static LongOperationMonitorWindow show(String title, LongOperation longOperation, ProgressMonitor monitor, int refreshRate) { - LongOperationMonitorWindow wind = new LongOperationMonitorWindow(longOperation, monitor, refreshRate); - wind.setTitle(title); - wind.setPosition("center"); - wind.doModal(); - return wind; - } - - /** - * Run and show a progress window for a long-running operation - */ - public static LongOperationMonitorWindow start(String title, Consumer operation, Callback onFinish) { + public static LongOperationMonitorWindow start(String title, + String finishMessage, + java.util.function.Consumer op) { var monitor = new ProgressMonitor(); - var longOp = LongOperation.create() - .execute(() -> operation.accept(monitor)) - .onFinish(onFinish); + LongOperation longOp = LongOperation.create() + .execute(() -> op.accept(monitor)) + .onFinish(() -> UIMessages.showMessage(finishMessage)); longOp.start(); - return show(title, longOp, monitor); } - /** - * Run and show a progress window for a long-running operation - */ - public static LongOperationMonitorWindow start(String title, String finishMessage, Consumer operation) { - return start(title, operation, () -> UIMessages.showMessage(finishMessage)); - } - - private void initMonitor() { + private void bindEventListeners() { if (monitor != null) { - StopWatch stopWatch = new StopWatch(refreshRate); - monitor.onProgressChanged(evt -> { - if ((stopWatch.now() && !monitor.isStopped()) || monitor.getCurrent() >= monitor.getMax()) { - longOperation.updateUI(() -> { - progress.setValue(evt.getPercent()); - progress.setTooltiptext(evt.getPercent() + "%"); - messageLabel.setValue(evt.getMessage()); - setTitle(Messages.format(messageTemplate, evt.getCurrent(), evt.getMax(), evt.getPercent())); - }); + monitor.onProgressChanged(longOperation::progress); + } + + longOperation.onEvent(event -> { + switch (event.getType()) { + case START -> showStartUI(); + case PROGRESS -> updateProgress(event.getProgress()); + case FINISH, CANCEL -> finish(); + case EXCEPTION -> { + UIMessages.showMessageDialog(messages.get("OperationErrorMessage") + ": " + event.getError().getMessage(), + messages.get("OperationErrorTitle"), MessageType.ERROR); + finish(); } - }); + } + }); - longOperation.onCleanup(this::finish); - } + // Cleanup on window close → cancel op + addEventListener(Events.ON_CLOSE, evt -> { + evt.stopPropagation(); + longOperation.cancel(); + }); + } + private void showStartUI() { + titleCaption.setIconSclass("fa fa-refresh fa-spin fa-2x"); } - private void finish() { - if (ZKUtil.isInEventListener()) { - detach(); - } else { - longOperation.updateUI(this::detach); + private void updateProgress(ProgressEvent evt) { + progress.setValue(evt.getPercent()); + progress.setTooltiptext(evt.getPercent() + "%"); + messageLabel.setValue(evt.getMessage()); + if (isShowLog()) { + var item = logListbox.appendItem(DateTimeUtils.formatTime(new Date()) + " - " + evt.getMessage(), ""); + logListbox.scrollToIndex(item.getIndex()); } + String title = Messages.format(messageTemplate, + evt.getCurrent(), evt.getMax(), evt.getPercent()); + setTitle(title); } - private void initUI() { + private void finish() { + titleCaption.setIconSclass("fa fa-check"); + detach(); // UI thread safe — EventQueue guarantees it + } + private void initUI() { setWidth("500px"); setClosable(true); - setStyle("padding: 10px"); + setStyle("padding:10px"); + titleCaption = new Caption(""); - titleCaption.setIconSclass("fa fa-refresh fa-spin fa-2x"); titleCaption.setParent(this); Vlayout layout = new Vlayout(); layout.setHflex("1"); layout.setParent(this); + logListbox = new Listbox(); + logListbox.setVisible(false); + logListbox.setHeight("150px"); + logListbox.setParent(layout); + progress = new Progressmeter(); progress.setHflex("2"); - progress.setParent(layout); progress.setValue(0); + progress.setParent(layout); - Div messageContainer = new Div(); - messageContainer.setStyle("text-align: center"); + Div msg = new Div(); + msg.setStyle("text-align:center"); messageLabel = new Label(); - messageLabel.setParent(messageContainer); - messageContainer.setParent(layout); - + messageLabel.setParent(msg); + msg.setParent(layout); Hlayout hlayout = new Hlayout(); hlayout.setHflex("1"); @@ -179,37 +180,33 @@ private void initUI() { } - private void stop() { + protected void stop() { try { longOperation.onFinish(null); monitor.stop(); } catch (Exception e) { - LoggingService.get(getClass()).error("Error stopping long operation", e); + LoggingService.get(LongOperationMonitorWindow.class).error("Error stopping long operation", e); } finally { finish(); } } - public String getMessageTemplate() { - return messageTemplate; + @Override + public void setTitle(String title) { + titleCaption.setLabel(title); } public void setMessageTemplate(String messageTemplate) { this.messageTemplate = messageTemplate; } - public int getRefreshRate() { - return refreshRate; - } - - public void setRefreshRate(int refreshRate) { - this.refreshRate = refreshRate; + public boolean isShowLog() { + return showLog; } - @Override - public void setTitle(String title) { - titleCaption.setLabel(title); + public void setShowLog(boolean showLog) { + this.showLog = showLog; + logListbox.setVisible(showLog); } - } diff --git a/zk/src/main/java/tools/dynamia/zk/util/LongOperation.java b/zk/src/main/java/tools/dynamia/zk/util/LongOperation.java index aad1385b..5e5c1b60 100644 --- a/zk/src/main/java/tools/dynamia/zk/util/LongOperation.java +++ b/zk/src/main/java/tools/dynamia/zk/util/LongOperation.java @@ -16,245 +16,296 @@ */ package tools.dynamia.zk.util; -import org.springframework.core.task.TaskExecutor; -import org.zkoss.zk.ui.Desktop; -import org.zkoss.zk.ui.DesktopUnavailableException; -import org.zkoss.zk.ui.Executions; -import org.zkoss.zk.ui.Sessions; -import org.zkoss.zk.ui.WebApps; -import org.zkoss.zk.ui.sys.DesktopCache; -import org.zkoss.zk.ui.sys.DesktopCtrl; -import org.zkoss.zk.ui.sys.WebAppCtrl; +import org.zkoss.zk.ui.event.Event; +import org.zkoss.zk.ui.event.EventListener; +import org.zkoss.zk.ui.event.EventQueue; +import org.zkoss.zk.ui.event.EventQueues; import tools.dynamia.commons.Callback; import tools.dynamia.commons.logger.LoggingService; +import tools.dynamia.integration.ProgressEvent; +import tools.dynamia.integration.scheduling.SchedulerUtil; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; +/** + * Executes a long-running operation asynchronously while safely notifying + * the UI through a session-scoped EventQueue. + *

+ * This class provides callback hooks for start, execution, finish, + * cancel, cleanup and exception handling without requiring + * manual server push or desktop activation. + * + *

Usage example:

+ *
{@code
+ * LongOperation.create()
+ *     .onStart(() -> showBusyIndicator())
+ *     .execute(() -> runHeavyTask())
+ *     .onFinish(() -> showSuccess())
+ *     .onCleanup(() -> hideBusyIndicator())
+ *     .start();
+ * }
+ */ public class LongOperation implements Runnable { - private String desktopId; - private DesktopCache desktopCache; - private Thread thread; - private TaskExecutor taskExecutor; - private final AtomicBoolean cancelled = new AtomicBoolean(false); - private Callback onFinishCallbak; - private Callback onCancelCallback; - private Consumer onExceptionConsumer; - private Callback onCleanupCallback; - private Callback executeCallback; - private Callback onStartCallback; + private static final LoggingService LOGGER = LoggingService.get(LongOperation.class); + private EventQueue opQueue; - public LongOperation() { + /** + * Types of UI events triggered by a running LongOperation. + */ + public enum LongOpEventType { + START, FINISH, CANCEL, EXCEPTION, CLEANUP, PROGRESS + } + + /** + * Simple UI event payload for queue notifications. + */ + public static class LongOpEvent extends Event { + private LongOpEventType type; + private ProgressEvent progress; + private Exception error; + + public LongOpEvent(String name) { + super(name); + } + + public LongOpEvent(LongOpEventType type, ProgressEvent progress, Exception error) { + super(type.name()); + this.type = type; + this.progress = progress; + this.error = error; + } + + public LongOpEventType getType() { + return type; + } + + public ProgressEvent getProgress() { + return progress; + } + public Exception getError() { + return error; + } } - public LongOperation(TaskExecutor taskExecutor) { - super(); - this.taskExecutor = taskExecutor; + private final UUID taskId = UUID.randomUUID(); + private String name; + + private Callback onStartCallback; + private Callback executeCallback; + private Callback onFinishCallback; + private Callback onCancelCallback; + private Callback onCleanupCallback; + private List> onExceptionConsumer; + private List> onProgressConsumer; + + private CompletableFuture future; + private final AtomicBoolean cancelled = new AtomicBoolean(false); + + /** + * Create a new LongOperation instance. + */ + public LongOperation() { + this.name = "LongOperation-" + taskId; } - public LongOperation onStart(Callback onStart) { - this.onStartCallback = onStart; + /** + * Assign a human-readable name used for logs and debugging. + */ + public LongOperation name(String name) { + this.name = name; return this; } /** - * asynchronous callback for your long operation code + * Register callback executed before the long task begins. */ - public LongOperation execute(Callback executeCallback) { - this.executeCallback = executeCallback; + public LongOperation onStart(Callback cb) { + this.onStartCallback = cb; return this; } /** - * optional callback method when the task has completed successfully + * Register long-running asynchronous operation code. */ - public LongOperation onFinish(Callback onFinishCallback) { - this.onFinishCallbak = onFinishCallback; + public LongOperation execute(Callback cb) { + this.executeCallback = cb; return this; } /** - * optional callback method when the task has been cancelled or was - * interrupted otherwise + * Register callback when task completes successfully. */ - public LongOperation onCancel(Callback onCancelCallback) { - this.onCancelCallback = onCancelCallback; + public LongOperation onFinish(Callback cb) { + this.onFinishCallback = cb; return this; } /** - * optional callback method when the task has completed with an uncaught - * Excepion + * Register callback when task is cancelled. */ - public LongOperation onException(Consumer onExceptionConsumer) { - this.onExceptionConsumer = onExceptionConsumer; + public LongOperation onCancel(Callback cb) { + this.onCancelCallback = cb; return this; } /** - * optional callback method when the task has completed (always called) + * Register callback when an uncaught exception occurs. Supports multiple consumers. */ - public LongOperation onCleanup(Callback onCleanupCallback) { - this.onCleanupCallback = onCleanupCallback; + public LongOperation onException(Consumer cb) { + if (onExceptionConsumer == null) { + onExceptionConsumer = new ArrayList<>(); + } + this.onExceptionConsumer.add(cb); return this; } /** - * set the cancelled flag and try to interrupt the thread + * Register callback always executed at the end. */ - public final void cancel() { - cancelled.set(true); - if (thread != null) { - thread.interrupt(); - } + public LongOperation onCleanup(Callback cb) { + this.onCleanupCallback = cb; + return this; } /** - * check the cancelled flag + * Register callback to receive UI progress reports (0-100). Sopports multiple consumers. */ - public final boolean isCancelled() { - return cancelled.get(); + public LongOperation onProgress(Consumer cb) { + if (onProgressConsumer == null) { + onProgressConsumer = new ArrayList<>(); + } + this.onProgressConsumer.add(cb); + return this; } /** - * activate the thread (and cached desktop) for UI updates call - * {@link #deactivate()} once done updating the UI + * Notify the UI of current progress. + * Safe to call from background execution. */ - protected final void activate() throws InterruptedException { - Executions.activate(getDesktop()); + public void progress(ProgressEvent progress) { + postEvent(LongOpEventType.PROGRESS, progress, null); } /** - * deactivate the current active (see: {@link #activate()}) thread/desktop - * after updates are done + * Cancel the running task. + * It will trigger a CANCEL event on the UI. */ - protected final void deactivate() { - Executions.deactivate(getDesktop()); + public void cancel() { + cancelled.set(true); + if (future != null) future.cancel(true); + postEvent(LongOpEventType.CANCEL, null, null); + LOGGER.warn("Task {} cancelled", name); } /** - * Checks if the task thread has been interrupted. Use this to check whether - * or not to exit a busy operation in case. - * - * @throws InterruptedException when the current task has been - * cancelled/interrupted + * @return whether this operation has been cancelled */ - protected final void checkCancelled() throws InterruptedException { - if (Thread.currentThread() != this.thread && taskExecutor == null) { - throw new IllegalStateException("this method can only be called in the worker thread (i.e. during execute)"); - } - boolean interrupted = Thread.interrupted(); - if (interrupted || cancelled.get()) { - cancelled.set(true); - throw new InterruptedException(); - } + public boolean isCancelled() { + return cancelled.get(); } /** - * launch the long operation + * Launch the long-running operation. + * Registers internal UI event listeners automatically. */ - public final LongOperation start() { - // not caching the desktop directly to enable garbage collection, in - // case the desktop destroyed during the long operation - this.desktopId = Executions.getCurrent().getDesktop().getId(); - this.desktopCache = ((WebAppCtrl) WebApps.getCurrent()).getDesktopCache(Sessions.getCurrent()); - enableServerPushForThisTask(); - if (taskExecutor == null) { - thread = new Thread(this); - thread.start(); - } else { - taskExecutor.execute(this); - } + public LongOperation start() { + registerQueueListener(); + postEvent(LongOpEventType.START, null, null); + LOGGER.info("Starting task: {}", name); + future = SchedulerUtil.run(this); return this; } @Override - public final void run() { + public void run() { try { - try { - runCallback(onStartCallback); - checkCancelled(); // avoid unnecessary execution - execute(); - checkCancelled(); // final cancelled check before calling - // onFinish - activate(); - finish(); - deactivate(); - } catch (InterruptedException e) { - try { - cancelled.set(true); - activate(); - runCallback(onCancelCallback); - deactivate(); - } catch (InterruptedException e1) { - throw new RuntimeException("interrupted onCancel handling", e1); - } - } catch (Exception rte) { - try { - activate(); - if (onExceptionConsumer != null) { - onExceptionConsumer.accept(rte); - } - deactivate(); - } catch (InterruptedException e1) { - throw new RuntimeException("interrupted onException handling", e1); - } - throw rte; - } + if (!cancelled.get()) safeExecute(executeCallback); + if (!cancelled.get()) postEvent(LongOpEventType.FINISH, null, null); + } catch (Exception ex) { + postEvent(LongOpEventType.EXCEPTION, null, cast(ex)); } finally { - updateUI(onCleanupCallback); - disableServerPushForThisTask(); + postEvent(LongOpEventType.CLEANUP, null, null); } } - protected void finish() { - runCallback(onFinishCallbak); + private Exception cast(Throwable t) { + return (t instanceof Exception e) ? e : new Exception(t); } - protected void execute() { - runCallback(executeCallback); + private void safeExecute(Callback cb) { + if (cb != null) cb.doSomething(); } - protected void runCallback(Callback callback) { - if (callback != null) { - callback.doSomething(); + // ======================== + // UI Notification Layer + // ======================== + + private EventQueue queue() { + if (opQueue == null) { + opQueue = EventQueues.lookup(name + "-" + taskId, EventQueues.SESSION, true); } + return opQueue; + } + private void postEvent(LongOpEventType type, ProgressEvent progress, Exception ex) { + queue().publish(new LongOpEvent(type, progress, ex)); } - private final UUID taskId = UUID.randomUUID(); + private void registerQueueListener() { + queue().subscribe(event -> { + try { + switch (event.getType()) { + case START -> safeExecute(onStartCallback); + case FINISH -> safeExecute(onFinishCallback); + case CANCEL -> safeExecute(onCancelCallback); + case EXCEPTION -> safeException(event.getError()); + case PROGRESS -> safeProgress(event.getProgress()); + case CLEANUP -> cleanup(); + } + } catch (Exception e) { + LOGGER.error("Error during UI callback for {}", name, e); + } + }); + } + + public void onEvent(EventListener listener) { + queue().subscribe(listener); + } - private void enableServerPushForThisTask() { - ((DesktopCtrl) getDesktop()).enableServerPush(true, taskId); + private void safeException(Exception e) { + if (onExceptionConsumer != null) onExceptionConsumer.forEach(c -> c.accept(e)); + LOGGER.error("Unhandled exception in task {}", name, e); } - private void disableServerPushForThisTask() { - ((DesktopCtrl) getDesktop()).enableServerPush(false, taskId); + private void safeProgress(ProgressEvent p) { + if (onProgressConsumer != null && p != null) onProgressConsumer.forEach(c -> c.accept(p)); } - private Desktop getDesktop() { - return desktopCache.getDesktop(desktopId); + private void cleanup() { + safeExecute(onCleanupCallback); + removeQueue(); + LOGGER.info("Task {} finished and cleaned", name); } - public void updateUI(Callback callback) { + private void removeQueue() { try { - activate(); - runCallback(callback); - deactivate(); - } catch (DesktopUnavailableException | InterruptedException e) { - LoggingService.get(getClass()).error("Error updating UI", e); + EventQueues.remove(name + "-" + taskId, EventQueues.SESSION); + } catch (Exception e) { + LOGGER.warn("Failed cleaning queue for task {}", name, e); } } + /** + * Factory shortcut + */ public static LongOperation create() { return new LongOperation(); } - - public static LongOperation create(TaskExecutor taskExecutor) { - return new LongOperation(taskExecutor); - } - } diff --git a/zk/src/main/java/tools/dynamia/zk/util/ResultLongOperation.java b/zk/src/main/java/tools/dynamia/zk/util/ResultLongOperation.java deleted file mode 100644 index 162c4b11..00000000 --- a/zk/src/main/java/tools/dynamia/zk/util/ResultLongOperation.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2023 Dynamia Soluciones IT S.A.S - NIT 900302344-1 - * Colombia / South America - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package tools.dynamia.zk.util; - -import org.springframework.core.task.TaskExecutor; - -import java.util.function.Consumer; -import java.util.function.Supplier; - -/** - * Let you perform LongOperations that returns something and is UI events aware. - * Use executeWithResult() method instead execute() - * - * @author Mario - * - * @param - */ -public class ResultLongOperation extends LongOperation { - - private R result; - private Supplier executeSupplier; - private Consumer onResultConsumer; - - public ResultLongOperation() { - super(); - - } - - public ResultLongOperation(TaskExecutor taskExecutor) { - super(taskExecutor); - - } - - public ResultLongOperation executeWithResult(Supplier supplier) { - this.executeSupplier = supplier; - return this; - } - - public ResultLongOperation onResult(Consumer onResultConsumer) { - this.onResultConsumer = onResultConsumer; - return this; - } - - @Override - protected void execute() { - result = executeSupplier.get(); - } - - @Override - protected void finish() { - if (onResultConsumer != null) { - onResultConsumer.accept(result); - } - super.finish(); - } - - public static ResultLongOperation create(Class expectedClass) { - return new ResultLongOperation<>(); - } - - public static ResultLongOperation create(Class expectedClass, TaskExecutor taskExecutor) { - return new ResultLongOperation<>(taskExecutor); - } - -} diff --git a/zk/src/main/java/tools/dynamia/zk/util/ZKUtil.java b/zk/src/main/java/tools/dynamia/zk/util/ZKUtil.java index 33036f4f..79cc7d09 100644 --- a/zk/src/main/java/tools/dynamia/zk/util/ZKUtil.java +++ b/zk/src/main/java/tools/dynamia/zk/util/ZKUtil.java @@ -23,6 +23,7 @@ import org.zkoss.zk.ui.event.EventListener; import org.zkoss.zk.ui.util.Clients; import org.zkoss.zul.*; +import org.zkoss.zul.Timer; import org.zkoss.zul.ext.Paginal; import org.zkoss.zul.impl.InputElement; import org.zkoss.zul.impl.LabelImageElement; @@ -52,6 +53,7 @@ import java.io.IOException; import java.io.InputStreamReader; +import java.time.Duration; import java.util.*; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -1291,4 +1293,104 @@ private static void flat(TreeModel treeModel, ArrayList result, Object p } } + /** + * Schedules a runnable to execute after a specified duration within a given page context. + * Uses a ZK Timer component to delay execution. If not in desktop scope, runs immediately. + * + * @param duration the duration to wait before executing the runnable + * @param page the ZK page context to associate the timer with + * @param runnable the runnable task to execute + */ + public static void runLater(Duration duration, Page page, Runnable runnable) { + if (ZKUtil.isInDesktopScope()) { + buildTimer(duration, page, false, runnable); + } else { + //If not in desktop scope, run immediately + runnable.run(); + } + } + + /** + * Schedules a runnable to execute after a specified duration within the first page context. + * Uses a ZK Timer component to delay execution. + * + * @param duration the duration to wait before executing the runnable + * @param runnable the runnable task to execute + */ + public static void runLater(Duration duration, Runnable runnable) { + runLater(duration, getFirstPage(), runnable); + } + + /** + * Schedules a runnable to execute after 500ms within a given page context. + * Uses a ZK Timer component to delay execution. + * + * @param runnable the runnable task to execute + */ + public static void runLater(Runnable runnable) { + runLater(Duration.ofMillis(500), getFirstPage(), runnable); + } + + /** + * Schedules a runnable to execute at regular intervals within a given page context. + * Uses a ZK Timer component to repeatedly execute the task. If not in desktop scope, runs immediately. + * + * @param interval the interval duration between executions + * @param page the ZK page context to associate the timer with + * @param runnable the runnable task to execute + */ + public static void runAtIntervals(Duration interval, Page page, Runnable runnable) { + if (ZKUtil.isInDesktopScope()) { + buildTimer(interval, page, true, runnable); + } else { + //If not in desktop scope, run immediately + runnable.run(); + } + } + + /** + * Schedules a runnable to execute at regular intervals within the first page context. + * Uses a ZK Timer component to repeatedly execute the task. + * + * @param interval the interval duration between executions + * @param runnable the runnable task to execute + */ + public static void runAtIntervals(Duration interval, Runnable runnable) { + runAtIntervals(interval, getFirstPage(), runnable); + } + + /** + * Builds a ZK Timer component with specified delay, runnable task, and repeat behavior. + * + * @param delay the delay duration before executing the task + * @param repeats true to repeat execution at intervals, false for single execution + * @param runnable the runnable task to execute + * @return the configured Timer component + */ + public static Timer buildTimer(Duration delay, boolean repeats, Runnable runnable) { + Timer timer = new Timer(); + timer.setDelay((int) delay.toMillis()); + timer.addEventListener(Events.ON_TIMER, e -> { + runnable.run(); + if (!repeats) { + timer.detach(); + } + }); + return timer; + } + + /** + * Builds a ZK Timer component with specified delay, page context, runnable task, and repeat behavior. + * + * @param delay the delay duration before executing the task + * @param page the ZK page context to associate the timer with + * @param repeats true to repeat execution at intervals, false for single execution + * @param runnable the runnable task to execute + * @return the configured Timer component + */ + public static Timer buildTimer(Duration delay, Page page, boolean repeats, Runnable runnable) { + var timer = buildTimer(delay, repeats, runnable); + timer.setPage(page); + return timer; + } } diff --git a/zk/src/main/resources/metainfo/zk/lang-addon.xml b/zk/src/main/resources/metainfo/zk/lang-addon.xml index 1c3405a8..de842f23 100644 --- a/zk/src/main/resources/metainfo/zk/lang-addon.xml +++ b/zk/src/main/resources/metainfo/zk/lang-addon.xml @@ -40,7 +40,7 @@ - + diff --git a/zk/src/main/resources/static/dynamia-tools/js/dynamia-tools.js b/zk/src/main/resources/static/dynamia-tools/js/dynamia-tools.js deleted file mode 100644 index 1be9a6c2..00000000 --- a/zk/src/main/resources/static/dynamia-tools/js/dynamia-tools.js +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (C) 2023 Dynamia Soluciones IT S.A.S - NIT 900302344-1 - * Colombia / South America - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -function changeHash(value) { - if (window.history && window.history.pushState) { - - history.pushState({}, "Page " + value, getContextPath() + "/page/" - + value); - } else { - window.location.hash = value; - } -} - -function sendMeHash(uuid) { - var hash = window.location.hash; - if (hash) { - hash = hash.substring(1, hash.length); - - zAu.send(new zk.Event(zk.Widget.$(uuid), 'onHash', hash)); - } -} - -function getContextPath() { - var path = window.location.pathname.substring(0, window.location.pathname - .indexOf("/", 2)); - - if (path == '/page') { - path = ''; - } - - return path; -} - -function getServerPath() { - return window.location.href.substring(0, window.location.href - .indexOf(window.location.pathname)); -} - -function getFullContextPath() { - return getServerPath() + getContextPath(); -} - -function openURL(url) { - window.open(url, "_blank"); -} - -function copyToClipboard(txt) { - window.prompt("Presione Ctrl+C y luego Enter", txt); -} - -function toggleFullScreen() { - if ((document.fullScreenElement && document.fullScreenElement !== null) - || (!document.mozFullScreen && !document.webkitIsFullScreen)) { - if (document.documentElement.requestFullScreen) { - document.documentElement.requestFullScreen(); - } else if (document.documentElement.mozRequestFullScreen) { - document.documentElement.mozRequestFullScreen(); - } else if (document.documentElement.webkitRequestFullScreen) { - document.documentElement - .webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT); - } - } else { - if (document.cancelFullScreen) { - document.cancelFullScreen(); - } else if (document.mozCancelFullScreen) { - document.mozCancelFullScreen(); - } else if (document.webkitCancelFullScreen) { - document.webkitCancelFullScreen(); - } - } -} - -function changeURI(title,uri) { - if (window.history && window.history.pushState) { - history.pushState({}, title, uri); - } -} \ No newline at end of file diff --git a/zk/src/main/resources/static/dynamia-tools/js/dynamia-tools-ws.js b/zk/src/main/resources/static/dynamia-tools/js/zk/dynamia-tools-ws.js similarity index 100% rename from zk/src/main/resources/static/dynamia-tools/js/dynamia-tools-ws.js rename to zk/src/main/resources/static/dynamia-tools/js/zk/dynamia-tools-ws.js diff --git a/zk/src/main/resources/static/dynamia-tools/js/zk/dynamia-tools.js b/zk/src/main/resources/static/dynamia-tools/js/zk/dynamia-tools.js new file mode 100644 index 00000000..08a0a0be --- /dev/null +++ b/zk/src/main/resources/static/dynamia-tools/js/zk/dynamia-tools.js @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2023 Dynamia Soluciones IT S.A.S - NIT 900302344-1 + * Colombia / South America + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Changes the browser URL hash or history state. + * @param {string} value - The new hash value or page identifier. + */ +function changeHash(value) { + if (window.history && window.history.pushState) { + history.pushState({}, "Page " + value, getContextPath() + "/page/" + value); + } else { + window.location.hash = value; + } +} + +/** + * Sends the current URL hash to a ZK component. + * @param {string} uuid - The UUID of the ZK component. + */ +function sendMeHash(uuid) { + var hash = window.location.hash; + if (hash) { + hash = hash.substring(1, hash.length); + if (typeof zAu !== 'undefined' && typeof zk !== 'undefined') { + zAu.send(new zk.Event(zk.Widget.$(uuid), 'onHash', hash)); + } + } +} + +/** + * Gets the context path of the application. + * @returns {string} The context path. + */ +function getContextPath() { + var path = window.location.pathname.substring(0, window.location.pathname.indexOf("/", 2)); + if (path === '/page') { + path = ''; + } + return path; +} + +/** + * Gets the server path (protocol + domain + port). + * @returns {string} The server path. + */ +function getServerPath() { + if (window.location.origin) { + return window.location.origin; + } + return window.location.href.substring(0, window.location.href.indexOf(window.location.pathname)); +} + +/** + * Gets the full context path (server path + context path). + * @returns {string} The full context path. + */ +function getFullContextPath() { + return getServerPath() + getContextPath(); +} + +/** + * Opens a URL in a new tab. + * @param {string} url - The URL to open. + */ +function openURL(url) { + window.open(url, "_blank"); +} + +/** + * Copies text to the clipboard. + * Tries to use the modern Clipboard API, falls back to execCommand, and finally to prompt. + * @param {string} txt - The text to copy. + */ +function copyToClipboard(txt) { + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard.writeText(txt).catch(function(err) { + fallbackCopyToClipboard(txt); + }); + } else { + fallbackCopyToClipboard(txt); + } +} + +function fallbackCopyToClipboard(txt) { + var textArea = document.createElement("textarea"); + textArea.value = txt; + + // Avoid scrolling to bottom + textArea.style.top = "0"; + textArea.style.left = "0"; + textArea.style.position = "fixed"; + + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + try { + var successful = document.execCommand('copy'); + if (!successful) { + window.prompt("Press Ctrl+C and then Enter", txt); + } + } catch (err) { + window.prompt("Press Ctrl+C and then Enter", txt); + } + + document.body.removeChild(textArea); +} + +/** + * Toggles full screen mode. + */ +function toggleFullScreen() { + if (document.fullScreenElement || + (!document.mozFullScreen && !document.webkitIsFullScreen)) { + if (document.documentElement.requestFullScreen) { + document.documentElement.requestFullScreen(); + } else if (document.documentElement.mozRequestFullScreen) { + document.documentElement.mozRequestFullScreen(); + } else if (document.documentElement.webkitRequestFullScreen) { + document.documentElement.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT); + } + } else { + if (document.cancelFullScreen) { + document.cancelFullScreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.webkitCancelFullScreen) { + document.webkitCancelFullScreen(); + } + } +} + +/** + * Changes the browser URI using history.pushState. + * @param {string} title - The title of the new state. + * @param {string} uri - The new URI. + */ +function changeURI(title, uri) { + if (window.history && window.history.pushState) { + history.pushState({}, title, uri); + } +} \ No newline at end of file