From 715c508599ffb15a99c3014d92dc232feeb9f3f3 Mon Sep 17 00:00:00 2001 From: delchev Date: Fri, 11 Sep 2026 15:38:15 +0300 Subject: [PATCH] templates: seven more authored strings reach a generated Java literal escaped, through one shared escape (#7295) #7262 routed four message families through JavaLiterals.escape and its body said every other authored string was still interpolated verbatim. It was, at seven more sites: an entity `label:` pattern's literal segments and formats, a `setField` step's `value:`, a report parameter's `initial:`, a field `description:` (the entity's @Documentation argument), a `number:` series name, the seeded status names a lifecycle refusal quotes, and the `cron` / `destination` / `path` a job, a listener and a webhook declare. A `description: Customer's "trade" name` or a `setField` value with a quote ends the literal it is written into and fails the javac of the WHOLE generated module - the client Java batch is all-or-nothing, so one authored quote takes every generated class with it. Each site now renders from an escaped twin derived once, where every other model value is derived: ModelParameterProcessor for the property and label values, GlueGenerator for the glue descriptors, the report adapter for a parameter's initial. The raw values stay for the surfaces that render them as text. The seeded status names were the one site an escape alone could not fix: they travelled as a single `id=name,` join that the generated repository split apart at class-init time, so a comma in a name shifted every entry after it. They are now carried structurally (`lifecycleStatusNameList`, a structured .edm attribute transform-edm parses back) and emitted one escaped put() per pair; a .model written before this still carries the join and is read back, so nothing regenerates differently for a name that never held a separator. And the helper stopped multiplying: every hand-rolled copy of the loop - NotificationSupport, ScheduleSupport, IntegrationSupport, four in GlueIntentGenerator, MappingCompiler, and widgetPatternJava/widgetPatternJs in ModelParameterProcessor - now calls JavaLiterals.escape (or JsLiterals.escape). The copies were not identical: one dropped a carriage return, several escaped neither the newline nor the control characters, and a lone CR used to survive raw into a mapping literal and break its line. Verified: IntentEmissionCoverageIT's fixture carries a quote in a label pattern, a field description, a setField value and a report parameter initial, so the generated module is javac'd and published with them, and the assertions name the escaped value each site wrote; the runtime label assertions read the quotes back off the stored display name. ide-template + engine-intent unit suites, plus ModelGenerationIT and IntentEngineIT (81 tests), green. formatter:validate green with the cache wiped; the release javadoc profile builds both modules clean. Fixes #7295 Co-Authored-By: Claude Opus 5 --- .../intent/generator/GlueIntentGenerator.java | 20 +--- .../intent/generator/IntegrationSupport.java | 5 +- .../intent/generator/NotificationSupport.java | 7 +- .../intent/generator/ScheduleSupport.java | 5 +- .../generator/edm/EdmIntentGenerator.java | 18 ++- .../generator/edm/EdmIntentGeneratorTest.java | 12 +- .../model/ConsumedAttributesAudit.java | 17 +-- .../template/service/model/GlueGenerator.java | 33 ++++++ .../template/service/model/JavaLiterals.java | 6 +- .../service/model/MappingCompiler.java | 11 +- .../model/ModelParameterProcessor.java | 110 ++++++++++++++++-- .../service/model/ModelTemplateAdapters.java | 5 + .../service/model/GlueGeneratorTest.java | 32 +++++ .../model/ModelParameterProcessorTest.java | 94 +++++++++++++++ .../data/Entity.java.template | 2 +- .../data/Repository.java.template | 19 ++- .../data/reportFileEntity.java.template | 2 +- .../events/InboundFile.java.template | 2 +- .../events/InboundMessage.java.template | 2 +- .../events/Job.java.template | 2 +- .../events/Numbering.java.template | 8 +- .../events/Outbound.java.template | 2 +- .../events/SetField.java.template | 2 +- .../events/Webhook.java.template | 2 +- .../editor-entity/template/transform-edm.js | 2 +- .../tests/api/IntentEmissionCoverageIT.java | 56 +++++++-- 26 files changed, 383 insertions(+), 93 deletions(-) diff --git a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/GlueIntentGenerator.java b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/GlueIntentGenerator.java index 77fe8f27c70..c6f9d13cda2 100644 --- a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/GlueIntentGenerator.java +++ b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/GlueIntentGenerator.java @@ -2778,9 +2778,7 @@ private static void putDerivedDefault(Map target, EntityIntent e } // Always a QUOTED literal, even for a default that reads as a number: the column holds // a string, and `same()` would compare a bare 0 against the stored "0" as unequal. - target.put("derivedDefault", '"' + text.replace("\\", "\\\\") - .replace("\"", "\\\"") - + '"'); + target.put("derivedDefault", '"' + JavaLiterals.escape(text) + '"'); return; } } @@ -3413,9 +3411,7 @@ private static String computedCellExpression(String value, CellMeta meta, java.u * {@code Calc.eval("", source, )} - the calculated-field / posting-amount convention. */ private static String calcExpression(String expr, int scale) { - return "Calc.eval(\"" + expr.replace("\\", "\\\\") - .replace("\"", "\\\"") - + "\", source, " + scale + ")"; + return "Calc.eval(\"" + JavaLiterals.escape(expr) + "\", source, " + scale + ")"; } /** @@ -3449,11 +3445,7 @@ private static String stringCellExpression(String v, java.util.Set sourc if (copy != null) { return copy; } - return "\"" + v.replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\n", "\\n") - .replace("\r", "\\r") - + "\""; + return "\"" + JavaLiterals.escape(v) + "\""; } /** @@ -3559,11 +3551,7 @@ private static String literalExpression(String value, String temporalKind) { if (v.matches("-?\\d+\\.\\d+")) { return "new java.math.BigDecimal(\"" + v + "\")"; } - return "\"" + v.replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\n", "\\n") - .replace("\r", "\\r") - + "\""; + return "\"" + JavaLiterals.escape(v) + "\""; } /** The junction's to-one relation whose target is the given entity, or null. */ diff --git a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/IntegrationSupport.java b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/IntegrationSupport.java index 62580e6baac..396ffcae465 100644 --- a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/IntegrationSupport.java +++ b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/IntegrationSupport.java @@ -11,6 +11,7 @@ import java.util.Map; import java.util.Set; +import org.eclipse.dirigible.components.ide.template.service.model.JavaLiterals; /** * Translates an {@code IntegrationIntent}'s {@code method}/{@code url} into the Java the generated @@ -71,9 +72,7 @@ public static String urlExpression(String url) { .trim() + "\")"; } - return "\"" + trimmed.replace("\\", "\\\\") - .replace("\"", "\\\"") - + "\""; + return "\"" + JavaLiterals.escape(trimmed) + "\""; } private static String normalize(String method) { diff --git a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/NotificationSupport.java b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/NotificationSupport.java index b10849c1b57..ac3e48547c6 100644 --- a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/NotificationSupport.java +++ b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/NotificationSupport.java @@ -14,6 +14,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.eclipse.dirigible.components.ide.template.service.model.JavaLiterals; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -373,11 +374,7 @@ private static String literalToJava(String rhs) { } static String quote(String value) { - return "\"" + value.replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\n", "\\n") - .replace("\r", "") - + "\""; + return "\"" + JavaLiterals.escape(value) + "\""; } /** diff --git a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/ScheduleSupport.java b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/ScheduleSupport.java index 85e7ab36ac4..b27b851341a 100644 --- a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/ScheduleSupport.java +++ b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/ScheduleSupport.java @@ -14,6 +14,7 @@ import java.time.format.DateTimeParseException; import java.util.List; import java.util.Map; +import org.eclipse.dirigible.components.ide.template.service.model.JavaLiterals; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -222,8 +223,6 @@ private static String valueToJava(Object value) { return moment.javaExpression(); } String text = value.toString(); - return "\"" + text.replace("\\", "\\\\") - .replace("\"", "\\\"") - + "\""; + return "\"" + JavaLiterals.escape(text) + "\""; } } diff --git a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGenerator.java b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGenerator.java index 0bf39a12000..aa0b8ea56cd 100644 --- a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGenerator.java +++ b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGenerator.java @@ -2633,16 +2633,23 @@ private static void putLifecycle(Map entityMap, EntityIntent ent } entityMap.put("lifecycleStatusProperty", IntentNaming.pascalCase(status.getName())); entityMap.put("lifecycleEdges", String.join(",", edges)); - List names = new ArrayList<>(); + // The seeded names a refusal quotes, as a STRUCTURED list rather than the `id=name,` join this + // used to carry (#7295): a name is authored prose, and a comma in one silently mis-parsed the + // join while a quote or a backslash broke the Java literal the template writes it into. A + // `.model` written before this still carries the join, which the parameter pass reads back. + List> names = new ArrayList<>(); for (Map.Entry seeded : LifecycleStages.seededStatuses(model, status.getTo()) .entrySet()) { if (seeded.getValue() != null && !seeded.getValue() .isBlank()) { - names.add(seeded.getKey() + "=" + seeded.getValue()); + Map name = new LinkedHashMap<>(); + name.put("id", String.valueOf(seeded.getKey())); + name.put("name", seeded.getValue()); + names.add(name); } } if (!names.isEmpty()) { - entityMap.put("lifecycleStatusNames", String.join(",", names)); + entityMap.put("lifecycleStatusNameList", names); } if (status.getInit() != null && status.getInit() .matches("-?\\d+")) { @@ -3908,8 +3915,9 @@ private static String sanitizeId(String raw) { * {@code transform-edm} rebuilds into {@code uniqueConstraints}. Emitting it here too would write * it twice and round-trip it as a duplicate. */ - private static final Set STRUCTURED_ATTRIBUTES = Set.of("rollupGuard", "checks", "labelParts", "aggregateKeys", "groupingKeys", - "relatedEntities", "scopedCalendars", "lookupColumns", "languages", "widgets", "customActionLabels", "processTaskLabels"); + private static final Set STRUCTURED_ATTRIBUTES = + Set.of("rollupGuard", "checks", "labelParts", "aggregateKeys", "groupingKeys", "relatedEntities", "scopedCalendars", + "lifecycleStatusNameList", "lookupColumns", "languages", "widgets", "customActionLabels", "processTaskLabels"); /** * Compact, non-HTML-escaping JSON for the structured {@code .edm} attributes. Compact so the value diff --git a/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGeneratorTest.java b/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGeneratorTest.java index 8e92990c41d..413c123c062 100644 --- a/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGeneratorTest.java +++ b/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGeneratorTest.java @@ -791,8 +791,16 @@ void lifecycleEmitsTheStateMachineTheRepositoryEnforces() { Map entry = entityByName(entities(model), "JournalEntry"); assertEquals("Status", entry.get("lifecycleStatusProperty")); assertEquals("1>2,1>3,2>4", entry.get("lifecycleEdges")); - // The seeded names ride along so a rejection reads "cannot move from POSTED to DRAFT". - assertEquals("1=DRAFT,2=POSTED,3=CANCELLED,4=VOIDED", entry.get("lifecycleStatusNames")); + // The seeded names ride along so a rejection reads "cannot move from POSTED to DRAFT" - as + // STRUCTURED pairs, because a name is authored prose and the `id=name,` join it used to be + // mis-parsed on a comma and broke the generated Java literal on a quote (#7295). + List> statusNames = (List>) entry.get("lifecycleStatusNameList"); + assertEquals(List.of("1", "2", "3", "4"), statusNames.stream() + .map(name -> name.get("id")) + .toList()); + assertEquals(List.of("DRAFT", "POSTED", "CANCELLED", "VOIDED"), statusNames.stream() + .map(name -> name.get("name")) + .toList()); // With a declared start, a record cannot be CREATED mid-lifecycle either. assertEquals("1", entry.get("lifecycleInitialStatus")); // An entity without a lifecycle carries none of it. diff --git a/components/ide/ide-template/src/main/java/org/eclipse/dirigible/components/ide/template/service/model/ConsumedAttributesAudit.java b/components/ide/ide-template/src/main/java/org/eclipse/dirigible/components/ide/template/service/model/ConsumedAttributesAudit.java index a6989dbfb82..90694d9782e 100644 --- a/components/ide/ide-template/src/main/java/org/eclipse/dirigible/components/ide/template/service/model/ConsumedAttributesAudit.java +++ b/components/ide/ide-template/src/main/java/org/eclipse/dirigible/components/ide/template/service/model/ConsumedAttributesAudit.java @@ -90,14 +90,15 @@ public class ConsumedAttributesAudit { */ private static final Set PIPELINE_CLAIMED = Set.of("dataCount", "dataNullable", "dataNotNull", "dataScale", "dataPrecision", "dataOrderBy", "dataOrderBySort", "dataUnique", "generateBusinessKey", "generateDefaultRoles", "generateEvents", - "generateReopens", "generateReport", "identityProperty", "immutableStatusProperty", "immutableStatusValues", "locksWithMaster", - "periodClosedValues", "periodEndProperty", "periodLockDateProperty", "periodLockEntity", "periodStartProperty", - "periodStatusProperty", "perspectiveIcon", "perspectiveLabel", "projectionReferencedModel", "extensionReferencedEntity", - "extensionReferencedModel", "relationshipCardinality", "relationshipIdentityLabel", "relationshipIdentityProperty", - "relationshipMasterDeleteRefused", "relationshipPartnerIdentityLabel", "relationshipPartnerIdentityProperty", - "relationshipPartner", "relationshipPersonal", "relationshipPersonalReadOnly", "widgetDependsOnHeaderEntity", - "widgetDependsOnValueBy", "widgetDependsOnValueByHeaderEntity", "widgetLength", "widgetOptionsEntityPerspectiveName", - "widgetOptionsFilterBy", "widgetOptionsFilterValue", "widgetOptionsFilterValueJs"); + "generateReopens", "generateReport", "identityProperty", "immutableStatusProperty", "immutableStatusValues", + "lifecycleStatusNameList", "lifecycleStatusNames", "locksWithMaster", "periodClosedValues", "periodEndProperty", + "periodLockDateProperty", "periodLockEntity", "periodStartProperty", "periodStatusProperty", "perspectiveIcon", + "perspectiveLabel", "projectionReferencedModel", "extensionReferencedEntity", "extensionReferencedModel", + "relationshipCardinality", "relationshipIdentityLabel", "relationshipIdentityProperty", "relationshipMasterDeleteRefused", + "relationshipPartnerIdentityLabel", "relationshipPartnerIdentityProperty", "relationshipPartner", "relationshipPersonal", + "relationshipPersonalReadOnly", "widgetDependsOnHeaderEntity", "widgetDependsOnValueBy", "widgetDependsOnValueByHeaderEntity", + "widgetLength", "widgetOptionsEntityPerspectiveName", "widgetOptionsFilterBy", "widgetOptionsFilterValue", + "widgetOptionsFilterValueJs"); /** * Attributes the entity editor owns: it keeps them in the model for its own authoring surface diff --git a/components/ide/ide-template/src/main/java/org/eclipse/dirigible/components/ide/template/service/model/GlueGenerator.java b/components/ide/ide-template/src/main/java/org/eclipse/dirigible/components/ide/template/service/model/GlueGenerator.java index 1aa5c902546..64e335468bf 100644 --- a/components/ide/ide-template/src/main/java/org/eclipse/dirigible/components/ide/template/service/model/GlueGenerator.java +++ b/components/ide/ide-template/src/main/java/org/eclipse/dirigible/components/ide/template/service/model/GlueGenerator.java @@ -355,6 +355,7 @@ private static void bindDeleteAbort(Map item, Map item, Map context, Map parameters) { copy(context, item, "process", "className", "entity", "perspective", "keyProperty", "keyAccessor", "field", "value", "relation", "errorMessage"); + copyJavaLiterals(context, item, "value"); context.put("javaPerspective", sanitize(item, "perspective")); } @@ -406,6 +407,7 @@ private static void bindSchedule(Map item, Map c // before it existed, which `copy` turns into an absent context key - so the guard's // `#if` is false and such a job renders byte-identically to what it always did. "hasGenUnique", "genUnique"); + copyJavaLiterals(context, item, "cron"); context.put("javaPerspective", sanitize(item, "perspective")); // The source's generation folder is the owner model's when the source is cross-model, else // this project's - always supplied, so a local source stays unchanged. @@ -482,6 +484,7 @@ private static void bindIntegration(Map item, Map item, Map context, Map parameters) { copy(context, item, "name", "className", "entity", "perspective", "path"); + copyJavaLiterals(context, item, "path"); context.put("javaPerspective", sanitize(item, "perspective")); bindArrival(item, context); } @@ -496,6 +499,7 @@ private static void bindInbound(Map item, Map co */ private static void bindInboundMessage(Map item, Map context, Map parameters) { copy(context, item, "name", "className", "entity", "perspective", "destination", "listenerKind"); + copyJavaLiterals(context, item, "destination"); context.put("javaPerspective", sanitize(item, "perspective")); bindArrival(item, context); } @@ -510,6 +514,7 @@ private static void bindInboundMessage(Map item, Map item, Map context, Map parameters) { copy(context, item, "name", "className", "entity", "perspective", "folder", "cron"); + copyJavaLiterals(context, item, "cron"); context.put("javaPerspective", sanitize(item, "perspective")); bindArrival(item, context); } @@ -551,6 +556,7 @@ private static void bindArrival(Map item, Map co private static void bindOutbound(Map item, Map context, Map parameters) { copy(context, item, "name", "className", "entity", "perspective", "topicSuffix", "destination", "channel", "producerMethod", "guardExpression", "hasGuard", "hasPayload", "payloadFields"); + copyJavaLiterals(context, item, "destination"); context.put("javaPerspective", sanitize(item, "perspective")); context.put("relationLoads", relationLoads(item.get("relationLoads"), parameters)); } @@ -988,6 +994,7 @@ private static void bindNumbering(Map item, Map // an event topic is built from the raw perspective (the sanitized form is the Java package). // perDefault: the partition a null FK falls back to (the relation's init:, #7101). copy(context, item, "entity", "masterPk", "field", "series", "per", "perDefault", "perspective"); + copyJavaLiterals(context, item, "series", "perDefault"); context.put("javaPerspective", sanitize(item, "perspective")); } @@ -1309,6 +1316,32 @@ private static void copy(Map target, Map source, } } + /** + * Copies the escaped twin of each named descriptor value, for the templates that write it into a + * Java string literal. + * + *

+ * Every one of these is authored: a setter's value, a series name, a cron expression, a queue or + * topic name, a webhook path. Interpolated verbatim, a quote or a backslash in any of them ends the + * literal it is written into and fails the compile of the whole generated module - not just the one + * class carrying it (#7295, the #7241 class). A key the descriptor does not carry is removed rather + * than emptied, so the template's {@code #if} reads its absence exactly as it reads the raw key's. + * + * @param target the template context + * @param source the descriptor + * @param keys the keys whose twins to derive + */ + static void copyJavaLiterals(Map target, Map source, String... keys) { + for (String key : keys) { + String value = str(source, key); + if (value == null) { + target.remove(key + "JavaLiteral"); + } else { + target.put(key + "JavaLiteral", JavaLiterals.escape(value)); + } + } + } + /** * Sanitizes a descriptor's value into a Java identifier. * diff --git a/components/ide/ide-template/src/main/java/org/eclipse/dirigible/components/ide/template/service/model/JavaLiterals.java b/components/ide/ide-template/src/main/java/org/eclipse/dirigible/components/ide/template/service/model/JavaLiterals.java index 1073353bbc8..1e1d6198e33 100644 --- a/components/ide/ide-template/src/main/java/org/eclipse/dirigible/components/ide/template/service/model/JavaLiterals.java +++ b/components/ide/ide-template/src/main/java/org/eclipse/dirigible/components/ide/template/service/model/JavaLiterals.java @@ -23,8 +23,10 @@ * {@link #escape(String)} is THE escape for "an authored value inside a generated Java literal", * and it is public for that reason: the loop was copied into engine-intent twice before (dirigible * #7287), where a copy that drifts is a compile error in a generated module nobody sees until a - * regen. Anything that renders a Java literal from a model value calls this - it does not grow a - * fourth copy. + * regen. Every hand-rolled copy across this package and engine-intent was replaced by a call to it + * (dirigible #7295), and the copies differed - one dropped a carriage return, several escaped + * neither the newline nor the control characters - so "the same loop" was never quite true. + * Anything that renders a Java literal from a model value calls this; it does not grow a copy. */ public final class JavaLiterals { diff --git a/components/ide/ide-template/src/main/java/org/eclipse/dirigible/components/ide/template/service/model/MappingCompiler.java b/components/ide/ide-template/src/main/java/org/eclipse/dirigible/components/ide/template/service/model/MappingCompiler.java index b270bef80cc..66131bd8d42 100644 --- a/components/ide/ide-template/src/main/java/org/eclipse/dirigible/components/ide/template/service/model/MappingCompiler.java +++ b/components/ide/ide-template/src/main/java/org/eclipse/dirigible/components/ide/template/service/model/MappingCompiler.java @@ -236,12 +236,11 @@ private static int scaleOf(Map column) { * @return the literal, quotes included */ private static String javaString(Object value) { - String text = String.valueOf(value); - return "\"" + text.replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\r\n", "\\n") - .replace("\n", "\\n") - + "\""; + // Through the one escape, with the line separator normalised first so a CRLF keeps rendering + // as the single \n it always did (a lone CR used to survive raw and break the literal's line). + String text = String.valueOf(value) + .replace("\r\n", "\n"); + return "\"" + JavaLiterals.escape(text) + "\""; } } diff --git a/components/ide/ide-template/src/main/java/org/eclipse/dirigible/components/ide/template/service/model/ModelParameterProcessor.java b/components/ide/ide-template/src/main/java/org/eclipse/dirigible/components/ide/template/service/model/ModelParameterProcessor.java index b60fe2e09e0..2444739ca4f 100644 --- a/components/ide/ide-template/src/main/java/org/eclipse/dirigible/components/ide/template/service/model/ModelParameterProcessor.java +++ b/components/ide/ide-template/src/main/java/org/eclipse/dirigible/components/ide/template/service/model/ModelParameterProcessor.java @@ -124,6 +124,7 @@ private static void processEntity(Map entity, List()); splitChecks(entity, parameters); resolveUniqueConstraintLiterals(entity); + resolveLifecycleStatusNames(entity); resolveDataOrder(entity); for (Map property : asMaps(entity.get("properties"))) { @@ -291,6 +292,59 @@ private static void resolveUniqueConstraintLiterals(Map entity) } } + /** + * Normalizes the seeded status names a lifecycle refusal quotes into the escaped entries the + * generated repository builds its lookup map from. + * + *

+ * The names used to travel as one {@code id=name,} join that the template split back apart at + * class-init time (#7295). A status name is authored prose: a comma in one shifted every following + * entry, and a quote or a backslash ended the Java literal the join was written into and failed the + * compile of the whole generated module. The model now carries the pairs structurally; a + * {@code .model} written before that still carries the join and is read back here, so nothing + * regenerates differently for a name that never held a separator. + * + * @param entity the entity + */ + private static void resolveLifecycleStatusNames(Map entity) { + List entries = new ArrayList<>(); + for (Map declared : asMaps(entity.get("lifecycleStatusNameList"))) { + addLifecycleStatusName(entries, str(declared, "id"), str(declared, "name")); + } + if (entries.isEmpty()) { + String joined = str(entity, "lifecycleStatusNames"); + if (joined != null && !joined.isEmpty()) { + for (String seeded : joined.split(",")) { + int separator = seeded.indexOf('='); + if (separator > 0) { + addLifecycleStatusName(entries, seeded.substring(0, separator), seeded.substring(separator + 1)); + } + } + } + } + if (!entries.isEmpty()) { + entity.put("lifecycleStatusNameEntries", entries); + } + } + + /** + * Adds one seeded status name, escaped for the Java literal the generated repository writes it + * into. + * + * @param entries the entries collected so far + * @param id the status id + * @param name the seeded name + */ + private static void addLifecycleStatusName(List entries, String id, String name) { + if (id == null || id.isEmpty() || name == null || name.isEmpty()) { + return; + } + Map entry = new LinkedHashMap<>(); + entry.put("idJavaLiteral", JavaLiterals.escape(id)); + entry.put("nameJavaLiteral", JavaLiterals.escape(name)); + entries.add(entry); + } + /** * Resolves a check's declared path hops to the generated classes that load them - the reader of a * {@code Relation.field} value must fetch the related record before it can read the field. @@ -376,6 +430,20 @@ private static void processProperty(Map property, Map> entities) { part.put("repositoryClass", foreignKey.get("targetRepositoryClass")); kept.add(part); } + for (Object part : kept) { + resolveLabelPartLiterals(asMap(part)); + } entity.put("labelParts", kept); entity.put("hasLabel", Boolean.TRUE); } } + /** + * Derives the escaped twins of a label part's authored text, for the generated name computation + * that writes them into Java string literals. + * + *

+ * A label pattern is prose an author writes around the fields it interpolates - a quote in a + * literal segment, or in a format, is interpolated verbatim into the {@code computeName} body, + * where it ends the literal it is written into and fails the compile of every generated class of + * the module (#7295, the #7241 class). The raw value stays for the surfaces that render it as text; + * only the Java site reads the twin. + * + * @param part the label part + */ + private static void resolveLabelPartLiterals(Map part) { + if (part == null) { + return; + } + String text = str(part, "text"); + if (text != null) { + part.put("textJavaLiteral", JavaLiterals.escape(text)); + } + String format = str(part, "format"); + if (format != null) { + part.put("formatJavaLiteral", JavaLiterals.escape(format)); + } + } + /** * Resolves the lookup URLs a dependent widget needs at runtime. This runs as its own sweep so it * works regardless of property order - the trigger is always a dropdown, whose URL the property @@ -1339,21 +1437,15 @@ private static void resolveDependsOn(Map property, Map property) { String widgetPattern = str(property, "widgetPattern"); if (widgetPattern != null && !widgetPattern.isEmpty()) { - property.put("widgetPatternJs", "'" + widgetPattern.replace("\\", "\\\\") - .replace("'", "\\'") - + "'"); + property.put("widgetPatternJs", "'" + JsLiterals.escape(widgetPattern) + "'"); // The same expression as the body of a Java string literal, without the quotes: an // unescaped backslash would make the generated controller fail to compile, and the // client Java batch is all-or-nothing. - property.put("widgetPatternJava", widgetPattern.replace("\\", "\\\\") - .replace("\"", "\\\"")); + property.put("widgetPatternJava", JavaLiterals.escape(widgetPattern)); } if (property.get("widgetOptionsFilterBy") != null && property.containsKey("widgetOptionsFilterValue")) { String raw = String.valueOf(property.get("widgetOptionsFilterValue")); - property.put("widgetOptionsFilterValueJs", raw.matches("-?\\d+(\\.\\d+)?") ? raw - : "'" + raw.replace("\\", "\\\\") - .replace("'", "\\'") - + "'"); + property.put("widgetOptionsFilterValueJs", raw.matches("-?\\d+(\\.\\d+)?") ? raw : "'" + JsLiterals.escape(raw) + "'"); } } diff --git a/components/ide/ide-template/src/main/java/org/eclipse/dirigible/components/ide/template/service/model/ModelTemplateAdapters.java b/components/ide/ide-template/src/main/java/org/eclipse/dirigible/components/ide/template/service/model/ModelTemplateAdapters.java index a8b68bdaa73..86dde9a52e8 100644 --- a/components/ide/ide-template/src/main/java/org/eclipse/dirigible/components/ide/template/service/model/ModelTemplateAdapters.java +++ b/components/ide/ide-template/src/main/java/org/eclipse/dirigible/components/ide/template/service/model/ModelTemplateAdapters.java @@ -240,6 +240,11 @@ private static PreparedModel report(String modelText, Map parame ModelDataTypes.DataType dataType = ModelDataTypes.parse(str(parameter, "type")); parameter.put("typeJava", dataType.java()); parameter.put("typeTypescript", dataType.typescript()); + // The authored fallback the parameter is bound with when the caller leaves the input empty. + // The generated repository writes it into a Java string literal, so an apostrophe-carrying + // value is fine but a quote or a backslash would end that literal and fail the compile of + // the whole generated module (#7295). + parameter.put("initialJavaLiteral", JavaLiterals.escape(strOr(parameter, "initial", ""))); String placeholder = ":" + str(parameter, "name"); for (Map condition : asMaps(model.get("conditions"))) { if (placeholder.equals(str(condition, "right")) && "string".equals(dataType.typescript()) diff --git a/components/ide/ide-template/src/test/java/org/eclipse/dirigible/components/ide/template/service/model/GlueGeneratorTest.java b/components/ide/ide-template/src/test/java/org/eclipse/dirigible/components/ide/template/service/model/GlueGeneratorTest.java index 19f135a5847..a7d1dd2db08 100644 --- a/components/ide/ide-template/src/test/java/org/eclipse/dirigible/components/ide/template/service/model/GlueGeneratorTest.java +++ b/components/ide/ide-template/src/test/java/org/eclipse/dirigible/components/ide/template/service/model/GlueGeneratorTest.java @@ -111,6 +111,38 @@ void aHeaderAssignmentCarryingTheCurrentSpellingIgnoresTheFormerOne() { .containsEntry("hoisted", Boolean.FALSE); } + /** + * The authored descriptor values a glue template writes into a Java string literal - a setter's + * value, a series, a cron, a destination, a webhook path - reach it escaped, so a quote in any of + * them cannot end the literal and fail the compile of the whole generated module (#7295). + */ + @Test + void derivesTheEscapedTwinOfAnAuthoredDescriptorValue() { + Map item = new LinkedHashMap<>(); + item.put("value", "the \"issued\" one"); + item.put("series", "C:\\Sales"); + Map context = new LinkedHashMap<>(); + + GlueGenerator.copyJavaLiterals(context, item, "value", "series"); + + assertThat(context).containsEntry("valueJavaLiteral", "the \\\"issued\\\" one") + .containsEntry("seriesJavaLiteral", "C:\\\\Sales"); + } + + /** + * A key the descriptor does not carry is REMOVED rather than emptied: the context starts as a copy + * of the generation parameters, and the template reads the key's absence. + */ + @Test + void aKeyTheDescriptorDoesNotCarryIsRemoved() { + Map context = new LinkedHashMap<>(); + context.put("perDefaultJavaLiteral", "left over from another descriptor"); + + GlueGenerator.copyJavaLiterals(context, new LinkedHashMap<>(), "perDefault"); + + assertThat(context).doesNotContainKey("perDefaultJavaLiteral"); + } + private static Map cell(String name) { Map cell = new LinkedHashMap<>(); cell.put("name", name); diff --git a/components/ide/ide-template/src/test/java/org/eclipse/dirigible/components/ide/template/service/model/ModelParameterProcessorTest.java b/components/ide/ide-template/src/test/java/org/eclipse/dirigible/components/ide/template/service/model/ModelParameterProcessorTest.java index abcf9e12576..0c284fec23f 100644 --- a/components/ide/ide-template/src/test/java/org/eclipse/dirigible/components/ide/template/service/model/ModelParameterProcessorTest.java +++ b/components/ide/ide-template/src/test/java/org/eclipse/dirigible/components/ide/template/service/model/ModelParameterProcessorTest.java @@ -837,6 +837,100 @@ private static Map lookup(Map column) { return (Map) column.get("lookup"); } + /** + * The authored prose a generated class writes into a Java string literal, escaped once here so no + * template has to re-derive it - and so a quote in it mis-values one field instead of failing the + * compile of the whole generated module (#7295). + */ + @Test + void escapesTheAuthoredProseTheGeneratedJavaWritesIntoALiteral() { + Map property = property("Name", "VARCHAR"); + property.put("description", "Customer's \"trade\" name"); + property.put("numberSeries", "Sales \"Invoice\""); + ModelParameterProcessor.process(model(entity("Book", "Books", property)), parameters()); + + assertEquals("Customer's \\\"trade\\\" name", property.get("descriptionJavaLiteral")); + assertEquals("Sales \\\"Invoice\\\"", property.get("numberSeriesJavaLiteral")); + // The raw values stay for the surfaces that render them as text. + assertEquals("Customer's \"trade\" name", property.get("description")); + } + + /** + * A property carrying neither gets no twin: a template reads the key's absence. + */ + @Test + void aPropertyWithNoProseCarriesNoLiteralTwin() { + Map property = property("Name", "VARCHAR"); + ModelParameterProcessor.process(model(entity("Book", "Books", property)), parameters()); + + assertNull(property.get("descriptionJavaLiteral")); + assertNull(property.get("numberSeriesJavaLiteral")); + } + + /** + * A label pattern is authored around the fields it interpolates, and its literal segments and + * formats are written into the generated name computation as Java literals. + */ + @Test + void escapesTheLiteralSegmentsAndFormatsOfALabelPattern() { + Map property = property("Code", "VARCHAR"); + Map entity = entity("Book", "Books", property); + Map literal = new LinkedHashMap<>(); + literal.put("kind", "literal"); + literal.put("text", "the \"good\" one - "); + Map field = new LinkedHashMap<>(); + field.put("kind", "field"); + field.put("property", "Code"); + field.put("format", "dd\\MM"); + entity.put("labelParts", List.of(literal, field)); + Map parameters = parameters(); + // The label computation is a client-Java surface, so the pass that resolves it runs there. + parameters.put("javaRuntime", "true"); + ModelParameterProcessor.process(model(entity), parameters); + + assertEquals("the \\\"good\\\" one - ", literal.get("textJavaLiteral")); + assertEquals("dd\\\\MM", field.get("formatJavaLiteral")); + } + + /** + * The seeded status names a lifecycle refusal quotes travel structurally, escaped per entry - a + * comma in a name used to shift every entry after it, a quote broke the literal. + */ + @Test + void readsTheSeededStatusNamesStructurally() { + Map entity = entity("Invoice", "Invoices", property("Id", "INTEGER")); + entity.put("lifecycleStatusNameList", + List.of(Map.of("id", "1", "name", "Sent, awaiting reply"), Map.of("id", "2", "name", "\"P\""))); + ModelParameterProcessor.process(model(entity), parameters()); + + List> entries = (List>) entity.get("lifecycleStatusNameEntries"); + assertEquals(2, entries.size()); + assertEquals("1", entries.get(0) + .get("idJavaLiteral")); + assertEquals("Sent, awaiting reply", entries.get(0) + .get("nameJavaLiteral")); + assertEquals("\\\"P\\\"", entries.get(1) + .get("nameJavaLiteral")); + } + + /** + * A model written before the structured list still carries the {@code id=name,} join, and the pass + * reads it back so nothing regenerates differently for a name that never held a separator. + */ + @Test + void fallsBackToTheJoinedSeededStatusNames() { + Map entity = entity("Invoice", "Invoices", property("Id", "INTEGER")); + entity.put("lifecycleStatusNames", "1=DRAFT,2=POSTED"); + ModelParameterProcessor.process(model(entity), parameters()); + + List> entries = (List>) entity.get("lifecycleStatusNameEntries"); + assertEquals(2, entries.size()); + assertEquals("DRAFT", entries.get(0) + .get("nameJavaLiteral")); + assertEquals("2", entries.get(1) + .get("idJavaLiteral")); + } + /** * Builds a model around the given entities. * diff --git a/components/template/template-application-dao-java/src/main/resources/META-INF/dirigible/template-application-dao-java/data/Entity.java.template b/components/template/template-application-dao-java/src/main/resources/META-INF/dirigible/template-application-dao-java/data/Entity.java.template index cdb6cabeb28..6ab666369e1 100644 --- a/components/template/template-application-dao-java/src/main/resources/META-INF/dirigible/template-application-dao-java/data/Entity.java.template +++ b/components/template/template-application-dao-java/src/main/resources/META-INF/dirigible/template-application-dao-java/data/Entity.java.template @@ -35,7 +35,7 @@ public class ${name}Entity { #end @Column(name = "${property.dataName}"#if($property.dataLength && $property.dataType != "DECIMAL"), length = ${property.dataLength}#end#if($property.dataPrecision), precision = ${property.dataPrecision}#end#if($property.dataScale), scale = ${property.dataScale}#end#if(!$property.dataPrimaryKey && $property.dataNotNull), nullable = false#elseif(!$property.dataPrimaryKey && !$property.dataNotNull), nullable = true#end#if($property.dataUnique), unique = true#end) #if($property.description) - @Documentation("${property.description}") + @Documentation("${property.descriptionJavaLiteral}") #else @Documentation("${property.name}") #end diff --git a/components/template/template-application-dao-java/src/main/resources/META-INF/dirigible/template-application-dao-java/data/Repository.java.template b/components/template/template-application-dao-java/src/main/resources/META-INF/dirigible/template-application-dao-java/data/Repository.java.template index 37d833b8d5d..a72edd762b2 100644 --- a/components/template/template-application-dao-java/src/main/resources/META-INF/dirigible/template-application-dao-java/data/Repository.java.template +++ b/components/template/template-application-dao-java/src/main/resources/META-INF/dirigible/template-application-dao-java/data/Repository.java.template @@ -406,9 +406,9 @@ public class ${name}Repository extends JavaRepository<${name}Entity> { #if($property.numberStampOnCreate) if (entity.${property.name} == null || entity.${property.name}.isBlank()) { #if($property.numberPer && $property.numberPer != "") - entity.${property.name} = org.eclipse.dirigible.sdk.numbering.DocumentNumbers.next("${property.numberSeries}", entity.${property.numberPer} == null ? null : String.valueOf(entity.${property.numberPer})); + entity.${property.name} = org.eclipse.dirigible.sdk.numbering.DocumentNumbers.next("${property.numberSeriesJavaLiteral}", entity.${property.numberPer} == null ? null : String.valueOf(entity.${property.numberPer})); #else - entity.${property.name} = org.eclipse.dirigible.sdk.numbering.DocumentNumbers.next("${property.numberSeries}"); + entity.${property.name} = org.eclipse.dirigible.sdk.numbering.DocumentNumbers.next("${property.numberSeriesJavaLiteral}"); #end } #end @@ -938,13 +938,8 @@ public class ${name}Repository extends JavaRepository<${name}Entity> { private static java.util.Map lifecycleStatusNames() { java.util.Map names = new java.util.HashMap<>(); -#if($lifecycleStatusNames) - for (String seeded : "${lifecycleStatusNames}".split(",")) { - int separator = seeded.indexOf('='); - if (separator > 0) { - names.put(seeded.substring(0, separator), seeded.substring(separator + 1)); - } - } +#foreach($seeded in $lifecycleStatusNameEntries) + names.put("${seeded.idJavaLiteral}", "${seeded.nameJavaLiteral}"); #end return names; } @@ -1364,16 +1359,16 @@ public class ${name}Repository extends JavaRepository<${name}Entity> { StringBuilder label = new StringBuilder(); #foreach($part in $labelParts) #if($part.kind == "literal") - label.append("${part.text}"); + label.append("${part.textJavaLiteral}"); #elseif($part.kind == "field") - label.append(formatLabelValue(entity.${part.property}, #if($part.format)"${part.format}"#{else}null#end)); + label.append(formatLabelValue(entity.${part.property}, #if($part.format)"${part.formatJavaLiteral}"#{else}null#end)); #else { var related = entity.${part.relation} == null ? null : new ${part.repositoryClass}().findOne(entity.${part.relation}) .orElse(null); label.append(formatLabelValue(related == null ? null : related.${part.property}, - #if($part.format)"${part.format}"#{else}null#end)); + #if($part.format)"${part.formatJavaLiteral}"#{else}null#end)); } #end #end diff --git a/components/template/template-application-dao-java/src/main/resources/META-INF/dirigible/template-application-dao-java/data/reportFileEntity.java.template b/components/template/template-application-dao-java/src/main/resources/META-INF/dirigible/template-application-dao-java/data/reportFileEntity.java.template index 5ed7d802f33..5def35c08e0 100644 --- a/components/template/template-application-dao-java/src/main/resources/META-INF/dirigible/template-application-dao-java/data/reportFileEntity.java.template +++ b/components/template/template-application-dao-java/src/main/resources/META-INF/dirigible/template-application-dao-java/data/reportFileEntity.java.template @@ -196,7 +196,7 @@ public class ${name}Repository { parameters.add(parameter("language", "VARCHAR", language())); #end #foreach($parameter in $parameters) - parameters.add(parameter("${parameter.name}", "${parameter.type}", value(filter, "${parameter.name}", "${parameter.initial}"))); + parameters.add(parameter("${parameter.name}", "${parameter.type}", value(filter, "${parameter.name}", "${parameter.initialJavaLiteral}"))); #end return parameters; } diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/InboundFile.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/InboundFile.java.template index 1fdd6821bfc..1c425e9ed27 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/InboundFile.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/InboundFile.java.template @@ -60,7 +60,7 @@ public class ${className}FileImport implements JobHandler { @Override public String cron() { - return "${cron}"; + return "${cronJavaLiteral}"; } @Override diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/InboundMessage.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/InboundMessage.java.template index 6dde6bc56d8..0361a00ae90 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/InboundMessage.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/InboundMessage.java.template @@ -41,7 +41,7 @@ public class ${className}Consumer implements MessageHandler { @Override public String destination() { - return "${destination}"; + return "${destinationJavaLiteral}"; } @Override diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Job.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Job.java.template index ba66917b789..4e604e5e9e0 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Job.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Job.java.template @@ -44,7 +44,7 @@ public class ${className}Job implements JobHandler { @Override public String cron() { - return "${cron}"; + return "${cronJavaLiteral}"; } @Override diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Numbering.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Numbering.java.template index fd1e6dee8a6..06f20e97947 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Numbering.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Numbering.java.template @@ -46,14 +46,14 @@ public class ${entity}NumberStamp implements JavaDelegate { ## A null partition FK resolves to the relation's init: default (the value the row carries by ## construction), never to the series' base row - the default company is a partition like any other. #if($perDefault && $perDefault != "") - String number = org.eclipse.dirigible.sdk.numbering.DocumentNumbers.next("${series}", - entity.${per} == null ? "${perDefault}" : String.valueOf(entity.${per})); + String number = org.eclipse.dirigible.sdk.numbering.DocumentNumbers.next("${seriesJavaLiteral}", + entity.${per} == null ? "${perDefaultJavaLiteral}" : String.valueOf(entity.${per})); #else - String number = org.eclipse.dirigible.sdk.numbering.DocumentNumbers.next("${series}", + String number = org.eclipse.dirigible.sdk.numbering.DocumentNumbers.next("${seriesJavaLiteral}", entity.${per} == null ? null : String.valueOf(entity.${per})); #end #else - String number = org.eclipse.dirigible.sdk.numbering.DocumentNumbers.next("${series}"); + String number = org.eclipse.dirigible.sdk.numbering.DocumentNumbers.next("${seriesJavaLiteral}"); #end if (repository.updateProperty(id, "${field}", number) == 0) { return; diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Outbound.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Outbound.java.template index 525a0a2ff78..4011bd0eff3 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Outbound.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Outbound.java.template @@ -82,7 +82,7 @@ public class ${className}Publisher implements MessageHandler { String body = message; #end try { - Producer.${producerMethod}("${destination}", body); + Producer.${producerMethod}("${destinationJavaLiteral}", body); } catch (Exception ex) { LOG.error("Outbound ${name}: publishing to [${destination}] failed - the ${entity} write stands", ex); } diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/SetField.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/SetField.java.template index 313308b868b..0e14b5bb10f 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/SetField.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/SetField.java.template @@ -41,7 +41,7 @@ public class ${className} implements JavaDelegate { int updated = repository.updateProperty(((Number) key).${keyAccessor}(), "${field}", errorMessage == null ? "" : errorMessage.toString()); #else - int updated = repository.updateProperty(((Number) key).${keyAccessor}(), "${field}", "${value}"); + int updated = repository.updateProperty(((Number) key).${keyAccessor}(), "${field}", "${valueJavaLiteral}"); #end if (updated == 0) { return; diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Webhook.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Webhook.java.template index 1c8adb1ac91..2f27ff5c3ef 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Webhook.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Webhook.java.template @@ -45,7 +45,7 @@ public class ${className}Webhook { private static final Logger LOG = Logging.getLogger("gen.events.${javaGenFolderName}.${className}Webhook"); #end - @Post("${path}") + @Post("${pathJavaLiteral}") #if($hasEnvelope) public String ingest(@Body java.util.Map envelope) { #{else} diff --git a/components/ui/editor-entity/src/main/resources/META-INF/dirigible/editor-entity/template/transform-edm.js b/components/ui/editor-entity/src/main/resources/META-INF/dirigible/editor-entity/template/transform-edm.js index 68e2b9693dc..823a53324b4 100644 --- a/components/ui/editor-entity/src/main/resources/META-INF/dirigible/editor-entity/template/transform-edm.js +++ b/components/ui/editor-entity/src/main/resources/META-INF/dirigible/editor-entity/template/transform-edm.js @@ -22,7 +22,7 @@ import { XML } from "@aerokit/sdk/utils"; // uniqueConstraints is intentionally NOT here: the composite-unique-key feature emits it as a // / section that transformUniqueKey (below) rebuilds, so parsing it here too would // duplicate it (#6826). -const ENTITY_STRUCTURED = ['rollupGuard', 'checks', 'labelParts', 'aggregateKeys', 'groupingKeys', 'relatedEntities', 'scopedCalendars']; +const ENTITY_STRUCTURED = ['rollupGuard', 'checks', 'labelParts', 'aggregateKeys', 'groupingKeys', 'relatedEntities', 'scopedCalendars', 'lifecycleStatusNameList']; const PROPERTY_STRUCTURED = ['lookupColumns']; // Document level (#6882): the structured values the .model carries ABOVE its entities. const MODEL_STRUCTURED = ['languages', 'widgets', 'customActionLabels', 'processTaskLabels']; diff --git a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEmissionCoverageIT.java b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEmissionCoverageIT.java index 39be1db0773..3f5e85bf6f4 100644 --- a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEmissionCoverageIT.java +++ b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEmissionCoverageIT.java @@ -457,10 +457,15 @@ class IntentEmissionCoverageIT extends IntegrationTest { - name: Claim audit: true history: true - label: "{note} ({Person.name}) {period|yyyy MMMM}" + # The label's literal segments carry a quote, as authored prose does. Every one of + # them is written into the generated computeName() as a Java literal, so an + # unescaped one ends that literal and fails the compile of the whole module (#7295). + label: "the \\"{note}\\" ({Person.name}) {period|yyyy MMMM}" fields: - { name: id, type: integer, primaryKey: true, generated: true } - - { name: note, type: string, length: 200 } + # description: reaches the generated entity as an @Documentation argument - a Java + # string literal, so the quote here is the #7295 assertion. + - { name: note, type: string, length: 200, description: 'The claim''s "short" note' } # a boolean: a real checkbox on the power form AND on the personal one (#7103) - { name: urgent, type: boolean } - { name: period, type: month } @@ -1107,7 +1112,9 @@ class IntentEmissionCoverageIT extends IntegrationTest { - name: ShipmentFlow trigger: { onCreate: Shipment } steps: - - { name: dispatch, kind: serviceTask, args: { setField: note, value: DISPATCHED, next: settle } } + # The written value is authored prose and is emitted as a Java string literal, so + # the quote in it is the #7295 assertion on the setter delegate. + - { name: dispatch, kind: serviceTask, args: { setField: note, value: 'DISPATCHED "in full"', next: settle } } - { name: settle, kind: serviceTask, args: { setField: note, value: SETTLED, next: end } } - { name: end, kind: end } @@ -1534,6 +1541,16 @@ class IntentEmissionCoverageIT extends IntegrationTest { parameters: - { name: minTotal, target: totalCost, op: ge, initial: "0" } - { name: note, target: note, op: like } + # A report parameter's `initial:` is authored prose bound on every call, and the + # generated repository writes it into a Java string literal - a quote in it used to + # end that literal and fail the compile of the whole module (#7295). Its own report, + # because the fallback narrows every unparameterized call by construction. + - name: ClaimNotes + source: Claim + dimensions: [note] + measures: ["count(*)"] + parameters: + - { name: search, target: note, op: like, initial: 'O''Neil "the" note' } # kind: statement (#6938): the line classification is emitted as a generated # _LINES .view artifact - published with the project and provisioned by the # ViewsSynchronizer AFTER the tables - and the .report keeps a thin windowed join over @@ -3136,8 +3153,11 @@ private void assertEmission() { // other writer (a REST update, a workflow setter, a glue action) free to jump anywhere. String docRepository = contentOf("gen/emission/data/doc/DocRepository.java"); assertTrue(docRepository.contains("\"1>2,2>3\".split(\",\")"), "the lifecycle must emit the whole legal edge set"); - assertTrue(docRepository.contains("1=DRAFT,2=POSTED,3=CANCELLED"), - "the seeded status names must ride along so a rejection names statuses, not positional ids"); + // The names ride along as INDIVIDUAL escaped literals, not as one `id=name,` join: a status + // name is authored prose, and a comma in one shifted every entry after it while a quote broke + // the literal the join was written into (#7295). + assertTrue(docRepository.contains("names.put(\"1\", \"DRAFT\");") && docRepository.contains("names.put(\"3\", \"CANCELLED\");"), + "the seeded status names must ride along so a rejection names statuses, not positional ids: " + docRepository); assertTrue(docRepository.contains("enforceLifecycle(entity);"), "a full-row update must be validated against the graph"); assertTrue(docRepository.contains("enforceLifecycleMove(lifecyclePrevious, entity.Status);"), "a targeted write (transition button, workflow setter) must be validated against the graph too"); @@ -3393,6 +3413,22 @@ private void assertEmission() { // must parse it back to a temporal - otherwise the label degrades to the raw "2026-07". assertTrue(claimRepository.contains("YearMonth.parse"), "a |format token on a month field must parse the YYYY-MM string back to a temporal"); + // Every authored string the generated Java writes into a string literal is ESCAPED on the way + // in (#7295, the #7241/#7154 class): a label's literal segments here, the field description + // the entity's @Documentation carries, the series a number is allocated from, the seeded + // status names a lifecycle refusal quotes, a setter's written value and a report parameter's + // bound fallback. The module javac's as a whole, so an unescaped one of them fails EVERY + // generated class - these assertions say which value each site wrote, not merely that it did. + assertTrue(claimRepository.contains("label.append(\"the \\\"\");"), + "a label's literal segment must reach computeName escaped: " + claimRepository); + assertTrue(contentOf("gen/emission/data/claim/ClaimEntity.java").contains("@Documentation(\"The claim's \\\"short\\\" note\")"), + "an authored field description must reach @Documentation escaped"); + assertTrue(contentOf("gen/events/emission/ShipmentFlowDispatch.java").contains("\"DISPATCHED \\\"in full\\\"\""), + "a setField value must reach the setter delegate escaped"); + String claimNotesRepository = contentOf("gen/claimnotes/data/reports/ClaimNotesRepository.java"); + assertTrue(claimNotesRepository.contains("value(filter, \"search\", \"O'Neil \\\"the\\\" note\")"), + "an authored report parameter initial must be bound escaped: " + claimNotesRepository); + // A workflow setter/writer targeted write keeps the stored display Name current: the label // repository OVERRIDES updateProperties to recompute it on that path too. assertTrue(claimRepository.contains("public int updateProperties(") && claimRepository.contains("computeName(entity)"), @@ -4807,9 +4843,11 @@ private void assertRuntimeEnforcement() { .body("Person", equalTo(1)) .body("Rate", nullValue()) // label: the stored display name computed on write - - // "{note} ({Person.name}) {period|yyyy MMMM}"; the month - // value formats through the pattern, never the raw 2026-07. - .body("Name", equalTo("spoofed (Admin) 2026 July")) + // the \"{note}\" ({Person.name}) {period|yyyy MMMM}; the + // month value formats through the pattern, never the raw + // 2026-07, and the pattern's own quotes survive the Java + // literal they are written into (#7295). + .body("Name", equalTo("the \"spoofed\" (Admin) 2026 July")) .extract() .path("Id"))); @@ -4839,7 +4877,7 @@ private void assertRuntimeEnforcement() { .body("Note", equalTo("edited")) .body("Person", equalTo(1)) .body("Rate", equalTo(50.0F)) - .body("Name", equalTo("edited (Admin)"))); + .body("Name", equalTo("the \"edited\" (Admin)"))); // The personal-assignee task landed in the owner's (admin's) Inbox - assigned, not just // claimable (the trigger + BPMN chain resolved the identity mapping at start time).