jakartaee / jakartaee/faces

Let a tag handler reproduce its build time decision

Open
#2,240 4 comments 0 reactions 0 assignees View on GitHub
Dominant language
Java
Stars
127
Forks
59
Avg merge
23h
Merged PRs (30d)
7

Description

#2234 asks what the build which restores a view reproduces, and #2235 answers it for the constructs an implementation provides: the test of a conditional, the branch of a choice, the range or the items of an iteration, the path of a dynamic inclusion. A `TagHandler` outside the implementation which decides what it builds the same way is not covered, and cannot be, because there is nowhere for it to write down what it decided.

The mechanism exists on both sides and neither exposes it. MyFaces ships it: `ComponentSupport.saveInitialTagState` and `restoreInitialTagState` in `org.apache.myfaces.view.facelets.tag.faces`, writing into a `FaceletState` held on the view root. Mojarra has it on the branch of eclipse-ee4j/mojarra#5965, as `SavedBuildTimeDecisions`, keyed by the id a tag generates for the build and carried in the state map, behind a context parameter which is off by default. Both are implementation packages, so a handler can only participate by compiling against one implementation, which is not participation at all.

The obvious workaround does not work either, which is worth recording so nobody spends an afternoon on it. Writing the decision into the attributes of the view root looks portable, since those are saved with the state, but under partial state saving the tree is built first and per-component state is applied after, so at the moment `apply` runs there is nothing there to read. That ordering is precisely why Mojarra seeds its map from the raw state map before the build rather than from the view root, and why MyFaces lifts the `FaceletState` out in its state management strategy.

The failure mode for an application is a silently dropped submitted value: the handler rebuilds a different subtree than the one that was rendered, the state saved for a component in it is restored into nothing, and a value submitted for that component is decoded by nothing.

## Proposal

Three protected methods on `jakarta.faces.view.facelets.TagHandler`, so that a handler reaches its own decision and no other:

```java
/**
* Save what this handler decided with the state of the view, so that the build which restores this view reproduces
* what this build produced.
*/
protected void saveBuildTimeDecision(FaceletContext context, Serializable decision);

/**
* What this handler decided in the build which rendered the view, or null where this build is not the one which
* restores that view, or nothing was saved.
*/
protected Serializable restoreBuildTimeDecision(FaceletContext context);

/**
* What this handler decided in the build which rendered the view, of the given type, or null where there is nothing
* to restore or what is there is of another type.
*/
protected T restoreBuildTimeDecision(FaceletContext context, Class type);
```

The key is implicit, which is the point of putting this on the handler: a portable handler has no way to compute one that is stable across builds. What the implementation owes for it:

1. Unique per tag, and stable across the builds of one view, so that the build which restores a view finds what the build which rendered it wrote.
2. Generated anew per round of an enclosing iteration, so that a decision nested in a `c:forEach` replays the value it had in its own round. Without this the facility is useless where it is needed most.
3. `restoreBuildTimeDecision` returns non-null only during the build which restores the view — not on an initial request, and not during the build which precedes Render Response, which is the build that follows the model.
4. Where the decision replayed by the restoring build differs from what the build preceding Render Response evaluates, the state of the subtree the handler builds is the implementation's to settle, not the handler's.

Those make the natural idiom correct without the handler knowing which build it is in:

```java
Boolean rendered = restoreBuildTimeDecision(context, Boolean.class);
boolean test = rendered != null ? rendered : evaluate(context);
saveBuildTimeDecision(context, test);
```

The typed accessor is not a convenience. A key names a decision only for as long as the facelet is the one that generated it: an edit shifts what every id after it names, so a handler reading back whatever stands under its key gets another handler's decision and casts it into a `ClassCastException` on the first postback after the edit. A handler must read back what it saved rather than what is there.

`Serializable` rather than `Object` in the signature puts the constraint where the author can see it. Discovering it while the state is written is a poor diagnostic, and far from the tag which caused it. Two limits belong in the javadoc rather than in the reader's assumptions: the check stops at the top level, since a `HashMap` is `Serializable` whatever it holds, and it deliberately excludes what an attached object would carry — a `StateHolder`, a `ValueExpression`. A decision is a value, not an attached object.

`save` and `restore` are the verbs of the API this joins — `StateHolder`, `UIComponentBase.saveAttachedState`, `StateManagementStrategy` — and this is state saving. `get` and `set` would read as a property of the handler instance, which it is not: a handler is shared across views and requests, the decision lives in the state of one view, and what is set is not what is read back in the same build.

Against a `FaceletState` shaped type in the signature: a container is what the implementations hold, not what a handler needs. Handing one out means handing out the key, which a portable handler cannot compute, and lets one handler read or overwrite the decision of another. The value type is the only thing the API has to name.

#2235 calls what a build evaluates a build time condition. The value such a condition produces is the decision, and one sentence there saying so keeps the two from reading as separate concepts.

## Scope

This is the decision axis only. Acting on the components a handler's own body just created is a separate omission — a body-wrapping handler has no way to ask what it just built, and reaches for `ComponentHandler.isNew` on the enclosing component instead, which is true only in the build which creates that component. It deserves its own accessor and its own discussion.

#2235 has the build which restores a view reproduce what the constructs an implementation provides produced. A handler an application writes decides the same thing and owes the same guarantee, so the storage is owed whenever a handler asks for it. It is cheap: nothing is carried for a view whose handlers ask for nothing.

Suggest prototyping in Mojarra first, over the mechanism eclipse-ee4j/mojarra#5965 adds there, so the key semantics are exercised before the signature is fixed. That prototype has one thing to settle beyond the signature: `BuildTimeDecisions` there already names the request scoped mechanism of eclipse-ee4j/mojarra#5960, which records what a build decided to prove that re-applying the facelet would reproduce it. Two mechanisms under one noun, one of them public, is one too many.

🤖 Generated with Claude Opus 5

Contributor guide

Open the contributing guide

Research direction

Start with jakarta.faces.view.facelets.TagHandler and the Mojarra SavedBuildTimeDecisions mechanism from eclipse-ee4j/mojarra#5965. Prototype the key semantics in Mojarra first, including initial, restoring, and iteration builds, then determine how the public API and implementation behavior should be finalized.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
backend-api-design
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.