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 @@ -2779,9 +2779,7 @@ private static void putDerivedDefault(Map<String, Object> 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;
}
}
Expand Down Expand Up @@ -3428,9 +3426,7 @@ private static String computedCellExpression(String value, CellMeta meta, java.u
* {@code Calc.eval("<expr>", source, <scale>)} - 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 + ")";
}

/**
Expand Down Expand Up @@ -3464,11 +3460,7 @@ private static String stringCellExpression(String v, java.util.Set<String> sourc
if (copy != null) {
return copy;
}
return "\"" + v.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
+ "\"";
return "\"" + JavaLiterals.escape(v) + "\"";
}

/**
Expand Down Expand Up @@ -3574,11 +3566,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. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -416,11 +417,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) + "\"";
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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) + "\"";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2633,16 +2633,23 @@ private static void putLifecycle(Map<String, Object> entityMap, EntityIntent ent
}
entityMap.put("lifecycleStatusProperty", IntentNaming.pascalCase(status.getName()));
entityMap.put("lifecycleEdges", String.join(",", edges));
List<String> 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<Map<String, Object>> names = new ArrayList<>();
for (Map.Entry<Integer, String> seeded : LifecycleStages.seededStatuses(model, status.getTo())
.entrySet()) {
if (seeded.getValue() != null && !seeded.getValue()
.isBlank()) {
names.add(seeded.getKey() + "=" + seeded.getValue());
Map<String, Object> 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+")) {
Expand Down Expand Up @@ -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<String> STRUCTURED_ATTRIBUTES = Set.of("rollupGuard", "checks", "labelParts", "aggregateKeys", "groupingKeys",
"relatedEntities", "scopedCalendars", "lookupColumns", "languages", "widgets", "customActionLabels", "processTaskLabels");
private static final Set<String> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -791,8 +791,16 @@ void lifecycleEmitsTheStateMachineTheRepositoryEnforces() {
Map<String, Object> 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<Map<String, Object>> statusNames = (List<Map<String, Object>>) 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,15 @@ public class ConsumedAttributesAudit {
*/
private static final Set<String> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ private static void bindDeleteAbort(Map<String, Object> item, Map<String, Object
private static void bindSetter(Map<String, Object> item, Map<String, Object> context, Map<String, Object> parameters) {
copy(context, item, "process", "className", "entity", "perspective", "keyProperty", "keyAccessor", "field", "value", "relation",
"errorMessage");
copyJavaLiterals(context, item, "value");
context.put("javaPerspective", sanitize(item, "perspective"));
}

Expand Down Expand Up @@ -413,6 +414,7 @@ private static void bindSchedule(Map<String, Object> item, Map<String, Object> c
// The days-past-due escalation ladder (issue #7276), likewise absent on an older .glue.
"hasEscalation", "escalationEntity", "escalationLocal", "escalationKeyProperty", "escalationAfterProperty",
"escalationSinceProperty", "escalationIntoProperty");
copyJavaLiterals(context, item, "cron");
context.put("generates", generates);
context.put("notifies", notifies);
context.put("javaPerspective", sanitize(item, "perspective"));
Expand Down Expand Up @@ -498,6 +500,7 @@ private static void bindIntegration(Map<String, Object> item, Map<String, Object
*/
private static void bindInbound(Map<String, Object> item, Map<String, Object> context, Map<String, Object> parameters) {
copy(context, item, "name", "className", "entity", "perspective", "path");
copyJavaLiterals(context, item, "path");
context.put("javaPerspective", sanitize(item, "perspective"));
bindArrival(item, context);
}
Expand All @@ -512,6 +515,7 @@ private static void bindInbound(Map<String, Object> item, Map<String, Object> co
*/
private static void bindInboundMessage(Map<String, Object> item, Map<String, Object> context, Map<String, Object> parameters) {
copy(context, item, "name", "className", "entity", "perspective", "destination", "listenerKind");
copyJavaLiterals(context, item, "destination");
context.put("javaPerspective", sanitize(item, "perspective"));
bindArrival(item, context);
}
Expand All @@ -526,6 +530,7 @@ private static void bindInboundMessage(Map<String, Object> item, Map<String, Obj
*/
private static void bindInboundFile(Map<String, Object> item, Map<String, Object> context, Map<String, Object> parameters) {
copy(context, item, "name", "className", "entity", "perspective", "folder", "cron");
copyJavaLiterals(context, item, "cron");
context.put("javaPerspective", sanitize(item, "perspective"));
bindArrival(item, context);
}
Expand Down Expand Up @@ -567,6 +572,7 @@ private static void bindArrival(Map<String, Object> item, Map<String, Object> co
private static void bindOutbound(Map<String, Object> item, Map<String, Object> context, Map<String, Object> 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));
}
Expand Down Expand Up @@ -1004,6 +1010,7 @@ private static void bindNumbering(Map<String, Object> item, Map<String, Object>
// 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"));
}

Expand Down Expand Up @@ -1325,6 +1332,32 @@ private static void copy(Map<String, Object> target, Map<String, Object> source,
}
}

/**
* Copies the escaped twin of each named descriptor value, for the templates that write it into a
* Java string literal.
*
* <p>
* 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<String, Object> target, Map<String, Object> 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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,12 +236,11 @@ private static int scaleOf(Map<String, Object> 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) + "\"";
}

}
Loading
Loading