Skip to content

Fixed year not appearing on citation key for In-* entries due to orphaned crossref links (#9071)#15582

Merged
Siedlerchr merged 15 commits into
JabRef:mainfrom
programmer1128:fix-issue-9071-orphaned-crossref
Apr 28, 2026
Merged

Fixed year not appearing on citation key for In-* entries due to orphaned crossref links (#9071)#15582
Siedlerchr merged 15 commits into
JabRef:mainfrom
programmer1128:fix-issue-9071-orphaned-crossref

Conversation

@programmer1128

@programmer1128 programmer1128 commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

Related issues and pull requests

Closes #9071

PR Description

Previously, the BibDatabase.citationIndex only updated upon entry creation or deletion, leading to a stale index cache when individual link fields like crossref were edited. This PR intercepts FieldChangedEvent to safely update the index map dynamically whenever a SINGLE_ENTRY_LINK or MULTIPLE_ENTRY_LINK is modified. This ensures the map stays perfectly synced and prevents child crossref links from becoming permanently orphaned when a parent's citation key is later auto-generated.

Steps to test

  1. Open a new blank JabRef document.
  2. Create a new Proceedings entry manually. Set its citation key to TestParent and year to 2026.
  3. Create a new InProceedings entry manually and in its crossref field give TestParent.
  4. Generate citation key for InProceedings (child) by selecting that entry and pressing ctrl+G or "Generate Citation Key"
  5. The new generated citation key for the InProceedings will include the year 2026.
  6. Select Proceedings (parent) and generate a citation key for it.
  7. Now observe the InProceeding (child) crossref field change from previous parent key to newly generated key, allowing the year to generate properly and no orphaned crossref links.

Checklist

  • I own the copyright of the code submitted and I license it under the MIT license

  • 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)

  • 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

  • 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 our user documentation repository

JabRefTest

Test Video

JabRefTestingVideo.mp4

@github-actions github-actions Bot added the status: changes-required Pull requests that are not yet complete label Apr 19, 2026
@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Fix orphaned crossref links on parent citation key change

🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Intercepts FieldChangedEvent to dynamically update citation index
• Prevents orphaned crossref links when parent citation key changes
• Ensures child entries reflect parent year in generated citation keys
• Updates CHANGELOG.md with fix description
Diagram
flowchart LR
  A["Parent Entry Citation Key Changes"] -->|FieldChangedEvent| B["relayEntryChangeEvent Intercepts"]
  B -->|Check if SINGLE_ENTRY_LINK or MULTIPLE_ENTRY_LINK| C["Update Citation Index Map"]
  C -->|Remove old reference| D["Remove from old key set"]
  C -->|Add new reference| E["Add to new key set"]
  E -->|Child entry synced| F["Child crossref stays valid"]
  F -->|Year now available| G["Citation key includes year"]
Loading

Grey Divider

File Changes

1. jablib/src/main/java/org/jabref/model/database/BibDatabase.java 🐞 Bug fix +38/-2

Dynamic citation index update on field changes

• Enhanced relayEntryChangeEvent method to intercept and handle field changes for linked entry
 fields
• Added logic to detect SINGLE_ENTRY_LINK and MULTIPLE_ENTRY_LINK field property changes
• Dynamically updates citationIndex map by removing entries from old key sets and adding to new
 key sets
• Ensures citation index stays synchronized when crossref or other link fields are modified

jablib/src/main/java/org/jabref/model/database/BibDatabase.java


2. CHANGELOG.md 📝 Documentation +1/-0

Document orphaned crossref fix in changelog

• Added entry documenting the fix for orphaned crossref links issue #9071
• Describes that citation key generator now properly shows date for child entries

CHANGELOG.md


Grey Divider

Qodo Logo

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

qodo-free-for-open-source-projects Bot commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (1) 📎 Requirement gaps (0)

Grey Divider


Action required

1. oldValue/newValue empty defaults📘 Rule violation ≡ Correctness
Description
The new code converts missing link values to empty strings (e.g., == null ? "" : ...) and then
proceeds as if a value exists, which is explicitly discouraged and can create/retain citationIndex
entries under an empty key. This makes absence handling unclear and risks corrupting link-index
state when a link field is cleared or unset.
Code

jablib/src/main/java/org/jabref/model/database/BibDatabase.java[R560-581]

+            String oldValue = event.getOldValue() == null ? "" : event.getOldValue();
+            String newValue = event.getNewValue() == null ? "" : event.getNewValue();
+
+            //Remove from the old key's set
+            if (oldValue != null)
+            {
+                Set<BibEntry> referenceEntries = citationIndex.get(oldValue);
+                if (referenceEntries != null)
+                {
+                    referenceEntries.remove(entry);
+                    if (referenceEntries.isEmpty())
+                    {
+                        citationIndex.remove(oldValue);
+                    }
+                }
+            }
+
+            // Add to the new key's set
+            if (newValue != null)
+            {
+                citationIndex.computeIfAbsent(newValue, k -> ConcurrentHashMap.newKeySet()).add(entry);
+            }
Evidence
PR Compliance ID 17 forbids substituting missing values with empty strings instead of handling
absence explicitly. The changed code assigns empty strings when
event.getOldValue()/event.getNewValue() are null and then uses those values to remove/add
entries in citationIndex.

AGENTS.md
jablib/src/main/java/org/jabref/model/database/BibDatabase.java[560-581]

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

## Issue description
`event.getOldValue()` and `event.getNewValue()` are converted to `""` when null and then treated like real citation keys.
## Issue Context
This logic updates `citationIndex` when link fields change. Using `""` as a stand-in for “absent” makes the behavior ambiguous and can introduce an empty-string key in the index.
## Fix Focus Areas
- jablib/src/main/java/org/jabref/model/database/BibDatabase.java[560-581]

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


2. No tests for index update 📘 Rule violation ☼ Reliability
Description
Core model behavior was changed by updating citationIndex during FieldChangedEvent handling, but
this PR does not include corresponding test additions/updates to prevent regressions. This violates
the requirement to adapt/update tests when modifying org.jabref.model behavior.
Code

jablib/src/main/java/org/jabref/model/database/BibDatabase.java[R548-586]

+    private void relayEntryChangeEvent(FieldChangedEvent event)
+    {
+        Field field = event.getField();
+
+        // Check if the field changed was a single entry or multiple entry link
+        boolean isLinkedField = field.getProperties().contains(FieldProperty.SINGLE_ENTRY_LINK) ||
+                field.getProperties().contains(FieldProperty.MULTIPLE_ENTRY_LINK);
+
+
+        if (isLinkedField)
+        {
+            BibEntry entry = event.getBibEntry();
+            String oldValue = event.getOldValue() == null ? "" : event.getOldValue();
+            String newValue = event.getNewValue() == null ? "" : event.getNewValue();
+
+            //Remove from the old key's set
+            if (oldValue != null)
+            {
+                Set<BibEntry> referenceEntries = citationIndex.get(oldValue);
+                if (referenceEntries != null)
+                {
+                    referenceEntries.remove(entry);
+                    if (referenceEntries.isEmpty())
+                    {
+                        citationIndex.remove(oldValue);
+                    }
+                }
+            }
+
+            // Add to the new key's set
+            if (newValue != null)
+            {
+                citationIndex.computeIfAbsent(newValue, k -> ConcurrentHashMap.newKeySet()).add(entry);
+            }
+        }
+
+        // After changes, post the event just like before
eventBus.post(event);
}
Evidence
PR Compliance ID 28 requires adapting/updating tests when org.jabref.model changes beyond imports.
The PR modifies BibDatabase behavior in relayEntryChangeEvent(...) but includes no accompanying
test changes in the provided diff.

AGENTS.md
jablib/src/main/java/org/jabref/model/database/BibDatabase.java[548-586]

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 behavior change in `BibDatabase` (updating `citationIndex` on `FieldChangedEvent`) is not covered by tests in this PR.
## Issue Context
This code lives under `org.jabref.model` and affects crossref/link behavior; a regression here can reintroduce stale/orphan link indexing.
## Fix Focus Areas
- jablib/src/main/java/org/jabref/model/database/BibDatabase.java[548-586]

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



Remediation recommended

3. Trivial link-update comments📘 Rule violation ⚙ Maintainability
Description
New comments mostly restate what the code directly does (e.g., “Remove from the old key's set”, “Add
to the new key's set”) rather than explaining intent or rationale. This adds noise and violates the
guidance that comments should explain the “why”.
Code

jablib/src/main/java/org/jabref/model/database/BibDatabase.java[R552-585]

+        // Check if the field changed was a single entry or multiple entry link
+        boolean isLinkedField = field.getProperties().contains(FieldProperty.SINGLE_ENTRY_LINK) ||
+                field.getProperties().contains(FieldProperty.MULTIPLE_ENTRY_LINK);
+
+
+        if (isLinkedField)
+        {
+            BibEntry entry = event.getBibEntry();
+            String oldValue = event.getOldValue() == null ? "" : event.getOldValue();
+            String newValue = event.getNewValue() == null ? "" : event.getNewValue();
+
+            //Remove from the old key's set
+            if (oldValue != null)
+            {
+                Set<BibEntry> referenceEntries = citationIndex.get(oldValue);
+                if (referenceEntries != null)
+                {
+                    referenceEntries.remove(entry);
+                    if (referenceEntries.isEmpty())
+                    {
+                        citationIndex.remove(oldValue);
+                    }
+                }
+            }
+
+            // Add to the new key's set
+            if (newValue != null)
+            {
+                citationIndex.computeIfAbsent(newValue, k -> ConcurrentHashMap.newKeySet()).add(entry);
+            }
+        }
+
+        // After changes, post the event just like before
eventBus.post(event);
Evidence
PR Compliance ID 8 requires that new comments add rationale instead of paraphrasing the
implementation. The added comments describe obvious operations already clear from the code structure
and method names.

AGENTS.md
jablib/src/main/java/org/jabref/model/database/BibDatabase.java[552-585]

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

## Issue description
Several newly added comments restate the code’s mechanics instead of documenting rationale.
## Issue Context
This area is already readable via identifiers (`citationIndex`, `computeIfAbsent`, etc.). If comments are kept, they should explain why this index update is necessary and what invariants it maintains.
## Fix Focus Areas
- jablib/src/main/java/org/jabref/model/database/BibDatabase.java[552-585]

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


4. Inconsistent brace/spacing style📘 Rule violation ⚙ Maintainability
Description
The touched code introduces inconsistent formatting (e.g., extra space in unregisterListener
signature and brace placement on new lines) that does not match surrounding methods in the same
class. This reduces diff readability and risks violating project formatting/quality gates.
Code

jablib/src/main/java/org/jabref/model/database/BibDatabase.java[R538-541]

+    public void unregisterListener(Object listener)  {
try {
this.eventBus.unregister(listener);
} catch (IllegalArgumentException e) {
Evidence
PR Compliance ID 4 requires preserving existing formatting and avoiding unrelated reformatting. The
modified lines introduce style inconsistencies in spacing and brace placement within the changed
region.

AGENTS.md
jablib/src/main/java/org/jabref/model/database/BibDatabase.java[538-559]

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

## Issue description
Formatting in the modified section is inconsistent with nearby methods (spacing and brace placement).
## Issue Context
Keeping formatting consistent improves reviewability and reduces the chance of checkstyle/formatting gate failures.
## Fix Focus Areas
- jablib/src/main/java/org/jabref/model/database/BibDatabase.java[538-559]
- jablib/src/main/java/org/jabref/model/database/BibDatabase.java[548-559]

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


Grey Divider

Qodo Logo

Comment thread jablib/src/main/java/org/jabref/model/database/BibDatabase.java Outdated
Comment on lines 548 to 586
private void relayEntryChangeEvent(FieldChangedEvent event)
{
Field field = event.getField();

// Check if the field changed was a single entry or multiple entry link
boolean isLinkedField = field.getProperties().contains(FieldProperty.SINGLE_ENTRY_LINK) ||
field.getProperties().contains(FieldProperty.MULTIPLE_ENTRY_LINK);


if (isLinkedField)
{
BibEntry entry = event.getBibEntry();
String oldValue = event.getOldValue() == null ? "" : event.getOldValue();
String newValue = event.getNewValue() == null ? "" : event.getNewValue();

//Remove from the old key's set
if (oldValue != null)
{
Set<BibEntry> referenceEntries = citationIndex.get(oldValue);
if (referenceEntries != null)
{
referenceEntries.remove(entry);
if (referenceEntries.isEmpty())
{
citationIndex.remove(oldValue);
}
}
}

// Add to the new key's set
if (newValue != null)
{
citationIndex.computeIfAbsent(newValue, k -> ConcurrentHashMap.newKeySet()).add(entry);
}
}

// After changes, post the event just like before
eventBus.post(event);
}

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.

Action required

2. No tests for index update 📘 Rule violation ☼ Reliability

Core model behavior was changed by updating citationIndex during FieldChangedEvent handling, but
this PR does not include corresponding test additions/updates to prevent regressions. This violates
the requirement to adapt/update tests when modifying org.jabref.model behavior.
Agent Prompt
## Issue description
A behavior change in `BibDatabase` (updating `citationIndex` on `FieldChangedEvent`) is not covered by tests in this PR.

## Issue Context
This code lives under `org.jabref.model` and affects crossref/link behavior; a regression here can reintroduce stale/orphan link indexing.

## Fix Focus Areas
- jablib/src/main/java/org/jabref/model/database/BibDatabase.java[548-586]

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

}
}

// After changes, post the event just like before

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.

There is no "before" or "after" in a codebase. A person reading will only know the current state.

This is a telltale sign of AI usage. Seems like you did not read our contributing guidelines at all.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

here for after i meant after the checks i made.

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.

There is also "just like before" at the end of the sentence.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this part is written by me as i had to restyle to match the styling convention of jabref and then again write tests for the Junit to get accepted in the qodo ai review

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.

This discussion is in context of the highlighted comment above, not code style conventions or unit tests.

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.

Your thought process was - it would be unclear to reviewers that the functionality of an untouched part of code is still the same unless you add a comment stating that the functionality of an untouched part of code is unchanged?

You've talked about JabRef style/conventions above. If you had even read this class once yourself, or at least the surrounding code, you'd have realized that nobody writes obvious comments like "delete from old key set" or "add to new key set" or refer to history of code. That, however takes time.
We are really not new to this experience, we've dealt with an uncountable number of pull requests now (and the senior maintainers here even more), and some patterns which you're unaware of, are persistent every time someone uses AI. They may not be obvious to you and it might feel really easy to claim they're intentionally made thinking we won't understand, but that's not really the case. We can see through them.

I've made mistakes like you (which are not even considered mistakes for newcomers), and also use AI during my day to day. Only thing that matters is the honesty and consistency of the human being involved.

I do not wish to continue this discussion further, so I leave it on other maintainers

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.

Since you at least tested the changes - we will not reject your contribution and give you another chance. Please be careful henceforth.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I am being honest @subhramit . I am really serious about this contribution. I went through the other parts of the code in the same class as well. I went through a considerable part of the codebase during the debugging process. I have said in my previous comment that i have taken help of ai assistant to guide me. I mentioned that in my previous gitter chats as well. I have no reason to lie as it would harm my own efforts. I am being honest, that I wrote that part of code myself including that comment line. I used AI to assist me. But the entire code is written by me. I have not copy pasted a single line in that method. I can share my logic report as well as to how i formulated the logic. I humbly appeal to you, please check my code again, I will be happy to explain my thought process for any logic part. I apologise if i overstepped at any point. I hope you will consider my request. Thank you for guiding me, I have learnt a lot from this first contribution and wish to improve further.

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.

It's fine. You deserve a benefit of doubt, and you shall be given that.
I have reviewed your code in the next comments, considering your contribution honest.

Please remove this comment from the code.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thank you @subhramit for your consideration. I will try to explain your questions in comments with as much clarity as i can.
Thank you for your guidance. I will remove that comment from the code and commit the changes. If i need to make any other changes, please mention that, i would try to fix them as well.

@github-actions github-actions Bot added status: no-bot-comments and removed status: changes-required Pull requests that are not yet complete labels Apr 19, 2026

@subhramit subhramit 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.

Some comments/questions

String newValue = event.getNewValue() == null ? "" : event.getNewValue();

// Remove from the old key's set
if (oldValue != null) {

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.

How can oldvalue ever be null?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

When a child (ex - InProceedings) is added as entry for first time, there is no crossref field attached. This field is not there for the child. So this will be null. When the user types the crossref and saves it, then it is saved with some value. As the field was empty before, the old value can be null. Now since the citationIndex in BibDatabase.java is initialised as a final ConcurrentHashMap and ConcurrentHashMap does not support null values, there can be a null pointer exception. Hence I added this check. if there is no crossref for a new entry, then it will be seen as empty string to prevent any null pointer exception error.

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.

I understand that you are talking about getOldValue(). I inquired about oldValue.
You define oldValue as

String oldValue = event.getOldValue() == null ? "" : event.getOldValue();

above.
Hence, it can be "" but never null.
Hence your logic regarding ConcurrentHashMap doesn't follow, nor this check if (oldValue != null) because it will always be non-null.

@programmer1128 programmer1128 Apr 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes the if statement is redundant part. When i first wrote the code i did the check with

if(oldVaue!=null&&!oldValue.isEmpty())
{
}

But then while checking i realised that i need the empty strings for checking too otherwise the new entry will not be linked. So i removed the isEmpty part and added a ternary operator above. I forgot to remove the redundant if parts for this and the newvalue. I will rectify them the same time when i remove the comment you suggested.

}

// Add to the new key's set
if (newValue != null) {

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.

How can newvalue ever be null?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The new value can be null if suddenly a user deletes a crossref field for the child. If there is only 1 crossref entry for a child, and user deletes it, then that field will be removed from the BibEntry object of this entry. Then the new value becomes null. Now as i mentioned in my previous old value comment, the citationIndex in Bibdatabase.java is initialised with a final ConcurrentHashMap. and ConcurrentHashMap does not support null keys. So trying to put a null key will cause null pointer exception. Hence i made the check to keep an empty string for the null value to avoid that error.

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.

Similar response as above - what does this check signify?
Did you read your code before submitting?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I mentioned my response in the above comment about my initial code. This is a screenshot of my earlier code. I have kept the entire process in a word document. i can share it if you want
JabRefCodeFix

@subhramit subhramit Apr 19, 2026

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.

See @programmer1128 - it doesn't matter how many preliminary steps you went through before arriving at a final state and submitting a PR as ready for review. The bottom line is that you did not check your final code before submitting - hence we are spending time on this.
We understand - you're a human and this can happen, but as far as the PR review is concerned, the maintainers really don't have time for these endless excuses or knowing the entire history of your process. Please just say "oh I missed this from a previous draft" and just fix it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I apologise for my mistake @subhramit . I will make all the necessary changes that you have suggested, recheck my code and then commit it. Thank you for your thorough review and time to check my code and guide me.

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.

No issues, Aritra, as blunt my tone may sound, I am keeping in mind that this is your first time - and trying to make you pick up as many things as possible.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I really appreciate it @subhramit . I have learnt a lot from this and I am still learning and will continue to do so. It is a really good experience for me. Thank you for taking the time to mentor me.


// Add to the new key's set
if (newValue != null) {
citationIndex.computeIfAbsent(newValue, k -> ConcurrentHashMap.newKeySet()).add(entry);

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.

Why a concurrent hashmap?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I have used a concurrent hashmap here firstly because the citationIndex field itself is initialised as a final ConcurrentHashMap in the BibDatabase.java file.

private final Map<String, Set<BibEntry>> citationIndex = new ConcurrentHashMap<>();

To ensure that the key insertion, update and deletion is thread safe, I followed the same ConcurrentHashMap as done in the class previously.

Also, as I understood, jabref uses javafx for ui. So there is JavaFx ui thread plus many other task threads. Now the change i made is inside a method that is relayEntryChangeEvent(FieldChangedEvent event). This method is called, whenever an event is posted to the eventbus and these events can come from many threads. Now if we use a normal HashMap a thread that posted event to change citationIndex can interfere with the JavaFx thread reading it to show in interface which can cause a ConcurrentModificationException error in java.
So if we use concurrent hashmap then the operations can happen safely with all the threads without the ui thread being interfered.

@subhramit subhramit Apr 19, 2026

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.

You are correct about javafx and background threads posting to the eventbus, and relayEntryChangeEvent can run off the fx thread while the UI reads the index, so thread-safety does matter here.

However, ConcurrentHashMap doesn't make compound operations atomic. Consider your L562 onwards:

Set<BibEntry> referenceEntries = citationIndex.get(oldValue);
if (referenceEntries != null) {
    referenceEntries.remove(entry);
    if (referenceEntries.isEmpty()) {
        citationIndex.remove(oldValue);
    }
}

Between isEmpty() and remove(oldValue), another thread can computeIfAbsent(oldValue...) and repopulate the set, the remove would then delete a non-empty mapping. However as far as I see, a similar pattern already exists in the code which you probably faithfully followed (in removeEntryFromIndex), so for the scope of this PR you can ignore this - and the situation is unlikely to occur during JabRef's practical use.
Just thought of putting this here as the topic of concurrency was raised, so that you can also take this opportunity learn about this more deeply.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thank you @subhramit for your guidance. I think this is similar to time of check to time of use condition. I may be wrong. I will keep your advice in mind . Thank you for pointing it out and reviewing my PR thouroughly.

@subhramit

Copy link
Copy Markdown
Member

@LoayTarek5 would you like to review this? Since you delved into the issue a bit, this may be a good opportunity!

@LoayTarek5

Copy link
Copy Markdown
Collaborator

@LoayTarek5 would you like to review this? Since you delved into the issue a bit, this may be a good opportunity!

Sure, i will do it

@github-actions github-actions Bot added status: changes-required Pull requests that are not yet complete and removed status: no-bot-comments labels Apr 19, 2026
@github-actions github-actions Bot added status: no-bot-comments and removed status: changes-required Pull requests that are not yet complete labels Apr 19, 2026
@programmer1128

Copy link
Copy Markdown
Contributor Author

Hi @subhramit , I have updated the PR based on your suggestions. I have removed the redundant if checks. While fixing, another test case came to my mind. If the new value is kept empty by user, then the entry will be registered with a crossref of empty value, which is not valid as a empty crossref is not a valid one. Hence I have added a if check for the new value, that checks if the new value is empty or not and accordingly places the entry in the map.

@calixtus

Copy link
Copy Markdown
Member

Please note that AI use needs to be disclosed.

@programmer1128

Copy link
Copy Markdown
Contributor Author

Hi @calixtus, I have taken help of AI assistant to guide me through the open source contribution process, but I have done the entire debugging process myself, with detailed reports at every step as I commented on issue page, guidance from JabRef mentors as well, and the entire code has been written by me myself with my own logic formulation. The code may not have been very clean at the first, but I made changes as per guidance of mentors and then submitted the proper PR. I have learnt a lot from this contribution and I am continuing to learn. I thank the entire community for guiding me towards a proper PR and a better contributor.

Comment on lines +552 to +562
boolean isLinkedField = field.getProperties().contains(FieldProperty.SINGLE_ENTRY_LINK) ||
field.getProperties().contains(FieldProperty.MULTIPLE_ENTRY_LINK);

if (isLinkedField) {
BibEntry entry = event.getBibEntry();
String oldValue = event.getOldValue() == null ? "" : event.getOldValue();
String newValue = event.getNewValue() == null ? "" : event.getNewValue();

// Remove from the old key's set

Set<BibEntry> referenceEntries = citationIndex.get(oldValue);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

MULTIPLE_ENTRY_LINK fields can hold multiple keys in one value, so citationIndex.get(oldValue) misses when oldValue is something like "Key1, Key2".

BibDatabase.java already has forEachCitationKey(), which parses link fields with entry.getEntryLinkList(field, this) and is reused by indexEntry and removeEntryFromIndex, consider using it here too so all link parsing stays in one place

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hi @LoayTarek5, Thank you for reviewing my code. i though initially that crossref multiple entry fields were stored individually by default in separate strings all containing same child. Thank you for pointing out my mistake. I will try to implemet the logic you suggested using the forEachCitationKey() method and correct my logic and test it with a testcase similar to "key1,key2.." for multiple entry fields.

Comment on lines +491 to +506
void crossrefChangeUpdatesCitationIndex() {
BibDatabase database = new BibDatabase();
BibEntry parent = new BibEntry(StandardEntryType.Proceedings)
.withCitationKey("TestParent");
BibEntry child = new BibEntry(StandardEntryType.InProceedings);

database.insertEntry(parent);
database.insertEntry(child);

// Setting the crossref field after insertion should trigger the FieldChangedEvent
// and safely update the citation index without crashing.
child.setField(StandardField.CROSSREF, "TestParent");

// Verify that the database successfully linked the child to the parent
assertEquals(Optional.of(parent), database.getReferencedEntry(child));
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The existing crossrefChanged test in KeyChangeListenerTest.java sets the crossref before insertEntry, so it never triggers the stale index, the real bug when crossref is set after insertion

Please add a test that:
inserts parent and child separately, then sets child.crossref after insertion.
renames the parent key --> verifies the child's crossref follows
renames the parent key a second time --> verifies it still follows

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thank you @LoayTarek5 for this new test case. I will test it with my new correct logic with a new test that will implement the 2 time rename of parent. I will try to record the results and post it here.

@github-actions github-actions Bot added the status: changes-required Pull requests that are not yet complete label Apr 22, 2026
Siedlerchr
Siedlerchr previously approved these changes Apr 27, 2026
@Siedlerchr Siedlerchr added status: ready-for-review Pull Requests that are ready to be reviewed by the maintainers status: awaiting-second-review For non-trivial changes labels Apr 27, 2026
String newValue = event.getNewValue();

// split the old multiple key string into individual keys and remove the entry from all old keys
if (oldValue != null && !oldValue.trim().isEmpty()) {

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.

Use StringUtil isBlank()

Comment on lines +561 to +562
String oldKey = rawKey.trim();
if (!oldKey.isEmpty()) {

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.

Use isBlank

Comment thread jablib/src/main/java/org/jabref/model/database/BibDatabase.java Outdated
}

// split the new multiple key string into individual keys and add the entry to all new keys
if (newValue != null && !newValue.trim().isEmpty()) {

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.

isBlank

Comment on lines +579 to +580
String newKey = rawKey.trim();
if (!newKey.isEmpty()) {

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.

isBlank

@programmer1128

Copy link
Copy Markdown
Contributor Author

Hi @subhramit , Thank you for reviewing my code and suggesting the changes. I will implement these changes and commit the corrected code.

Comment thread jablib/src/test/java/org/jabref/model/database/BibDatabaseTest.java
@subhramit subhramit removed the status: ready-for-review Pull Requests that are ready to be reviewed by the maintainers label Apr 27, 2026
Comment thread jablib/src/test/java/org/jabref/model/database/BibDatabaseTest.java Outdated
…latedIndexUpdatesWhenCommaSeparatedFieldChanges() into 2 separate tests as childIsIndexedUnderEachKeyInCommaSeparatedField and changingOneKeyInCommaSeparatedFieldUpdatesIndex
String newValue = event.getNewValue();

// split the old multiple key string into individual keys and remove the entry from all old keys
if (oldValue != null && !oldValue.isBlank()) {

@subhramit subhramit Apr 27, 2026

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.

When a change is suggested, please don't just blindly replace as per your gut feeling. Go into the function isBlank, see what it does. If it does the same as isEmpty(), a critical thinker would think - then why the need for replacement?
If you go into the function body, you'll find that the null check is already being done there - so the review comments were intended to reduce extra code.

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.

Also, you did not use StringUtil as mentioned in #15582 (comment)

public static boolean isBlank(@Nullable String string) {
return (string == null) || string.isBlank();
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I apologise @subhramit , i misunderstood your suggested change. I thought you meant the built in isBlank() method for string class in java and since the built in method does not check the null, i kept the null check.
For the isEmpty() part I thought you have suggested the isBlank() because .trim() creates a new String object in java and to avoid that.
I apologise for my mistakes. I will fix them immediately and commit the corrected code.

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.

Same comments on the other related parts.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thank you @subhramit for providing the code segment. It is a great help. I will fix the code immediately to use this method and then commit the corrected code.
If there is any more change that i need to make, please mention them, i will fix them and commit the proper code.

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.

No, that is all. Thanks.

// split the old multiple key string into individual keys and remove the entry from all old keys
if (oldValue != null && !oldValue.isBlank()) {
for (String rawKey : oldValue.split(",")) {
String oldKey = rawKey.trim();

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.

Trim is also no longer needed. See what isBlank does by Ctrl+click.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hi @subhramit , I saw what the method does. By using !StringUtil.isBlank(rawKey) i can avoid the new string creation that trim does even for empty invalid strings.
I just want to clarify, that when the string is not empty and contains characters with leading spaces or spaces at end, then i have to do trim so that the citationIndex can properly match the keys. For that reason, i think i should move the trim part to inside the if statement that trims the rawKey only when it is not empty and checks in citationIndex. Can you please clarify my approach.

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.

You can keep the trim where you are using the trimmed version beyond the checks

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks a lot @subhramit for clarifying my approach. I will make the fixes immediately and commit the proper code with the changes you suggested.
Thank you for your guidance.

@programmer1128

Copy link
Copy Markdown
Contributor Author

Hi @subhramit and JabRef Community, I have made the suggested changes in my code. I have updated the .isEmpty() to .isBlank() method written in StringUtil.java in jabref/logic and commited the updated code.
If there are any more changes to make, please suggest, I will fix them.

Thank you for your guidance at every step.

@Siedlerchr Siedlerchr added this pull request to the merge queue Apr 28, 2026
@github-actions github-actions Bot added the status: to-be-merged PRs which are accepted and should go into the merge-queue. label Apr 28, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Apr 28, 2026
@Siedlerchr Siedlerchr enabled auto-merge April 28, 2026 08:50
@programmer1128

Copy link
Copy Markdown
Contributor Author

Thanks a lot @subhramit and @Siedlerchr and JabRef Community for approving my pull request. It really means a lot to me.
Thank you for your guidance at every step. I have learnt a lot from this experience.

I am seeing some binaries install issues on the checks made for the merge. If i can contribute in any manner from my end to resolve this please tell me, i will try to help and fix the checks.

@Siedlerchr Siedlerchr disabled auto-merge April 28, 2026 14:11
@Siedlerchr Siedlerchr merged commit 0423fc1 into JabRef:main Apr 28, 2026
82 of 94 checks passed
Siedlerchr added a commit that referenced this pull request May 1, 2026
…urityIssues

* upstream/fixSecurityIssues:
  fix jbang (#15649)
  Add initial claude action support
  Chore(deps): Bump org.glassfish.grizzly:grizzly-bom in /versions (#15648)
  Update IntelliJ settings (#15629)
  Chore(deps): Bump org.openrewrite.recipe:rewrite-recipe-bom (#15626)
  Fixed year not appearing on citation key for In-* entries due to orphaned crossref links (#9071) (#15582)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

citation key generator gives no date in the key for In-* entries (crossref)

5 participants