Skip to content

Groovy DSL Enhancement - Part 2 - #2038

Merged
ashishvijaywargiya merged 14 commits into
apache:trunkfrom
ashishvijaywargiya:groovy-dsl-enhancement-applied2
Sep 16, 2026
Merged

ashishvijaywargiya merged 14 commits into
apache:trunkfrom
ashishvijaywargiya:groovy-dsl-enhancement-applied2

Conversation

@ashishvijaywargiya

Copy link
Copy Markdown
Contributor
  1. Applied the require()/fail() control-flow sugar across accounting, content, manufacturing, marketing, order, party, product, workeffort, and framework/common, converting return error(...) guard clauses to throw-based validation so failures propagate correctly out of nested closures and helper methods instead of silently falling through.

  2. Applied the runAsyncService()/runAsync DSL sugar to notification-email sends in product, framework/common, order, and party, converting manual dispatcher.runAsync() calls and select synchronous run service: calls to the equivalent one-line async form.

  3. Found and fixed a real pre-existing bug uncovered during the rollout: deleteWorkEffort and duplicateWorkEffort had permission checks that were inverted and incomplete, allowing/denying access incorrectly.

  4. Full branch review found and fixed a regression where fail()/require() was left uncaught in 5 files invoked as widget or mini-lang scripts rather than declared services (EditCategory, EditProductContent, EditProductConfigItemContent, ImageUpload, SetDefaultImage), reverting those 10 guard sites back to the request-aware return error(...) pattern.

  5. Full branch review found and fixed a regression where 5 notification-email call sites had been switched to runAsync service:, silently dropping error propagation to the caller; reverted all 5 back to synchronous run service:.

  6. Rewrote two compound double-negation guards, PaymentServices.groovy's createPayment()/updatePayment() permission check and LeadServices.groovy's createLead() name/group check, into fully positive require() conditions via De Morgan's laws, verified logically equivalent to the originals by exhaustive truth-table check.

Converts 49 `if (cond) { return error(...) }` guard-clause sites across 8
files to require()/fail(), out of 59 `return error(...)` sites originally
scanned. Found-but-left-untouched, per file:

- AcctgAdminServices.createFuturePeriod(): 2 sites inside nested .each {}
  closures -- return error() there only aborts the closure, not the service;
  swapping to a throw would change that reachability without the call-site
  audit this task doesn't do.
- FixedAssetServices.calculateFixedAssetDepreciation(), PaymentServices.
  updatePaymentContent(), RateServices.updateRateAmount()/expireRateAmount():
  4 sites shaped as "if (cond) { ...real work...; return success/store }
  return error(...)" (or the equivalent if/else with substantive code in the
  non-error branch) -- not the guard-clause shape this pass converts;
  flattening into a leading require() would mean hoisting a branch's body out
  and restructuring control flow, not a mechanical swap.
- PaymentServices.createPaymentAndApplicationForParty(),
  cancelCheckRunPayments(): same non-error-branch-has-real-work shape, inside
  a for loop.
- PaymentServices.createPaymentAndPaymentGroupForInvoices(): FinAccount
  status checks are an if/else-if chain where every branch is a bare
  return error(). Left as an if/else-if rather than split into sequential
  require() calls -- that flattening is behaviorally correct here (the two
  statusId values are mutually exclusive) but reads as less clear than the
  original branching, so if/else-if chains are out of scope for this pass
  regardless of provable mutual exclusivity.

Covered by the accounting component test suite (105/105 passing) plus
checkstyleMain/codenarcMain (clean after fixing 3 LineLength violations the
conversion introduced).
Converts 7 `if (cond) { return error(...) }` guard-clause sites across 4
files to require()/fail(), out of 16 `return error(...)` sites originally
scanned. Found-but-left-untouched, per file:

- ContentServicesScript.assocContent(): the error is an unconditional
  fallback return at the end of the method, following an if-block that
  returns early on success (`if (cond) { ...; return result }
  return error(...)`), not a `return error(...)` inside its own guard -- not
  the guard-clause shape this pass converts.
- DataServicesScript.prepareServiceContext(GenericValue, String): a
  parameterized helper called as a plain Groovy method, not a top-level
  service entry point invoked by the engine.
- ContentPermissionServices.viewContentPermission()/
  createContentPermission()/updateContentPermission()/
  checkContentOperationSecurity()/checkRoleSecurity()/
  findAllContentPurposes()/findAllParentContent(): all parameterized helper
  methods (5-2 args each), called as plain Groovy method calls from other
  methods in this script rather than via `run service:` -- not top-level
  service entries, so out of scope per the Global Constraints. (checkOwnership()
  in the same file, by contrast, IS registered as its own service and
  invoked via `run service: 'checkOwnership'` elsewhere in this file, so its
  guard clause was converted.)

Covered by the content component test suite (14/14 passing) plus
checkstyleMain/codenarcMain (clean).
Converts 11 `if (cond) { return error(...) }` guard-clause sites across 4
files to require()/fail(), out of 14 `return error(...)` sites originally
scanned. Found-but-left-untouched:

- ProductionRunServicesScript.handleManualIssuanceOverride(Map parameters):
  3 sites -- a parameterized helper called as a plain Groovy method (not via
  `run service:`), not a top-level service entry point.

Two shapes worth calling out specifically since they're new to this pass:
- ProductionRunResServices.reserveWorkEffortInventory()/
  reserveWorkEffortInventoryItem(): the guard has extra statements (a
  GenericValue lookup, logError()) ahead of the return -- per the
  extra-statement rule, the enclosing `if` was left intact and only
  `return error(errMsg)` was swapped to `fail(errMsg)`, same as
  ProductionRunServicesScript.issueProductionRunTask()/
  issueProductionRunTaskComponent()'s two similarly-shaped sites.
- RoutingServicesScript.removeCalendar()/removeCalendarWeek(): two
  independent sequential `if (cond) { return error(...) }` guards each
  (not an if/else-if chain), each converted to its own require() in place --
  distinct from the if/else-if flattening the accounting task's review
  flagged and the Global Constraints now disallow.

Covered by the manufacturing (48/48) and marketing (1/1) component test
suites plus checkstyleMain/codenarcMain (clean).
Converts 47 `if (cond) { return error(...) }` guard-clause sites across 8
files to require()/fail(), out of 67 `return error(...)` sites originally
scanned. Found-but-left-untouched, per file:

- CheckoutMapProcs.shipToAddress(Map)/billToAddress(Map): 10 sites -- both
  static methods take an explicit Map parameter and are called as plain
  Groovy method calls from OrderServicesScript.groovy (a different file),
  not via `run service:`; not top-level service entries. Their guard shape
  is also `if (cond) { realWork } else { return error(...) }` -- the
  real-work-in-the-non-error-branch shape this pass has consistently left
  alone since the accounting task.
- OrderRequirementServicesScript.checkCreateStockRequirement(String
  methodId): a parameterized helper whose return value is propagated
  directly (implicit return, no ServiceUtil.isError check) by its two
  top-level callers (checkCreateStockRequirementQoh/Atp) -- proving that
  propagation safe would require auditing every call site, which this pass
  doesn't do.
- OrderReturnServicesScript.informError(String label): explicitly `private`.
- OrderServicesScript: none left untouched -- all 7 sites in this file
  qualified.
- QuoteServicesScript: none left untouched -- all 31 sites in this file
  qualified (every method here is a top-level, no-arg service entry).
- RequirementServicesScript.deleteRequirementAndRelated()/
  autoAssignRequirementToSupplier(): both are the same "if (cond) {
  ...real work...; return success() } return error(...)" shape as the
  CheckoutMapProcs sites above.
- ShoppingListServicesScript.calculateShoppingListDeepTotalPrice(): 2 of its
  4 sites are inside `.each {}` closures (lines iterating ShoppingListItem
  and child ShoppingList records) -- return error() there only aborts the
  closure iteration, not the service; out of scope per the Global
  Constraints.

Covered by the order component test suite (99/99 passing) plus
checkstyleMain/codenarcMain (clean).
Converts 16 `if (cond) { return error(...) }` guard-clause sites across 5
files to require()/fail(), out of 23 `return error(...)` sites originally
scanned. Found-but-left-untouched, per file:

