fix/issue 222#230
Conversation
Jakubk15
commented
Jul 2, 2026
- fix: strip null/empty item stacks from parcel content (Exception in future due to NPE #221)
- fix: stop using legacy ItemStack serialization for parcel content
- Update src/main/java/com/eternalcode/parcellockers/content/ParcelContent.java
- test: cover null items list in ParcelContent guard
- fix: guard against null parcel description in AdminParcelEditGui
- fix: restrict parcel collection to destination locker (Every parcel can be collected from every locker #222)
- refactor: filter collectible parcels in the query, not after paging
Empty/air inventory slots could slip past the GUI write filters and round-trip through the persister as null elements, causing a NullPointerException when CollectionGui read itemStack.getType() on a collected parcel. Normalize the item list in the ParcelContent record's compact constructor so null and empty stacks are dropped and the exposed list is immutable. This is the single chokepoint every read and write path goes through, so it heals already-corrupted rows on read and prevents bad writes. Adds Mockito to mock ItemStack in unit tests, since paper-api ItemStacks cannot be constructed without a running server (RegistryAccess). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R4YqhTV42FvN95HanC7XJc
Legacy (Spigot map) ItemStack serialization drops empty/air stacks to null, the upstream cause of the issue #221 NPE. The jackson-bukkit Paper module defaults to an NBT byte-array format (ItemStack#serializeAsBytes) that round-trips empties safely, and its deserializer auto-detects the format, so previously stored legacy-format rows still read correctly and are rewritten in the new format on next save. The ParcelContent null/empty guard remains as defense in depth. Note: the serialization round-trip cannot be exercised by the existing test harness (paper-api ItemStacks require a running server, and the integration tests are Docker-gated), so this relies on the library's documented backward-compatible auto-detection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R4YqhTV42FvN95HanC7XJc
…ent.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Adds a case verifying the compact constructor defaults a null items list to an empty list, closing the coverage gap on the defensive guard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R4YqhTV42FvN95HanC7XJc
A parcel description may legitimately be null (SendingGui stores a blank description as null). AdminParcelEditGui.button() passed the value straight to String.replace(), which throws NPE when the replacement is null, crashing the admin edit GUI on InventoryClickEvent. Coerce null placeholder values to an empty string via a testable nullToEmpty() helper and cover it with unit tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Egi4whB4fVKQL3XH4PpkX9
Parcels were collectible from any locker because CollectionGui listed every DELIVERED parcel of the receiver regardless of the locker opened. Filter the collection list to parcels destined for the current locker, guarded by the new PluginConfig.Settings#allowCollectingFromAnyLocker flag (default false) which restores the legacy collect-anywhere behavior. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R4YqhTV42FvN95HanC7XJc
CollectionGui paged receiver parcels (28/page) then filtered DELIVERED and destination-locker client-side, so a page could render fewer than its size and the has-next probe counted ineligible rows. Move the receiver + DELIVERED (+ optional destination locker) filter into the ORMLite query via findCollectible, so pagination operates on the eligible set. Drops the now redundant client-side filters and the unused Parcel#isDestinedFor helper; adds a Docker-gated integration test for the paged query. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R4YqhTV42FvN95HanC7XJc
There was a problem hiding this comment.
Code Review
This pull request introduces a new configuration option allowCollectingFromAnyLocker to control whether players can collect parcels from any locker or only their designated destination locker. It refactors parcel retrieval to filter collectible parcels (status DELIVERED and matching locker, if restricted) directly in the database query rather than in-memory, improving pagination consistency. Additionally, it addresses NPE issues (such as Issue #221) by filtering out null/empty items in ParcelContent and removing legacy item stack serialization in ItemStackPersister, and adds a helper to handle null placeholders in AdminParcelEditGui. Feedback is provided regarding a potential logic bypass in GuiManager#getCollectibleParcels where a null currentLocker could unintentionally bypass the locker restriction when allowCollectingFromAnyLocker is false.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| public CompletableFuture<PageResult<Parcel>> getCollectibleParcels(UUID receiver, UUID currentLocker, Page page) { | ||
| UUID destinationLocker = this.allowCollectingFromAnyLocker ? null : currentLocker; | ||
| return this.parcelService.getCollectible(receiver, destinationLocker, page); | ||
| } |
There was a problem hiding this comment.
If allowCollectingFromAnyLocker is false but currentLocker is null, destinationLocker will be set to null. In ParcelRepositoryOrmLite#findCollectible, a null destination locker bypasses the locker restriction filter entirely, allowing the player to collect parcels from any locker. To prevent this security/logic bypass, we should ensure that a non-existent/dummy UUID is used when currentLocker is null and restriction is active.
| public CompletableFuture<PageResult<Parcel>> getCollectibleParcels(UUID receiver, UUID currentLocker, Page page) { | |
| UUID destinationLocker = this.allowCollectingFromAnyLocker ? null : currentLocker; | |
| return this.parcelService.getCollectible(receiver, destinationLocker, page); | |
| } | |
| public CompletableFuture<PageResult<Parcel>> getCollectibleParcels(UUID receiver, UUID currentLocker, Page page) { | |
| UUID destinationLocker = this.allowCollectingFromAnyLocker ? null : (currentLocker != null ? currentLocker : new UUID(0L, 0L)); | |
| return this.parcelService.getCollectible(receiver, destinationLocker, page); | |
| } |
Addresses PR #230 review: getCollectibleParcels passed currentLocker straight through, and getCollectible/findCollectible treat a null destination locker as "any locker". A null currentLocker while allowCollectingFromAnyLocker is false would therefore silently drop the locker restriction (fail-open authorization bypass). Not reachable today (the locker UUID always comes from a real Locker via LockerInteractionController), but the invariant was implicit. Make it explicit and fail-closed with Objects.requireNonNull, and only pass null on the deliberate collect-from-any-locker path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UWGThsFyex5LiEyN63L3Wu
…rcelLockers into fix/issue-222
|
@claude review |
|
Claude finished @Jakubk15's task in 1m 22s —— View job Review of fix/issue-222 (merged)Todo list:
Gemini's flagged bug — resolved correctlyGemini flagged that a public CompletableFuture<PageResult<Parcel>> getCollectibleParcels(UUID receiver, UUID currentLocker, Page page) {
if (this.allowCollectingFromAnyLocker) {
return this.parcelService.getCollectible(receiver, null, page);
}
Objects.requireNonNull(currentLocker,
"currentLocker must not be null when collection is restricted to the destination locker");
return this.parcelService.getCollectible(receiver, currentLocker, page);
}Failing fast with I traced the only caller path ( Rest of the diff
No further issues found. Nice fix — the Branch · |