Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

395. A resource is opened to whoever reads it

Date: 2026-09-18

Status

Accepted. Fixes the showcase’s Canvas screen and gives ImageSource.resource the diagnostic Stylesheet.resource already had.

Amends ADR-0093, whose “A bare opens in the application’s module” says the showcase opens the package “to the core module only”. That was right when the only resource being read was a stylesheet and is wrong now: a package is opened to whoever reads what is in it, and for an image that is :widgets.

Follows ADR-0387, which is the same fact — a resource directory is a package — biting from the other side.

Context

The showcase logged this, four times:

WARN ImageState - image resource:…example.ui.CanvasScreen:canvas-sample.jpg did not load:
  java.io.IOException: no image resource "canvas-sample.jpg" at resource:…:canvas-sample.jpg

The file is there. example/src/main/resources/io/github/digitalsmile/goldberry/example/ui/canvas-sample.jpg has been there all along, and DeclaredResourcesTest asserts it is.

JPMS encapsulates resources, and only on the module path. A file in a package of a named module is invisible to other modules unless the package is opens — exports does not do it, because it governs types rather than bytes. The showcase knew that and said so in its module-info, and then got the target wrong:

opens io.github.digitalsmile.goldberry.example.ui to io.github.digitalsmile.goldberry.core;

Its own comment explained the choice: the package is opened “to whoever loads them, which is exactly one module”, and ADR-0093 said the same. That was true when the only thing being read was a stylesheet. It stopped being true when the Canvas screen grew an image card, because ImageSource.Resource.load is :widgets’ code, and a package opened to :core is closed to :widgets.

Three things then went wrong at once, and each is worth fixing on its own.

Decision

The package opens to both modules that read it

One line, and a corrected comment above it. :core parses the stylesheet and the markup; :widgets loads the picture; the package names both.

Verified on the module path in both directions, because a classpath test cannot see this at all:

  • with the open, ImageSource.resource(CanvasScreen.class, "canvas-sample.jpg").load() returns a 96 × 64 image;
  • with it reverted, it fails — with the message below.

The failure says what is wrong instead of what is missing

“No image resource” is a true sentence and a misleading one: it sends somebody looking for a file that is sitting exactly where they put it. Stylesheet.resource had already learned to tell the two cases apart, and ImageSource.Resource now does the same:

the image resource "canvas-sample.jpg" at resource:…CanvasScreen:canvas-sample.jpg
is encapsulated: module io.github.digitalsmile.goldberry.example does not open
io.github.digitalsmile.goldberry.example.ui to io.github.digitalsmile.goldberry.widgets,
and JPMS encapsulates resources as well as classes. Add `opens
io.github.digitalsmile.goldberry.example.ui to io.github.digitalsmile.goldberry.widgets;`
to its module-info — the file itself may well be there.

It names the module that must be opened to, computed from where the reading happens rather than written down, so it stays right if the loader ever moves.

A broken source is reported once

One picture drawn at four fit values is four ImageViews, four loads and four identical lines about one file. ImageState now keeps a bounded set of the source keys it has complained about — OverflowLog’s argument and OverflowLog’s answer, down to the 256-entry cap.

Keyed on the source, not the view: that is what failed, and two views of one key share a cache entry, so the reason cannot differ between them.

A test that can see what the suite cannot

This is the part worth keeping. Every test in this repository passes with the opens wrong, because tests run on the class path where nothing is encapsulated. The bug was invisible to the suite and obvious in the application.

OpenResourcePackagesTest therefore reads the repository rather than the running JVM: it walks src/main/resources, derives the package each file is in, and asserts that a package holding an image opens to :widgets and a package holding a stylesheet or markup opens to :core. DeclaredResourcesTest is its sibling and exists for the same reason — a native image’s missing resource is another failure a passing suite cannot see.

Checked by reverting the fix: the test fails, and its message names the package and the module to add.

Consequences

The Canvas screen draws its sample. On the module path, which is where it did not.

An application that hits this is told how to fix it. The message is long, and deliberately: the reader is looking at a file that exists and being told it does not, and nothing shorter closes that gap.

The rule is now stated where an application can copy it. “Open the package to whoever reads it” is a sentence somebody has to get right per package and per module, and the showcase is the worked example. :html needs no open because it takes an ImageSource the application supplies and reads nothing itself.

The test is the showcase’s, not the toolkit’s. It encodes which toolkit module reads which kind of file, which is a fact about the toolkit — so it will need amending if a third module ever reads an application’s resources. That is a cheap price for a check that catches a whole class of module-path-only failure, and the alternative is a rule that lives only in prose.