- ContactMechServicesScript.createEmailAddress()/updateEmailAddress(): the
  isEmail-format check in each is `if (UtilValidate.isEmail(...)) { ...real
  work...; return serviceReturn } return error(...)` -- the
  real-work-in-the-non-error-branch tail shape this pass has consistently
  left alone since the accounting task.
- ContactMechServicesScript.createFtpAddress(): `if (contactMechId) {
  ...real work... } else { return error(...) }` -- same if/else shape,
  error in the else branch.
- PartyPermissionServices.accAndDecPartyInvitationPermissionCheck()/
  cancelPartyInvitationPermissionCheck(): one site each is `if (cond) {
  ...sets hasPermission...} else { return error(...) }` -- same if/else
  shape; left untouched for consistency even though the if-branch itself
  doesn't return (unlike the other skipped shapes above, this one could
  arguably be flattened safely, but doing so would introduce a new,
  undocumented exception to the if/else policy mid-pass).

Two extra-statement guards worth noting (kept their enclosing `if`, only
`return error()` swapped to `fail()`, same as prior tasks):
- CommunicationEventServicesScript.setCommunicationEventStatus()/
  setCommunicationEventRoleStatus(): logError() ahead of the error.
- PartyPermissionServices.cancelPartyInvitationPermissionCheck(): message
  construction + logError() ahead of the error.

Covered by the party component test suite (96/96 passing) plus
checkstyleMain/codenarcMain (clean).
Converts 24 `return error(...)` guard-clause sites across 8 files to
require()/fail() -- all 24 sites originally scanned qualified, so nothing was
found-but-left-untouched in this part.

Per file:
- EditCategory.groovy, EditProductConfigItemContent.groovy, ImageUpload.groovy,
  EditProductContent.groovy (2 sites each), SetDefaultImage.groovy (2 sites),
  CostServices.groovy (1 site): all are path-traversal/upload-failure guards of
  the shape `if (cond) { logError(...); return error(...) }` or with an extra
  try/catch + message-building statement ahead of the error -- kept their
  enclosing `if`, only `return error()` swapped to `fail()`, same as prior
  tasks (the extra statements are preserved verbatim ahead of the call).
- ProductFeatureServicesScript.groovy (3 sites): plain
  `if (!cond) { return error(...) }` permission/format guards in
  createProductFeatureType()/createProductFeatureApplAttr(), converted to
  require().
- CategoryServicesScript.groovy (10 sites across 7 service methods --
  createProductCategory(), duplicateCategoryEntities(),
  duplicateProductCategory(), createProductCategoryAttribute(),
  updateProductCategoryAttribute(), deleteProductCategoryAttribute(),
  productCategoryGenericPermission()): all bare guard clauses (permission
  checks, existence checks, a checkValidDatabaseId() result check), no
  accompanying else, converted to require().

