Skip to main content

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.

MARLO’s web tier is split between two independent routing layers that never overlap. Struts 2 owns all form-driven user interactions — the screens where researchers enter data, upload files, and submit section saves. Spring MVC owns the REST API consumed by external systems such as CLARISA, Power BI ingestion pipelines, and the CGIAR AI services. This page covers the Struts 2 side in full and explains where the boundary between the two layers is enforced.

Two routing layers

LayerURL patternDeclared inPurpose
Struts 2*.do, *.json (legacy)struts.xml + struts-*.xml filesForm submissions, page renders, file downloads
Spring MVC/api/*MarloRestApiConfig.javaREST API for external integrations
The boundary is enforced by a single constant in marlo-web/src/main/resources/struts.xml:
<constant name="struts.action.excludePattern" value="/api/*" />
Any path under /api/ is invisible to the Struts dispatcher. Spring MVC handles it exclusively. The two other routing constants that define what Struts accepts:
<constant name="struts.action.extension" value="do,,json" />
<constant name="struts.patternMatcher" value="regex" />
struts.patternMatcher = regex enables the {crp} wildcard segments in action names. struts.action.extension allows both .do (primary) and .json (legacy, restricted) extensions.

URL structure

The URL pattern for almost every MARLO action is:
/{namespace}/{crp}/{action}.do
For example:
  • /projects/ccafs/description.do — project description form for the CCAFS CRP.
  • /impactPathway/wheat/outcomes.do — impact pathway outcomes for the WHEAT CRP.
  • /admin/biodiv/portfolioManagement.do — portfolio management in the Bioversity admin area.
The {crp} segment is matched by the {crp} regex wildcard in the action name and is passed to the validCrp interceptor, which verifies the slug identifies a real global unit and loads the corresponding GlobalUnit into the session.

Configuration files

The main struts.xml defines global constants, all interceptors by name, and all interceptor stacks. Individual domain packages with their action mappings live in per-domain files that struts.xml includes:
FileDomain package / namespace
struts.xmlGlobal constants, interceptor definitions, interceptor stacks, marlo-default package
struts-projects.xmlprojects / /projects
struts-fundingSources.xmlfundingSources / /fundingSources
struts-impactPathway.xmlimpactPathway / /impactPathway
struts-powb.xmlpowb / /powb
struts-annualReport.xmlannualReport / /annualReport
struts-admin.xmladmin / /admin
struts-publications.xmlpublications / /publications
struts-studies.xmlstudies / /studies
struts-synthesis.xmlsynthesis / /synthesis
struts-summaries.xmlsummaries / /summaries
struts-json.xmlJSON endpoints (existing only)
struts-superadmin.xmlsuperadmin / /superadmin
struts-home.xmlhome / /home
struts-bi.xmlBI dashboard actions
struts-ai.xmlAI screen actions
struts-qa.xmlQuality assurance actions
struts-tip.xmlTechnology Innovation Profile actions
struts-center-*.xmlCGIAR Center-specific modules
All packages extend marlo-default, which inherits from Struts struts-default and provides the FreeMarker result type as the default.

Interceptor stacks

Every action mapping must declare one interceptor stack. The stacks are defined in struts.xml under the marlo-default package. They share a common structure: i18n loading → CRP validation → authentication → session validation → domain-specific edit gate → redirect-message preservation → stage access → input trimming → Struts default stack. The domain-specific edit gate is the key differentiator between stacks. It enforces section-level write permissions before any action code runs.

Available stacks

Stack nameEdit gate interceptorTypical use
editProjectsStackcanEditProjectProject sections (description, partners, deliverables, outcomes, budgets, etc.)
editFSStackeditFundingFunding source form
editPowbStackcanEditPowbSynthesisPOWB synthesis sections
editReportSynthesisStackcanEditReportSynthesisAnnual report synthesis sections
impactPathwayStackcanEditImpactPathwayImpact pathway outcomes and milestones
crpAdminStackaccessibleAdmin + canEditCrpAdminCRP admin pages (portfolios, parameters, users)
editProjectListStack(none — lightweight)Used before per-action interceptors like editDeliverable
editPublicationStackcanEditPublicationPublication forms
editStudyStackeditExpectedStudyExpected study forms
projectListStackSecurityControlExisting JSON endpoints only
homeStackrequireUserHome dashboard and authenticated read-only pages
superAdminStacksuperAdminValidationSystem-level administration pages

Stack definition example

editProjectsStack as declared in struts.xml:
<interceptor-stack name="editProjectsStack">
  <interceptor-ref name="i18nFile" />
  <interceptor-ref name="validCrp" />
  <interceptor-ref name="requireUser" />
  <interceptor-ref name="validSessionCrp" />
  <interceptor-ref name="canEditProject" />
  <interceptor-ref name="keepRedirectMessages" />
  <interceptor-ref name="accessibleStage" />
  <interceptor-ref name="trimInputs" />
  <interceptor-ref name="defaultStack" />
</interceptor-stack>
crpAdminStack adds two edit-gate interceptors instead of one:
<interceptor-stack name="crpAdminStack">
  <interceptor-ref name="i18nFile" />
  <interceptor-ref name="validCrp" />
  <interceptor-ref name="requireUser" />
  <interceptor-ref name="validSessionCrp" />
  <interceptor-ref name="accessibleAdmin" />
  <interceptor-ref name="canEditCrpAdmin" />
  <interceptor-ref name="keepRedirectMessages" />
  <interceptor-ref name="accessibleStage" />
  <interceptor-ref name="trimInputs" />
  <interceptor-ref name="defaultStack" />
</interceptor-stack>

Action mapping anatomy

A minimal action mapping in a struts-<area>.xml file:
<package name="projects" namespace="/projects" extends="marlo-default">

  <action name="{crp}/description"
      class="org.cgiar.ccafs.marlo.action.projects.ProjectDescriptionAction">
    <interceptor-ref name="editProjectsStack" />
    <result name="input">
      /WEB-INF/crp/views/projects/projectDescription.ftl
    </result>
    <result name="success" type="redirectAction">
      <param name="actionName">${crpSession}/description</param>
      <param name="edit">true</param>
      <param name="phaseID">${phaseID}</param>
      <param name="projectID">${projectID}</param>
    </result>
    <result name="cancel" type="redirectAction">
      <param name="actionName">${crpSession}/description</param>
      <param name="edit">true</param>
      <param name="phaseID">${phaseID}</param>
      <param name="cancel">true</param>
      <param name="projectID">${projectID}</param>
    </result>
  </action>

</package>
Key points:
  • name="input" maps to the FreeMarker template rendered when validation fails (the Struts INPUT result string).
  • name="success" redirects back to the same form after a successful save (Post-Redirect-Get pattern).
  • name="cancel" redirects back with cancel=true so the template can show the discard-changes state.
  • The ${phaseID} and ${projectID} OGNL expressions are populated from properties on the action class.

Critical action routing catalog

The table below maps representative routes to their classes, stacks, and view templates:
ModuleRouteAction classStackTemplate
Projects{crp}/descriptionProjectDescriptionActioneditProjectsStackprojects/projectDescription.ftl
Projects{crp}/partnersProjectPartnerActioneditProjectsStackprojects/projectPartners.ftl
Projects{crp}/deliverableDeliverableActioneditProjectListStack + editDeliverable + defaultStackprojects/projectDeliverable.ftl
Funding Sources{crp}/fundingSourceFundingSourceActioneditFSStackfundingSources/fundingSource.ftl
POWB{crp}/financialPlanFinancialPlanActioneditPowbStackpowb/powb_financialPlan.ftl
POWB{crp}/managementGovernanceManagementGovernanceActioneditPowbStackpowb/powb_managementGovernance.ftl
Impact Pathway{crp}/outcomesOutcomesActionimpactPathwayStackimpactPathway/outcomes.ftl
Annual Report{crp}/meliaMeliaActioneditReportSynthesisStackannualReport/annualReport_melia.ftl
Annual Report{crp}/governanceManagementGovernanceActioneditReportSynthesisStackannualReport/annualReport_governance.ftl
Admin{crp}/portfolioManagementPortfolioManagementActioncrpAdminStackadmin/portfoliosManagement.ftl

Adding a new Struts action

1

Choose the right domain package

Determine which struts-<area>.xml file owns the domain your new action belongs to. If the area is Projects, use struts-projects.xml. If no existing domain fits, create a new struts-<area>.xml file and include it from struts.xml.
2

Add the action mapping

Inside the package declaration, add an <action> element with:
  • A name following the {crp}/<actionName> convention.
  • The fully-qualified action class.
  • An <interceptor-ref> pointing to a stack from the table above.
  • An input result pointing to the FreeMarker template.
  • A success result with the Post-Redirect-Get redirect.
  • A cancel result for discard-changes flows.
3

Create the action class

Extend BaseAction, implement Preparable, and follow the save pipeline described in The MARLO save pipeline. Annotate with @Inject on the constructor and inject only the managers the action needs.
4

Create the FreeMarker template

Place the .ftl file under marlo-web/src/main/webapp/WEB-INF/crp/views/<area>/. Include the standard layout fragments and import the customForm macro library from header.ftl. See MARLO FreeMarker templates: layout and composition for the composition model.

Struts JSON endpoints

*.json extension is configured (struts.action.extension = do,,json) and a small number of existing endpoints use it for partial-page data fetching (for example, partner search during project setup). The projectListStack stack, which includes the SecurityControl interceptor, guards these endpoints.
Do not introduce new *.json action mappings for new features. Existing JSON endpoints were built under an older pattern and exist for backward compatibility. New data-fetching needs for UI interactions should be assessed against the existing JSON patterns in the same module before a new path is considered. The Struts JSON surface is not intended to grow.

Build docs developers (and LLMs) love