Skip to content

Fix suggested groups after library load - #16552

Open
Siedlerchr wants to merge 3 commits into
mainfrom
agent/fix-suggested-groups-search-context
Open

Fix suggested groups after library load#16552
Siedlerchr wants to merge 3 commits into
mainfrom
agent/fix-suggested-groups-search-context

Conversation

@Siedlerchr

Copy link
Copy Markdown
Member

Summary

Fixes the failure when “Add JabRef suggested groups” is selected immediately after a library has loaded. The loading placeholder and parsed context can compare equal, preventing the group tree from being refreshed; the active context is now deliberately replaced after the search context is initialized.

jabref-contrib-policy:4.2:reviewed​:ok

Steps to test

  1. Start JabRef with a .bib library, including a large library.
  2. In the Groups pane, right-click “All entries” and select “Add JabRef suggested groups”.
  3. Confirm suggested groups are added without a No SearchContext registered assertion.

Validated manually with the generated large library. Normal tab changes retain their single active-database notification.

Related issues and pull requests

No matching issue found.

AI usage

OpenAI Codex (model GPT-5). The contributor reviewed, understood, and takes ownership of the change.

AI CHECKLIST.md walkthrough

1. Code self-review

Nullability and control flow
  • No == null / != null checks — JSpecify annotations (@NullMarked, @Nullable, @NonNull) used instead.
  • No Objects.requireNonNull(...) — nullability expressed via JSpecify annotations.
  • [/] New classes annotated with @NullMarked (org.jspecify.annotations.NullMarked). No production class was added; the test class follows existing package conventions.
  • Optional consumed with ifPresent / ifPresentOrElse / map / orElseThrow — never orElse(unusedValue) nor an isPresent() + get() block.
  • StringUtil.isBlank(...) used instead of s == null || s.isBlank().
Exceptions
  • No catch (Exception e) — only specific exceptions are caught.
  • No throw new RuntimeException(...) / IllegalStateException(...) — these tear down the whole application.
  • Logged exceptions are passed as the last logger argument (LOGGER.info("...", e)), not concatenated into the message string.
Style and idioms
  • [/] New BibEntry objects built with withers (withField, not setField). The test creates an empty entry without fields.
  • Modern Java used: List.of() / Map.of() / Set.of(), Path.of(), SequencedCollection / SequencedSet, text blocks.
  • Regexes use a precompiled Pattern.compile(...) constant, not String.matches(...).
  • Background work uses org.jabref.logic.util.BackgroundTask, not new Thread().
  • No commented-out code, no trivial comments restating the code, no AI-disclosure comments in source.
  • Markdown Javadoc (///) uses Markdown syntax, not JavaDoc inline tags: `code` instead of {@code}, and [ClassName] instead of {@link}.
User-facing text
  • [/] All user-facing text localized (Localization.lang in Java, % prefix in FXML). No UI text was added.
  • [/] Sentence case (not Title Case); no trailing !; labels do not end with :. No UI text was added.
  • [/] Variance expressed with placeholders ("...: %0"), not string concatenation. No UI text was added.
Security
  • [/] User-controlled data is HTML-escaped before being written into any text/html response. No HTML response was changed.
Tests
  • [/] Behavior changes in org.jabref.model / org.jabref.logic have added or updated tests. The behavior change is in the GUI/state manager.
  • Tests assert object contents (assertEquals), use plain JUnit asserts, have no @DisplayName, do not catch exceptions, and use @TempDir instead of manual temp directories.

2. Verification commands

  • ./gradlew :jablib:check
  • ./gradlew checkstyleMain checkstyleTest checkstyleJmh
  • ./gradlew modernizer
  • ./gradlew --no-configuration-cache :rewriteDryRun
  • ./gradlew javadoc
  • npx markdownlint-cli2 "docs/**/*.md" "*.md"
  • [/] IntelliJ formatter. rewriteDryRun reported no changes.

3. Documentation

  • CHANGELOG.md entry added for this user-visible fix, with TODO awaiting the PR number.
  • Searched JabRef and jabref-koppor issues; no confident matching issue found.
  • [/] Requirement not added: this is a focused bug fix.
  • [/] Developer documentation not changed: implementation behavior is internal and self-contained.

4. Pull request

  • PR body built from .github/PULL_REQUEST_TEMPLATE.md, every section filled.
  • All checklist items kept and marked [x], [ ], or [/].
  • All HTML comments removed from the PR body.
  • PR created with gh pr create --body-file.
  • The TODO changelog reference will be replaced with the real PR link immediately after creation, then committed and pushed.

Checklist

  • I own the copyright of the code submitted and I license it under the MIT license
  • If AI tools were used, I disclosed them in the "AI usage" section and reviewed, understood, and take full ownership of all AI-generated code
  • I manually tested my changes in running JabRef (always required)
  • I added JUnit tests for changes (if applicable)
  • [/] I added screenshots in the PR description (if change is visible to the user). This is an error-handling fix with no UI change.
  • [/] I added a screenshot in the PR description showing a library with a single entry with me as author and as title the issue number. No related issue exists.
  • I described the change in CHANGELOG.md in a way that can be understood by the average user (if change is visible to the user)
  • [/] I checked the user documentation for up to dateness and submitted a pull request to the user documentation repository. This does not change documented user behavior.

@Siedlerchr
Siedlerchr marked this pull request as ready for review August 12, 2026 20:03
@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Fix suggested groups failing right after library load

🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Force an active-database change notification after library load to refresh group UI state.
• Ensure SearchContext is registered before exposing databases to avoid missing-context assertions.
• Add regression tests and document the fix in the changelog.
Diagram

graph TD
  LT["LibraryTab"] --> RAD["replaceActiveDatabase()"] --> ADBP(["activeDatabaseProperty"]) --> LSN["UI/group listeners"] --> GRF["Group tree refresh"]
  SRV["JabRefSrvStateManager"] --> RSC["registerSearchContext()"] --> ODB["openDatabases exposed"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Make loading placeholder context not equal to loaded context
  • ➕ Avoids needing a new StateManager API method
  • ➕ Keeps active-database updates as a single setter call
  • ➖ Requires changing BibDatabaseContext equality/UID semantics or placeholder construction
  • ➖ Higher risk of unintended side effects across model/logic relying on current equality behavior
2. Fire explicit 'library loaded' / 'context replaced' event instead of toggling Optional
  • ➕ More semantically precise than forcing a property change via empty+set
  • ➖ Introduces a new event channel and more wiring for existing listeners
  • ➖ Bigger refactor than needed for a narrow regression
3. Always clear active database via setActiveDatabase(null) before setting loaded context
  • ➕ No interface expansion if done locally in LibraryTab
  • ➖ Less intention-revealing API; encourages ad-hoc workarounds at call sites
  • ➖ May increase null-handling patterns across callers

Recommendation: Current approach is a good, minimal-risk fix: a clearly named replaceActiveDatabase makes the intent explicit (force change notification even when contexts compare equal) and keeps the workaround centralized in the StateManager implementation. The jabsrv constructor reorder is also aligned with the documented lifecycle invariant (register SearchContext before exposing databases).

Files changed (7) +62 / -6

Bug fix (4) +12 / -4
JabRefGuiStateManager.javaAdd active-database replacement to force listener notification +6/-0

Add active-database replacement to force listener notification

• Introduces 'replaceActiveDatabase', which clears the active database and then sets the provided context to ensure observers see a change even when contexts compare equal.

jabgui/src/main/java/org/jabref/gui/JabRefGuiStateManager.java

LibraryTab.javaReplace active database after load to refresh group-related state +3/-3

Replace active database after load to refresh group-related state

• Moves adding the loaded context to 'openDatabases' to after component/listener initialization, and uses 'replaceActiveDatabase' for the selected tab so dependent UI refreshes reliably after load.

jabgui/src/main/java/org/jabref/gui/LibraryTab.java

StateManager.javaExpose 'replaceActiveDatabase' on the GUI StateManager API +2/-0

Expose 'replaceActiveDatabase' on the GUI StateManager API

• Extends the 'StateManager' interface with a dedicated method to replace the active database context (as distinct from a simple set) for notification semantics.

jabgui/src/main/java/org/jabref/gui/StateManager.java

JabRefSrvStateManager.javaRegister SearchContext before exposing databases in server state manager +1/-1

Register SearchContext before exposing databases in server state manager

• Reorders initialization so 'registerSearchContext' happens before adding a parsed database context to 'openDatabases', preventing callers from observing a context without a registered SearchContext.

jabsrv/src/main/java/org/jabref/http/JabRefSrvStateManager.java

Tests (1) +48 / -0
JabRefGuiStateManagerTest.javaAdd regression tests for active-database replacement notifications +48/-0

Add regression tests for active-database replacement notifications

• Adds tests asserting that replacing an equal-comparing context triggers two notifications (empty then new), while switching to a genuinely different context notifies only once.

jabgui/src/test/java/org/jabref/gui/JabRefGuiStateManagerTest.java

Documentation (2) +2 / -2
CHANGELOG.mdDocument fix for suggested-groups action after library load +1/-0

Document fix for suggested-groups action after library load

• Adds a Fixed entry noting that adding suggested groups immediately after a library loads no longer errors, linking to the PR.

CHANGELOG.md

SrvStateManager.javaAlign SearchContext contract documentation with lifecycle behavior +1/-2

Align SearchContext contract documentation with lifecycle behavior

• Updates the 'getSearchContext' documentation to emphasize the invariant without describing a specific exception type.

jabsrv/src/main/java/org/jabref/http/SrvStateManager.java

@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (1) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. replaceActiveDatabase nullability unclear 📘 Rule violation ≡ Correctness
Description
The new replaceActiveDatabase(BibDatabaseContext database) public API has no explicit JSpecify
nullability contract, but the implementation calls Optional.of(database), which will throw an NPE
if null is ever passed. This violates the requirement to enforce explicit nullability in new
public APIs and avoid ambiguous null contracts.
Code

jabgui/src/main/java/org/jabref/gui/JabRefGuiStateManager.java[R168-171]

+    public void replaceActiveDatabase(BibDatabaseContext database) {
+        activeDatabaseProperty().set(Optional.empty());
+        activeDatabaseProperty().set(Optional.of(database));
+    }
Evidence
PR Compliance ID 7 requires explicit nullability for new public APIs. The PR introduces
replaceActiveDatabase(...) without nullability annotations in the public StateManager interface,
while the implementation uses Optional.of(database), which assumes database is non-null but does
not encode/enforce that contract via JSpecify.

AGENTS.md: Enforce explicit nullability via JSpecify (no null returns in new public APIs; @NullMarked on new classes; do not use Objects.requireNonNull)
jabgui/src/main/java/org/jabref/gui/StateManager.java[65-68]
jabgui/src/main/java/org/jabref/gui/JabRefGuiStateManager.java[168-171]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A new public method `replaceActiveDatabase(BibDatabaseContext database)` was added without explicit JSpecify nullability, yet the implementation uses `Optional.of(database)` which is unsafe if `database` can ever be `null`.

## Issue Context
The project requires explicit nullability via JSpecify for new public APIs. Here, the method contract should clearly guarantee non-null (e.g., via `@NonNull` or `@NullMarked` scope) or explicitly accept nullable and handle it safely.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/StateManager.java[65-68]
- jabgui/src/main/java/org/jabref/gui/JabRefGuiStateManager.java[168-171]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Transient active-db empty 🐞 Bug ☼ Reliability
Description
LibraryTab#setDatabaseContext now unconditionally calls replaceActiveDatabase, which forces an
intermediate Optional.empty() active-database event even when the old/new contexts are not equal.
This can cause subscribers (e.g., groups and git status) to clear/reset and then rebuild
immediately, creating avoidable churn and potential UI flicker.
Code

jabgui/src/main/java/org/jabref/gui/LibraryTab.java[R380-382]

        if (isSelectedTab) {
-            stateManager.setActiveDatabase(bibDatabaseContext);
+            stateManager.replaceActiveDatabase(bibDatabaseContext);
            stateManager.activeTabProperty().set(Optional.of(this));
Evidence
replaceActiveDatabase explicitly publishes an intermediate empty active database value, and
multiple listeners interpret an empty active database as “no database” and reset/clear state;
calling it unconditionally therefore introduces extra reset/rebuild work beyond the equal-context
edge case.

jabgui/src/main/java/org/jabref/gui/JabRefGuiStateManager.java[159-171]
jabgui/src/main/java/org/jabref/gui/LibraryTab.java[357-383]
jabgui/src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java[163-186]
jabgui/src/main/java/org/jabref/gui/git/GitStatusViewModel.java[48-57]
jabgui/src/main/java/org/jabref/gui/LibraryTab.java[213-217]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`StateManager.replaceActiveDatabase(...)` intentionally emits two active-database values (`Optional.empty()` then the new context) to force downstream notifications when contexts compare equal. However, `LibraryTab#setDatabaseContext` currently calls it unconditionally for the selected tab, meaning *all* subscribers see a transient “no active database” even when a normal `setActiveDatabase(...)` would notify correctly.

### Issue Context
This transient empty state triggers reset/clear logic in multiple subscribers (groups tree, git status, etc.), which is only required in the special “old equals new” case the PR is fixing.

### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/LibraryTab.java[357-385]

### Suggested change
In `setDatabaseContext`, only call `replaceActiveDatabase` when the currently active database is present and `equals(...)` the new `bibDatabaseContext` (the problematic case). Otherwise call `setActiveDatabase(bibDatabaseContext)` to avoid the intermediate empty event.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can enable the Remediation agent and Qodo fixes findings in a dedicated fix PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread jabgui/src/main/java/org/jabref/gui/JabRefGuiStateManager.java
Comment on lines 380 to 382
if (isSelectedTab) {
stateManager.setActiveDatabase(bibDatabaseContext);
stateManager.replaceActiveDatabase(bibDatabaseContext);
stateManager.activeTabProperty().set(Optional.of(this));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Remediation recommended

2. Transient active-db empty 🐞 Bug ☼ Reliability

LibraryTab#setDatabaseContext now unconditionally calls replaceActiveDatabase, which forces an
intermediate Optional.empty() active-database event even when the old/new contexts are not equal.
This can cause subscribers (e.g., groups and git status) to clear/reset and then rebuild
immediately, creating avoidable churn and potential UI flicker.
Agent Prompt
### Issue description
`StateManager.replaceActiveDatabase(...)` intentionally emits two active-database values (`Optional.empty()` then the new context) to force downstream notifications when contexts compare equal. However, `LibraryTab#setDatabaseContext` currently calls it unconditionally for the selected tab, meaning *all* subscribers see a transient “no active database” even when a normal `setActiveDatabase(...)` would notify correctly.

### Issue Context
This transient empty state triggers reset/clear logic in multiple subscribers (groups tree, git status, etc.), which is only required in the special “old equals new” case the PR is fixing.

### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/LibraryTab.java[357-385]

### Suggested change
In `setDatabaseContext`, only call `replaceActiveDatabase` when the currently active database is present and `equals(...)` the new `bibDatabaseContext` (the problematic case). Otherwise call `setActiveDatabase(bibDatabaseContext)` to avoid the intermediate empty event.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

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.

@qodo Equals on bibdatabsecontext is expensive as it will compare the whole library entry by entry

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That’s a valid concern—BibDatabaseContext.equals(...) should not be used on this path because it can traverse the entire library.

The behavior issue still stands, but the conditional should use a cheap lifecycle/identity check instead of deep equality—for example, compare a stable context/database identity (or the canonical database path, where available) and call replaceActiveDatabase only for that known reload case. Use setActiveDatabase for ordinary context changes. If no suitable stable identity exists, the reload operation should explicitly signal the replacement rather than inferring it via equals(...).

@github-actions github-actions Bot added the status: changes-required Pull requests that are not yet complete label Aug 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Your pull request conflicts with the target branch.

Please merge upstream/main with your code. For a step-by-step guide to resolve merge conflicts, see https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/addressing-merge-conflicts/resolving-a-merge-conflict-using-the-command-line.

@InAnYan

InAnYan commented Aug 13, 2026

Copy link
Copy Markdown
Member

So we "add to the changes" an Optional.empty(), because when we set a database it uses equals comparison with a previous library? And with Optional.empty() it's faster?

@Siedlerchr

Copy link
Copy Markdown
Member Author

This is related to the search context setup. Just open a new library and select create suggested groups and you will get an exception.
And yes, clearing the content and setting a new database is more peformant because there is equals called (equals checks all entires in the library)

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

Labels

status: changes-required Pull requests that are not yet complete

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants