Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions source/core-developers/constant-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ In the [default.properties](default-properties) file, each entry is treated as a

In the [web.xml](web-xml) file, any FilterDispatcher initialization parameters are loaded as constants.

Since Struts 7.5.0 a package can override selected constants for its own actions, see
[Scoped constants](package-configuration#scoped-constants).

### Value substitution

Since Apache Struts 2.5.6 it is possible to use value substitution when defining `constant`s in `struts.xml` file.
Expand Down
114 changes: 114 additions & 0 deletions source/core-developers/package-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,117 @@ The optional `abstract` attribute creates a base package that can omit the actio
</package>
</struts>
```

## Scoped constants

Since Struts 7.5.0 a package can override selected [constants](constant-configuration) for its own actions with
the `<scoped-constant>` element. Packages that `extends` it inherit the override, and an override declared in

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two small additions could go here:

  • With several parents (extends="a,b"), the first listed parent wins on a conflict, the same rule as result types.
  • Packages built by the Convention plugin get overrides only by inheriting them from their XML parent package (@ParentPackage).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1d215b5. Both points are added. I checked the first-parent rule against the code: PackageConfig.Builder.addParent inserts at index 0, so the first package in extends is merged last and wins. For Convention I also mentioned struts.convention.default.parent.package, since it is the fallback when @ParentPackage is missing.

the child package wins. When a package extends several packages (`extends="a, b"`) and they override the same
constant, the first listed package wins, the same rule as for result types.

Packages created by the [Convention plugin](../plugins/convention/) have no `<scoped-constant>` of their own.
They only get overrides from their parent package, set with `@ParentPackage` or
`struts.convention.default.parent.package`.

The element requires the [struts-7.5.dtd](../dtds/struts-7.5.dtd) and must come before any other child of
`<package>`:

```xml
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 7.5//EN"
"https://struts.apache.org/dtds/struts-7.5.dtd">
<struts>
<package name="admin" extends="struts-default" namespace="/admin">
<scoped-constant name="some.scopable.constant" value="x"/>
...
</package>
</struts>
```

| Attribute | Required | Description |
|-----------|----------|-------------------------------------------|
| name | **yes** | the name of a scopable constant |
| value | **yes** | the value used for actions of the package |

Scoped constants are not a way to override an arbitrary constant per package. Constants are injected into
framework components once, at startup, so a package value only takes effect where a component explicitly reads
it per request. Such a component declares the constants it reads as _scopable_, and only those can appear
in `<scoped-constant>`.

{:.alert .alert-warning}
Struts core does not declare any constant as scopable yet. A `<scoped-constant>` only works with a constant
declared scopable by a plugin or by your own application code.

The configuration is validated at startup, and loading fails when:

- a package declares a constant that nothing has declared scopable. This also applies to abstract packages.
The error message lists the scopable constants that are available.
- a package declares the same constant more than once.

Values are used as they are. Unlike in `<constant>`, `${...}` [value substitution](constant-configuration#value-substitution)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accurate for the current code. Filed WW-5767 to apply the same load-time value substitution as <constant> (environment variables and system properties). That is not the per-request evaluation WW-3476 ruled out. If it lands before 7.5.0, this sentence goes away.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this as is for now. If WW-5767 lands before 7.5.0, the sentence will be replaced with a link to value substitution.

is not supported.

### Making a constant scopable

A component makes a constant scopable in two steps. First, register a bean that implements
`org.apache.struts2.config.ScopableConstants` and returns the constant names. You can register more than one
such bean. The scopable set is the union of all of them.

```java
import org.apache.struts2.config.ScopableConstants;

import java.util.Set;

public class MyScopableConstants implements ScopableConstants {
@Override
public Set<String> getNames() {
return Set.of("myplugin.theme");
}
}
```

```xml
<bean type="org.apache.struts2.config.ScopableConstants" name="myplugin"
class="com.example.MyScopableConstants"/>
```

Second, read the value per request through `org.apache.struts2.config.ScopedConstantProvider`, not through
an `@Inject`-ed constant:

```java
import org.apache.struts2.config.ScopedConstantProvider;
import org.apache.struts2.inject.Inject;

private ScopedConstantProvider scopedConstantProvider;

@Inject

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest showing the imports here, at least import org.apache.struts2.inject.Inject;. Readers tend to reach for jakarta.inject.Inject / javax.inject.Inject, which the Struts container ignores, so the setter would silently never be called. Adding the ScopableConstants / ScopedConstantProvider imports to the two snippets would help too.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1d215b5. Both snippets now show their imports, including org.apache.struts2.inject.Inject. An info note under the snippets explains that the container ignores jakarta.inject.Inject / javax.inject.Inject.

public void setScopedConstantProvider(ScopedConstantProvider scopedConstantProvider) {
this.scopedConstantProvider = scopedConstantProvider;
}

public String getTheme() {
return scopedConstantProvider.getValue("myplugin.theme");
}
```

{:.alert .alert-info}
Use `org.apache.struts2.inject.Inject`. The Struts container ignores `jakarta.inject.Inject` and
`javax.inject.Inject`, so the setter would never be called.

`getValue(name)` returns the value for the package of the action being executed, taking `extends` into account.
Outside an action invocation, or when no package in the hierarchy overrides the constant, it returns the global
constant, or `null` when that is not set. It throws `IllegalArgumentException` for a name that is not scopable.
The global value injected elsewhere is never changed.

Some constants can never be scoped, because they are read before the package of a request is known, for
example the [ActionMapper](action-mapper) or the multipart upload limits. To use a different mapper per URL space,
use the [PrefixBasedActionMapper](action-mapper#prefixbasedactionmapper) instead.

{:.alert .alert-danger}
Do not declare security-related constants scopable, such as `struts.parameters.requireAnnotations`,
`struts.enable.DynamicMethodInvocation` or `struts.devMode`. Otherwise a single package can relax a setting
meant to protect the whole application.

The default `ScopedConstantProvider` implementation can be replaced with the `struts.scopedConstantProvider`

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This currently holds for XML and properties configuration only. Java-based configuration cannot set struts.scopedConstantProvider yet, because ConstantConfig has no mapping for it (WW-5766, 7.5.0). Either add a short note, or update this when WW-5766 ships.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1d215b5. Added a note that the provider can be replaced only from XML or properties configuration for now. It will be removed when WW-5766 ships.

[extension point](../plugins/plugins-architecture#extension-points). For now this works only from XML or
properties configuration. Java-based configuration cannot set it yet.
158 changes: 158 additions & 0 deletions source/dtds/struts-6.5.dtd
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
-->
<!-- START SNIPPET: strutsDtd -->

<!--
Struts configuration DTD.
Use the following DOCTYPE

<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 6.5//EN"
"https://struts.apache.org/dtds/struts-6.5.dtd">
-->

<!ELEMENT struts ((package|include|bean|constant)*,bean-selection?, unknown-handler-stack?)>
<!ATTLIST struts
order CDATA #IMPLIED
>

<!ELEMENT package (result-types?, interceptors?, default-interceptor-ref?, default-action-ref?, default-class-ref?, global-results?, global-allowed-methods?, global-exception-mappings?, action*)>
<!ATTLIST package
name CDATA #REQUIRED
extends CDATA #IMPLIED
namespace CDATA #IMPLIED
abstract CDATA #IMPLIED
final CDATA #IMPLIED
strict-method-invocation (true|false) "true"
>

<!ELEMENT result-types (result-type+)>

<!ELEMENT result-type (param*)>
<!ATTLIST result-type
name CDATA #REQUIRED
class CDATA #REQUIRED
default (true|false) "false"
>

<!ELEMENT interceptors (interceptor|interceptor-stack)+>

<!ELEMENT interceptor (param*)>
<!ATTLIST interceptor
name CDATA #REQUIRED
class CDATA #REQUIRED
>

<!ELEMENT interceptor-stack (interceptor-ref*)>
<!ATTLIST interceptor-stack
name CDATA #REQUIRED
>

<!ELEMENT interceptor-ref (param*)>
<!ATTLIST interceptor-ref
name CDATA #REQUIRED
>

<!ELEMENT default-interceptor-ref (#PCDATA)>
<!ATTLIST default-interceptor-ref
name CDATA #REQUIRED
>

<!ELEMENT default-action-ref (#PCDATA)>
<!ATTLIST default-action-ref
name CDATA #REQUIRED
>

<!ELEMENT default-class-ref (#PCDATA)>
<!ATTLIST default-class-ref
class CDATA #REQUIRED
>

<!ELEMENT global-results (result+)>

<!ELEMENT global-allowed-methods (#PCDATA)>

<!ELEMENT global-exception-mappings (exception-mapping+)>

<!ELEMENT action ((param|result|interceptor-ref|exception-mapping)*,allowed-methods?)>
<!ATTLIST action
name CDATA #REQUIRED
class CDATA #IMPLIED
method CDATA #IMPLIED
converter CDATA #IMPLIED
>

<!ELEMENT param (#PCDATA)>
<!ATTLIST param
name CDATA #REQUIRED
>

<!ELEMENT result (#PCDATA|param)*>
<!ATTLIST result
name CDATA #IMPLIED
type CDATA #IMPLIED
>

<!ELEMENT exception-mapping (#PCDATA|param)*>
<!ATTLIST exception-mapping
name CDATA #IMPLIED
exception CDATA #REQUIRED
result CDATA #REQUIRED
>

<!ELEMENT allowed-methods (#PCDATA)>

<!ELEMENT include (#PCDATA)>
<!ATTLIST include
file CDATA #REQUIRED
>

<!ELEMENT bean (#PCDATA)>
<!ATTLIST bean
type CDATA #IMPLIED
name CDATA #IMPLIED
class CDATA #REQUIRED
scope CDATA #IMPLIED
static CDATA #IMPLIED
optional CDATA #IMPLIED
>

<!ELEMENT bean-selection (#PCDATA)>
<!ATTLIST bean-selection
name CDATA #IMPLIED
class CDATA #IMPLIED
>

<!ELEMENT constant (#PCDATA)>
<!ATTLIST constant
name CDATA #REQUIRED
value CDATA #REQUIRED
>

<!ELEMENT unknown-handler-stack (unknown-handler-ref*)>
<!ELEMENT unknown-handler-ref (#PCDATA)>
<!ATTLIST unknown-handler-ref
name CDATA #REQUIRED
>

<!-- END SNIPPET: strutsDtd -->

Loading