diff --git a/components/ide/ide-template/src/main/java/org/eclipse/dirigible/components/ide/template/service/model/AuthoredDefaults.java b/components/ide/ide-template/src/main/java/org/eclipse/dirigible/components/ide/template/service/model/AuthoredDefaults.java
index 01519012c74..cef0736f552 100644
--- a/components/ide/ide-template/src/main/java/org/eclipse/dirigible/components/ide/template/service/model/AuthoredDefaults.java
+++ b/components/ide/ide-template/src/main/java/org/eclipse/dirigible/components/ide/template/service/model/AuthoredDefaults.java
@@ -28,11 +28,17 @@ private AuthoredDefaults() {}
/**
* Whether an authored boolean default reads as true.
*
+ *
+ * Read through {@link #unquote(String)}, so the SQL-quoted shape a working DB DEFAULT needs
+ * ({@code 'true'}) reads as the bare one - and reads the same way in every generated language,
+ * which is this class's whole reason to exist.
+ *
* @param defaultValue the authored default
* @return true when it does
*/
static boolean readsAsTrue(String defaultValue) {
- return "true".equals(defaultValue) || "TRUE".equals(defaultValue) || "1".equals(defaultValue);
+ String value = unquote(defaultValue);
+ return "true".equals(value) || "TRUE".equals(value) || "1".equals(value);
}
/**
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..2190bedd294 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
@@ -9,6 +9,8 @@
*/
package org.eclipse.dirigible.components.ide.template.service.model;
+import java.util.function.Consumer;
+
/**
* Java literals for values a model carries as text.
*
@@ -69,31 +71,63 @@ public static String escape(String value) {
* the property has no default that a Java literal can stand in for.
*
*
- * A numeric default is parsed from its authored text rather than inlined as a numeric literal, so
- * an author's {@code "8.0"} on an integer column fails that one create instead of failing the whole
- * generated build. A string default is read in either authoring shape ({@link AuthoredDefaults}),
- * and both yield the string the column would hold. A date/time or binary column has no literal: its
- * DEFAULT is emitted verbatim into the DDL and is typically a SQL expression ({@code CURRENT_DATE},
- * {@code now()}).
+ * Every arm reads the authored text through {@link AuthoredDefaults}, so both authoring shapes -
+ * bare and SQL-quoted - yield the value the column would hold. Reading it in only one arm is how
+ * {@code defaultValue: "'20'"} on an integer column seeded {@code 20} in the item dialog and
+ * emitted {@code Integer.valueOf("'20'")} in the repository, a {@code NumberFormatException} on
+ * every create that relied on the default (dirigible #7293).
+ *
+ *
+ * A numeric default is parsed from its text rather than inlined as a numeric literal - the same
+ * parse the generated expression performs, which is why it is run HERE: an unparsable numeric
+ * default is refused while the author is generating, naming the property, instead of compiling into
+ * an expression that throws on every create of that entity. A date/time or binary column has no
+ * literal: its DEFAULT is emitted verbatim into the DDL and is typically a SQL expression
+ * ({@code CURRENT_DATE}, {@code now()}).
*
* @param javaClass the property's Java class, as the parameter graph resolved it
* @param defaultValue the authored default, as the model carries it
+ * @param property the property the default is authored on, for the refusal message
* @return the Java expression, or null when there is none
+ * @throws IllegalArgumentException when a numeric property's default is not a value of its type
*/
- public static String defaultValueExpression(String javaClass, String defaultValue) {
+ public static String defaultValueExpression(String javaClass, String defaultValue, String property) {
if (javaClass == null || defaultValue == null || defaultValue.isEmpty()) {
return null;
}
+ String value = AuthoredDefaults.unquote(defaultValue);
return switch (javaClass) {
- case "java.math.BigDecimal" -> "new java.math.BigDecimal(\"" + escape(defaultValue) + "\")";
- case "Double" -> "Double.valueOf(\"" + escape(defaultValue) + "\")";
- case "Float" -> "Float.valueOf(\"" + escape(defaultValue) + "\")";
- case "Long" -> "Long.valueOf(\"" + escape(defaultValue) + "\")";
- case "Integer" -> "Integer.valueOf(\"" + escape(defaultValue) + "\")";
- case "Short" -> "Short.valueOf(\"" + escape(defaultValue) + "\")";
+ case "java.math.BigDecimal" -> numericExpression("new java.math.BigDecimal", value, javaClass, property,
+ java.math.BigDecimal::new);
+ case "Double" -> numericExpression("Double.valueOf", value, javaClass, property, Double::valueOf);
+ case "Float" -> numericExpression("Float.valueOf", value, javaClass, property, Float::valueOf);
+ case "Long" -> numericExpression("Long.valueOf", value, javaClass, property, Long::valueOf);
+ case "Integer" -> numericExpression("Integer.valueOf", value, javaClass, property, Integer::valueOf);
+ case "Short" -> numericExpression("Short.valueOf", value, javaClass, property, Short::valueOf);
case "Boolean" -> AuthoredDefaults.readsAsTrue(defaultValue) ? "Boolean.TRUE" : "Boolean.FALSE";
- case "String" -> "\"" + escape(AuthoredDefaults.unquote(defaultValue)) + "\"";
+ case "String" -> "\"" + escape(value) + "\"";
default -> null;
};
}
+
+ /**
+ * A numeric default as the factory call the generated code applies it through, refusing a text the
+ * very same factory cannot read.
+ *
+ * @param factory the factory the expression calls
+ * @param value the authored default, unquoted
+ * @param javaClass the property's Java class, for the refusal message
+ * @param property the property the default is authored on, for the refusal message
+ * @param parse the factory itself, run here on the authored text
+ * @return the factory call
+ */
+ private static String numericExpression(String factory, String value, String javaClass, String property, Consumer parse) {
+ try {
+ parse.accept(value);
+ } catch (NumberFormatException ex) {
+ throw new IllegalArgumentException("Property [" + property + "] declares the default [" + value
+ + "], which is not a value of its type [" + javaClass + "] - every create applying it would fail.", ex);
+ }
+ return factory + "(\"" + escape(value) + "\")";
+ }
}
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..b8b1abd1432 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
@@ -412,7 +412,7 @@ private static void processProperty(Map property, Map property, Map property) {
+ private static void resolveDefaultValueLiterals(Map property, Map entity) {
String defaultValue = str(property, "dataDefaultValue");
if (defaultValue == null || defaultValue.isEmpty()) {
return;
@@ -460,7 +461,8 @@ private static void resolveDefaultValueLiterals(Map property) {
if (Boolean.TRUE.equals(property.get("dataPrimaryKey")) || Boolean.TRUE.equals(property.get("dataAutoIncrement"))) {
return;
}
- String expression = JavaLiterals.defaultValueExpression(str(property, "dataTypeJavaClass"), defaultValue);
+ String expression = JavaLiterals.defaultValueExpression(str(property, "dataTypeJavaClass"), defaultValue,
+ str(entity, "name") + "." + str(property, "name"));
if (expression != null) {
property.put("dataDefaultValueJavaLiteral", expression);
}
diff --git a/components/ide/ide-template/src/test/java/org/eclipse/dirigible/components/ide/template/service/model/JavaLiteralsTest.java b/components/ide/ide-template/src/test/java/org/eclipse/dirigible/components/ide/template/service/model/JavaLiteralsTest.java
index bdf4d7bd983..e7855dcc680 100644
--- a/components/ide/ide-template/src/test/java/org/eclipse/dirigible/components/ide/template/service/model/JavaLiteralsTest.java
+++ b/components/ide/ide-template/src/test/java/org/eclipse/dirigible/components/ide/template/service/model/JavaLiteralsTest.java
@@ -13,6 +13,8 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Tests the Java literals a model value is written into a generated source as.
@@ -50,36 +52,58 @@ void escapesTheControlCharactersThatWouldEndTheLine() {
}
/**
- * A numeric default is parsed from its authored text rather than inlined, so an author's "8.0" on
- * an integer column fails that one create instead of failing the whole generated build.
+ * A numeric default is parsed from its authored text rather than inlined as a numeric literal -
+ * which keeps BigDecimal exact and needs no per-type suffix.
*/
@Test
void parsesANumericDefaultFromItsAuthoredText() {
- assertEquals("new java.math.BigDecimal(\"20.00\")", JavaLiterals.defaultValueExpression("java.math.BigDecimal", "20.00"));
- assertEquals("Double.valueOf(\"1.5\")", JavaLiterals.defaultValueExpression("Double", "1.5"));
- assertEquals("Float.valueOf(\"1.5\")", JavaLiterals.defaultValueExpression("Float", "1.5"));
- assertEquals("Long.valueOf(\"7\")", JavaLiterals.defaultValueExpression("Long", "7"));
- assertEquals("Integer.valueOf(\"7\")", JavaLiterals.defaultValueExpression("Integer", "7"));
- assertEquals("Short.valueOf(\"7\")", JavaLiterals.defaultValueExpression("Short", "7"));
- assertEquals("Integer.valueOf(\"8.0\")", JavaLiterals.defaultValueExpression("Integer", "8.0"));
+ assertEquals("new java.math.BigDecimal(\"20.00\")", JavaLiterals.defaultValueExpression("java.math.BigDecimal", "20.00", "E.P"));
+ assertEquals("Double.valueOf(\"1.5\")", JavaLiterals.defaultValueExpression("Double", "1.5", "E.P"));
+ assertEquals("Float.valueOf(\"1.5\")", JavaLiterals.defaultValueExpression("Float", "1.5", "E.P"));
+ assertEquals("Long.valueOf(\"7\")", JavaLiterals.defaultValueExpression("Long", "7", "E.P"));
+ assertEquals("Integer.valueOf(\"7\")", JavaLiterals.defaultValueExpression("Integer", "7", "E.P"));
+ assertEquals("Short.valueOf(\"7\")", JavaLiterals.defaultValueExpression("Short", "7", "E.P"));
+ assertEquals("Integer.valueOf(\"-7\")", JavaLiterals.defaultValueExpression("Integer", "-7", "E.P"));
}
/**
- * A numeric default is the one place a malformed value cannot even be escaped into something that
- * parses - so it must still compile, and fail at that one create.
+ * The defect: the SQL-quoted shape was read in the String arm only, so an integer column's
+ * {@code '20'} seeded 20 in the item dialog and emitted {@code Integer.valueOf("'20'")} in the
+ * repository - a NumberFormatException on every create that relied on the default (#7293).
*/
@Test
- void escapesAMalformedNumericDefaultTooRatherThanBreakingTheCompile() {
- assertEquals("Integer.valueOf(\"7\\\"\")", JavaLiterals.defaultValueExpression("Integer", "7\""));
+ void readsANumericDefaultInEitherAuthoringShape() {
+ assertEquals("Integer.valueOf(\"20\")", JavaLiterals.defaultValueExpression("Integer", "'20'", "E.P"));
+ assertEquals("Long.valueOf(\"20\")", JavaLiterals.defaultValueExpression("Long", "'20'", "E.P"));
+ assertEquals("new java.math.BigDecimal(\"20.00\")", JavaLiterals.defaultValueExpression("java.math.BigDecimal", "'20.00'", "E.P"));
+ }
+
+ /**
+ * A text the property's own factory cannot read is refused while the author is generating, naming
+ * the property - it used to compile into an expression that threw on every create of that entity.
+ */
+ @Test
+ void refusesANumericDefaultThatIsNotAValueOfItsType() {
+ IllegalArgumentException refusal =
+ assertThrows(IllegalArgumentException.class, () -> JavaLiterals.defaultValueExpression("Integer", "8.0", "Order.Lines"));
+ assertTrue(refusal.getMessage()
+ .contains("Order.Lines"),
+ "the refusal must name the property, got: " + refusal.getMessage());
+ assertThrows(IllegalArgumentException.class, () -> JavaLiterals.defaultValueExpression("Integer", "7\"", "E.P"));
+ assertThrows(IllegalArgumentException.class, () -> JavaLiterals.defaultValueExpression("Integer", "N/A", "E.P"));
+ assertThrows(IllegalArgumentException.class, () -> JavaLiterals.defaultValueExpression("java.math.BigDecimal", "1 or 2", "E.P"));
+ assertThrows(IllegalArgumentException.class, () -> JavaLiterals.defaultValueExpression("Long", "nextval('s')", "E.P"));
}
@Test
void readsABooleanDefaultInEveryAuthoredShape() {
- assertEquals("Boolean.TRUE", JavaLiterals.defaultValueExpression("Boolean", "true"));
- assertEquals("Boolean.TRUE", JavaLiterals.defaultValueExpression("Boolean", "TRUE"));
- assertEquals("Boolean.TRUE", JavaLiterals.defaultValueExpression("Boolean", "1"));
- assertEquals("Boolean.FALSE", JavaLiterals.defaultValueExpression("Boolean", "false"));
- assertEquals("Boolean.FALSE", JavaLiterals.defaultValueExpression("Boolean", "0"));
+ assertEquals("Boolean.TRUE", JavaLiterals.defaultValueExpression("Boolean", "true", "E.P"));
+ assertEquals("Boolean.TRUE", JavaLiterals.defaultValueExpression("Boolean", "TRUE", "E.P"));
+ assertEquals("Boolean.TRUE", JavaLiterals.defaultValueExpression("Boolean", "1", "E.P"));
+ assertEquals("Boolean.FALSE", JavaLiterals.defaultValueExpression("Boolean", "false", "E.P"));
+ assertEquals("Boolean.FALSE", JavaLiterals.defaultValueExpression("Boolean", "0", "E.P"));
+ assertEquals("Boolean.TRUE", JavaLiterals.defaultValueExpression("Boolean", "'true'", "E.P"));
+ assertEquals("Boolean.FALSE", JavaLiterals.defaultValueExpression("Boolean", "'false'", "E.P"));
}
/**
@@ -88,24 +112,24 @@ void readsABooleanDefaultInEveryAuthoredShape() {
*/
@Test
void readsAStringDefaultInEitherAuthoringShape() {
- assertEquals("\"DRAFT\"", JavaLiterals.defaultValueExpression("String", "DRAFT"));
- assertEquals("\"DRAFT\"", JavaLiterals.defaultValueExpression("String", "'DRAFT'"));
- assertEquals("\"'\"", JavaLiterals.defaultValueExpression("String", "'"));
- assertEquals("\"6\\\"\"", JavaLiterals.defaultValueExpression("String", "6\""));
- assertEquals("\"6\\\"\"", JavaLiterals.defaultValueExpression("String", "'6\"'"));
+ assertEquals("\"DRAFT\"", JavaLiterals.defaultValueExpression("String", "DRAFT", "E.P"));
+ assertEquals("\"DRAFT\"", JavaLiterals.defaultValueExpression("String", "'DRAFT'", "E.P"));
+ assertEquals("\"'\"", JavaLiterals.defaultValueExpression("String", "'", "E.P"));
+ assertEquals("\"6\\\"\"", JavaLiterals.defaultValueExpression("String", "6\"", "E.P"));
+ assertEquals("\"6\\\"\"", JavaLiterals.defaultValueExpression("String", "'6\"'", "E.P"));
}
@Test
void hasNoExpressionForATypeWhoseDefaultIsASqlExpression() {
- assertNull(JavaLiterals.defaultValueExpression("java.time.LocalDate", "CURRENT_DATE"));
- assertNull(JavaLiterals.defaultValueExpression("java.time.Instant", "now()"));
- assertNull(JavaLiterals.defaultValueExpression("byte[]", "x"));
+ assertNull(JavaLiterals.defaultValueExpression("java.time.LocalDate", "CURRENT_DATE", "E.P"));
+ assertNull(JavaLiterals.defaultValueExpression("java.time.Instant", "now()", "E.P"));
+ assertNull(JavaLiterals.defaultValueExpression("byte[]", "x", "E.P"));
}
@Test
void hasNoExpressionWithoutADefault() {
- assertNull(JavaLiterals.defaultValueExpression("String", null));
- assertNull(JavaLiterals.defaultValueExpression("String", ""));
- assertNull(JavaLiterals.defaultValueExpression(null, "DRAFT"));
+ assertNull(JavaLiterals.defaultValueExpression("String", null, "E.P"));
+ assertNull(JavaLiterals.defaultValueExpression("String", "", "E.P"));
+ assertNull(JavaLiterals.defaultValueExpression(null, "DRAFT", "E.P"));
}
}
diff --git a/components/ide/ide-template/src/test/java/org/eclipse/dirigible/components/ide/template/service/model/JsLiteralsTest.java b/components/ide/ide-template/src/test/java/org/eclipse/dirigible/components/ide/template/service/model/JsLiteralsTest.java
index 6aa7db1a203..758b249e575 100644
--- a/components/ide/ide-template/src/test/java/org/eclipse/dirigible/components/ide/template/service/model/JsLiteralsTest.java
+++ b/components/ide/ide-template/src/test/java/org/eclipse/dirigible/components/ide/template/service/model/JsLiteralsTest.java
@@ -59,6 +59,8 @@ void seedsEachWidgetInTheShapeTheDraftHolds() {
assertEquals("true", JsLiterals.defaultValueExpression("CHECKBOX", false, "TRUE"));
assertEquals("true", JsLiterals.defaultValueExpression("CHECKBOX", false, "1"));
assertEquals("false", JsLiterals.defaultValueExpression("CHECKBOX", false, "false"));
+ assertEquals("true", JsLiterals.defaultValueExpression("CHECKBOX", false, "'true'"));
+ assertEquals("false", JsLiterals.defaultValueExpression("CHECKBOX", false, "'false'"));
assertEquals("20.00", JsLiterals.defaultValueExpression("TEXTBOX", true, "20.00"));
assertEquals("'DRAFT'", JsLiterals.defaultValueExpression("TEXTBOX", false, "DRAFT"));
// A dropdown's FK stays a string even though it reads as a number, so it matches an option's
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..9e016d8020a 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
@@ -167,6 +167,50 @@ void carriesTheAuthoredDefaultAsAnEscapedJavaScriptSeed() {
assertEquals("20", rate.get("dataDefaultValueJsLiteral"));
}
+ /**
+ * The defect: one authored default reaches several generated languages, and the SQL-quoted shape
+ * was read in the Java String arm only - so an integer column's {@code '20'} seeded 20 in the item
+ * dialog and emitted {@code Integer.valueOf("'20'")} in the repository, a NumberFormatException on
+ * every create that relied on it (#7293). The two literals are asserted together because agreeing
+ * is the whole invariant.
+ */
+ @Test
+ void readsAQuotedNumericDefaultTheSameWayInBothLanguages() {
+ Map quantity = property("Quantity", "INTEGER");
+ quantity.put("dataDefaultValue", "'20'");
+ Map rate = property("VatRate", "DECIMAL");
+ rate.put("dataDefaultValue", "'20.00'");
+ Map billable = property("Billable", "BOOLEAN");
+ billable.put("widgetType", "CHECKBOX");
+ billable.put("dataDefaultValue", "'true'");
+ ModelParameterProcessor.process(model(entity("Line", "Lines", quantity, rate, billable)), parameters());
+
+ assertEquals("Integer.valueOf(\"20\")", quantity.get("dataDefaultValueJavaLiteral"));
+ assertEquals("20", quantity.get("dataDefaultValueJsLiteral"));
+ assertEquals("new java.math.BigDecimal(\"20.00\")", rate.get("dataDefaultValueJavaLiteral"));
+ assertEquals("20.00", rate.get("dataDefaultValueJsLiteral"));
+ assertEquals("Boolean.TRUE", billable.get("dataDefaultValueJavaLiteral"));
+ assertEquals("true", billable.get("dataDefaultValueJsLiteral"));
+ }
+
+ /**
+ * A numeric default the property's own type cannot read is refused while the author is generating,
+ * naming the property - it used to compile into an expression that threw on every create.
+ */
+ @Test
+ void refusesANumericDefaultThatIsNotAValueOfItsType() {
+ Map quantity = property("Quantity", "INTEGER");
+ quantity.put("dataDefaultValue", "8.0");
+ Map model = model(entity("Line", "Lines", quantity));
+
+ IllegalArgumentException refusal =
+ assertThrows(IllegalArgumentException.class, () -> ModelParameterProcessor.process(model, parameters()));
+
+ assertTrue(refusal.getMessage()
+ .contains("Line.Quantity"),
+ "the refusal must name the property, got: " + refusal.getMessage());
+ }
+
/**
* The key's presence is what the template reads as "this property has a default to seed", so a
* property with none must leave it absent rather than null.