This is an automated email from the ASF dual-hosted git repository.
lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git
The following commit(s) were added to refs/heads/main by this push:
new f179011d8 WW-5698 Scope the ModelDriven exemption in
StrutsParameterAuthorizer to the model object (#1872)
f179011d8 is described below
commit f179011d8ac9151e819f750ce8deafb41d61fd6a
Author: Lukasz Lenart <[email protected]>
AuthorDate: Mon Aug 31 20:01:44 2026 +0200
WW-5698 Scope the ModelDriven exemption in StrutsParameterAuthorizer to the
model object (#1872)
* WW-5698 fix(params): scope the ModelDriven exemption to the model object
isAuthorized returned true for every parameter name once the action
implemented
ModelDriven. OGNL then resolves that name against the whole CompoundRoot,
which
holds the model on top of the action, so authorization was decided about the
model while the write could land on the action. In effect the
@StrutsParameter
requirement did not apply to a ModelDriven action's own members: an
unannotated
setter declared on the action was bound, where the identical setter on a
plain
action is rejected.
The exemption now covers what it was meant to cover. A property declared by
the
model is exempt, since returning an object from getModel() declares it
request
surface. A property declared by the action is subject to the annotation
requirement as usual. A property declared by neither is still allowed,
because
it cannot be reaching a member of the action - that case is typically a
model
bound through a custom OGNL property accessor, such as a Map-backed model,
and
rejecting it would break those applications.
The model is checked first so that a model property shadowing an action
property
still binds without an annotation, matching OGNL's own resolution against
the
stack top.
Co-Authored-By: Claude Opus 5 <[email protected]>
* WW-5698 fix(params): let transition mode reach ModelDriven actions
The ModelDriven branch returned before the transition mode check, so
requireAnnotations.transitionMode never applied to a ModelDriven action.
That
did not matter while the exemption authorized everything, but once it is
scoped
to the model the action's own members are rejected, and those are exactly
the
members transition mode exists to keep binding during migration.
Checking transition mode first gives the affected applications the same
migration path they would have on any other action.
Co-Authored-By: Claude Opus 5 <[email protected]>
* WW-5698 fix(params): scope the exemption by what the model can bind, not
by name
Copilot's review of #1872 found three ways the scoped exemption still let a
parameter through to the action's own members. All three reproduce.
Keying the exemption on the property name alone is not enough, because OGNL
walks the stack until an object actually accepts the assignment:
- a getter-only property on the model cannot take a depth-0 parameter, so
OGNL moves on and the action's unannotated setter takes it. Verified on a
real value stack: the action's field ends up holding the value.
- an inherited public field on the action was invisible to
getDeclaredField,
so the parameter counted as declared on neither model nor action and took
the exemption meant for Map-backed models. OGNL sets inherited public
fields as readily as declared ones.
- a public static final namesake on the model cannot absorb a parameter
either, and would have stood in for a real field.
declaresProperty therefore now asks what the object can bind at this depth -
the setter for a depth-0 parameter, the getter for a nested one, or a public
instance field - rather than whether the name appears anywhere.
Also rejects a parameter name that begins with a nesting character. It names
no root property, and computing one ran charAt(0) on an empty string.
Both changed methods are new in this PR, so their signatures are not yet
API.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01P9Pjt4rvb1ASASjSTHsUhL
* WW-5698 fix(params): do not let a class path take the unknown-property
fallback
The remaining half of Copilot's first review comment on #1872: "class" was
exempted for a ModelDriven action, where the ordinary path rejects it.
Not for the reason the comment gives, though. OgnlUtil introspects with
Object
as the stop class, so "class" never appears among the property descriptors
at
all; it was not being matched as a read-only descriptor but taking the
fallback
for a property declared on neither model nor action, which exists to let a
Map-backed model bind through its own OGNL accessor. That fallback is the
wrong
home for it: "class" is not an unknown name, it is Object.getClass() on
every
object alike, and the non-ModelDriven path rejects it for want of an
annotation.
Rejected there rather than earlier, so a model or action that really does
declare a "class" property is still decided on its own terms.
This is defence in depth, not a live bypass. Navigating a class path is
already
inert: java.lang.Class and java.lang.ClassLoader are both in the default
struts.excludedClasses, and SecurityMemberAccess refuses their members -
checked
on a real value stack, where every class.* read returns null and every set
has
no effect. Worth closing anyway, because the two paths disagreeing is the
very
thing this ticket is about.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01P9Pjt4rvb1ASASjSTHsUhL
* WW-5698 refactor(params): match the public field by scanning rather than
lookup
SonarCloud failed the gate on javasecurity:S6173 — the request-derived
property
name reaching Class.getField as a reflection lookup. In substance a false
positive: nothing is constructed or invoked, the Field is only inspected
for its
modifiers. But the sink is avoidable at no cost, so avoid it.
Class.getFields() selects exactly the fields getField(name) searches —
public,
declared and inherited — so scanning them and comparing the name is the same
decision without the name reaching a reflection API. It also reads
consistently
with the property descriptor stream just above it.
Behaviour unchanged: core 3212, json 166, rest 124 all green.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01P9Pjt4rvb1ASASjSTHsUhL
* WW-5698 docs(params): state what the neither-arm fallback actually
guarantees
The javadoc said a property declared on neither the model nor the action
"cannot be
reaching a member of the action". That is stronger than what holds.
CompoundRootAccessor
walks the whole root, so such a name still lands on the action wherever the
action absorbs
it by a route this introspection does not model - being a Map itself, or
declaring a setter
OGNL matches on name and arity while java.beans.Introspector does not, a
fluent one for
instance, which is tracked as WW-5709.
Neither case is more permissive than the blanket exemption this scoping
replaces, so the
fallback stays as it is; only the claim made for it is corrected.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ24oMdSkZDid4GM6JA1Jv
---------
Co-authored-by: Claude Opus 5 <[email protected]>
---
.../parameter/StrutsParameterAuthorizer.java | 111 ++++++++++-
.../parameter/ParameterAuthorizerTest.java | 203 +++++++++++++++++++++
2 files changed, 304 insertions(+), 10 deletions(-)
diff --git
a/core/src/main/java/org/apache/struts2/interceptor/parameter/StrutsParameterAuthorizer.java
b/core/src/main/java/org/apache/struts2/interceptor/parameter/StrutsParameterAuthorizer.java
index 03cc22c0e..423b885b0 100644
---
a/core/src/main/java/org/apache/struts2/interceptor/parameter/StrutsParameterAuthorizer.java
+++
b/core/src/main/java/org/apache/struts2/interceptor/parameter/StrutsParameterAuthorizer.java
@@ -60,6 +60,13 @@ public class StrutsParameterAuthorizer implements
ParameterAuthorizer {
private static final Logger LOG =
LogManager.getLogger(StrutsParameterAuthorizer.class);
+ /**
+ * {@link OgnlUtil#getBeanInfo(Class)} introspects with {@link Object} as
the stop class, so this one never
+ * appears among the property descriptors and cannot be told apart from a
genuinely unknown name by evidence
+ * alone. It is not unknown, though: it resolves to {@link
Object#getClass()} on every object alike.
+ */
+ private static final String CLASS_PROPERTY = "class";
+
private boolean requireAnnotations = false;
private boolean requireAnnotationsTransitionMode = false;
private boolean devMode = false;
@@ -115,28 +122,112 @@ public class StrutsParameterAuthorizer implements
ParameterAuthorizer {
long paramDepth = parameterName.codePoints().mapToObj(c -> (char)
c).filter(NESTING_CHARS::contains).count();
- // ModelDriven exemption: only exempt when the action explicitly
implements ModelDriven
- // and the target is its model object. This prevents non-ModelDriven
root objects
- // (e.g. JSONInterceptor's configurable rootObject) from bypassing
annotation checks.
- if (target != action && action instanceof ModelDriven) {
- LOG.debug("ModelDriven target detected (action implements
ModelDriven), exempting from @StrutsParameter annotation requirement");
- return true;
+ int nestingIndex = indexOfAny(parameterName, NESTING_CHARS_STR);
+ String rootProperty = nestingIndex == -1 ? parameterName :
parameterName.substring(0, nestingIndex);
+ if (rootProperty.isEmpty()) {
+ LOG.debug("Parameter [{}] begins with a nesting character, so it
names no root property to authorize; rejecting",
+ parameterName);
+ return false;
}
+ String normalisedRootProperty =
Character.toLowerCase(rootProperty.charAt(0)) + rootProperty.substring(1);
- // Transition mode: depth-0 (non-nested) parameters are exempt
+ // Transition mode: depth-0 (non-nested) parameters are exempt.
Checked before the ModelDriven
+ // exemption so that it also covers a ModelDriven action's own
members, which would otherwise
+ // have no migration path once the exemption is scoped to the model.
if (requireAnnotationsTransitionMode && paramDepth == 0) {
LOG.debug("Annotation transition mode enabled, exempting
non-nested parameter [{}] from @StrutsParameter annotation requirement",
parameterName);
return true;
}
- int nestingIndex = indexOfAny(parameterName, NESTING_CHARS_STR);
- String rootProperty = nestingIndex == -1 ? parameterName :
parameterName.substring(0, nestingIndex);
- String normalisedRootProperty =
Character.toLowerCase(rootProperty.charAt(0)) + rootProperty.substring(1);
+ // ModelDriven exemption: only exempt when the action explicitly
implements ModelDriven
+ // and the target is its model object. This prevents non-ModelDriven
root objects
+ // (e.g. JSONInterceptor's configurable rootObject) from bypassing
annotation checks.
+ if (target != action && action instanceof ModelDriven) {
+ return isAuthorizedOnModelDrivenAction(normalisedRootProperty,
target, action, paramDepth);
+ }
return hasValidAnnotatedMember(normalisedRootProperty, target,
paramDepth);
}
+ /**
+ * Decides authorization for a {@link ModelDriven} action, whose model is
on top of the value stack.
+ * <p>
+ * Returning an object from {@code getModel()} declares that object to be
request surface, so anything the
+ * model itself can take is exempt from the {@link StrutsParameter}
requirement. The exemption stops there:
+ * OGNL resolves the parameter name against the whole stack, which also
holds the action, so a property
+ * declared on the action is still subject to the annotation requirement.
Without that distinction a
+ * ModelDriven action would silently expose its own members.
+ * <p>
+ * A property declared on neither is allowed: typically it is bound by a
custom OGNL property accessor on
+ * the model, such as a Map-backed model. That fallback guarantees less
than it may appear to - only that
+ * the name reaches no member {@link #declaresProperty} can see. OGNL
walks the whole stack, so such a name
+ * can still land on the action wherever the action absorbs it by a route
introspection here does not model:
+ * being a {@code Map} itself, or declaring a setter that OGNL matches on
name and arity while
+ * {@link java.beans.Introspector} does not, a fluent one for instance -
see WW-5709. Neither case is more
+ * permissive than the blanket exemption this scoping replaces.
+ * <p>
+ * {@code class} is the exception to that fallback: it is invisible to
introspection here rather than absent,
+ * so it is rejected instead of taking the fallback, which keeps a
ModelDriven action from handing OGNL a
+ * {@code class} path that the ordinary non-ModelDriven path would have
rejected for want of an annotation.
+ */
+ protected boolean isAuthorizedOnModelDrivenAction(String rootProperty,
Object model, Object action, long paramDepth) {
+ if (declaresProperty(model, rootProperty, paramDepth)) {
+ LOG.debug("Property [{}] belongs to the ModelDriven model,
exempting from @StrutsParameter annotation requirement",
+ rootProperty);
+ return true;
+ }
+ if (!declaresProperty(action, rootProperty, paramDepth)) {
+ if (CLASS_PROPERTY.equals(rootProperty)) {
+ LOG.debug("Property [class] is not an unknown property but
Object.getClass() on every object alike, so the fallback for a custom accessor
does not apply; rejecting");
+ return false;
+ }
+ LOG.debug("Property [{}] is declared on neither the model nor the
action, exempting from @StrutsParameter annotation requirement",
+ rootProperty);
+ return true;
+ }
+ LOG.debug("Property [{}] is declared on the ModelDriven action itself,
applying the @StrutsParameter annotation requirement",
+ rootProperty);
+ return hasValidAnnotatedMember(rootProperty, action, paramDepth);
+ }
+
+ /**
+ * Whether {@code target} can itself take {@code property} at this depth -
as a bean property whose relevant
+ * accessor exists, the setter for a depth-0 parameter and the getter for
a nested one, or as a public instance
+ * field. Any {@link StrutsParameter} annotation is irrelevant here; this
asks only what the object can absorb.
+ * <p>
+ * It has to be bindability rather than the name alone, because OGNL walks
the stack until an object actually
+ * accepts the assignment. A model which merely names the property without
being able to take it - a getter-only
+ * property under a depth-0 parameter, say - does not absorb that
parameter: OGNL moves on to the action, and an
+ * exemption granted on the name alone would hand over the action's own
member, which is the very thing this
+ * scoping exists to prevent. Inherited public fields count for the same
reason, that OGNL can set them.
+ */
+ protected boolean declaresProperty(Object target, String property, long
paramDepth) {
+ BeanInfo beanInfo = getBeanInfo(target);
+ if (beanInfo != null &&
Arrays.stream(beanInfo.getPropertyDescriptors())
+ .filter(desc -> desc.getName().equals(property))
+ .anyMatch(desc -> (paramDepth == 0 ? desc.getWriteMethod() :
desc.getReadMethod()) != null)) {
+ return true;
+ }
+ return declaresBindablePublicField(target, property, paramDepth);
+ }
+
+ /**
+ * Whether {@code target} exposes {@code property} as a public instance
field that this parameter could bind
+ * through. {@link Class#getFields} covers inherited fields as well as
declared ones, an inherited public field
+ * being just as settable as a declared one. Static fields are not
per-instance request surface, and a final
+ * field cannot take a depth-0 assignment, so neither counts as absorbing
the parameter.
+ * <p>
+ * Scanning the fields and matching the name here, rather than looking the
name up with {@code getField},
+ * keeps the request-derived property name out of a reflection lookup. The
two select the same fields.
+ */
+ protected boolean declaresBindablePublicField(Object target, String
property, long paramDepth) {
+ return Arrays.stream(ultimateClass(target).getFields())
+ .filter(field -> field.getName().equals(property))
+ .anyMatch(field -> !Modifier.isStatic(field.getModifiers())
+ && (paramDepth > 0 ||
!Modifier.isFinal(field.getModifiers())));
+ }
+
protected boolean hasValidAnnotatedMember(String rootProperty, Object
target, long paramDepth) {
LOG.debug("Checking target [{}] for a matching, correctly annotated
member for property [{}]",
target.getClass().getSimpleName(), rootProperty);
diff --git
a/core/src/test/java/org/apache/struts2/interceptor/parameter/ParameterAuthorizerTest.java
b/core/src/test/java/org/apache/struts2/interceptor/parameter/ParameterAuthorizerTest.java
index 43eb57bcc..1157ed81b 100644
---
a/core/src/test/java/org/apache/struts2/interceptor/parameter/ParameterAuthorizerTest.java
+++
b/core/src/test/java/org/apache/struts2/interceptor/parameter/ParameterAuthorizerTest.java
@@ -132,6 +132,127 @@ public class ParameterAuthorizerTest {
assertThat(authorizer.isAuthorized("nested.deep", model,
action)).isTrue();
}
+ @Test
+ public void modelDriven_unannotatedActionMember_rejected() {
+ // The exemption covers the model, which is declared request surface
by getModel().
+ // It must not reach members declared on the action itself.
+ var action = new ModelActionWithOwnMembers();
+ assertThat(authorizer.isAuthorized("actionSecret", action.getModel(),
action)).isFalse();
+ }
+
+ @Test
+ public void modelDriven_annotatedActionMember_authorized() {
+ var action = new ModelActionWithOwnMembers();
+ assertThat(authorizer.isAuthorized("actionAllowed", action.getModel(),
action)).isTrue();
+ }
+
+ @Test
+ public void modelDriven_modelProperty_stillAuthorizedWithoutAnnotation() {
+ // The whole point of the exemption: model properties need no
annotation.
+ var action = new ModelActionWithOwnMembers();
+ assertThat(authorizer.isAuthorized("name", action.getModel(),
action)).isTrue();
+ }
+
+ @Test
+ public void modelDriven_propertyOnNeitherModelNorAction_authorized() {
+ // A model bound through a custom OGNL property accessor (e.g. a
Map-backed model) declares no
+ // bean property, and such a name cannot be reaching a member of the
action either.
+ var action = new ModelActionWithOwnMembers();
+ assertThat(authorizer.isAuthorized("noSuchPropertyAnywhere",
action.getModel(), action)).isTrue();
+ }
+
+ @Test
+ public void
modelDriven_modelPropertyShadowingUnannotatedActionProperty_authorized() {
+ // Declared on both. OGNL resolves against the stack top, which is the
model, so the model's
+ // property wins and needs no annotation even though the action's
namesake is unannotated.
+ var action = new ModelActionWithOwnMembers();
+ assertThat(authorizer.isAuthorized("shared", action.getModel(),
action)).isTrue();
+ }
+
+ @Test
+ public void transitionMode_modelDrivenUnannotatedActionMember_exempt() {
+ // Transition mode exists so an application can turn
requireAnnotations on while it works
+ // through annotating. It must reach ModelDriven actions too, or the
actions affected by
+ // scoping the exemption have no migration path.
+
authorizer.setRequireAnnotationsTransitionMode(Boolean.TRUE.toString());
+ var action = new ModelActionWithOwnMembers();
+ assertThat(authorizer.isAuthorized("actionSecret", action.getModel(),
action)).isTrue();
+ }
+
+ @Test
+ public void
modelDriven_readOnlyModelPropertyShadowingUnannotatedActionSetter_rejected() {
+ // Verified against a real value stack: with the model on top and only
a getter for "shadow",
+ // OGNL cannot assign to the model and moves on to the action, whose
unannotated setter takes
+ // the value. Exempting on the name alone would therefore expose the
action's own member.
+ var action = new ModelActionWithReadOnlyModelProperty();
+ assertThat(authorizer.isAuthorized("shadow", action.getModel(),
action)).isFalse();
+ }
+
+ @Test
+ public void
modelDriven_readOnlyModelProperty_stillAuthorizedForNestedParameter() {
+ // A getter is all a nested parameter needs of the root property: OGNL
reads "shadow" from the
+ // model and assigns further in. The model does absorb this one, so
the exemption still applies.
+ var action = new ModelActionWithReadOnlyModelProperty();
+ assertThat(authorizer.isAuthorized("shadow.anything",
action.getModel(), action)).isTrue();
+ }
+
+ @Test
+ public void modelDriven_inheritedPublicFieldOnAction_rejected() {
+ // OGNL sets inherited public fields as readily as declared ones, so a
field the action inherits
+ // is still the action's own member and still needs the annotation.
+ var action = new ModelActionInheritingPublicField();
+ assertThat(authorizer.isAuthorized("inheritedSecret",
action.getModel(), action)).isFalse();
+ }
+
+ @Test
+ public void modelDriven_inheritedPublicFieldOnModel_authorized() {
+ // The mirror case: a public field the model inherits is model surface
like any other.
+ var action = new ModelActionWithInheritingModel();
+ assertThat(authorizer.isAuthorized("inheritedModelField",
action.getModel(), action)).isTrue();
+ }
+
+ @Test
+ public void
modelDriven_staticFieldNamesakeOfUnannotatedActionProperty_rejected() {
+ // A constant is not per-instance request surface and cannot absorb
the parameter, so it must not
+ // stand in for the model the way a real field would.
+ var action = new ModelActionWithConstantNamesake();
+ assertThat(authorizer.isAuthorized("constant", action.getModel(),
action)).isFalse();
+ }
+
+ @Test
+ public void modelDriven_classProperty_rejected() {
+ // OgnlUtil introspects with Object as the stop class, so "class"
shows up on no descriptor list
+ // and looks like a name declared nowhere - the shape the
custom-accessor fallback exempts. It is
+ // not unknown, it is Object.getClass() on everything, and the
non-ModelDriven path rejects it for
+ // want of an annotation. The exemption must not make a ModelDriven
action the exception.
+ var action = new ModelActionWithOwnMembers();
+ assertThat(authorizer.isAuthorized("class.classLoader.foo",
action.getModel(), action)).isFalse();
+ assertThat(authorizer.isAuthorized("class", action.getModel(),
action)).isFalse();
+ }
+
+ @Test
+ public void nonModelDrivenAction_classProperty_rejected() {
+ // The behaviour the case above is being aligned with.
+ var action = new SecureAction();
+ assertThat(authorizer.isAuthorized("class.classLoader.foo", action,
action)).isFalse();
+ }
+
+ @Test
+ public void parameterNameBeginningWithNestingChar_rejected() {
+ // Such a name has no root property to authorize. It used to reach
charAt(0) on an empty string.
+ var action = new ModelActionWithOwnMembers();
+ assertThat(authorizer.isAuthorized(".actionSecret", action.getModel(),
action)).isFalse();
+ assertThat(authorizer.isAuthorized("[0].actionSecret",
action.getModel(), action)).isFalse();
+ assertThat(authorizer.isAuthorized("(actionSecret)",
action.getModel(), action)).isFalse();
+ }
+
+ @Test
+ public void
parameterNameBeginningWithNestingChar_nonModelDriven_rejected() {
+ var action = new SecureAction();
+ assertThat(authorizer.isAuthorized(".annotatedProp", action,
action)).isFalse();
+ assertThat(authorizer.isAuthorized("[0].annotatedProp", action,
action)).isFalse();
+ }
+
@Test
public void nonModelDrivenAction_differentTarget_notExempt() {
// Regression test: when target != action but action does NOT
implement ModelDriven,
@@ -267,9 +388,91 @@ public class ParameterAuthorizerTest {
public Pojo getModel() { return new Pojo(); }
}
+ public static class ModelActionWithOwnMembers implements ModelDriven<Pojo>
{
+ private final Pojo model = new Pojo();
+ private String actionSecret;
+ private String actionAllowed;
+
+ @Override
+ public Pojo getModel() { return model; }
+
+ // NO @StrutsParameter — declared on the action, so the model
exemption must not cover it
+ public void setActionSecret(String actionSecret) { this.actionSecret =
actionSecret; }
+ public String getActionSecret() { return actionSecret; }
+
+ @StrutsParameter
+ public void setActionAllowed(String actionAllowed) {
this.actionAllowed = actionAllowed; }
+ public String getActionAllowed() { return actionAllowed; }
+
+ // Namesake of a model property, deliberately unannotated
+ private String shared;
+ public void setShared(String shared) { this.shared = shared; }
+ public String getShared() { return shared; }
+ }
+
+ public static class ReadOnlyShadowModel {
+ public String getShadow() { return "read-only"; }
+ }
+
+ public static class ModelActionWithReadOnlyModelProperty implements
ModelDriven<ReadOnlyShadowModel> {
+ private final ReadOnlyShadowModel model = new ReadOnlyShadowModel();
+ private String shadow;
+
+ @Override
+ public ReadOnlyShadowModel getModel() { return model; }
+
+ // NO @StrutsParameter — the model only reads "shadow", so a depth-0
parameter lands here
+ public void setShadow(String shadow) { this.shadow = shadow; }
+ public String getShadow() { return shadow; }
+ }
+
+ public static class BaseWithPublicField {
+ public String inheritedSecret;
+ }
+
+ public static class ModelActionInheritingPublicField extends
BaseWithPublicField implements ModelDriven<Pojo> {
+ private final Pojo model = new Pojo();
+
+ @Override
+ public Pojo getModel() { return model; }
+ }
+
+ public static class ModelInheritingPublicField extends
BaseWithPublicModelField {
+ }
+
+ public static class BaseWithPublicModelField {
+ public String inheritedModelField;
+ }
+
+ public static class ModelActionWithInheritingModel implements
ModelDriven<ModelInheritingPublicField> {
+ private final ModelInheritingPublicField model = new
ModelInheritingPublicField();
+
+ @Override
+ public ModelInheritingPublicField getModel() { return model; }
+ }
+
+ public static class ModelWithConstant {
+ public static final String constant = "not request surface";
+ }
+
+ public static class ModelActionWithConstantNamesake implements
ModelDriven<ModelWithConstant> {
+ private final ModelWithConstant model = new ModelWithConstant();
+ private String constant;
+
+ @Override
+ public ModelWithConstant getModel() { return model; }
+
+ // NO @StrutsParameter
+ public void setConstant(String constant) { this.constant = constant; }
+ public String getConstant() { return constant; }
+ }
+
public static class Pojo {
private String name;
+ private String shared;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
+ public String getShared() { return shared; }
+ public void setShared(String shared) { this.shared = shared; }
}
}