Every write operation in MARLO — from a project description to a funding source — passes through the same mandatory pipeline before a single byte reaches the database. Understanding this pipeline is the prerequisite for adding or modifying any save path in the codebase.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/CCAFS/MARLO/llms.txt
Use this file to discover all available pages before exploring further.
Why the pipeline is mandatory
MARLO is a multi-tenant, phase-aware system. User context, CRP session, and edit rights vary by request, and the same form can reach different phase chains depending on whether the active cycle is PLANNING or REPORTING. A deviation from the pipeline — such as callingsave() logic without running the interceptor stack, or skipping the if (save) guard on validate() — bypasses access control checks and breaks phase replication integrity. The pipeline is a constitutional rule, not a style preference.
Pipeline overview
Struts maps the request to an action
Every The
.do URL is matched in a struts-*.xml file. The mapping declares the action class and, critically, the interceptor stack. For example, the project description form is declared in marlo-web/src/main/resources/struts-projects.xml:{crp} segment in the action name is a regex wildcard. MARLO uses struts.patternMatcher = regex so every action URL is namespaced to the active CRP or platform.The interceptor stack runs before any action code
The stack declared on the action executes before Each interceptor either passes the request on or short-circuits with a redirect:
prepare(), validate(), or save() is called. editProjectsStack is defined in marlo-web/src/main/resources/struts.xml:validCrp— validates the CRP slug in the URL is a known global unit.requireUser— redirects unauthenticated requests to the login page.validSessionCrp— verifies the user’s session is bound to this CRP.canEditProject(EditProjectInterceptor) — checks that the current user has write access to this specific project in this phase.accessibleStage— ensures the section is open for edits in the current cycle.
Action.prepare() loads domain data
After the interceptor stack completes, Struts calls
prepare() (because BaseAction implements Preparable). This is where the action loads the entity from the database and populates list data for the template. The save flag is already present in the request parameters at this point.Action.validate() guards field-level errors
Struts calls Without the
validate() before executing the result method. Every action in MARLO wraps its validator call in an if (save) guard. From ProjectDescriptionAction at marlo-web/src/main/java/org/cgiar/ccafs/marlo/action/projects/ProjectDescriptionAction.java:1307:if (save) guard, validation would run on every GET request — including history views and cancel operations — and incorrectly surface field errors for read-only renders.The validator.validate(this, project, true) call (where this is the action, project is the bound entity, and true signals a save context) inspects required fields and business rules. When a rule fails, the validator calls this.addFieldError(fieldName, message) or populates the invalid-fields map on BaseAction.Action.save() persists if no errors were raised
Struts only invokes the result method — The action first diffs what the form submitted against what was in the database (in
save(), named execute() in ActionSupport terms but declared as save() in MARLO’s convention — when !hasErrors(). The method starts with a permission check that mirrors the interceptor-level check:prepare()), then delegates each piece to its Manager.Manager save chain persists and replicates forward
Each Manager implementation persists the entity and then replicates it through future phases. This is automatic — the action does not drive replication, it only calls the Manager once. See Phase replication contract in MARLO ManagerImpl for the full replication logic.
Audit listeners record the change automatically
HibernateAuditLogListener and AuditColumnHibernateListener in marlo-data capture inserts and updates on entities that implement IAuditLog. The action does not need to write audit records explicitly; they are committed as part of the same Hibernate session that persists the entity.The validate() guard in detail
Theif (save) pattern is consistent across all critical sections. The table below shows verified examples from the codebase:
| Action class | validate() guard | Validator called |
|---|---|---|
ProjectDescriptionAction | if (save) | validator.validate(this, project, true) |
ProjectPartnerAction | if (save) | projectPartnersValidator.validate(this, project, true) |
DeliverableAction | if (save) | deliverableValidator.validate(this, deliverable, true) |
FundingSourceAction | if (save) | validator.validate(this, fundingSource, true) |
FinancialPlanAction | if (save) | validator.validate(this, powbSynthesis, true) |
OutcomesAction | if (save) | validator.validate(this, outcomes, selectedProgram, true) |
MeliaAction | if (save) | validator.validate(this, reportSynthesis, true) |
save boolean is set by Struts when the form POSTs with a save=true request parameter. The hidden input that triggers it is rendered by the shared save-button partial in the FreeMarker templates.
Debugging save failures
When a save does not persist as expected, work through the pipeline from the outside in:- Confirm the action mapping — check the relevant
struts-*.xmlfor the action name and the stack it declares. - Check interceptor denial — if the page redirected to a 403 or login, the interceptor stack blocked the request before any action code ran.
- Verify
save=trueis in the POST — if the request did not includesave=true,validate()ran with the guard off andhasErrors()may return false incorrectly. - Inspect validator output — after a POST, the template renders
[@s.fielderror]tags for each field that the validator populated. Missing errors often mean the validator was not called. - Check
hasPermission("canEdit")— the save method itself re-checks edit rights; a permission gap here returnsNOT_AUTHORIZEDsilently if the template does not surface it.
Adding a new save path
When you introduce a new Struts.do action that writes data:
- Declare the action in the appropriate
struts-<area>.xmlwith an interceptor stack that matches the access level of the section (editProjectsStack,crpAdminStack,impactPathwayStack, etc.). - Extend
BaseActionand implementPreparable. - Implement
validate()with theif (save) { ... }guard calling a new or existingValidatorclass undermarlo-web/.../validation/. - Implement
save()gated onthis.hasPermission("canEdit"), delegating persistence to the Manager layer. - Ensure the Manager follows the phase replication contract (see Phase replication contract in MARLO ManagerImpl).