Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,9 @@ private static void bindGenerate(Map<String, Object> item, Map<String, Object> 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ private static void processEntity(Map<String, Object> entity, List<Map<String, O
}
entity.put("referencedProjections", new ArrayList<>());
splitChecks(entity, parameters);
resolveUniqueConstraintLiterals(entity);
resolveDataOrder(entity);

for (Map<String, Object> property : asMaps(entity.get("properties"))) {
Expand Down Expand Up @@ -218,6 +219,7 @@ private static void splitChecks(Map<String, Object> entity, Map<String, Object>
List<Object> documentChecks = new ArrayList<>();
for (Map<String, Object> check : checks) {
String kind = str(check, "kind");
resolveMessageLiteral(check);
resolveCheckPathLoads(check, parameters);
if ("exactlyOne".equals(kind) || "compare".equals(kind)) {
rowChecks.add(check);
Expand All @@ -237,6 +239,47 @@ private static void splitChecks(Map<String, Object> entity, Map<String, Object>
entity.put("documentChecks", documentChecks);
}

/**
* Derives the escaped twin of an authored message, for the templates that write it into a Java
* string literal.
*
* <p>
* 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.
*
* <p>
* 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<String, Object> 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<String, Object> entity) {
for (Map<String, Object> 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,53 @@ void splitsTheDeclarativeChecksByTheScopeThatEnforcesThem() {
.size());
}

@Test
void carriesEveryAuthoredMessageAsAnEscapedJavaLiteralToo() {
Map<String, Object> row = new LinkedHashMap<>();
row.put("kind", "compare");
row.put("message", "A \"due\" date is never before the invoice date");
Map<String, Object> guard = new LinkedHashMap<>();
guard.put("kind", "guard");
guard.put("message", "Insufficient balance in C:\\ledger");
Map<String, Object> document = new LinkedHashMap<>();
document.put("kind", "itemsMin");
document.put("message", "An invoice needs at least one \"line\"");
Map<String, Object> unique = new LinkedHashMap<>();
unique.put("name", "INVOICE_\"NUMBER\"");
unique.put("message", "This \"number\" is already registered");
Map<String, Object> 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<String, Object> check = new LinkedHashMap<>();
check.put("kind", "exactlyOne");
Map<String, Object> unique = new LinkedHashMap<>();
unique.put("name", "INVOICE_NUMBER");
Map<String, Object> 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<String, Object> hop = new LinkedHashMap<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ package gen.${javaGenFolderName}.data.${javaPerspectiveName};
}
#else
if (!guardWithin) {
throw new ValidationException("${guard.message}");
throw new ValidationException("${guard.messageJavaLiteral}");
}
#end
}
Expand Down Expand Up @@ -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;
Expand All @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand All @@ -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`).
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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`).
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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`).
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Loading
Loading