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 ec321f74fb2..3573459804c 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 @@ -709,6 +709,9 @@ private static void bindGenerate(Map item, Map c // always had rather than its own literal into Java that would not compile. context.put("itemWhere", strOr(item, "itemWhere", "")); context.put("itemRefuse", strOr(item, "itemRefuse", "")); + // The refusal is written into a Java string literal, so a quote or a backslash in the authored + // message would end that literal and fail the compile of the whole generated module (#7241). + context.put("itemRefuseJavaLiteral", JavaLiterals.escape(strOr(item, "itemRefuse", ""))); // The one-hop `relation.field` map sources: one load per distinct relation, which the template // emits before the mapping reads a field off it. A .glue written before this key existed carries // none, and the loop renders nothing - the direct-property mapping it always had. 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 dcf69374936..10906dc3bd9 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 @@ -123,6 +123,7 @@ private static void processEntity(Map entity, List()); splitChecks(entity, parameters); + resolveUniqueConstraintLiterals(entity); resolveDataOrder(entity); for (Map property : asMaps(entity.get("properties"))) { @@ -218,6 +219,7 @@ private static void splitChecks(Map entity, Map List documentChecks = new ArrayList<>(); for (Map check : checks) { String kind = str(check, "kind"); + resolveMessageLiteral(check); resolveCheckPathLoads(check, parameters); if ("exactlyOne".equals(kind) || "compare".equals(kind)) { rowChecks.add(check); @@ -237,6 +239,47 @@ private static void splitChecks(Map entity, Map entity.put("documentChecks", documentChecks); } + /** + * Derives the escaped twin of an authored message, for the templates that write it into a Java + * string literal. + * + *

+ * A check's, a guard's or a unique key's message is prose an author writes - and the very messages + * the DSL's own examples suggest quote a field name ({@code A "due" date is never before the + * invoice date}). Interpolated verbatim, that quote ends the literal it is written into and fails + * the compile of every generated class of the module, not just the one carrying the message (#7241, + * the sibling of #7154). The raw value is left in place for the surfaces that render it as text; + * only the Java sites read the twin. + * + *

+ * A holder carrying no message is left untouched rather than given an empty twin, as the default + * value literal is: the key's absence is what a template reads. + * + * @param holder the check or unique constraint + */ + private static void resolveMessageLiteral(Map holder) { + String message = str(holder, "message"); + if (message != null) { + holder.put("messageJavaLiteral", JavaLiterals.escape(message)); + } + } + + /** + * Derives the escaped twins of a unique key's authored name and message, both of which the REST + * controllers write into Java string literals when they translate a constraint violation. + * + * @param entity the entity + */ + private static void resolveUniqueConstraintLiterals(Map entity) { + for (Map constraint : asMaps(entity.get("uniqueConstraints"))) { + resolveMessageLiteral(constraint); + String name = str(constraint, "name"); + if (name != null) { + constraint.put("nameJavaLiteral", JavaLiterals.escape(name)); + } + } + } + /** * 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. 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 45475197a55..117e8b2d29e 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 @@ -326,6 +326,53 @@ void splitsTheDeclarativeChecksByTheScopeThatEnforcesThem() { .size()); } + @Test + void carriesEveryAuthoredMessageAsAnEscapedJavaLiteralToo() { + Map row = new LinkedHashMap<>(); + row.put("kind", "compare"); + row.put("message", "A \"due\" date is never before the invoice date"); + Map guard = new LinkedHashMap<>(); + guard.put("kind", "guard"); + guard.put("message", "Insufficient balance in C:\\ledger"); + Map document = new LinkedHashMap<>(); + document.put("kind", "itemsMin"); + document.put("message", "An invoice needs at least one \"line\""); + Map unique = new LinkedHashMap<>(); + unique.put("name", "INVOICE_\"NUMBER\""); + unique.put("message", "This \"number\" is already registered"); + Map entity = entity("Invoice", "Invoices", property("Total", "DECIMAL")); + entity.put("checks", List.of(row, guard, document)); + entity.put("uniqueConstraints", List.of(unique)); + + ModelParameterProcessor.process(model(entity), parameters()); + + // The raw value stays for the surfaces that render it as text; only the Java sites read the + // twin, whose quote is escaped rather than ending the literal it is written into (#7241). + assertEquals("A \"due\" date is never before the invoice date", row.get("message")); + assertEquals("A \\\"due\\\" date is never before the invoice date", row.get("messageJavaLiteral")); + assertEquals("Insufficient balance in C:\\\\ledger", guard.get("messageJavaLiteral")); + assertEquals("An invoice needs at least one \\\"line\\\"", document.get("messageJavaLiteral")); + assertEquals("This \\\"number\\\" is already registered", unique.get("messageJavaLiteral")); + assertEquals("INVOICE_\\\"NUMBER\\\"", unique.get("nameJavaLiteral")); + } + + @Test + void leavesTheMessageLiteralAbsentWhereNoMessageIsAuthored() { + Map check = new LinkedHashMap<>(); + check.put("kind", "exactlyOne"); + Map unique = new LinkedHashMap<>(); + unique.put("name", "INVOICE_NUMBER"); + Map entity = entity("Invoice", "Invoices", property("Total", "DECIMAL")); + entity.put("checks", List.of(check)); + entity.put("uniqueConstraints", List.of(unique)); + + ModelParameterProcessor.process(model(entity), parameters()); + + assertFalse(check.containsKey("messageJavaLiteral")); + assertFalse(unique.containsKey("messageJavaLiteral")); + assertEquals("INVOICE_NUMBER", unique.get("nameJavaLiteral")); + } + @Test void resolvesTheHopsAConditionalRequirementReadsItsValueThrough() { Map hop = new LinkedHashMap<>(); 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 511ae0f4e1f..9caedcff877 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 @@ -224,7 +224,7 @@ package gen.${javaGenFolderName}.data.${javaPerspectiveName}; } #else if (!guardWithin) { - throw new ValidationException("${guard.message}"); + throw new ValidationException("${guard.messageJavaLiteral}"); } #end } @@ -1049,7 +1049,7 @@ public class ${name}Repository extends JavaRepository<${name}Entity> { #end Object requiredValue = ${check.valueExpression}; if ((${check.guard}) && (requiredValue == null || String.valueOf(requiredValue).isBlank())) { - throw new ValidationException("${check.message}"); + throw new ValidationException("${check.messageJavaLiteral}"); } #elseif($check.kind == "itemsSumEqual") java.math.BigDecimal sum${check.overA} = java.math.BigDecimal.ZERO; @@ -1064,12 +1064,12 @@ public class ${name}Repository extends JavaRepository<${name}Entity> { } } if (sum${check.overA}.compareTo(sum${check.overB}) != 0) { - throw new ValidationException("${check.message}"); + throw new ValidationException("${check.messageJavaLiteral}"); } #else if (new ${check.itemsEntity}Repository().findAll( Criteria.create().eq("${check.itemsFk}", entity.#foreach($property in $properties)#if($property.dataPrimaryKey)${property.name}#end#end)).size() < ${check.count}) { - throw new ValidationException("${check.message}"); + throw new ValidationException("${check.messageJavaLiteral}"); } #end } diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Generate.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Generate.java.template index 911f6ff6267..9f1d40faf8b 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Generate.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Generate.java.template @@ -261,7 +261,7 @@ public class ${className}Generate { } if (!unqualified.isEmpty()) { throw new org.eclipse.dirigible.sdk.db.ValidationException( - "${itemRefuse} (${fromItemEntity} " + unqualified + ")"); + "${itemRefuseJavaLiteral} (${fromItemEntity} " + unqualified + ")"); } #end if (qualifying.isEmpty()) { diff --git a/components/template/template-application-rest-java/src/main/resources/META-INF/dirigible/template-application-rest-java/api/EntityController.java.template b/components/template/template-application-rest-java/src/main/resources/META-INF/dirigible/template-application-rest-java/api/EntityController.java.template index e95bb622390..78b28054fb9 100644 --- a/components/template/template-application-rest-java/src/main/resources/META-INF/dirigible/template-application-rest-java/api/EntityController.java.template +++ b/components/template/template-application-rest-java/src/main/resources/META-INF/dirigible/template-application-rest-java/api/EntityController.java.template @@ -406,7 +406,7 @@ public class ${name}Controller { .thenComparing(java.util.Comparator.naturalOrder())); #if($uniqueConstraints) #foreach($uniqueConstraint in $uniqueConstraints) - messages.put("${uniqueConstraint.name}".toUpperCase(Locale.ROOT), "${uniqueConstraint.message}"); + messages.put("${uniqueConstraint.nameJavaLiteral}".toUpperCase(Locale.ROOT), "${uniqueConstraint.messageJavaLiteral}"); #end #end #foreach($property in $properties) @@ -885,7 +885,7 @@ public class ${name}Controller { #end Object requiredValue = ${check.valueExpression}; if ((${check.guard}) && (requiredValue == null || String.valueOf(requiredValue).isBlank())) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "${check.message}"); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "${check.messageJavaLiteral}"); } } #elseif($check.kind == "compare") @@ -899,7 +899,7 @@ public class ${name}Controller { #else && !(entity.${check.field}.compareTo(entity.${check.than}) ${check.op} 0)) { #end - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "${check.message}"); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "${check.messageJavaLiteral}"); } #else // Row-level check (intent `checks: exactlyOne`). @@ -911,7 +911,7 @@ public class ${name}Controller { } #end if (assigned != 1) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "${check.message}"); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "${check.messageJavaLiteral}"); } } #end diff --git a/components/template/template-application-rest-java/src/main/resources/META-INF/dirigible/template-application-rest-java/api/EntityMyController.java.template b/components/template/template-application-rest-java/src/main/resources/META-INF/dirigible/template-application-rest-java/api/EntityMyController.java.template index 005da9de4cf..46759ef0c40 100644 --- a/components/template/template-application-rest-java/src/main/resources/META-INF/dirigible/template-application-rest-java/api/EntityMyController.java.template +++ b/components/template/template-application-rest-java/src/main/resources/META-INF/dirigible/template-application-rest-java/api/EntityMyController.java.template @@ -626,7 +626,7 @@ public class ${name}MyController { #end Object requiredValue = ${check.valueExpression}; if ((${check.guard}) && (requiredValue == null || String.valueOf(requiredValue).isBlank())) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "${check.message}"); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "${check.messageJavaLiteral}"); } } #elseif($check.kind == "compare") @@ -640,7 +640,7 @@ public class ${name}MyController { #else && !(entity.${check.field}.compareTo(entity.${check.than}) ${check.op} 0)) { #end - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "${check.message}"); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "${check.messageJavaLiteral}"); } #else // Row-level check (intent `checks: exactlyOne`). @@ -652,7 +652,7 @@ public class ${name}MyController { } #end if (assigned != 1) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "${check.message}"); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "${check.messageJavaLiteral}"); } } #end @@ -747,7 +747,7 @@ public class ${name}MyController { .thenComparing(java.util.Comparator.naturalOrder())); #if($uniqueConstraints) #foreach($uniqueConstraint in $uniqueConstraints) - messages.put("${uniqueConstraint.name}".toUpperCase(Locale.ROOT), "${uniqueConstraint.message}"); + messages.put("${uniqueConstraint.nameJavaLiteral}".toUpperCase(Locale.ROOT), "${uniqueConstraint.messageJavaLiteral}"); #end #end #foreach($property in $properties) diff --git a/components/template/template-application-rest-java/src/main/resources/META-INF/dirigible/template-application-rest-java/api/EntityPartnerController.java.template b/components/template/template-application-rest-java/src/main/resources/META-INF/dirigible/template-application-rest-java/api/EntityPartnerController.java.template index 2f08e3a30f0..1c5b804dc5e 100644 --- a/components/template/template-application-rest-java/src/main/resources/META-INF/dirigible/template-application-rest-java/api/EntityPartnerController.java.template +++ b/components/template/template-application-rest-java/src/main/resources/META-INF/dirigible/template-application-rest-java/api/EntityPartnerController.java.template @@ -582,7 +582,7 @@ public class ${name}PartnerController { #end Object requiredValue = ${check.valueExpression}; if ((${check.guard}) && (requiredValue == null || String.valueOf(requiredValue).isBlank())) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "${check.message}"); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "${check.messageJavaLiteral}"); } } #elseif($check.kind == "compare") @@ -596,7 +596,7 @@ public class ${name}PartnerController { #else && !(entity.${check.field}.compareTo(entity.${check.than}) ${check.op} 0)) { #end - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "${check.message}"); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "${check.messageJavaLiteral}"); } #else // Row-level check (intent `checks: exactlyOne`). @@ -608,7 +608,7 @@ public class ${name}PartnerController { } #end if (assigned != 1) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "${check.message}"); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "${check.messageJavaLiteral}"); } } #end @@ -702,7 +702,7 @@ public class ${name}PartnerController { .thenComparing(java.util.Comparator.naturalOrder())); #if($uniqueConstraints) #foreach($uniqueConstraint in $uniqueConstraints) - messages.put("${uniqueConstraint.name}".toUpperCase(Locale.ROOT), "${uniqueConstraint.message}"); + messages.put("${uniqueConstraint.nameJavaLiteral}".toUpperCase(Locale.ROOT), "${uniqueConstraint.messageJavaLiteral}"); #end #end #foreach($property in $properties) 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 027a5ff9225..07bd8a6a2b4 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 @@ -218,7 +218,7 @@ class IntentEmissionCoverageIT extends IntegrationTest { history: true immutableWhen: "Status == 2" checks: - - { kind: itemsMin, count: 1, status: 2, message: "Entry needs at least one line" } + - { kind: itemsMin, count: 1, status: 2, message: 'An entry needs at least one "line"' } - { kind: itemsSumEqual, over: [debit, credit], status: 2, message: "Debits must equal credits" } # requiredWhen (#7094), gated + over a relation hop: the value lives on the related # account, so the generated repository loads it by FK before it can read it, and the @@ -227,7 +227,7 @@ class IntentEmissionCoverageIT extends IntegrationTest { message: "An audited entry must be booked against a named account" } # Two values of the SAME row, related (#7095) - one temporal pair and one numeric, # the two comparison families the generated code emits differently. - - { kind: compare, field: due, op: ge, than: date, message: "Due cannot be before the entry date" } + - { kind: compare, field: due, op: ge, than: date, message: 'A "due" date is never before the entry date' } - { kind: compare, field: paid, op: le, than: debit, message: "Paid cannot exceed the debit total" } fields: - { name: id, type: integer, primaryKey: true, generated: true } @@ -293,7 +293,7 @@ class IntentEmissionCoverageIT extends IntegrationTest { # answered with the authored message rather than a server error. - name: PartyCode unique: - - { fields: [party, code], message: "This code is already registered for the party" } + - { fields: [party, code], message: 'This "code" is already registered for the party' } fields: - { name: id, type: integer, primaryKey: true, generated: true } - { name: code, type: string, required: true, length: 50 } @@ -804,7 +804,7 @@ class IntentEmissionCoverageIT extends IntegrationTest { - kind: guard aggregate: ledgerTotal minimum: 0 - message: Insufficient balance + message: 'Insufficient "balance"' enabledBy: EMISSION_BLOCK_NEGATIVE_LEDGER - name: LedgerTotal fields: @@ -1452,7 +1452,7 @@ class IntentEmissionCoverageIT extends IntegrationTest { to: BillLine where: - { field: amount, op: gt, value: 0 } - refuse: "Stay night carries no amount" + refuse: 'Stay night carries no "amount"' map: Amount: amount defaults: @@ -1827,7 +1827,7 @@ private void assertEmission() { assertTrue( entryController.contains("if (entity.Due != null && entity.Date != null") && entryController.contains("!(entity.Due.compareTo(entity.Date) >= 0)") - && entryController.contains("Due cannot be before the entry date"), + && entryController.contains("A \\\"due\\\" date is never before the entry date"), "checks: compare over two dates must emit a compareTo comparison in the REST controller, got: " + entryController); assertTrue(entryController.contains( "!(new java.math.BigDecimal(entity.Paid.toString()).compareTo(new java.math.BigDecimal(entity.Debit.toString())) <= 0)"), @@ -1940,8 +1940,8 @@ private void assertEmission() { "an ungated check is not the repository's - a gate it does not carry cannot be tested there"); String entryRepository = contentOf("gen/emission/data/entry/EntryRepository.java"); - assertTrue(entryRepository.contains("Entry needs at least one line"), - "checks: itemsMin must emit its authored message into the repository gate"); + assertTrue(entryRepository.contains("An entry needs at least one \\\"line\\\""), + "checks: itemsMin must emit its authored message into the repository gate, escaped for the literal it lands in"); assertTrue(entryRepository.contains("Debits must equal credits"), "checks: itemsSumEqual must emit its authored message into the repository gate"); // A value required only under a condition (#7094). The rule reaches the value THROUGH the @@ -2050,7 +2050,7 @@ private void assertEmission() { // layer dropped it. assertTrue(schema.contains("\"PartyCode_Party_Code\""), "the composite business key must be emitted into the schema: " + schema); String partyCodeController = contentOf("gen/emission/api/partycode/PartyCodeController.java"); - assertTrue(partyCodeController.contains("This code is already registered for the party"), + assertTrue(partyCodeController.contains("This \\\"code\\\" is already registered for the party"), "the generated controller must carry the authored conflict message"); assertTrue(schema.contains("EMISSION_UNIT_LANG"), "multilingual must emit the _LANG translation table into the schema"); // manyToMany: the link entity is an ordinary entity from parse time on, so it must reach the @@ -2327,7 +2327,7 @@ private void assertEmission() { "a guard must test every grouping key for null before it recomputes: " + ledgerRepository); assertTrue(ledgerRepository.contains("boolean guardWithin = true;") && ledgerRepository.contains("if (guardKeyed) {"), "a row belonging to no key-tuple must pass the guard untouched - no throw, no marker, no forced status"); - assertTrue(ledgerRepository.contains("throw new ValidationException(\"Insufficient balance\")"), + assertTrue(ledgerRepository.contains("throw new ValidationException(\"Insufficient \\\"balance\\\"\")"), "outcome block must fail the write with the authored message"); assertTrue(ledgerRepository.contains("Configurations.get(\"EMISSION_BLOCK_NEGATIVE_LEDGER\""), "enabledBy must wrap the guard in a config gate, so a tenant can turn it off"); @@ -3351,7 +3351,7 @@ private void assertEmission() { // The other reading: the refusal names the rows, so the caller knows which of a hundred lines // to go and fix - the whole question they have. String checkedBillFromStay = contentOf("gen/events/emission/CheckedBillFromStayGenerate.java"); - assertTrue(checkedBillFromStay.contains("\"Stay night carries no amount (StayNight \" + unqualified + \")\""), + assertTrue(checkedBillFromStay.contains("\"Stay night carries no \\\"amount\\\" (StayNight \" + unqualified + \")\""), "refuse: must throw the authored message carrying the keys of the offending rows"); // generates on the step axis + mode: append (#6800): the listener binds the step-scoped topic @@ -3920,7 +3920,7 @@ private void assertRuntimeEnforcement() { .post(API + "/entry/EntryController") .then() .statusCode(400) - .body("message", containsString("Due cannot be before the entry date"))); + .body("message", containsString("A \"due\" date is never before the entry date"))); restAssuredExecutor.execute(() -> given().contentType("application/json") .body("{\"Date\":\"2026-01-15\",\"Due\":\"2026-01-15\",\"Account\":2}") .when() @@ -4938,7 +4938,7 @@ private void assertGeneratesItemsRuleRuntime() { .post("/services/java/" + PROJECT + "/gen/events/emission/CheckedBillFromStayGenerate/run") .then() .statusCode(400) - .body(containsString("Stay night carries no amount"))); + .body("message", containsString("Stay night carries no \"amount\""))); // No rule qualifies a row, so the document would have no lines at all - refused, not committed. restAssuredExecutor.execute(() -> given().contentType("application/json") diff --git a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/PersonalSurfaceCreateValidationTemplateIT.java b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/PersonalSurfaceCreateValidationTemplateIT.java index 69d60a02b62..72d2394bdce 100644 --- a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/PersonalSurfaceCreateValidationTemplateIT.java +++ b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/PersonalSurfaceCreateValidationTemplateIT.java @@ -236,11 +236,16 @@ private static Map note() { return property; } - /** intent `checks: exactlyOne` - a row-level refusal, the same one the power surface applies. */ + /** + * intent `checks: exactlyOne` - a row-level refusal, the same one the power surface applies. The + * escaped twin is the key the surfaces read at the Java site (#7241); the raw message stays for the + * surfaces that render it as text. + */ private static Map rowCheck() { Map check = new LinkedHashMap<>(); check.put("fields", List.of("FromDate", "Note")); check.put("message", "exactly one of FromDate / Note must be set"); + check.put("messageJavaLiteral", "exactly one of FromDate / Note must be set"); return check; } } diff --git a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/UniqueFieldConflictControllerTemplateIT.java b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/UniqueFieldConflictControllerTemplateIT.java index a384aa6b5b8..cd18253db74 100644 --- a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/UniqueFieldConflictControllerTemplateIT.java +++ b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/UniqueFieldConflictControllerTemplateIT.java @@ -104,6 +104,13 @@ class UniqueFieldConflictControllerTemplateIT { "ERROR: duplicate key value violates unique constraint \"vacations_public_holiday_pkey\"\n" + " Detail: Key (PUBLIC_HOLIDAY_ID)=(5) already exists."; + /** + * The authored message of the composite key, quoting the field it is about - the shape the DSL's + * own examples suggest, and the one that ends the Java literal it lands in unless the twin the + * processor derives is what the template writes (#7241). + */ + private static final String COMPOSITE_KEY_MESSAGE = "This \"day\" is already a holiday of the company"; + private final VelocityGenerationEngine velocityGenerationEngine = new VelocityGenerationEngine(); @Test @@ -220,7 +227,7 @@ void aCompositeKeyStillAnswersWithItsAuthoredMessage() throws Exception { context.put("properties", List.of(primaryKey(), column("Company", "PUBLIC_HOLIDAY_COMPANY"), column("Day", "PUBLIC_HOLIDAY_DAY"))); context.put("uniqueConstraints", List.of(compositeKey())); - assertEquals("This day is already a holiday of the company", + assertEquals(COMPOSITE_KEY_MESSAGE, mapping(context).answerFor("duplicate key value violates unique constraint \"PublicHoliday_Company_Day\"", UNIQUE_VIOLATION), "the authored message must still be the answer for the constraint the model named"); @@ -462,12 +469,19 @@ private static Map context() { return parameters; } - /** The shape {@code EdmIntentGenerator} puts on a composite {@code unique:} declaration. */ + /** + * The shape {@code EdmIntentGenerator} plus {@code ModelParameterProcessor} put on a composite + * {@code unique:} declaration. The message carries a quote on purpose: the name and the message are + * both written into Java string literals, so only the escaped twins the processor derives (#7241) + * leave the mapping compilable - and this test compiles what it renders. + */ private static Map compositeKey() { Map constraint = new LinkedHashMap<>(); constraint.put("name", "PublicHoliday_Company_Day"); + constraint.put("nameJavaLiteral", "PublicHoliday_Company_Day"); constraint.put("columns", List.of(Map.of("name", "PUBLIC_HOLIDAY_COMPANY"), Map.of("name", "PUBLIC_HOLIDAY_DAY"))); - constraint.put("message", "This day is already a holiday of the company"); + constraint.put("message", COMPOSITE_KEY_MESSAGE); + constraint.put("messageJavaLiteral", COMPOSITE_KEY_MESSAGE.replace("\"", "\\\"")); return constraint; }