ModelDriven, Preparable & CRUD
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Apache Struts 7.4.0 (released 2026-10-02) running on Apache Tomcat 11.0.26 and Java 21; the
facts were checked on 2026-10-08 against the struts-default.xml file inside the 7.4.0 struts2-core jar, the 7.4.0
sources of ModelDrivenInterceptor, ScopedModelDrivenInterceptor, PrepareInterceptor and
PrefixMethodInvocationUtil, the Javadoc, and the Core Developer Guide and Getting Started pages on
https://struts.apache.org/. Struts 1 had ActionForm, a bean that the framework created, filled from the request and
handed to the action. Struts 2 has no forms: request parameters are copied directly into the properties of the action,
or, when the action asks for it, into a separate object called the model. Two small interfaces and two interceptors
handle this. ModelDriven lets an action return the object that should receive the request and be shown by the view, and
Preparable gives an action a hook that runs before the parameters are bound and, with the right stack, a second time
before the view is drawn again after a failed validation. Together they are the Struts 2 way to write a create, read,
update and delete screen, and this page builds one. The running scenario is the Bookshelf, a catalogue of books; every
code listing is the code of one Maven WAR, g9modeldriven.war, that was built, deployed to Tomcat 11.0.26 and requested
with curl, and every output shown is an output of that run. Pages of this section that are written separately (value
stack and OGNL, parameter injection, validation, action chaining and scopes and the others) are linked where they come up;
the interceptor mechanics are on Interceptors and the action
basics on Actions.
The Bookshelf Used on This Page
The WAR is the Struts Bookshelf of the Getting Started page of this section (the Struts BOM
org.apache.struts:struts2-bom:7.4.0 imported in dependencyManagement, struts2-core, the Servlet and Pages APIs with
scope provided, StrutsPrepareAndExecuteFilter mapped to /*, the constant struts.allowlist.packageNames set to
org.example.bookshelf so that OGNL may read the example classes) with extra actions for the experiments. It is deployed
under the context path /g9modeldriven, and every URL uses the default extension .action. Imports and package lines
are left out of the listings; the package is org.example.bookshelf.
| Class | Mapping | Concept |
|---|---|---|
|
(model) |
The request DTO of the Bookshelf: |
|
(model) |
The entity with an |
|
|
The smallest |
|
|
A |
|
|
What the interceptor does when |
|
|
Validation of the model, driven by an XML file |
|
|
Records which |
|
|
The CRUD action: |
|
|
A |
ModelDriven and the Model Driven Interceptor
By default the parameters of a request are copied into the action, and the action is what the view reads. That mixes two
roles in one class: a controller that decides what happens and a bean that holds the form data. ModelDriven separates
them. An action that implements org.apache.struts2.ModelDriven<T> has one extra method, T getModel(), and the
modelDriven interceptor, which belongs to defaultStack, calls it when the action is about to run. The Core Developer
Guide describes the result in three sentences: Struts populates the fields of the returned object with the request
parameters, the object is put on top of the value stack when the action executes, and validation is also performed on
the model instead of the action.
| Piece | Role |
|---|---|
|
The interface (the package was |
|
Registered as |
|
A |
|
Registered as |
Source: Struts Javadoc — ModelDriven
The interceptor is short. It is the 7.4.0 code, reduced to the lines that matter (threadAllowlist is the OGNL allowlist
of the 7.x series, which the interceptor extends with the model’s class so that its properties can be evaluated):
public String intercept(ActionInvocation invocation) throws Exception {
Object action = invocation.getAction();
if (action instanceof ModelDriven<?> modelDriven) {
ValueStack stack = invocation.getStack();
Object model = modelDriven.getModel();
if (model != null) {
stack.push(model);
threadAllowlist.allowClassHierarchy(model.getClass());
}
if (refreshModelBeforeResult) {
invocation.addPreResultListener(new RefreshModelBeforeResult(modelDriven, model));
}
}
return invocation.invoke();
}
Three facts follow from these lines, and each is demonstrated below. The model is pushed only when it is not null, so a
null model is silently ignored. getModel() is called once by the interceptor, and the object it returned then stays on
the stack for the rest of the request, whatever later calls return. The model is pushed above the action, so a property
that exists on both is read, and written, on the model.
The Smallest ModelDriven Action
PlainModelAction keeps a BookForm in a final field and returns it. It also has two properties of its own, note and
flag, whose setters are written like those of any action; only setFlag carries @StrutsParameter.
public class PlainModelAction extends ActionSupport implements ModelDriven<BookForm> {
private final BookForm form = new BookForm();
private String note = "unset";
private String flag = "unset";
@Override
public BookForm getModel() {
return form;
}
public String getNote() { return note; }
public void setNote(String note) { this.note = note; } // no annotation
public String getFlag() { return flag; }
@StrutsParameter
public void setFlag(String flag) { this.flag = flag; }
}
public class BookForm {
private String title;
private String author;
private int year;
public static BookForm from(Book book) {
BookForm form = new BookForm();
form.title = book.getTitle();
form.author = book.getAuthor();
form.year = book.getYear();
return form;
}
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getAuthor() { return author; }
public void setAuthor(String author) { this.author = author; }
public int getYear() { return year; }
public void setYear(int year) { this.year = year; }
}
The mapping needs nothing special: the action inherits defaultStack, which already contains modelDriven.
<action name="plain-model" class="org.example.bookshelf.PlainModelAction">
<result>/WEB-INF/views/model.jsp</result>
</action>
Requesting it with four parameters, two of which belong to the model and two to the action, gave this answer on Tomcat
11.0.26 (the view prints the properties with <s:property value="title"/> and so on):
GET /g9modeldriven/plain-model.action?title=Dune&author=X&year=1965¬e=n1&flag=f1
title=Dune
author=X
year=1965
note=unset
flag=f1
The model received title, author and year although none of its setters is annotated; the action received flag
(annotated) and did not receive note (not annotated, value still unset). That is the rule of Struts 7: the model is the
authorization target of the parameter binding, and its members are exempt from the @StrutsParameter requirement, while
the action’s own setters still need the annotation (the next section).
The 7.x Authorization Rule for Models
Since Struts 7.0.0 the params interceptor requires @StrutsParameter on a setter (or on a getter, with a depth, to
reach into an object) before it copies a request parameter there; the constant struts.parameters.requireAnnotations
defaults to true in 7.4.0. A ModelDriven action is the exception, and the exception is written into the interface’s
contract: returning an object from getModel() designates that object as the request surface. The Core Developer Guide
states the rule in detail:
-
The model, not the action, is the authorization target once
modelDrivenhas pushed it, and the whole model is bindable, nested properties included, whether or not any member carries@StrutsParameter. This has been so since 7.0.0. -
The exemption holds on every input channel (request parameters, and the JSON and REST bodies of the plugins), because they all resolve the authorization target the same way.
-
Since 7.4.0 the exemption covers the model and only the model. A parameter name is resolved against the whole value stack, so it can land on a property of the action; the decision is made in this order: the model declares the property (bound, no annotation needed); the action declares the property (bound only if annotated); neither declares it (bound, to keep models with custom OGNL accessors, such as a
Map-backed model, working). Because the model is checked first, a model property that has the same name as an action property is bound without an annotation. -
The exemption lifts only the annotation requirement. The accepted and excluded parameter-name patterns and
ParameterNameAwarestill apply to a model’s parameters. -
Before 7.4.0 every parameter sent to a
ModelDrivenaction was exempt, including those that reached the action’s own unannotated setters. An application that relied on that annotates those setters, or setsstruts.parameters.requireAnnotations.transitionModetotruewhile it does so.
The run above is the 7.4.0 behavior: note was not bound. The guide’s practical advice follows from the rule. Because the
whole model is bindable, shape it as a request DTO that carries only the fields the action means to accept, never a domain
or persistence entity; where member-level control is needed, use annotated action properties instead of ModelDriven.
A Model That Is an Entity
EntityModelAction shows what the advice prevents. Its model is the Book entity, which has an id and a featured
flag that only an administrator should set.
public class EntityModelAction extends ActionSupport implements ModelDriven<Book> {
private final Book book = new Book();
@Override
public Book getModel() {
return book;
}
}
GET /g9modeldriven/entity-model.action?title=Dune&featured=true&id=5
title=Dune
featured=true
The visitor chose featured=true and the application accepted it. The same request against plain-model (whose model is
the DTO BookForm, which has no featured property) would have been ignored. The classic name for this is mass
assignment, and it is the main reason why Struts 7 spells the model rule out so carefully.
What getModel() May Return
The interceptor calls getModel() and uses the return value for the rest of the request. Four behaviors of that call are
worth knowing, and each has a Bookshelf action.
| Mapping | What getModel() does |
Observed result |
|---|---|---|
|
Returns |
The view shows |
|
Returns |
The interceptor ignored the model (nothing pushed); the action’s own |
|
|
|
|
Returns the same final field every time |
The usual and recommended form: one stable instance |
The rule is that getModel() must return the same instance every time it is called during a request, unless the action
deliberately replaces it and refreshModelBeforeResult is on. The parameter is the only configuration of the
interceptor: when it is true a PreResultListener runs after the action and before the result, calls getModel()
again, and if the instance is not already on the stack removes the old model and pushes the new one. That is useful when
the action loads the model from the data layer inside the action method. The Javadoc states the cost: getModel() is
called at least twice.
modelDriven.refreshModelBeforeResult addresses the interceptor inside the stack)<action name="refresh-on" class="org.example.bookshelf.RefreshAction">
<interceptor-ref name="defaultStack">
<param name="modelDriven.refreshModelBeforeResult">true</param>
</interceptor-ref>
<result>/WEB-INF/views/model.jsp</result>
</action>
The simplest way to avoid every problem in the table is to create the model once, in a field or lazily, and return it:
the CRUD action below uses if (form == null) { form = new BookForm(); }.
The Model Driven Interceptor Must Come Before params
The interceptor guide says that modelDriven must come before staticParams and params if the parameters are to be
applied to the model, because it is the push on the value stack that redirects the binding. In defaultStack (position 10
of 24, right after scopedModelDriven) and in the sample modelDrivenStack (modelDriven followed by basicStack) that
is true. A hand-written stack that lists params first fills the action instead of the model, which is also what the
paramsPrepareParamsStack does on its first params, on purpose, as shown below.
Validation of the Model
The guide says that validation is performed on the model, not on the action. ValidatedModelAction has no validation code
of its own: it implements ModelDriven<BookForm>, returns a final BookForm, and also has a getter getTitle() that
always answers a non-empty string, so that the test can tell the model apart from the action. An XML file named after the
action class and the mapping, ValidatedModelAction-vm-validation.xml, declares the rules for the field names:
<!DOCTYPE validators PUBLIC
"-//Apache Struts//XWork Validator 1.0.3//EN"
"https://struts.apache.org/dtds/xwork-validator-1.0.3.dtd">
<validators>
<field name="title">
<field-validator type="requiredstring">
<message>A title is required</message>
</field-validator>
</field>
<field name="year">
<field-validator type="int">
<param name="min">1450</param>
<param name="max">2100</param>
<message>Year must be between 1450 and 2100</message>
</field-validator>
</field>
</validators>
The mapping sends the input result to a status-only result:
<action name="vm" class="org.example.bookshelf.ValidatedModelAction">
<result>/WEB-INF/views/model.jsp</result>
<result name="input" type="httpheader"><param name="status">422</param></result>
</action>
GET /g9modeldriven/vm.action?title=&year=3000 -> HTTP 422
GET /g9modeldriven/vm.action?title=Dune&year=1965 -> HTTP 200, title=Dune, year=1965
The empty title failed requiredstring although the action’s own getTitle() is never empty: the field was resolved on
the model, which sits on top of the stack. The Validation page of this section covers the framework in full; for a model
that contains other objects the visitor validator (see the Core Developer Guide page on it) applies the rules of the
nested class.
Preparable and the Prepare Interceptor
The second half of the pair solves a different problem. Often a form shows data that comes from a lookup, such as the
list of authors in a drop-down. The lookup is done by the action method that displays the form (input). When the user
submits the form and the validation fails, the validation and workflow interceptors return input and the framework
renders the input view, but the input method is not run again, so the lookup result is missing and the page is
rendered without its drop-down. The Getting Started tutorial on the Preparable interface describes exactly this
problem and its remedy: put the statements that must run whatever method is called, and also when validation fails, in a
prepare() method.
| Piece | Role |
|---|---|
|
One method, |
|
Registered as |
|
A public method named |
|
The alternative spelling: |
|
Interceptor parameter, default |
|
Interceptor parameter, default |
The interceptor, in the 7.4.0 source, decides the order in four lines of code, which is why the observed order below is not the order that people expect on first reading:
public String doIntercept(ActionInvocation invocation) throws Exception {
Object action = invocation.getAction();
if (action instanceof Preparable) {
String[] prefixes = firstCallPrepareDo
? new String[] {"prepareDo", "prepare"}
: new String[] {"prepare", "prepareDo"};
PrefixMethodInvocationUtil.invokePrefixMethod(invocation, prefixes); // first method found, only one
if (alwaysInvokePrepare) {
((Preparable) action).prepare(); // after the prefixed one
}
}
return invocation.invoke();
}
(The 7.4.0 source wraps the prefixed call in a try block that unwraps InvocationTargetException, so an exception that
a prepare method throws reaches the exception interceptor as itself; that part is left out.) PrefixMethodInvocationUtil
looks up the method name from the action proxy (execute when none is configured), tries each prefix in order and
invokes only the first method that exists.
Which prepare Methods Are Called, in What Order
PrepLogAction implements Preparable, has all of prepare(), prepareInput(), prepareDoInput() and prepareSave(),
and records every call in a list that the view prints. Four mappings use it with a wildcard method (the wildcard
mappings are covered on the Wildcard Mappings and Method Invocation page of this section).
public class PrepLogAction extends ActionSupport implements Preparable {
private final List<String> calls = new ArrayList<>();
@Override
public void prepare() { calls.add("prepare"); }
public void prepareInput() { calls.add("prepareInput"); }
public void prepareDoInput() { calls.add("prepareDoInput"); }
public void prepareSave() { calls.add("prepareSave"); }
@Override
public String input() { calls.add("input"); return SUCCESS; }
public String save() { calls.add("save"); return SUCCESS; }
@Override
public String execute() { calls.add("execute"); return SUCCESS; }
public List<String> getCalls() { return calls; }
}
<action name="prep-*" class="org.example.bookshelf.PrepLogAction" method="{1}">
<result>/WEB-INF/views/calls.jsp</result>
</action>
<action name="prepn-*" class="org.example.bookshelf.PrepLogAction" method="{1}">
<interceptor-ref name="defaultStack">
<param name="prepare.alwaysInvokePrepare">false</param>
</interceptor-ref>
<result>/WEB-INF/views/calls.jsp</result>
</action>
<action name="prepd-*" class="org.example.bookshelf.PrepLogAction" method="{1}">
<interceptor-ref name="defaultStack">
<param name="prepare.firstCallPrepareDo">true</param>
</interceptor-ref>
<result>/WEB-INF/views/calls.jsp</result>
</action>
<action name="prepx-*" class="org.example.bookshelf.PrepLogAction" method="{1}">
<interceptor-ref name="defaultStack">
<param name="prepare.excludeMethods">save</param>
</interceptor-ref>
<result>/WEB-INF/views/calls.jsp</result>
</action>
| Request | Calls, in order | Reading |
|---|---|---|
|
|
The prefixed method comes first, then the general |
|
|
|
|
|
No |
|
|
|
|
|
|
|
|
|
The documentation sentence "the prepare interceptor will call the prepareInput and the prepare methods before calling the
input method" is correct about the set of methods, and the table gives the order. The alwaysInvokePrepare setting is
the switch to use when prepare() holds work that only some methods need: set it to false and put the work in the
prepareXxx() methods. excludeMethods and includeMethods take the action method names (comma separated), never the
prepare method names, and includeMethods wins when both are present.
The paramsPrepareParams Stack
The second use of Preparable in the Prepare Interceptor page is the one that gives the stack its name: load an object
from the database so that request parameters can be set on the loaded object. The difficulty is the order. In
defaultStack of 7.4.0 the prepare interceptor is at position 7 and params at position 17, so prepare() runs
before any request parameter has been bound. A prepare() that wants the id of the book finds the field empty. The
sample stack paramsPrepareParamsStack of struts-default.xml fixes this by running params twice: once before prepare
and once after modelDriven.
The figure is the order of paramsPrepareParamsStack in the 7.4.0 jar, ignoring the first three entries
(exception, alias, i18n) and checkbox, datetime and multiselect. Two details matter. The first params runs
while the value stack holds only the action, so only parameters that name an action property are applied, and in 7.x only
those with @StrutsParameter (the id below; the rule is the topic of Parameter Injection and @StrutsParameter); every other parameter, such as title, finds no property and is ignored on
this pass. The second params runs when the model is on the stack, so the same request values now reach the model, which
prepare may have replaced. The model interceptor comes after prepare in both stacks: that is what allows prepare to
create or load the model and getModel() to return it afterwards.
The Same Action Behind Both Stacks
BookCrudAction, shown in full in the next section, is mapped twice for the edit page: book-edit (through the
book-* wildcard, with paramsPrepareParamsStack) and bookd-edit (through the bookd-* wildcard, with the inherited
defaultStack). The action records the id that prepareEdit() saw in the field idSeenByPrepare and the form prints it.
| Request | prepareEdit() saw id |
The form shows |
|---|---|---|
|
|
|
|
|
Empty title, no author selected, year |
That is the trap: nothing fails, the page just shows an empty form. The same lesson is on the interceptors page of this section with a smaller example.
The stack has costs that the Core Developer Guide does not list. Parameters are bound twice, so setters must be idempotent;
every parameter that names an annotated action property reaches the action before prepare() has authorized or loaded
anything; and because the first pass applies request values to the action, the id setter needs @StrutsParameter, as the
CRUD action does. Without the annotation the first params would ignore id, as it ignored note in the first example,
and prepareEdit() would see null.
A Full CRUD Action
The Bookshelf’s administration screen has five operations on the book resource: a list, a form for a new book, a form for
an existing book, a save that creates or updates, and a delete. One action class serves them all, each operation being one
method selected through the wildcard book-*. The model is the DTO BookForm; the entity Book never meets the request.
The pattern is post, redirect, get: after a successful save or delete the result is a redirectAction to the list, so a
browser refresh of the list does not repeat the POST. A failed validation returns input and re-renders the form in the
same request, with the field errors and the values the user typed.
public class BookCrudAction extends ActionSupport implements ModelDriven<BookForm>, Preparable {
private Long id;
private BookForm form;
private List<Book> books;
private List<String> authors;
private String idSeenByPrepare = "n/a";
public Long getId() { return id; }
@StrutsParameter
public void setId(Long id) { this.id = id; }
@Override
public BookForm getModel() {
if (form == null) {
form = new BookForm();
}
return form;
}
@Override
public void prepare() {
authors = BookStore.authors(); // needed by every view that renders the form
}
public void prepareEdit() {
idSeenByPrepare = String.valueOf(id);
Book book = BookStore.find(id);
if (book != null) {
form = BookForm.from(book);
}
}
public void prepareSave() {
Book book = BookStore.find(id);
if (book != null) {
form = BookForm.from(book); // start from the stored values
}
}
public String list() {
books = BookStore.all();
return SUCCESS;
}
public String edit() {
return BookStore.find(id) == null ? "notfound" : INPUT;
}
public void validateSave() {
BookForm f = getModel();
if (f.getTitle() == null || f.getTitle().isBlank()) {
addFieldError("title", "A title is required");
}
if (f.getYear() < 1450 || f.getYear() > 2100) {
addFieldError("year", "Year must be between 1450 and 2100");
}
}
public String save() {
Book book = id == null ? new Book() : BookStore.find(id);
if (book == null) {
return "notfound";
}
book.setTitle(form.getTitle());
book.setAuthor(form.getAuthor());
book.setYear(form.getYear());
BookStore.save(book);
return SUCCESS;
}
public String delete() {
BookStore.delete(id);
return SUCCESS;
}
public List<Book> getBooks() { return books; }
public List<String> getAuthors() { return authors; }
public String getIdSeenByPrepare() { return idSeenByPrepare; }
}
The class has these parts, in the order in which a request reaches them. setId is annotated because the first params
pass needs it. prepare() fills authors for every method, including after a failed validation, which is the whole point
of the interface. prepareEdit() loads the stored book into a fresh BookForm. prepareSave() does the same, so that a
failed validation redisplays the stored values for the fields that were not submitted, with the submitted values written
over them by the second params. getModel() creates the form lazily so that the input page for a new book has an empty
one. validateSave() is the per-method validation hook that ActionSupport offers (see the actions page): it runs before
save() and not before the other methods. The action method save() copies the DTO into the entity field by field, which
is the only place where the entity is written, and its list is explicit: a field added to the entity later is not
bindable until someone adds it here. The notfound result code handles an unknown id with a 404.
The Mapping
<action name="book-*" class="org.example.bookshelf.BookCrudAction" method="{1}">
<interceptor-ref name="paramsPrepareParamsStack"/>
<result name="success" type="redirectAction">book-list</result>
<result name="input">/WEB-INF/views/form.jsp</result>
<result name="notfound" type="httpheader">
<param name="status">404</param>
</result>
<allowed-methods>list,input,edit,save,delete</allowed-methods>
</action>
<action name="book-list" class="org.example.bookshelf.BookCrudAction" method="list">
<interceptor-ref name="paramsPrepareParamsStack"/>
<result>/WEB-INF/views/list.jsp</result>
</action>
Four points about the mapping. method="{1}" selects the method from the wildcard, and in Struts 7 a wildcard method
must be listed in allowed-methods or be one of the global ones (execute,input,back,cancel,browse,save,delete,list,index
in struts-default.xml); edit is the only one outside the global list, which is why the list is spelled out. The fixed
book-list mapping takes precedence over the wildcard, because an exact name always beats a pattern, and it has its own
success result (the list view) so that list() does not redirect to itself. The input result is returned by the
workflow interceptor when validation fails and by edit() and by the inherited input(). And notfound is a result
code of the action itself, mapped to the status-only httpheader result.
The Views
form.jsp uses the simple theme of the Struts tags, so the output is a plain form; the field names are the names of the
model’s properties, with no prefix.
<s:fielderror/>
<s:form action="book-save" method="post" theme="simple">
<s:hidden name="id"/>
Title: <s:textfield name="title"/>
Author: <s:select name="author" list="authors"/>
Year: <s:textfield name="year"/>
<s:submit/>
</s:form>
<p>idSeenByPrepare=<s:property value="idSeenByPrepare"/></p>
Source: Core Developer Guide — Model Driven, Example (the form addresses model properties without a prefix)
name="title" is evaluated against the value stack: the model is on top, so the field reads form.getTitle() and, on
submit, params writes setTitle on the model. name="id" and list="authors" find the action’s properties because the
model has none of that name; authors is filled by prepare(). The list view iterates books and builds the edit link
book-edit.action?id=... from each row.
The Whole Flow, Request by Request
The following requests were made against g9modeldriven.war in this order (the store was reset first and holds Dune,
id 1, and Neuromancer, id 2; the verdicts are the HTTP status and the observed output):
| Request | Status | Observed result |
|---|---|---|
|
200 |
Table with both books and the |
|
200 |
Form filled with Dune, Frank Herbert, 1965; the author drop-down lists the three authors; |
|
200 |
Empty form (year |
|
302 |
|
|
200 |
The form again, with the errors "A title is required" and "Year must be between 1450 and 2100", the typed year |
|
302 |
A third row, id 3, |
|
404 |
|
|
302 |
Redirect to the list, which no longer shows Neuromancer |
|
404 |
|
|
404 |
The method |
The failed save deserves a second look, because it is the reason for the pattern: the page that came back after the
validation failure has a populated drop-down. Without prepare(), the authors field would be null, the select would
have nothing to list and the user would lose the choice they were making. The validation interceptor runs after prepare
in the stack and the input result is rendered by the same request, so everything the view needs must be prepared by an
interceptor, not by the input method.
ScopedModelDriven
A model that lives longer than a request is the Struts answer to the Struts 1 ActionForm with scope="session": a
multi-page wizard, where each step fills part of the same object. The tool is ScopedModelDriven, an extension of
ModelDriven that adds three methods, and the scopedModelDriven interceptor. The interceptor acts only on actions that
implement the interface and only when their getModel() returns null; it then loads the model from the scope (building
it with the object factory when the scope has none), hands it to the action with setModel, and tells the action the key
it used with setScopeKey. The actual pushing on the stack is still done by modelDriven, which follows it in
defaultStack.
| Parameter | Meaning |
|---|---|
|
Class name of the model; defaults to the class that |
|
Key in the scope; defaults to the model class name |
|
|
public class WizardAction extends ActionSupport implements ScopedModelDriven<BookForm> {
private BookForm form;
private String scopeKey;
@Override
public BookForm getModel() { return form; }
@Override
public void setModel(BookForm model) { this.form = model; }
@Override
public void setScopeKey(String key) { this.scopeKey = key; }
@Override
public String getScopeKey() { return scopeKey; }
public String title() { return SUCCESS; }
public String author() { return SUCCESS; }
public String review() { return SUCCESS; }
}
getModel() returns the field, null at first, which is what makes the interceptor act; the three step methods do
nothing because the framework does the work. Two mappings point to the class, one with the interceptor parameters of the
session scope and one with the defaults:
<action name="wizard-*" class="org.example.bookshelf.WizardAction" method="{1}">
<interceptor-ref name="defaultStack">
<param name="scopedModelDriven.scope">session</param>
<param name="scopedModelDriven.name">bookWizard</param>
<param name="scopedModelDriven.className">org.example.bookshelf.BookForm</param>
</interceptor-ref>
<result>/WEB-INF/views/model.jsp</result>
<allowed-methods>title,author,review</allowed-methods>
</action>
<action name="wizreq-*" class="org.example.bookshelf.WizardAction" method="{1}">
<result>/WEB-INF/views/model.jsp</result>
<allowed-methods>title,author,review</allowed-methods>
</action>
Three requests of one browser session (cookie kept) to the session mapping, and two separate requests to the default one:
GET wizard-title.action?title=Foundation -> title=Foundation author= scopeKey=bookWizard
GET wizard-author.action?author=Asimov -> title=Foundation author=Asimov scopeKey=bookWizard
GET wizard-review.action -> title=Foundation author=Asimov scopeKey=bookWizard
GET wizreq-title.action?title=Foundation -> title=Foundation author= scopeKey=org.example.bookshelf.BookForm
GET wizreq-author.action?author=Asimov -> title= author=Asimov scopeKey=org.example.bookshelf.BookForm
In the session scope the second step still sees the title from the first and the third step sees both, because the same
BookForm instance is stored in the HttpSession under bookWizard; in the request scope each request gets a new model,
and the key defaults to the class name. The 7.4.0 source of resolveModel shows where the models live: the session scope
is the HttpSession map of the ActionContext, and the request scope is the ActionContext context map, that is, a map
that lives for the request and is not the servlet request’s attributes.
Remember that a session scoped model is shared by every browser tab of the session: two tabs that run the wizard
overwrite each other’s fields, and nothing removes the model when the wizard is finished. A real wizard ends with a
save method that removes the key from the session map (for instance through SessionAware or ActionContext.getSession()).
The session and scope interceptors, and the chain result, are on the Action Chaining, Sessions and Scopes page of this section.
Pitfalls
| Symptom | Cause | Fix |
|---|---|---|
The form for an existing book is empty, the id is in the hidden field (observed with |
|
Use |
|
The |
Annotate the setter that |
A visitor can set a field they should not, such as |
The model is a persistence entity and every property of the model is bindable without annotation |
Use a request DTO; copy it into the entity in the action method |
A request parameter for the action’s own property is ignored on a |
Since 7.4.0 the exemption covers the model only; the action’s setters need |
Annotate the setter, or move the property to the model |
Values typed into the form are lost and |
|
Create the model once and return the same instance |
The model never receives parameters, and |
|
Initialize the model in a field, or lazily, before returning it |
The view shows the old model after the action replaced it (observed with |
The model pushed before the action stays on the stack |
Set |
|
That sample stack of 7.4.0 does not contain |
Add the interceptor before |
|
|
Remove the entry; put method names in |
|
|
Keep one spelling, or set |
The drop-down is empty after a failed validation |
The data was loaded only in |
Load it in |
The |
The method is not in |
List it in |
The wizard model is shared by two tabs |
A session scoped model is one object per session |
Key the model by a conversation id, or keep wizard state in a form token |
|
The action’s |
Return |
What Changed Recently
| Version | Change |
|---|---|
2.0.2 (XWork) |
|
2.0.7 |
The interceptor names with hyphens ( |
7.0.0 |
Packages renamed: |
7.2.0 |
|
7.4.0 |
The exemption covers the model and only the model; an unannotated setter on the action itself is no longer bound on a |
Where the Books Are Outdated
The books of the section explain the idea correctly: a model object separate from the action, a prepare step before the parameters, and a stack that loads parameters twice. The details changed.
| In the books | Struts 7.4.0 |
|---|---|
The |
|
|
The model is bindable in full, without annotations, so a domain or persistence entity as a model is a security risk; use a DTO |
Any public setter of the action is bound, with or without a model |
Only |
|
Result names are the lower case values ( |
A |
Since 7.2.0 |
|
|
|
Still the standard answer, still a sample stack (not in |
Struts 1 |
No form class: a model, validated by the |
The Struts 1 ActionForm concepts map as follows: the form bean is the model (or the action’s own properties), reset()
is the constructor of a fresh model, validate() is validate() or XML validation on the model, the scope attribute of
the mapping is scopedModelDriven.scope, and the form acting as a "firewall" between the request and the business objects
is the DTO.
Inconsistencies in the Official Documentation
Collected while verifying this page against the 7.4.0 jar:
-
The Scoped Model Driven Interceptor page shows
<-- Basic usage -->(not a valid XML comment, it should be<!-- ... -->) and names the classcom.opensymphony.interceptor.ScopedModelDrivenInterceptor, a package that does not exist; the class isorg.apache.struts2.interceptor.ScopedModelDrivenInterceptor, registered asscopedModelDriveninstruts-default.xml. The page calls the default scoperequest, but the source reads theActionContextmap, not the servlet request. -
The Prepare Interceptor page lists only
alwaysInvokePrepare; the class also hasfirstCallPrepareDoand theexcludeMethodsandincludeMethodsit mentions in a note. It also still refers tocom.opensymphony.xwork2.ActionProxy. -
The Model Driven page’s example
ModelDrivenActionimplements the rawModelDriven; the interface is generic (ModelDriven<T>). -
The Getting Started tutorial on the interface is published under the misspelled address
preperable-interface(the page title is "Preparable Interface");preparable-interfaceanswers 404.
Summary
-
ModelDriven<T>lets an action return a model; themodelDriveninterceptor pushes a non-null model on the value stack, where parameters are bound and expressions resolved, and validation also sees the model. -
In 7.x the model is the
@StrutsParameterauthorization target, with every member bindable and nothing annotated; the action’s own setters still need the annotation (7.4.0). Use a request DTO as the model, never an entity. -
getModel()must return one stable instance;nullis ignored;modelDriven.refreshModelBeforeResultpushes a replaced instance before the result and callsgetModel()at least twice. -
Preparable.prepare()(a default method since 7.2.0) runs inprepare, which precedesvalidation, so it is the place for lookups that the input view needs after a failed validation. -
The order is
prepareXxx()(orprepareDoXxx(), only the first one found), thenprepare()unlessalwaysInvokePrepareisfalse, then the action method;excludeMethodsskips all of them. -
paramsPrepareParamsStackrunsparamstwice so thatprepare()can load by id and the second pass fills the loaded model; theidsetter needs@StrutsParameter. -
ScopedModelDrivenwith thescopedModelDriveninterceptor keeps a model in the session (or the action context forrequest), only whengetModel()returnsnull. -
A CRUD action is
list,input,edit,saveanddeleteon one class, a DTO model,preparefor the lookups,validateSave()for the checks, andredirectActionafter a successful write.
References
-
Core Developer Guide — Scoped Model Driven Interceptor, Parameters
-
Core Developer Guide — Scoped Model Driven Interceptor, Examples
-
Core Developer Guide — Struts Parameter Annotation, ModelDriven actions
-
Core Developer Guide — Struts Parameter Annotation, The action’s own members are not exempt
-
Struts Security Guide — Defining and annotating your action parameters
-
Struts 7.4.0 source — ScopedModelDrivenInterceptor.resolveModel