From 00144ddfd5a62428fed5e1a04f3fc952c09930c1 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Mon, 23 Feb 2026 14:59:58 +0100 Subject: [PATCH 1/8] [NAE-2382] Elastic bulk index fails if option fields have empty string - updated null or empty string key handling for elastic translation objects - added additional logging to RestResponseExceptionHandler --- .../workflow/web/RestResponseExceptionHandler.java | 7 +++++++ .../engine/objects/elastic/domain/MapField.java | 12 +++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/application-engine/src/main/java/com/netgrif/application/engine/workflow/web/RestResponseExceptionHandler.java b/application-engine/src/main/java/com/netgrif/application/engine/workflow/web/RestResponseExceptionHandler.java index 344c46ece8..effbed44d1 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/workflow/web/RestResponseExceptionHandler.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/workflow/web/RestResponseExceptionHandler.java @@ -33,9 +33,16 @@ protected ResponseEntity handleHttpMessageNotWritable(HttpMessageNotWrit } List path = jme.getPath(); + if (log.isDebugEnabled()) { + for (int i = 0; i < path.size(); i++) { + log.debug("Reference[{}]: {}", i, path.get(i)); + } + } if (path.size() > 3) { Object fieldFrom = path.getLast().getFrom(); + log.debug("Field of class [{}] from: {}", fieldFrom.getClass(), fieldFrom); Object caseFrom = path.get(path.size() - 3).getFrom(); + log.debug("Case of class [{}] from: {}", caseFrom.getClass(), caseFrom); if (fieldFrom instanceof Field field && caseFrom instanceof Case useCase) { log.debug("[{}] Could not parse value of field [{}], value [{}] | path={}", diff --git a/nae-object-library/src/main/java/com/netgrif/application/engine/objects/elastic/domain/MapField.java b/nae-object-library/src/main/java/com/netgrif/application/engine/objects/elastic/domain/MapField.java index 2b7ea8ec87..2b8336d6bf 100644 --- a/nae-object-library/src/main/java/com/netgrif/application/engine/objects/elastic/domain/MapField.java +++ b/nae-object-library/src/main/java/com/netgrif/application/engine/objects/elastic/domain/MapField.java @@ -13,6 +13,8 @@ @EqualsAndHashCode(callSuper = true) public abstract class MapField extends TextField { + public static final String NONE_OPTION_KEY = "none"; + protected List keyValue; protected Map keyValueTranslations; @@ -21,7 +23,7 @@ public MapField(MapField field) { this.keyValue = field.keyValue == null ? null : new ArrayList<>(field.keyValue); this.keyValueTranslations = field.keyValueTranslations == null ? null : field.keyValueTranslations.entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, entry -> new I18nString(entry.getValue()))); + .collect(Collectors.toMap(entry -> resolveTranslationPairKey(entry.getKey()), entry -> new I18nString(entry.getValue()))); } public MapField(Map.Entry valueTranslationPair) { @@ -36,9 +38,9 @@ public MapField(List> valueTranslationPairs) { this.keyValue = new ArrayList<>(); this.keyValueTranslations = new HashMap<>(); for (Map.Entry valueTranslationPair : valueTranslationPairs) { - this.keyValue.add(valueTranslationPair.getKey()); + this.keyValue.add(resolveTranslationPairKey(valueTranslationPair.getKey())); values.addAll(I18nStringUtils.collectTranslations(valueTranslationPair.getValue())); - this.keyValueTranslations.put(valueTranslationPair.getKey(), valueTranslationPair.getValue()); + this.keyValueTranslations.put(resolveTranslationPairKey(valueTranslationPair.getKey()), valueTranslationPair.getValue()); } this.textValue = values; this.fulltextValue = values; @@ -52,4 +54,8 @@ public Object getValue() { } return null; } + + private String resolveTranslationPairKey(String key) { + return key == null || key.isBlank() ? NONE_OPTION_KEY : key; + } } From a3a8953360665097af4d1380b3ec9e6709556245 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Mon, 23 Feb 2026 16:34:44 +0100 Subject: [PATCH 2/8] Fix MapField keyValueTranslations to use LinkedHashMap Updated the collector in MapField to ensure that keyValueTranslations maintains insertion order by using LinkedHashMap. Additionally, resolved potential duplicate key conflicts by explicitly handling them in the collector function. --- .../application/engine/objects/elastic/domain/MapField.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nae-object-library/src/main/java/com/netgrif/application/engine/objects/elastic/domain/MapField.java b/nae-object-library/src/main/java/com/netgrif/application/engine/objects/elastic/domain/MapField.java index 2b8336d6bf..7e415842cf 100644 --- a/nae-object-library/src/main/java/com/netgrif/application/engine/objects/elastic/domain/MapField.java +++ b/nae-object-library/src/main/java/com/netgrif/application/engine/objects/elastic/domain/MapField.java @@ -23,7 +23,10 @@ public MapField(MapField field) { this.keyValue = field.keyValue == null ? null : new ArrayList<>(field.keyValue); this.keyValueTranslations = field.keyValueTranslations == null ? null : field.keyValueTranslations.entrySet().stream() - .collect(Collectors.toMap(entry -> resolveTranslationPairKey(entry.getKey()), entry -> new I18nString(entry.getValue()))); + .collect(Collectors.toMap(entry -> + resolveTranslationPairKey(entry.getKey()), + entry -> new I18nString(entry.getValue()), + (existing, replacement) -> replacement, LinkedHashMap::new)); } public MapField(Map.Entry valueTranslationPair) { From 72eb842ec9cd09821e569f351a0f412e67d472d0 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Mon, 23 Feb 2026 16:56:56 +0100 Subject: [PATCH 3/8] Fix potential null pointer in debug logging Previously, debug logs for 'fieldFrom' and 'caseFrom' assumed non-null values, which could lead to a null pointer exception. This update adds checks to handle null values gracefully in log statements. --- .../engine/workflow/web/RestResponseExceptionHandler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application-engine/src/main/java/com/netgrif/application/engine/workflow/web/RestResponseExceptionHandler.java b/application-engine/src/main/java/com/netgrif/application/engine/workflow/web/RestResponseExceptionHandler.java index effbed44d1..61b46ac758 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/workflow/web/RestResponseExceptionHandler.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/workflow/web/RestResponseExceptionHandler.java @@ -40,9 +40,9 @@ protected ResponseEntity handleHttpMessageNotWritable(HttpMessageNotWrit } if (path.size() > 3) { Object fieldFrom = path.getLast().getFrom(); - log.debug("Field of class [{}] from: {}", fieldFrom.getClass(), fieldFrom); + log.debug("Field of class [{}] from: {}", fieldFrom == null ? "null" : fieldFrom.getClass(), fieldFrom); Object caseFrom = path.get(path.size() - 3).getFrom(); - log.debug("Case of class [{}] from: {}", caseFrom.getClass(), caseFrom); + log.debug("Case of class [{}] from: {}", caseFrom == null ? "null" : caseFrom.getClass(), caseFrom); if (fieldFrom instanceof Field field && caseFrom instanceof Case useCase) { log.debug("[{}] Could not parse value of field [{}], value [{}] | path={}", From 8fd185f0f79f4c77f7775752ae52bb6ad7b602e7 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Fri, 27 Feb 2026 17:53:50 +0100 Subject: [PATCH 4/8] Use LinkedHashMap for keyValueTranslations to preserve order Switched from HashMap to LinkedHashMap in keyValueTranslations to maintain the insertion order of entries. This change ensures predictable ordering when iterating over the map, which may be crucial for consistent processing or display. --- .../application/engine/objects/elastic/domain/MapField.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nae-object-library/src/main/java/com/netgrif/application/engine/objects/elastic/domain/MapField.java b/nae-object-library/src/main/java/com/netgrif/application/engine/objects/elastic/domain/MapField.java index 7e415842cf..f5b36108e9 100644 --- a/nae-object-library/src/main/java/com/netgrif/application/engine/objects/elastic/domain/MapField.java +++ b/nae-object-library/src/main/java/com/netgrif/application/engine/objects/elastic/domain/MapField.java @@ -39,7 +39,7 @@ public MapField(List> valueTranslationPairs) { } List values = new ArrayList<>(); this.keyValue = new ArrayList<>(); - this.keyValueTranslations = new HashMap<>(); + this.keyValueTranslations = new LinkedHashMap<>(); for (Map.Entry valueTranslationPair : valueTranslationPairs) { this.keyValue.add(resolveTranslationPairKey(valueTranslationPair.getKey())); values.addAll(I18nStringUtils.collectTranslations(valueTranslationPair.getValue())); From 227bb3df04205cc002c817062c8e9dd4c212123e Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Fri, 27 Feb 2026 17:58:07 +0100 Subject: [PATCH 5/8] Use LinkedHashMap for keyValueTranslations to preserve order Switched from HashMap to LinkedHashMap in keyValueTranslations to maintain the insertion order of entries. This change ensures predictable ordering when iterating over the map, which may be crucial for consistent processing or display. --- .../application/engine/objects/elastic/domain/MapField.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nae-object-library/src/main/java/com/netgrif/application/engine/objects/elastic/domain/MapField.java b/nae-object-library/src/main/java/com/netgrif/application/engine/objects/elastic/domain/MapField.java index f5b36108e9..df431557d1 100644 --- a/nae-object-library/src/main/java/com/netgrif/application/engine/objects/elastic/domain/MapField.java +++ b/nae-object-library/src/main/java/com/netgrif/application/engine/objects/elastic/domain/MapField.java @@ -41,9 +41,10 @@ public MapField(List> valueTranslationPairs) { this.keyValue = new ArrayList<>(); this.keyValueTranslations = new LinkedHashMap<>(); for (Map.Entry valueTranslationPair : valueTranslationPairs) { - this.keyValue.add(resolveTranslationPairKey(valueTranslationPair.getKey())); + String key = resolveTranslationPairKey(valueTranslationPair.getKey()); + this.keyValue.add(key); values.addAll(I18nStringUtils.collectTranslations(valueTranslationPair.getValue())); - this.keyValueTranslations.put(resolveTranslationPairKey(valueTranslationPair.getKey()), valueTranslationPair.getValue()); + this.keyValueTranslations.put(key, valueTranslationPair.getValue()); } this.textValue = values; this.fulltextValue = values; From 7810fa63845eebb847ab885e807de410b676506b Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Fri, 27 Feb 2026 18:06:05 +0100 Subject: [PATCH 6/8] Fix null translation handling in MapField keyValueTranslations Previously, the code directly assigned a nullable value, which could lead to potential issues. This change ensures that the value is wrapped in an I18nString instance if not null, improving safety and consistency when managing translations. --- .../application/engine/objects/elastic/domain/MapField.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nae-object-library/src/main/java/com/netgrif/application/engine/objects/elastic/domain/MapField.java b/nae-object-library/src/main/java/com/netgrif/application/engine/objects/elastic/domain/MapField.java index df431557d1..599d224bf6 100644 --- a/nae-object-library/src/main/java/com/netgrif/application/engine/objects/elastic/domain/MapField.java +++ b/nae-object-library/src/main/java/com/netgrif/application/engine/objects/elastic/domain/MapField.java @@ -44,7 +44,10 @@ public MapField(List> valueTranslationPairs) { String key = resolveTranslationPairKey(valueTranslationPair.getKey()); this.keyValue.add(key); values.addAll(I18nStringUtils.collectTranslations(valueTranslationPair.getValue())); - this.keyValueTranslations.put(key, valueTranslationPair.getValue()); + this.keyValueTranslations.put( + key, + valueTranslationPair.getValue() == null ? null : new I18nString(valueTranslationPair.getValue()) + ); } this.textValue = values; this.fulltextValue = values; From aa4fd407f18e3bbd7e77388bbaf94c80cfabab9c Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Mon, 2 Mar 2026 12:45:20 +0100 Subject: [PATCH 7/8] - updated according to suggested solution --- .../web/RestResponseExceptionHandler.java | 66 ++++++++++++++++--- 1 file changed, 58 insertions(+), 8 deletions(-) diff --git a/application-engine/src/main/java/com/netgrif/application/engine/workflow/web/RestResponseExceptionHandler.java b/application-engine/src/main/java/com/netgrif/application/engine/workflow/web/RestResponseExceptionHandler.java index 61b46ac758..2e72efd5a6 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/workflow/web/RestResponseExceptionHandler.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/workflow/web/RestResponseExceptionHandler.java @@ -21,10 +21,7 @@ public class RestResponseExceptionHandler extends ResponseEntityExceptionHandler private static final Logger log = LoggerFactory.getLogger(RestResponseExceptionHandler.class); @Override - protected ResponseEntity handleHttpMessageNotWritable(HttpMessageNotWritableException exception, - HttpHeaders headers, - HttpStatusCode status, - WebRequest request) { + protected ResponseEntity handleHttpMessageNotWritable(HttpMessageNotWritableException exception, HttpHeaders headers, HttpStatusCode status, WebRequest request) { try { Throwable cause = exception.getCause(); if (!(cause instanceof JsonMappingException jme)) { @@ -33,11 +30,22 @@ protected ResponseEntity handleHttpMessageNotWritable(HttpMessageNotWrit } List path = jme.getPath(); + + if (log.isTraceEnabled()) { + log.trace("JSON write failed (cause). msg={} | pathRef={}", + jme.getOriginalMessage(), + jme.getPathReference(), + jme); + + tracePathAll(path); + } + if (log.isDebugEnabled()) { for (int i = 0; i < path.size(); i++) { log.debug("Reference[{}]: {}", i, path.get(i)); } } + if (path.size() > 3) { Object fieldFrom = path.getLast().getFrom(); log.debug("Field of class [{}] from: {}", fieldFrom == null ? "null" : fieldFrom.getClass(), fieldFrom); @@ -48,12 +56,12 @@ protected ResponseEntity handleHttpMessageNotWritable(HttpMessageNotWrit log.debug("[{}] Could not parse value of field [{}], value [{}] | path={}", useCase.getStringId(), field.getStringId(), field.getValue(), jme.getPathReference()); } else { - log.error("JSON write failed: {} | path={}", - jme.getOriginalMessage(), jme.getPathReference(), jme); + log.error("JSON write failed: {} | path={} | details={}", + jme.getOriginalMessage(), jme.getPathReference(), describePath(path), jme); } } else { - log.error("JSON write failed: {} | path={}", - jme.getOriginalMessage(), jme.getPathReference(), jme); + log.error("JSON write failed: {} | path={} | details={}", + jme.getOriginalMessage(), jme.getPathReference(), describePath(path), jme); } } catch (Exception e) { @@ -61,4 +69,46 @@ protected ResponseEntity handleHttpMessageNotWritable(HttpMessageNotWrit } return super.handleHttpMessageNotWritable(exception, headers, status, request); } + + private static String describePath(List path) { + if (path == null || path.isEmpty()) return ""; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < path.size(); i++) { + JsonMappingException.Reference ref = path.get(i); + Object from = ref.getFrom(); + + if (i > 0) sb.append(" | "); + + sb.append(i).append(":"); + sb.append(from == null ? "null" : from.getClass().getSimpleName()); + + if (ref.getFieldName() != null) sb.append(".").append(ref.getFieldName()); + if (ref.getIndex() >= 0) sb.append("[").append(ref.getIndex()).append("]"); + + sb.append(" (").append(ref).append(")"); + } + return sb.toString(); + } + + private static void tracePathAll(List path) { + if (path == null || path.isEmpty()) { + log.trace("[JSON_WRITE][PATH] "); + return; + } + + for (int i = 0; i < path.size(); i++) { + JsonMappingException.Reference ref = path.get(i); + Object from = ref.getFrom(); + + String where = (ref.getFieldName() != null ? "." + ref.getFieldName() : "") + + (ref.getIndex() >= 0 ? "[" + ref.getIndex() + "]" : ""); + + log.trace("[JSON_WRITE][PATH] idx={} fromType={}{} ref={} from={}", + i, + (from == null ? "null" : from.getClass().getName()), + where, + ref, + from); + } + } } \ No newline at end of file From 95cdabebf5740cecb830e13f8a9b5fb8e523d866 Mon Sep 17 00:00:00 2001 From: renczesstefan Date: Mon, 2 Mar 2026 12:56:48 +0100 Subject: [PATCH 8/8] Fix error log message to clarify JSON write failure cause Updated the log message in RestResponseExceptionHandler to specify that the JSON write failure is due to the path being smaller than 3. This improves clarity and debugging efficiency when analyzing error logs. --- .../engine/workflow/web/RestResponseExceptionHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application-engine/src/main/java/com/netgrif/application/engine/workflow/web/RestResponseExceptionHandler.java b/application-engine/src/main/java/com/netgrif/application/engine/workflow/web/RestResponseExceptionHandler.java index 2e72efd5a6..449a901b9e 100644 --- a/application-engine/src/main/java/com/netgrif/application/engine/workflow/web/RestResponseExceptionHandler.java +++ b/application-engine/src/main/java/com/netgrif/application/engine/workflow/web/RestResponseExceptionHandler.java @@ -60,7 +60,7 @@ protected ResponseEntity handleHttpMessageNotWritable(HttpMessageNotWrit jme.getOriginalMessage(), jme.getPathReference(), describePath(path), jme); } } else { - log.error("JSON write failed: {} | path={} | details={}", + log.error("JSON write failed because of path is smaller than 3: {} | path={} | details={}", jme.getOriginalMessage(), jme.getPathReference(), describePath(path), jme); }