Skip to content

Update KeywordEditor to work with escaping#14929

Merged
Siedlerchr merged 19 commits intoJabRef:mainfrom
BOgdAnSAM-sudo:fix-for-issue-14780
Feb 24, 2026
Merged

Update KeywordEditor to work with escaping#14929
Siedlerchr merged 19 commits intoJabRef:mainfrom
BOgdAnSAM-sudo:fix-for-issue-14780

Conversation

@BOgdAnSAM-sudo
Copy link
Contributor

@BOgdAnSAM-sudo BOgdAnSAM-sudo commented Jan 25, 2026

Closes #14780
Follow up for #14637

Update KeywordEditor to work with escaping logic throught jablib/src/main/java/org/jabref/model/entry/KeywordList#Parse and jablib/src/main/java/org/jabref/model/entry/KeywordList#Serialize

Steps to test

  1. Start JabRef
  2. Create new empty library
  3. Click on "Add example entry"
  4. In example entry locate tab "General"
  5. In Keywords field enter paste following keywords: "Keyword \> Keyword", "Keyword > Keyword", "KeywordOne\,KeywordTwo", "KeywordOne,KeywordTwo", "keyword\", "keyword".
  6. Keywords field should show: "Keyword > Keyword" and "Keyword > Keyword" as two separate keywords, "KeywordOne,KeywordTwo" as single keyword, "KeywordOne" and "KeywordTwo" as two separate keywords, "KeywordOne", "KeywordOne".
  7. Locate tab "BibTeX sorce"
  8. Tab "BibTeX sorce" should contain line keywords = {Keyword > Keyword,Keyword > Keyword,KeywordOne,KeywordTwo, KeywordOne,KeywordTwo,keyword\,keyword}

This should work the same way both when pasting and entering manualy. Also, when using suggestions, suggested text should be added as tag and serialized version saved to "BibTeX sorce".

Proposed documntation changes JabRef/user-documentation#615

image

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

@qodo-free-for-open-source-projects
Copy link
Contributor

qodo-free-for-open-source-projects bot commented Jan 25, 2026

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status:
Unclear variable name: Variable substringWithSeparator is misleading as it represents a reversed substring before
the separator, not one containing the separator

Referred Code
String substringWithSeparator = new StringBuilder(keywordString.substring(0, separatorFirstOccurrence)).reverse().toString();

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Missing boundary validation: Method isSeparatedKeyword does not handle the case where separatorFirstOccurrence is -1
(separator not found), which would cause substring to fail

Referred Code
int separatorFirstOccurrence = keywordString.lastIndexOf(keywordSeparator);
String substringWithSeparator = new StringBuilder(keywordString.substring(0, separatorFirstOccurrence)).reverse().toString();

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-free-for-open-source-projects
Copy link
Contributor

qodo-free-for-open-source-projects bot commented Jan 25, 2026

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Improve method robustness and readability
Suggestion Impact:The commit implements the first part of the suggestion by adding the guard clause to check if separatorLastOccurrence is -1. It also renames the variable from separatorFirstOccurrence to separatorLastOccurrence as suggested. However, the commit does not include the full refactoring of the stream-based logic to a simpler loop.

code diff:

-        if (keywordString.isEmpty()) {
+        int separatorLastOccurrence = keywordString.lastIndexOf(keywordSeparator);
+        if (separatorLastOccurrence == -1) {
             return false;

Refactor the isSeparatedKeyword method to prevent a potential
StringIndexOutOfBoundsException and improve its readability. Replace the complex
stream-based logic with a simpler loop and add a guard clause to handle cases
where the separator is not found.

jabgui/src/main/java/org/jabref/gui/fieldeditors/KeywordsEditor.java [164-177]

 private boolean isSeparatedKeyword(String keywordString, String keywordSeparator) {
     if (keywordString.isEmpty()) {
         return false;
     }
 
-    int separatorFirstOccurrence = keywordString.lastIndexOf(keywordSeparator);
-    String substringWithSeparator = new StringBuilder(keywordString.substring(0, separatorFirstOccurrence)).reverse().toString();
+    int separatorLastOccurrence = keywordString.lastIndexOf(keywordSeparator);
+    if (separatorLastOccurrence == -1) {
+        return false;
+    }
 
-    AtomicBoolean isSeparatedKeyword = new AtomicBoolean(true);
-    substringWithSeparator.chars().takeWhile(symbol -> symbol == Keyword.DEFAULT_ESCAPE_SYMBOL)
-                          .forEachOrdered(_ -> isSeparatedKeyword.set(!isSeparatedKeyword.get()));
-
-    return isSeparatedKeyword.get();
+    String prefix = keywordString.substring(0, separatorLastOccurrence);
+    int escapeCharCount = 0;
+    for (int i = prefix.length() - 1; i >= 0; i--) {
+        if (prefix.charAt(i) == Keyword.DEFAULT_ESCAPE_SYMBOL) {
+            escapeCharCount++;
+        } else {
+            break;
+        }
+    }
+    return escapeCharCount % 2 == 0;
 }

[Suggestion processed]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a potential StringIndexOutOfBoundsException and a significant readability issue in the new isSeparatedKeyword method. The proposed refactoring makes the code safer, more efficient, and much easier to understand.

Medium
Fix inconsistent keyword conversion logic
Suggestion Impact:The commit implements the exact changes suggested: adds null check to the isEmpty condition, removes the inconsistent empty keyword creation logic, and replaces the manual list checking with a cleaner stream().findFirst().orElse(null) pattern.

code diff:

-                if (keywordString.isEmpty()) {
+                if (keywordString == null || keywordString.isEmpty()) {
                     return null;
                 }
 
-                KeywordList parsedKeywords = KeywordList.parse(keywordString, keywordSeparator);
-                Keyword resultKeyword = null;
-                if (parsedKeywords.isEmpty()) {
-                    return new Keyword("");
-                } else {
-                    resultKeyword = parsedKeywords.get(0);
-                }
-                return resultKeyword;
+                return KeywordList.parse(keywordString, keywordSeparator)
+                                  .stream()
+                                  .findFirst()
+                                  .orElse(null);

Simplify and fix inconsistent logic in the fromString method of the
StringConverter. The current implementation handles empty strings and strings
that parse to an empty list differently, which can lead to undesired empty tags.

jabgui/src/main/java/org/jabref/gui/fieldeditors/KeywordsEditorViewModel.java [74-88]

 @Override
 public Keyword fromString(String keywordString) {
-    if (keywordString.isEmpty()) {
+    if (keywordString == null || keywordString.isEmpty()) {
         return null;
     }
 
-    KeywordList parsedKeywords = KeywordList.parse(keywordString, keywordSeparator);
-    Keyword resultKeyword = null;
-    if (parsedKeywords.isEmpty()) {
-        return new Keyword("");
-    } else {
-        resultKeyword = parsedKeywords.get(0);
-    }
-    return resultKeyword;
+    return KeywordList.parse(keywordString, keywordSeparator)
+                      .stream()
+                      .findFirst()
+                      .orElse(null);
 }

[Suggestion processed]

Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies inconsistent behavior in the fromString method, where different empty-like inputs produce different results (null vs. new Keyword("")). The proposed change simplifies the logic and makes the behavior more consistent, preventing the creation of unwanted empty keywords.

Low
  • Update

@calixtus
Copy link
Member

Why dont you want to keep the converter static and reusable?

@BOgdAnSAM-sudo
Copy link
Contributor Author

Why dont you want to keep the converter static and reusable?

Because the converter needs access to the object's data to convert the strings.

@calixtus
Copy link
Member

Did it occur to you, that a keyword is different from a keywordlist?

@BOgdAnSAM-sudo
Copy link
Contributor Author

Did it occur to you, that a keyword is different from a keywordlist?

What do you mean by that?

@calixtus
Copy link
Member

The String converter for keywords is not meant to be used to parse a list of keywords, hoping that I ly the first one is the one you are looking for. Only clear defined keywords as strings to be converted are expected. Parsing for shg else must be done beforehand.

@calixtus
Copy link
Member

That's why the String converter was static. To keep the code clean.

@BOgdAnSAM-sudo
Copy link
Contributor Author

To create a Keyword from a string, the string must be parsed first. However, the logic in KeywordList#parse already returns a KeywordList populated with created Keywords. If we introduce a separate method to parse a single Keyword while keeping StringConverter static, the fromString method becomes redundant. This approach seems overcomplicated and leads to unused code.

@github-actions github-actions bot added the status: changes-required Pull requests that are not yet complete label Jan 27, 2026
@github-actions
Copy link
Contributor

Your pull request conflicts with the target branch.

Please merge 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.

@github-actions github-actions bot removed the status: changes-required Pull requests that are not yet complete label Jan 31, 2026
@BOgdAnSAM-sudo
Copy link
Contributor Author

That's why the String converter was static. To keep the code clean.

I understand your concern, but I don't see how to achieve this. I spent some time looking at the code and couldn't find a solution other than making the String converter non static. I would really appreciate it if you could share your thoughts on this.

@calixtus
Copy link
Member

Will do in a few days. Busy with day job.

@koppor koppor added the status: ready-for-review Pull Requests that are ready to be reviewed by the maintainers label Feb 2, 2026
@github-actions github-actions bot removed the status: ready-for-review Pull Requests that are ready to be reviewed by the maintainers label Feb 17, 2026
@testlens-app
Copy link

testlens-app bot commented Feb 22, 2026

✅ All tests passed ✅

🏷️ Commit: b3ed3a7
▶️ Tests: 11198 executed
⚪️ Checks: 51/51 completed


Learn more about TestLens at testlens.app.

@github-actions github-actions bot added status: no-bot-comments and removed status: changes-required Pull requests that are not yet complete labels Feb 22, 2026
@Siedlerchr Siedlerchr added the status: ready-for-review Pull Requests that are ready to be reviewed by the maintainers label Feb 23, 2026
calixtus
calixtus previously approved these changes Feb 23, 2026
@Siedlerchr Siedlerchr enabled auto-merge February 23, 2026 20:24
@calixtus
Copy link
Member

Looks good, thanks for coming back for this one.

@github-actions github-actions bot added status: changes-required Pull requests that are not yet complete and removed status: ready-for-review Pull Requests that are ready to be reviewed by the maintainers status: no-bot-comments labels Feb 24, 2026
auto-merge was automatically disabled February 24, 2026 15:12

Head branch was pushed to by a user without write access

@Siedlerchr Siedlerchr enabled auto-merge February 24, 2026 15:14
@github-actions github-actions bot added status: no-bot-comments and removed status: changes-required Pull requests that are not yet complete labels Feb 24, 2026
@Siedlerchr Siedlerchr added this pull request to the merge queue Feb 24, 2026
@github-actions github-actions bot added the status: to-be-merged PRs which are accepted and should go into the merge-queue. label Feb 24, 2026
Merged via the queue into JabRef:main with commit 4a18fa7 Feb 24, 2026
51 checks passed
statxc pushed a commit to statxc/jabref that referenced this pull request Feb 25, 2026
Resolve merge conflicts from main merge into fix/issue-15108 branch.
KeywordsEditor now properly extends TagsEditor and overrides the
separator key handler to preserve the escaped keyword support added
in main (JabRef#14929).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Siedlerchr added a commit that referenced this pull request Feb 25, 2026
* refs/heads/main:
  Increase max assignments from 2 to 3
  Chore(deps): Bump io.zonky.test:embedded-postgres in /versions (#15213)
  Chore(deps): Bump jablib/src/main/resources/csl-styles (#15211)
  New Crowdin updates (#15208)
  Fix: Prevent creating empty or duplicate fields  (#15168)
  chore(deps): update jackson monorepo to v3.1.0 (#15203)
  Update KeywordEditor to work with escaping (#14929)
  Chore(deps): Bump com.dlsc.gemsfx:gemsfx in /versions (#15205)
  Chore(deps): Bump tools.jackson:jackson-bom in /versions (#15206)
  Fix: Reset External File Type to default (#15167)
  docs: fix link formatting in ADR 0012 (#15201)
RakockiW pushed a commit to RakockiW/jabref that referenced this pull request Mar 1, 2026
* add default escape symbol constant to Keyword

* refactor getStringConverter to use new parsing logic

* refactor KeywordsEditor to use non-static viewModel.getStringConverter()

* update keyword separating logic

* fix fromString method

* add serialization for tag editing

* add tests for KeywordsEditorViewModel

* add CHANGELOG.md entry

* add check for missing separator

* add check for null and simplify loop

* update KeywordsEditor to use KeywordsEditorViewModel#parseKeyword instead of StringConverter for creating Keywords

* fix CHANGELOG.md

* update tests

* move CHANGELOG.md entry to unreleased

---------

Co-authored-by: Carl Christian Snethlage <50491877+calixtus@users.noreply.github.com>
priyanshu16095 pushed a commit to priyanshu16095/jabref that referenced this pull request Mar 3, 2026
* add default escape symbol constant to Keyword

* refactor getStringConverter to use new parsing logic

* refactor KeywordsEditor to use non-static viewModel.getStringConverter()

* update keyword separating logic

* fix fromString method

* add serialization for tag editing

* add tests for KeywordsEditorViewModel

* add CHANGELOG.md entry

* add check for missing separator

* add check for null and simplify loop

* update KeywordsEditor to use KeywordsEditorViewModel#parseKeyword instead of StringConverter for creating Keywords

* fix CHANGELOG.md

* update tests

* move CHANGELOG.md entry to unreleased

---------

Co-authored-by: Carl Christian Snethlage <50491877+calixtus@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Review effort 3/5 status: no-bot-comments status: to-be-merged PRs which are accepted and should go into the merge-queue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow manual entry of keywords with escaped delimiters

4 participants