feat(intent): a declarative fileName pattern names both server-side renders (#6899) - #6904
Merged
Conversation
…enders (#6899) The two PDF renders named their files with hardcoded, mutually inconsistent expressions. The snapshot mint used "<Entity> <id> v<n>.pdf" - the numeric PRIMARY KEY, even though the mint runs after the `number:` stamp, so the document number was available - while `attach: print` already used the number. One document reached the archive and the customer's inbox under two different names, and neither was configurable. A real archive wants a self-describing name: ORD0000042_20260822_MyCompany_AcmeLtd.pdf. A `fileName:` pattern is now authorable on the `function: Snapshot` child and inside a `notify` block (with `attach:` only - a plain-text message has no file to name): literal text plus {token} interpolations, no expression language. fileName: "{number}_{date:yyyyMMdd}_{company.shortName|company.name}" - {field} / one-hop {relation.field} - the SAME path vocabulary and authored names a notify subject resolves, against the same record. - {field:pattern} - a date/timestamp through a DateTimeFormatter pattern. - {A|B} - alternative operands, first non-blank wins (an optional short name beside the legal name is filled for some records and not others). - {Version} - a snapshot only; a pattern without it gets _v<n> appended, so two versions of a copy never share a name. New FileNameSupport translates a pattern once into a Java expression plus the one-hop relation loads it reads; the new SDK helper sdk.print.FileNames sanitizes every interpolated VALUE at run time (trim, whitespace -> one `_`, path/control characters stripped, non-ASCII deliberately KEPT - a local-language document legitimately carries a non-Latin name, and keeping names Latin is an application data convention, not the platform's guess). The literal separators between tokens are the author's and are emitted verbatim. Three things worth knowing: - The pattern's relation loads are MERGED into the notify block's own, deduped by local: both name the local after the relation, so declaring it twice would not compile - and a pattern may read a relation the message text never mentions. - `attach: recordPrint` renders the anchor once, before the per-row loop, where those locals do not exist, so a relation hop is refused there - the same one-field rule the `record.` scope already has. - The Snapshot delegate now always loads its master (the name is a property of the DOCUMENT, not of the copy) and returns early when it is gone. Absent a pattern the snapshot default CHANGES from the primary-key form to the same number-or-id expression the mail uses, plus the version - a deliberate, called-out behaviour change that finally makes the two agree. Already-stored copies are untouched. An unknown field/relation, a multi-hop path, an unbalanced or nested brace, a format on a non-date field, an invalid DateTimeFormatter pattern, a pattern that interpolates nothing and {Version} on a mailed copy are all validation errors: a token that silently rendered empty would produce archive names nobody can tell apart, which is the failure this replaces. Covered by FileNamesTest (the run-time sanitizer), FileNameIntentTest (every rejection), GlueFileNameTest (the emitted expressions, the merged loads, both defaults agreeing), IntentEngineIT (OrderCopy's date-modifier + relation-hop pattern in the generated mint) and IntentEmissionCoverageIT (SendBill's {A|B} pattern, whose client-Java compile also proves the emitted expression compiles). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| */ | ||
| private static PrintAttachment languageFromAttachment(String path, EntityIntent entity, Map<String, EntityIntent> byName, | ||
| Map<String, String> compositionParents, NotificationSupport.CrossModelLookup crossModel, boolean anchorScoped, String local) { | ||
| Map<String, String> compositionParents, NotificationSupport.CrossModelLookup crossModel, boolean anchorScoped, String local, |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6899.
The problem
The two server-side PDF renders named their files with hardcoded, mutually inconsistent expressions:
"<Entity> <id> v<n>.pdf", i.e. the numeric primary key, even though the mint runs after thenumber:stamp, so the document number was right there;attach: print) — the firstnumber:field with an id fallback.So one document reached the archive and the customer's inbox under two different names, and neither was configurable. A real archive wants a self-describing name:
ORD0000042_20260822_MyCompany_AcmeLtd.pdf.The change
A
fileName:pattern on thefunction: Snapshotchild and inside anotifyblock (withattach:only — a plain-text message has no file to name). Literal text plus{token}interpolations; no expression language.{field}{relation.field}{field:pattern}date/timestampthrough aDateTimeFormatterpattern{A|B}{Version}Tokens use the authored field/relation names, exactly as a notify
subjectdoes (the issue's PascalCase sketch is illustrative; the contract it names — "the same as notify interpolation" — is what shipped, so an author does not have to learn a second casing rule).New
FileNameSupporttranslates a pattern once into a Java expression plus the one-hop relation loads it reads. The new SDK helpersdk.print.FileNamessanitizes every interpolated value at run time: trim, whitespace → one_, path/control characters (/ \ : * ? " < > |) stripped, repeats collapsed. Non-ASCII is deliberately kept — a local-language document legitimately carries a non-Latin name, and keeping names Latin is an application data convention, not the platform's guess. The literal separators between tokens are the author's and are emitted verbatim.Three things worth a reviewer's attention:
SendBillin the IT does exactly that: a literal recipient,{Person.name}in the file name).attach: recordPrintrefuses a relation hop. That document is rendered once, inrenderDocument(source)before the per-row loop, where the block's relation locals do not exist. Same one-field rule therecord.scope already has, and rejected at parse rather than emitted as a read of an undeclared local.languageFrom.Called-out behaviour change: absent a pattern, the snapshot default changes from the primary-key form to the same number-or-id expression the mail uses, plus the version. That is the point — the two finally agree. Already-stored copies are untouched.
Rejected at parse/generation, never rendered empty: an unknown field/relation, a multi-hop path, unbalanced or nested braces, a format on a non-date field, a format
DateTimeFormatterwon't accept, a pattern that interpolates nothing, and{Version}on a mailed copy. A silently-dropped token would produce archive names nobody can tell apart, which is the failure being replaced.Tests
FileNamesTest(12) — the run-time sanitizer: whitespace, path/control characters, a stripped character not leaving a separator run, non-ASCII kept, both date shapes, a non-date value degrading, andfirst(...).FileNameIntentTest(16) — every rejection above plus the accepted full pattern on both sides.GlueFileNameTest(9) — the emitted expressions, the version-suffix rules, the merged/deduped loads, and both defaults asserted as exact strings, agreeing with each other.IntentEngineIT(58 green) —OrderCopygainsfileName: "{orderDate:yyyyMMdd}_{customer.name}"; asserts the load off the document, the SDK date call, the hop read, the version suffix, and that the old primary-key name is gone.IntentEmissionCoverageIT(green) —SendBillgainsfileName: "BILL_{note|Person.name}". That project's client-Java compile is the compile proof for the emitted expression (an undeclared local fails the whole batch and takes every REST assertion with it).Verified:
formatter:validateclean,mvn -P releasejavadoc clean onapi-modules-java+engine-intent, 852 + 28 + 77 unit tests green, both ITs green headless.Docs
CLAUDE.mdparagraph, and the intent assistant guide (a new "naming a rendered document" section + the snapshot/notify knob mentions).🤖 Generated with Claude Code