Verified every converted method is a service-engine entry point (checked
against servicedef/*.xml invoke= attributes), not an internal helper --
methods like checkCategoryRelatedPermission() that are only called
internally were left alone (they use `result = error(...)` + `return result`,
not `return error(...)`, so were out of scope anyway).

Covered by the product component test suite (76/76 passing) plus
checkstyleMain/codenarcMain (clean, no line-length or other violations
introduced).
Converts 72 `return error(...)` guard-clause sites across 8 files to
require()/fail(). 9 sites were found but left untouched, called out below with
the reason.

Per file:
- ImageManagementServicesScript.groovy (19 sites): bare guards in
  uploadProductImages()/removeProductContentAndImageFile()/
  removeProductContentForImageManagement()/createImageContentApproval()/
  resizeImages(), including guards nested inside for-loops (not closures, so
  in scope) and one guard-with-extra-statements (kept its if, swapped
  return error() to fail()). Left untouched: removeImageBySize()'s
  return error() inside a contentAssocs.each {} closure -- scoped to the
  closure only.
- InventoryServicesScript.groovy (7 sites): bare guards in
  facilityGenericPermission()/checkProductFacilityRelatedPermission()/
  createInventoryItem(). Left untouched: checkFacilityRelatedPermission() --
  an internal helper not registered as its own service, only called via plain
  method calls; createInventoryItem()'s lot-id if/else-if chain -- excluded
  per the if/else-if rule.
- PriceServicesScript.groovy (7 sites): permission guards in
  createProductPrice()/updateProductPrice()/deleteProductPrice()/
  createProductPriceCond()/updateProductPriceCond(). Left untouched:
  inlineHandlePriceWithTaxIncluded()'s tax-percentage guard -- an internal
  helper not registered as its own service.
- ProductServicesScript.groovy (7 sites): guards in createProduct()/
  duplicateProduct()/productGenericPermission()/productPriceGenericPermission(),
  plus a guard-with-extra-statements in setProductReviewStatus(). Left
  untouched: checkProductRelatedPermission(String, String)'s permission guard
  -- an internal helper (only checkProductRelatedPermissionService() wraps it
  in servicedef).
- PromoServicesScript.groovy (1 site): updateProductPromoCond()'s unconditional
  trailing return error() (already the terminal statement, no guarding if to
  negate) swapped directly to fail().
- ProductStoreServices.groovy (13 sites): guards in createProductStore()/
  updateProductStore()/reserveStoreInventory()/isStoreInventoryAvailable(),
  plus a guard-with-extra-statements in productStoreGenericPermission(). One
  of the 13 was return  error(...) with a double space, missed by a literal
  return error( grep but caught by a follow-up scan and converted like its
  neighbors. Left untouched: checkProductStoreRelatedPermission(Map)'s
  permission guard -- an internal helper not registered as its own service.
- ShipmentReceiptServices.groovy (2 sites): both in receiveInventoryProduct(),
  one a bare compound guard, one a guard-with-extra-statements.
- ShipmentServices.groovy (16 sites): bare and guard-with-extra-statements
  sites across updateShipment()/deleteShipmentPackage()/quickShipEntireOrder()/
  quickDropShipOrder()/createOrderShipmentPlan()/quickShipOrderByItem()/
  addOrderShipmentToShipment(). Left untouched: createShipmentForReturn()'s
  if/else-if/else chain -- excluded per the if/else-if rule;
  getOrderItemShipGroupLists()'s guard -- an internal helper not registered as
  its own service.

Every converted (or containing) method was verified against servicedef/*.xml
invoke= attributes as a direct service-engine entry point; every excluded
helper was excluded specifically because it is not registered there under its
own name.

Covered by the product component test suite (76/76 passing) plus
checkstyleMain/codenarcMain (clean, no violations introduced).
…/common

Converts 27 `return error(...)` guard-clause sites across 5 files to
require()/fail(). 2 sites were found but left untouched, called out below with
the reason.

Per file:
- WorkEffortServicesScript.groovy (18 sites): bare guards in
  checkAndCreateWorkEffort()/checkAndUpdateWorkEffort()/createWorkEffort()/
  updateWorkEffort()/deleteWorkEffort()/copyWorkEffort()/duplicateWorkEffort()/
  assocAcceptedCustRequestToWorkEffort()/assignPartyToWorkEffort()/
  createWorkEffortContactMech()/updateWorkEffortContactMech(), including guards
  nested inside regular if blocks (not closures, so in scope), plus a
  guard-with-extra-statements in createWorkEffortAndPartyAssign() (kept its if,
  swapped return error() to fail()).
- CommonServicesScript.groovy (3 sites): bare guards in convertUom()/
  convertUomCustom()/getVisualThemeResources().
- PortalPageServices.groovy (2 sites): bare guards in getPortletAttributes()/
  updatePortletSeqDragDrop(). Left untouched: checkOwnerShip()'s two
  return error() sites -- a private helper called from several top-level
  methods; proving every caller safely propagates its result requires auditing
  every call site, which this pass does not do.
- EmailServicesScript.groovy (1 site): a guard-with-extra-statements in
  sendMailFromTemplateSetting() (kept its if/logError, swapped return error()
  to fail()).
- CommonPermissionServices.groovy (3 sites): bare guards in
  genericBasePermissionCheck()/getAllCrudPermissions().

Every converted (or containing) method was verified against servicedef/*.xml
invoke= attributes as a direct service-engine entry point; checkOwnerShip()
was excluded specifically because it is a private helper, not a registered
service.

Covered by the workeffort (31/31) and common (5/5) component test suites,
plus checkstyleMain/codenarcMain (clean, no violations introduced).
…e inverted and incomplete

Both methods errored when the user HAD the WORKEFFORTMGR_DELETE permission
and lacked a personal WorkEffortPartyAssignment for the target work effort,
the opposite of the intended check (comment: must be associated OR have the
corresponding permission). This let a user with neither an assignment nor
the permission through, while blocking one who had the permission.

Also switched from hasPermission('WORKEFFORTMGR_DELETE', ...) to
hasEntityPermission('WORKEFFORTMGR', '_DELETE', ...) so a WORKEFFORTMGR_ADMIN
grant satisfies the check too, matching the existing WorkEffortDeletePermissionError
label text ("... WORKEFFORTMGR_DELETE or WORKEFFORTMGR_ADMIN permission") and
the SUPER security group's actual grant (WORKEFFORTMGR_ADMIN, not _DELETE) in
applications/datamodel/data/seed/WorkEffortSeedData.xml.

Found while working on the require()/fail() DSL rollout (unrelated task);
fixed as its own commit since it is a real logic change, not a mechanical
conversion. workeffort test suite: 31/31 passing (was 30/31 with the plain
negation fix alone, since the system test user only holds WORKEFFORTMGR_ADMIN
via SUPER). checkstyleMain/codenarcMain clean.
Converts 5 sync sends-a-notification-email call sites, across 3 files, to
runAsync service:/with:, out of the ~10 Email/Notification-named service
calls audited across the 5 files listed for this task. Every call site was
read in its full calling method, not matched by name alone.

Converted (both Row 4b conditions hold -- no result field read afterward,
and nothing about correctness depends on completion before the method
continues):
- OrderDeliveryServices.sendOrderDeliveryScheduleNotification(): the
  sendGenericNotificationEmail call is the last meaningful action before
  return success(); its result was never assigned to a variable.
- CommunicationEventServicesScript.sendContactUsEmailToCompany(): the
  sendMailFromScreen call is the last statement in the method (inside the
  bodyScreenLocation-present branch), unassigned result.
- PartyServicesScript.sendCreatePartyEmailNotification(),
  sendUpdatePersonalInfoEmailNotification(),
  sendAccountActivatedEmailNotification(): all three are dedicated
  "send a notification email" methods whose only real work, after building
  the email params, is the sendMailFromScreen call; each is the last
  statement in the method and its result is never assigned or checked.

sendMailFromScreen's own service name doesn't literally contain "Email"/
"Notif" (unlike sendGenericNotificationEmail or findPartyFromEmailAddress),
but it's the same fire-and-forget email-sending service used elsewhere in
this plan's precedent (accounting/InvoiceServicesScript.sendInvoicePerEmail
already calls it via runAsync), and four of these five sites are calling
methods explicitly named/documented as "...Notification" sends -- judged
in scope on that basis rather than a literal substring match.

Left synchronous (found, audited, and excluded -- not silently skipped):
- CheckoutServices.createUpdateCustomerAndShippingAddress():
  createUpdatePartyEmailAddress's result (serviceResultCUPEM) is read
  immediately afterward for parameters.emailContactMechId and
  result.emailContactMechId, which also drives a subsequent
  shoppingCart.addContactMechId() call -- condition (a) fails.
- CommunicationEventServicesScript.createCommunicationEvent() (x1),
  .updateCommunicationEvent() (x2), .sendContactUsEmailToCompany() (x1):
  all four getPartyEmail calls have their result's contactMechId/
  emailAddress field read and used in the same method immediately after
  the call -- condition (a) fails.
- CommunicationEventServicesScript.sendEmailDated(): the raw
  dispatcher.runSync('sendCommEventAsEmail', serviceContext, 7200, true)
  passes an explicit transactionTimeout and requireNewTransaction=true --
  an inline comment states this bypasses the run service:/with: sugar
  specifically for "the new transaction need". runAsyncService(name,
  ctxMap) has no equivalent parameters, so converting would silently drop
  that transactional isolation between each loop iteration's email send,
  which is more than a sync->async change -- left as-is.
- PartyPermissionServices.accAndDecPartyInvitationPermissionCheck() and
  .cancelPartyInvitationPermissionCheck(): both findPartyFromEmailAddress
  calls have their result's partyId read immediately afterward to decide
  hasPermission -- condition (a) fails.
- PartyServicesScript.quickCreateCustomer(): createPartyEmailAddress's
  result is immediately checked with ServiceUtil.isError() and returned
  on failure -- condition (b) fails.

Covered by the order and party component test suites (99 + 96 tests, all
passing) plus checkstyleMain/codenarcMain.
Converts 1 sync sends-a-notification-email call site, out of the 4
Email/Notification-named service calls audited across the 3 files listed for
this task. Every call site was read in its full calling method, not matched
by name alone.

Converted (both Row 4b conditions hold -- no result field read afterward,
and nothing about correctness depends on completion before the method
continues):
- ShipmentServices.sendShipmentScheduledNotification(): the
  sendGenericNotificationEmail call is the last meaningful action before
  return success() -- the else branch of its guarding if/else only logs, and
  execution falls through to return success() either way; its result was
  never assigned to a variable.

Left synchronous (found, audited, and excluded -- not silently skipped):
- ProductContentServicesScript.createEmailContentForProduct(): the
  createEmailContent call's result (serviceResult) has its contentId field
  read immediately afterward and assigned into createProductContent.contentId,
  which is then used by the following createProductContent call -- condition
  (a) fails. (This service creates a Content record of an email-template
  type; it doesn't send anything, but its name literally contains "Email" so
  it was audited on that basis.)
- EmailServicesScript.sendMailFromTemplateSetting(): both calls in this
  method fail the test --
  - getPartyEmail's result has its emailAddress field read immediately
    afterward into parameters.sendTo, which is then checked and used to
    decide whether to proceed -- condition (a) fails.
  - sendMailFromScreen's result has messageWrapper, body, and
    communicationEventId fields read afterward into the method's own result
    map, and is also checked with ServiceUtil.isSuccess() with an early
    return on failure -- both conditions (a) and (b) fail. This is the
    self-referential case flagged going into this task (one email/notification
    service calling another internally); each call was read on its own
    merits, not assumed in-scope because the file is about email.

Covered by the product and common component test suites (76 + 5 tests, all
passing) plus checkstyleMain/codenarcMain.
…ad-validation scripts

EditCategory.groovy, EditProductConfigItemContent.groovy, EditProductContent.groovy,
ImageUpload.groovy, and SetDefaultImage.groovy are invoked as widget <script> actions or
mini-lang <script location=.../> events, not as declared services. Unlike GroovyEngine.runSync,
those invocation paths (AbstractModelAction.ScriptAction, CallScript.exec via
ScriptUtil.executeScript) do not catch ServiceErrorException, so a routine upload/path-traversal
validation failure was surfacing as an uncaught exception instead of the expected inline error
message. Reverted these 10 guard-clause sites back to return error(...).
sendGenericNotificationEmail (OrderDeliveryServices, ShipmentServices) and sendMailFromScreen
(CommunicationEventServicesScript, PartyServicesScript x3) were switched from run service: to
runAsync service:, so a mail-config or template failure no longer propagated back to the calling
service -- callers now returned success even though the notification email silently failed to
send. Reverted these 5 sites back to run service:.
…s positive require() checks

PaymentServices.groovy's createPayment()/updatePayment() permission check and LeadServices.groovy's
createLead() name/group guard each had a nested double-negation (require(!(!a && (!b && c && d)), ...))
that reproduced the exact expression shape already responsible for one real inverted-permission-check
bug on this branch (deleteWorkEffort/duplicateWorkEffort, commit 515fa7e). Rewrote both via De
Morgan's laws into a direct positive require() with zero negations, verified logically equivalent to
the original by exhaustive truth-table check across all boolean inputs. No behavior change.
@ashishvijaywargiya
ashishvijaywargiya merged commit 56601b3 into apache:trunk Sep 16, 2026
7 checks passed
@ashishvijaywargiya
ashishvijaywargiya deleted the groovy-dsl-enhancement-applied2 branch September 17, 2026 11:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant