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 @@ -54,99 +54,106 @@ public class ${className}Notification implements MessageHandler {
if (!(${guardExpression})) {
return;
}
// The try opens HERE, before the relation loads and the attachment render (#7290): those read
// the database too - a connection blip, a foreign key at a row a concurrent delete removed, a
// broken .print template - and a throw from any of them used to propagate straight out of
// onMessage with no stamp: the broker redelivers forever and the record's outcome stays empty,
// exactly the silent state #7023 introduced the stamp to remove. Mirrors the notify branch of
// Job.java.template (#7278) and Transition.java.template/Send.java.template, which already open
// their try before the loads.
try {
#foreach($load in $relationLoads)
${load.targetEntity}Entity ${load.local} = entity.${load.fkProperty} == null ? null : new ${load.targetEntity}Repository().findById(entity.${load.fkProperty});
${load.targetEntity}Entity ${load.local} = entity.${load.fkProperty} == null ? null : new ${load.targetEntity}Repository().findById(entity.${load.fkProperty});
#end
#if($usesRecordUrl == "true")
// {recordUrl} - the deep link to the record this message is about. The ROUTE is composed HERE,
// in the layer that knows the generated application's URL layout; the intent layer contributes
// only the entity and its key, which is what keeps it path-agnostic.
String recordUrl = Configurations.get("DIRIGIBLE_APP_BASE_URL", "")
+ "/services/web/${projectName}/gen/${genFolderName}/index.html#/${recordUrlEntity}/" + entity.${recordUrlKeyProperty}
+ "/edit";
// {recordUrl} - the deep link to the record this message is about. The ROUTE is composed HERE,
// in the layer that knows the generated application's URL layout; the intent layer contributes
// only the entity and its key, which is what keeps it path-agnostic.
String recordUrl = Configurations.get("DIRIGIBLE_APP_BASE_URL", "")
+ "/services/web/${projectName}/gen/${genFolderName}/index.html#/${recordUrlEntity}/" + entity.${recordUrlKeyProperty}
+ "/edit";
#end
#if($usesInboxUrl == "true")
// {inboxUrl} - the deep link to the recipient's process Inbox.
String inboxUrl = Configurations.get("DIRIGIBLE_APP_BASE_URL", "")
+ "/services/web/${projectName}/gen/${genFolderName}/index.html#/inbox";
// {inboxUrl} - the deep link to the recipient's process Inbox.
String inboxUrl = Configurations.get("DIRIGIBLE_APP_BASE_URL", "")
+ "/services/web/${projectName}/gen/${genFolderName}/index.html#/inbox";
#end
String to = ${toExpression};
if (to == null || to.isBlank()) {
return;
}
String subject = ${subjectExpression};
String body = ${bodyExpression};
Map part = new HashMap();
part.put("type", "text");
part.put("contentType", "text/plain");
part.put("text", body);
List<Map> parts = new java.util.ArrayList<>();
parts.add(part);
String to = ${toExpression};
if (to == null || to.isBlank()) {
return;
}
String subject = ${subjectExpression};
String body = ${bodyExpression};
Map part = new HashMap();
part.put("type", "text");
part.put("contentType", "text/plain");
part.put("text", body);
List<Map> parts = new java.util.ArrayList<>();
parts.add(part);
#if($attach == "print")
// attach: print - render the record's own print template to PDF server-side (the generated
// feeder assembles the { document, items } payload the template binds) and attach it, so the
// recipient receives the document itself and not just a notice about it.
// attach: print - render the record's own print template to PDF server-side (the generated
// feeder assembles the { document, items } payload the template binds) and attach it, so the
// recipient receives the document itself and not just a notice about it.
#if($attachLanguageFkProperty != "")
${attachLanguageTargetEntity}Entity attachLanguageSource = entity.${attachLanguageFkProperty} == null ? null
: new ${attachLanguageTargetEntity}Repository().findById(entity.${attachLanguageFkProperty});
${attachLanguageTargetEntity}Entity attachLanguageSource = entity.${attachLanguageFkProperty} == null ? null
: new ${attachLanguageTargetEntity}Repository().findById(entity.${attachLanguageFkProperty});
#end
String language = ${attachLanguageExpression};
Map document = new HashMap();
document.put("type", "attachment");
document.put("contentType", "application/pdf");
document.put("fileName", ${attachFileNameExpression});
// Bind the render language to the thread so the feeder's multilingual overlay resolves nomenclature
// values in the SAME language as the template - there is no request here to carry Accept-Language
// (dirigible #6947). Cleared in the finally so the pooled worker thread never leaks it.
byte[] data;
org.eclipse.dirigible.sdk.security.User.setLanguage(language);
try {
data = org.eclipse.dirigible.sdk.print.Print.render("${attachEntity}", language,
new ${attachEntity}PrintFeeder().feed(entity.${attachKeyProperty}));
} finally {
org.eclipse.dirigible.sdk.security.User.clearLanguage();
}
document.put("data", data);
parts.add(document);
String language = ${attachLanguageExpression};
Map document = new HashMap();
document.put("type", "attachment");
document.put("contentType", "application/pdf");
document.put("fileName", ${attachFileNameExpression});
// Bind the render language to the thread so the feeder's multilingual overlay resolves nomenclature
// values in the SAME language as the template - there is no request here to carry Accept-Language
// (dirigible #6947). Cleared in the finally so the pooled worker thread never leaks it.
byte[] data;
org.eclipse.dirigible.sdk.security.User.setLanguage(language);
try {
data = org.eclipse.dirigible.sdk.print.Print.render("${attachEntity}", language,
new ${attachEntity}PrintFeeder().feed(entity.${attachKeyProperty}));
} finally {
org.eclipse.dirigible.sdk.security.User.clearLanguage();
}
document.put("data", data);
parts.add(document);
#elseif($attach == "report")
// attach: { report, bind } - run the declared report scoped to THIS recipient and attach the
// rendered PDF. The bindings ARE the report's parameters, which is what makes the rows this
// recipient's own: a customer statement is a period of rows, not one record's document.
// attach: { report, bind } - run the declared report scoped to THIS recipient and attach the
// rendered PDF. The bindings ARE the report's parameters, which is what makes the rows this
// recipient's own: a customer statement is a period of rows, not one record's document.
#if($attachLanguageFkProperty != "")
${attachLanguageTargetEntity}Entity attachLanguageSource = entity.${attachLanguageFkProperty} == null ? null
: new ${attachLanguageTargetEntity}Repository().findById(entity.${attachLanguageFkProperty});
${attachLanguageTargetEntity}Entity attachLanguageSource = entity.${attachLanguageFkProperty} == null ? null
: new ${attachLanguageTargetEntity}Repository().findById(entity.${attachLanguageFkProperty});
#end
String language = ${attachLanguageExpression};
Map<String, Object> reportFilter = new HashMap<>();
String language = ${attachLanguageExpression};
Map<String, Object> reportFilter = new HashMap<>();
#foreach($binding in $attachReportBindings)
reportFilter.put("${binding.parameter}", reportValue(${binding.expression}));
reportFilter.put("${binding.parameter}", reportValue(${binding.expression}));
#end
Map document = new HashMap();
document.put("type", "attachment");
document.put("contentType", "application/pdf");
document.put("fileName", ${attachFileNameExpression});
// Bind the render language to the thread so the report query's :language overlay resolves the
// translatable dimensions in the SAME language as the template - there is no request here to carry
// Accept-Language (dirigible #6947). Cleared in the finally so the pooled worker thread never leaks it.
byte[] data;
org.eclipse.dirigible.sdk.security.User.setLanguage(language);
try {
Map<String, Object> reportData = new HashMap<>();
// The bound values double as the rendered header, so the PDF states which slice it is - a
// table of rows never does.
reportData.put("document", reportFilter);
reportData.put("items", new gen.${attachReportGenFolder}.data.${attachReportPerspective}.${attachReport}Repository()
.findAll(null, null, reportFilter));
data = org.eclipse.dirigible.sdk.print.Print.render("${attachReport}", language,
org.eclipse.dirigible.sdk.utils.Json.stringify(reportData));
} finally {
org.eclipse.dirigible.sdk.security.User.clearLanguage();
}
document.put("data", data);
parts.add(document);
Map document = new HashMap();
document.put("type", "attachment");
document.put("contentType", "application/pdf");
document.put("fileName", ${attachFileNameExpression});
// Bind the render language to the thread so the report query's :language overlay resolves the
// translatable dimensions in the SAME language as the template - there is no request here to carry
// Accept-Language (dirigible #6947). Cleared in the finally so the pooled worker thread never leaks it.
byte[] data;
org.eclipse.dirigible.sdk.security.User.setLanguage(language);
try {
Map<String, Object> reportData = new HashMap<>();
// The bound values double as the rendered header, so the PDF states which slice it is - a
// table of rows never does.
reportData.put("document", reportFilter);
reportData.put("items", new gen.${attachReportGenFolder}.data.${attachReportPerspective}.${attachReport}Repository()
.findAll(null, null, reportFilter));
data = org.eclipse.dirigible.sdk.print.Print.render("${attachReport}", language,
org.eclipse.dirigible.sdk.utils.Json.stringify(reportData));
} finally {
org.eclipse.dirigible.sdk.security.User.clearLanguage();
}
document.put("data", data);
parts.add(document);
#end
String from = Configurations.get("DIRIGIBLE_MAIL_SENDER", "noreply@dirigible.io");
try {
String from = Configurations.get("DIRIGIBLE_MAIL_SENDER", "noreply@dirigible.io");
Mail.send(from, new String[] {to}, new String[0], new String[0], subject, parts);
#if($notifyOutcomeProperty != "")
stampNotifyOutcome(entity.${notifyOutcomeKeyProperty}, null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2450,6 +2450,74 @@ void a_scheduled_notification_keeps_its_row_loads_and_attachment_render_inside_t
"the tick's summary still reports the totals");
}

@Test
void an_event_notification_keeps_its_relation_loads_and_attachment_render_inside_the_fail_soft_try() {
// Issue #7290 (the single-record twin of #7233/#7278): the per-row try above covers a
// SCHEDULE's notify branch. A notifications: entry (one record, no loop) had the identical gap
// - only Mail.send sat inside the try, so a relation load or a broken .print template
// propagated straight out of onMessage with NO stamp: the broker redelivers forever and the
// record's outcome stays empty, exactly the silent state #7023 introduced the stamp to remove.
String yaml = """
name: billing
entities:
- name: Customer
fields:
- { name: id, type: integer, primaryKey: true, generated: true }
- { name: name, type: string }
- { name: email, type: string }
- { name: locale, type: string, length: 5 }
- name: Invoice
function: Document
fields:
- { name: id, type: integer, primaryKey: true, generated: true }
- { name: dueDate, type: date }
- { name: reminderOutcome, type: string, length: 128 }
relations:
- { name: Customer, kind: manyToOne, to: Customer }
- name: InvoiceItem
function: DocumentItem
fields:
- { name: id, type: integer, primaryKey: true, generated: true }
- { name: amount, type: decimal }
relations:
- { name: Invoice, kind: manyToOne, to: Invoice, composition: true, required: true }
notifications:
- name: invoiceIssued
event: { onCreate: Invoice }
to: Customer.email
subject: "Invoice {id} issued"
body: "Dear {Customer.name}, your invoice is attached."
attach: print
languageFrom: Customer.locale
outcome: reminderOutcome
""";
writeIntent(yaml);
restAssuredExecutor.execute(() -> given().when()
.post(GENERATE_URL)
.then()
.statusCode(200));
generateFromModel("template-application-events-java/template/template.js", "billing.glue");

String notification = codeOf("gen/events/billing/InvoiceIssuedNotification.java");
int method = notification.indexOf("public void onMessage(String message) {");
int tryOpens = notification.indexOf("try {", method);
int load = notification.indexOf("new CustomerRepository().findById(entity.Customer)");
int render = notification.indexOf("Print.render(\"Invoice\",");
int send = notification.indexOf("Mail.send(");
int catches = notification.indexOf("} catch (Exception ex) {");
assertTrue(method > 0 && tryOpens > 0 && load > 0 && render > 0 && send > 0 && catches > 0, "got: " + notification);
// The ordering is the whole fix: the try must open before the relation load.
assertTrue(tryOpens < load, "the recipient's one-hop relation load runs inside the fail-soft try");
assertTrue(load < render, "the attachment render follows the load, in the same try");
assertTrue(render < send && send < catches, "render, then send, then the catch - one try encloses both");
// Both outcome stamps survive: `sent` inside the try, `failed: <reason>` from the catch - so a
// relation load or render failure is stamped on the record exactly as a bounced mailbox is.
int stampSent = notification.indexOf("stampNotifyOutcome(entity.Id, null);");
int stampFailed = notification.indexOf("stampNotifyOutcome(entity.Id, ex);");
assertTrue(send < stampSent && stampSent < catches && catches < stampFailed,
"both delivery outcomes are still stamped, got: " + notification);
}

@Test
void a_recurring_template_schedule_keys_on_the_period_of_the_run() {
// Issue #7106: the recurring-template family had no key to declare. A monthly bill generated
Expand Down
Loading