Skip to content

WW-5827 check the preparer type before instantiating it and do not cache failed preparer lookups - #2084

Open
arunmanni-ai wants to merge 3 commits into
apache:mainfrom
arunmanni-ai:WW-5827-preparer-type-check
Open

arunmanni-ai wants to merge 3 commits into
apache:mainfrom
arunmanni-ai:WW-5827-preparer-type-check

Conversation

@arunmanni-ai

Copy link
Copy Markdown
Contributor

Fixes WW-5827

Addresses the two items in the ticket.

ClassUtil instantiates before checking the type

getClass(String, Class) resolved with initialize=true, so the named class ran its static initialiser on lookup, and instantiate(String, boolean) passed Object.class as the base type, which left the asSubclass check unable to fail. BasicPreparerFactory cast to ViewPreparer only afterwards, so a configured name that does not denote a ViewPreparer was loaded, initialised and constructed before it was rejected.

The class is now resolved without being initialised, and a new instantiate(String, Class<T>, boolean) applies the caller's base type before the constructor runs, so a name that does not conform is rejected without anything being constructed.

A name that resolves but does not conform is reported as CannotInstantiateObjectException rather than returned as null. Returning null here would be swallowed by the ignoreMissing path in BasicTilesContainer.prepare, turning a misconfigured preparer into a silent no-op; the exception keeps the failure as visible as the ClassCastException it replaces. returnNull continues to mean "class not found" only. Resolution and construction are caught separately, so an exception thrown by the constructor itself is never reported as a type mismatch.

The untyped instantiate methods are unchanged; TilesDispatchServlet is the only other caller and is migrated to the typed one.

BasicPreparerFactory caches failed lookups in a non-thread-safe map

getPreparer stored whatever createPreparer returned, including null, which containsKey then reported as resolved. A single failure became permanent for the lifetime of the factory even when its cause was transient. Failed lookups are no longer recorded, and the map is a ConcurrentHashMap, since it is read and written by request threads. get/putIfAbsent rather than computeIfAbsent, so class loading does not run while a bin of the map is locked.

Tests

mvn test -DskipAssembly -pl plugins/tiles — 572 tests, 0 failures.

New BasicPreparerFactoryTypeCheckTest covers rejection before initialisation and construction, non-retention of a name that cannot be resolved, and that a genuine ViewPreparer is still created and cached. The evidence flags live in a separate holder class, because reading a static member of the marker class would itself initialise it. Four cases added to ClassUtilTest for the typed overload.

PatternUtilTest is unchanged.

…che failed preparer lookups

ClassUtil.getClass() resolved a class with initialize=true, so the named
class ran its static initialiser on lookup, and instantiate(String, boolean)
passed Object.class as the base type, which left the asSubclass check unable
to fail. BasicPreparerFactory therefore cast to ViewPreparer only after the
named class had been loaded, initialised and constructed.

The class is now resolved without being initialised, and a typed
instantiate(String, Class, boolean) applies the caller's base type before the
constructor runs, so a class that does not denote a ViewPreparer is rejected
without anything being constructed. A name that resolves but does not conform
is reported as CannotInstantiateObjectException rather than returned as null,
which keeps the failure as visible as the ClassCastException it replaces;
returnNull continues to mean "class not found" only. Resolution and
construction are caught separately, so an exception thrown by the constructor
itself is never reported as a type mismatch.

BasicPreparerFactory.getPreparer() stored the result of createPreparer()
unconditionally, so a null was retained and thereafter reported as resolved by
containsKey(). That made a single failure permanent for the lifetime of the
factory even when its cause was transient. Failed lookups are no longer
recorded, and the map is a ConcurrentHashMap, since it is read and written by
request threads.

TilesDispatchServlet is the only other caller of the untyped helper and is
migrated to the typed one.
@arunmanni-ai

Copy link
Copy Markdown
Contributor Author

Hi @lukaszlenart ,

One scope question. In a Struts deployment StrutsPreparerFactory.createPreparer builds the preparer through the ObjectFactory and casts to ViewPreparer afterwards, so the type check in this PR is not reached there — and when the cast fails, catch (Exception) falls back to super.createPreparer, which constructs the class a second time. That is the double construction you mentioned; it is not in the ticket description.

Two things would fold into the same change. The fallback logs at ERROR with a full stack trace on every call, and now that failed lookups are no longer cached, a misconfigured preparer name logs that per request rather than once.

Should both go in this PR, or would you rather have a separate ticket? Happy either way — I just didn't want to widen this one without asking.

@lukaszlenart lukaszlenart left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks, this looks good: the scope matches WW-5827 and the tests cover both items. Two small things inline, neither blocking.

@arunmanni-ai

Copy link
Copy Markdown
Contributor Author

Dropped the inline comments in both files and the two “Prefer instantiate(String, Class, boolean)” paragraphs from the untyped methods’ javadoc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants