diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index cb65efe6a7c..ef4a1dd6b23 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -12,7 +12,7 @@ body: attributes: label: JabRef version options: - - "5.6 (latest release)" + - "5.7 (latest release)" - "3.8.2" - Latest development branch build (please note build date below) - Other (please describe below) diff --git a/.github/workflows/cleanup_pr.yml b/.github/workflows/cleanup_pr.yml index 63090aba74a..20747030207 100644 --- a/.github/workflows/cleanup_pr.yml +++ b/.github/workflows/cleanup_pr.yml @@ -27,7 +27,7 @@ jobs: echo "##[set-output name=branch;]$(echo ${{ github.event.pull_request.head.ref }})" - name: Delete folder on builds.jabref.org if: ${{ steps.checksecrets.outputs.secretspresent }} - uses: appleboy/ssh-action@v0.1.4 + uses: appleboy/ssh-action@v0.1.5 with: script: rm -rf /var/www/builds.jabref.org/www/${{ steps.extract_branch.outputs.branch }} || true host: build-upload.jabref.org diff --git a/CHANGELOG.md b/CHANGELOG.md index 30f70513fe9..2a0f551366f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,12 +11,30 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve ### Added +### Changed + +### Fixed + +### Removed + + + + + + + + +## [5.7] - 2022-08-05 + +### Added + - We added a fetcher for [Biodiversity Heritage Library](https://www.biodiversitylibrary.org/). [8539](https://github.com/JabRef/jabref/issues/8539) - We added support for multiple messages in the snackbar. [#7340](https://github.com/JabRef/jabref/issues/7340) - We added an extra option in the 'Find Unlinked Files' dialog view to ignore unnecessary files like Thumbs.db, DS_Store, etc. [koppor#373](https://github.com/koppor/jabref/issues/373) - JabRef now writes log files. Linux: `$home/.cache/jabref/logs/version`, Windows: `%APPDATA%\..\Local\harawata\jabref\version\logs`, Mac: `Users/.../Library/Logs/jabref/version` - We added an importer for Citavi backup files, support ".ctv5bak" and ".ctv6bak" file formats. [#8322](https://github.com/JabRef/jabref/issues/8322) - We added a feature to drag selected entries and drop them to other opened inactive library tabs [koppor521](https://github.com/koppor/jabref/issues/521). +- We added support for the [biblatex-apa](https://github.com/plk/biblatex-apa) legal entry types `Legislation`, `Legadminmaterial`, `Jurisdiction`, `Constitution` and `Legal` [#8931](https://github.com/JabRef/jabref/issues/8931) ### Changed @@ -42,6 +60,7 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve ### Fixed +- We fixed an issue where the user could not rate an entry in the main table when an entry was not yet ranked. [#5842](https://github.com/JabRef/jabref/issues/5842) - We fixed an issue that caused JabRef to sometimes open multiple instances when "Remote Operation" is enabled. [#8653](https://github.com/JabRef/jabref/issues/8653) - We fixed an issue where linked files with the filetype "application/pdf" in an entry were not shown with the correct PDF-Icon in the main table [8930](https://github.com/JabRef/jabref/issues/8930) - We fixed an issue where "open folder" for linked files did not open the folder and did not select the file unter certain Linux desktop environments [#8679](https://github.com/JabRef/jabref/issues/8679), [#8849](https://github.com/JabRef/jabref/issues/8849) @@ -59,20 +78,14 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve - We fixed a bug where switching between themes will cause an error/exception. [#8939](https://github.com/JabRef/jabref/pull/8939) - We fixed a bug where files that were deleted in the source bibtex file were kept in the index. [#8962](https://github.com/JabRef/jabref/pull/8962) - We fixed "Error while sending to JabRef" when the browser extension interacts with JabRef. [JabRef-Browser-Extension#479](https://github.com/JabRef/JabRef-Browser-Extension/issues/479) +- We fixed a bug where updating group view mode (intersection or union) requires re-selecting groups to take effect. [#6998](https://github.com/JabRef/jabref/issues/6998) +- We fixed a bug that prevented external group metadata changes from being merged. [#8873](https://github.com/JabRef/jabref/issues/8873) +- We fixed the shared database opening dialog to remember autosave folder and tick. [#7516](https://github.com/JabRef/jabref/issues/7516) ### Removed - We removed the social media buttons for our Twitter and Facebook pages. [#8774](https://github.com/JabRef/jabref/issues/8774) - - - - - - - - - ## [5.6] - 2022-04-25 ### Added @@ -843,7 +856,8 @@ The changelog of JabRef 4.x is available at the [v4.3.1 tag](https://github.com/ The changelog of JabRef 3.x is available at the [v3.8.2 tag](https://github.com/JabRef/jabref/blob/v3.8.2/CHANGELOG.md). The changelog of JabRef 2.11 and all previous versions is available as [text file in the v2.11.1 tag](https://github.com/JabRef/jabref/blob/v2.11.1/CHANGELOG). -[Unreleased]: https://github.com/JabRef/jabref/compare/v5.6...HEAD +[Unreleased]: https://github.com/JabRef/jabref/compare/v5.7...HEAD +[5.7]: https://github.com/JabRef/jabref/compare/v5.6...v5.7 [5.6]: https://github.com/JabRef/jabref/compare/v5.5...v5.6 [5.5]: https://github.com/JabRef/jabref/compare/v5.4...v5.5 [5.4]: https://github.com/JabRef/jabref/compare/v5.3...v5.4 diff --git a/build.gradle b/build.gradle index cf66cc71f28..1b72c735a24 100644 --- a/build.gradle +++ b/build.gradle @@ -85,7 +85,6 @@ repositories { } configurations { - antlr3 antlr4 // TODO: Remove the following workaround for split error messages such as // error: module java.xml.bind reads package javax.annotation from both jsr305 and java.annotation @@ -116,11 +115,11 @@ dependencies { implementation 'org.apache.pdfbox:fontbox:3.0.0-RC1' implementation 'org.apache.pdfbox:xmpbox:3.0.0-RC1' - implementation 'org.apache.lucene:lucene-core:9.2.0' - implementation 'org.apache.lucene:lucene-queryparser:9.2.0' - implementation 'org.apache.lucene:lucene-queries:9.2.0' - implementation 'org.apache.lucene:lucene-analysis-common:9.2.0' - implementation 'org.apache.lucene:lucene-highlighter:9.2.0' + implementation 'org.apache.lucene:lucene-core:9.3.0' + implementation 'org.apache.lucene:lucene-queryparser:9.3.0' + implementation 'org.apache.lucene:lucene-queries:9.3.0' + implementation 'org.apache.lucene:lucene-analysis-common:9.3.0' + implementation 'org.apache.lucene:lucene-highlighter:9.3.0' implementation group: 'org.apache.commons', name: 'commons-csv', version: '1.9.0' implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0' @@ -133,15 +132,12 @@ dependencies { implementation 'commons-cli:commons-cli:1.5.0' - implementation 'org.libreoffice:libreoffice:7.3.4' - implementation 'org.libreoffice:unoloader:7.3.4' + implementation 'org.libreoffice:libreoffice:7.3.5' + implementation 'org.libreoffice:unoloader:7.3.5' - implementation 'io.github.java-diff-utils:java-diff-utils:4.11' + implementation 'io.github.java-diff-utils:java-diff-utils:4.12' implementation 'info.debatty:java-string-similarity:2.0.0' - antlr3 'org.antlr:antlr:3.5.3' - implementation 'org.antlr:antlr-runtime:3.5.3' - antlr4 'org.antlr:antlr4:4.9.3' implementation 'org.antlr:antlr4-runtime:4.9.3' @@ -208,8 +204,8 @@ dependencies { implementation group: 'net.harawata', name: 'appdirs', version: '1.2.1' testImplementation 'io.github.classgraph:classgraph:4.8.149' - testImplementation 'org.junit.jupiter:junit-jupiter:5.8.2' - testImplementation 'org.junit.platform:junit-platform-launcher:1.8.2' + testImplementation 'org.junit.jupiter:junit-jupiter:5.9.0' + testImplementation 'org.junit.platform:junit-platform-launcher:1.9.0' testImplementation 'org.mockito:mockito-core:4.6.1' testImplementation 'org.xmlunit:xmlunit-core:2.9.0' @@ -220,7 +216,7 @@ dependencies { testImplementation "org.testfx:testfx-junit5:4.0.16-alpha" testImplementation "org.hamcrest:hamcrest-library:2.2" - checkstyle 'com.puppycrawl.tools:checkstyle:10.3.1' + checkstyle 'com.puppycrawl.tools:checkstyle:10.3.2' // xjc needs the runtime as well for the ant task, otherwise it fails xjc group: 'org.glassfish.jaxb', name: 'jaxb-xjc', version: '3.0.2' xjc group: 'org.glassfish.jaxb', name: 'jaxb-runtime', version: '3.0.2' @@ -273,14 +269,14 @@ task generateSource(dependsOn: ["generateBstGrammarSource", } tasks.register("generateBstGrammarSource", JavaExec) { - main = "org.antlr.Tool" - classpath = configurations.antlr3 + main = "org.antlr.v4.Tool" + classpath = configurations.antlr4 group = "JabRef" - description = 'Generates BstLexer.java and BstParser.java from the Bst.g grammar file using antlr3.' + description = 'Generates BstLexer.java and BstParser.java from the Bst.g grammar file using antlr4.' - inputs.dir('src/main/antlr3/org/jabref/bst/') + inputs.dir('src/main/antlr4/org/jabref/bst/') outputs.dir("src-gen/main/java/org/jabref/logic/bst/") - args = ["-o", "src-gen/main/java/org/jabref/logic/bst/" , "$projectDir/src/main/antlr3/org/jabref/bst/Bst.g" ] + args = ["-o", "src-gen/main/java/org/jabref/logic/bst/", "-visitor", "-no-listener", "-package", "org.jabref.logic.bst", "$projectDir/src/main/antlr4/org/jabref/bst/Bst.g4"] } tasks.register("generateSearchGrammarSource", JavaExec) { diff --git a/buildres/csl/csl-styles/apa-5th-edition.csl b/buildres/csl/csl-styles/apa-5th-edition.csl index a2784ca4b5b..87cc83196a3 100644 --- a/buildres/csl/csl-styles/apa-5th-edition.csl +++ b/buildres/csl/csl-styles/apa-5th-edition.csl @@ -183,7 +183,7 @@ - + @@ -196,23 +196,37 @@ - + - + - + + + + + + + + + + + + diff --git a/buildres/csl/csl-styles/apa-6th-edition-no-ampersand.csl b/buildres/csl/csl-styles/apa-6th-edition-no-ampersand.csl index b0f618ec31f..ff55b46b98d 100644 --- a/buildres/csl/csl-styles/apa-6th-edition-no-ampersand.csl +++ b/buildres/csl/csl-styles/apa-6th-edition-no-ampersand.csl @@ -897,32 +897,52 @@ - + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + @@ -1401,7 +1421,7 @@ - diff --git a/buildres/csl/csl-styles/apa-6th-edition.csl b/buildres/csl/csl-styles/apa-6th-edition.csl index 775857accbe..851a3096ab0 100644 --- a/buildres/csl/csl-styles/apa-6th-edition.csl +++ b/buildres/csl/csl-styles/apa-6th-edition.csl @@ -896,32 +896,52 @@ - + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + @@ -1400,7 +1420,7 @@ - diff --git a/buildres/csl/csl-styles/apa-annotated-bibliography.csl b/buildres/csl/csl-styles/apa-annotated-bibliography.csl index 014ae44f162..0636d93db58 100644 --- a/buildres/csl/csl-styles/apa-annotated-bibliography.csl +++ b/buildres/csl/csl-styles/apa-annotated-bibliography.csl @@ -320,8 +320,8 @@ @@ -389,7 +389,7 @@ - @@ -470,7 +470,7 @@ - @@ -529,7 +529,7 @@ - @@ -1294,7 +1294,7 @@ - @@ -1559,7 +1559,7 @@ - + @@ -1567,6 +1567,20 @@ + + + + + + + + + + + @@ -1574,7 +1588,7 @@ - diff --git a/buildres/csl/csl-styles/apa-cv.csl b/buildres/csl/csl-styles/apa-cv.csl index 5a766c3dc61..228ad3ee882 100644 --- a/buildres/csl/csl-styles/apa-cv.csl +++ b/buildres/csl/csl-styles/apa-cv.csl @@ -320,8 +320,8 @@ @@ -415,7 +415,7 @@ - @@ -1085,7 +1085,7 @@ - @@ -1301,12 +1301,12 @@ + both publisher-place and event-place. Remove this 'choose' when that is changed. --> - + @@ -1314,6 +1314,20 @@ + + + + + + + + + + + @@ -1321,7 +1335,7 @@ - diff --git a/buildres/csl/csl-styles/apa-fr-provost.csl b/buildres/csl/csl-styles/apa-fr-provost.csl index 4b068cf752d..9e7928dca71 100644 --- a/buildres/csl/csl-styles/apa-fr-provost.csl +++ b/buildres/csl/csl-styles/apa-fr-provost.csl @@ -245,7 +245,7 @@ - + @@ -298,17 +298,17 @@ - + - + - + @@ -317,6 +317,20 @@ + + + + + + + + + + + diff --git a/buildres/csl/csl-styles/apa-no-ampersand.csl b/buildres/csl/csl-styles/apa-no-ampersand.csl index 446103abb69..5759e34ec4a 100644 --- a/buildres/csl/csl-styles/apa-no-ampersand.csl +++ b/buildres/csl/csl-styles/apa-no-ampersand.csl @@ -320,8 +320,8 @@ @@ -389,7 +389,7 @@ - @@ -470,7 +470,7 @@ - @@ -529,7 +529,7 @@ - @@ -1294,7 +1294,7 @@ - @@ -1554,12 +1554,12 @@ + both publisher-place and event-place. Remove this 'choose' when that is changed. --> - + @@ -1567,6 +1567,20 @@ + + + + + + + + + + + @@ -1574,7 +1588,7 @@ - diff --git a/buildres/csl/csl-styles/apa-no-doi-no-issue.csl b/buildres/csl/csl-styles/apa-no-doi-no-issue.csl index 08306362d76..c72c8bdf126 100644 --- a/buildres/csl/csl-styles/apa-no-doi-no-issue.csl +++ b/buildres/csl/csl-styles/apa-no-doi-no-issue.csl @@ -903,32 +903,52 @@ - + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + @@ -1414,7 +1434,7 @@ - diff --git a/buildres/csl/csl-styles/apa-no-initials.csl b/buildres/csl/csl-styles/apa-no-initials.csl index b8bcab11e79..4695cd9b6fb 100644 --- a/buildres/csl/csl-styles/apa-no-initials.csl +++ b/buildres/csl/csl-styles/apa-no-initials.csl @@ -320,8 +320,8 @@ @@ -389,7 +389,7 @@ - @@ -470,7 +470,7 @@ - @@ -529,7 +529,7 @@ - @@ -1294,7 +1294,7 @@ - @@ -1559,7 +1559,7 @@ - + @@ -1567,6 +1567,20 @@ + + + + + + + + + + + @@ -1574,7 +1588,7 @@ - diff --git a/buildres/csl/csl-styles/apa-numeric-superscript-brackets.csl b/buildres/csl/csl-styles/apa-numeric-superscript-brackets.csl index c6d3de36f82..ba3dadf6d20 100644 --- a/buildres/csl/csl-styles/apa-numeric-superscript-brackets.csl +++ b/buildres/csl/csl-styles/apa-numeric-superscript-brackets.csl @@ -123,8 +123,8 @@ @@ -192,7 +192,7 @@ - @@ -273,7 +273,7 @@ - @@ -332,7 +332,7 @@ - @@ -1097,7 +1097,7 @@ - @@ -1362,7 +1362,7 @@ - + @@ -1370,6 +1370,20 @@ + + + + + + + + + + + @@ -1377,7 +1391,7 @@ - diff --git a/buildres/csl/csl-styles/apa-numeric-superscript.csl b/buildres/csl/csl-styles/apa-numeric-superscript.csl index 0a6af38540b..1ed0ae88b7c 100644 --- a/buildres/csl/csl-styles/apa-numeric-superscript.csl +++ b/buildres/csl/csl-styles/apa-numeric-superscript.csl @@ -123,8 +123,8 @@ @@ -192,7 +192,7 @@ - @@ -273,7 +273,7 @@ - @@ -332,7 +332,7 @@ - @@ -1097,7 +1097,7 @@ - @@ -1362,7 +1362,7 @@ - + @@ -1370,6 +1370,20 @@ + + + + + + + + + + + @@ -1377,7 +1391,7 @@ - diff --git a/buildres/csl/csl-styles/apa-old-doi-prefix.csl b/buildres/csl/csl-styles/apa-old-doi-prefix.csl index 02ecfc75fac..cdfaa73a451 100644 --- a/buildres/csl/csl-styles/apa-old-doi-prefix.csl +++ b/buildres/csl/csl-styles/apa-old-doi-prefix.csl @@ -897,32 +897,52 @@ - + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + @@ -1401,7 +1421,7 @@ - diff --git a/buildres/csl/csl-styles/apa-single-spaced.csl b/buildres/csl/csl-styles/apa-single-spaced.csl index c2157cb296a..ff224f6a405 100644 --- a/buildres/csl/csl-styles/apa-single-spaced.csl +++ b/buildres/csl/csl-styles/apa-single-spaced.csl @@ -320,8 +320,8 @@ @@ -389,7 +389,7 @@ - @@ -470,7 +470,7 @@ - @@ -529,7 +529,7 @@ - @@ -1294,7 +1294,7 @@ - @@ -1554,12 +1554,12 @@ + both publisher-place and event-place. Remove this 'choose' when that is changed. --> - + @@ -1567,6 +1567,20 @@ + + + + + + + + + + + @@ -1574,7 +1588,7 @@ - diff --git a/buildres/csl/csl-styles/apa-tr.csl b/buildres/csl/csl-styles/apa-tr.csl index 557066dafd8..d17be770985 100644 --- a/buildres/csl/csl-styles/apa-tr.csl +++ b/buildres/csl/csl-styles/apa-tr.csl @@ -223,7 +223,7 @@ - + @@ -241,15 +241,15 @@ - + - + - + @@ -257,6 +257,20 @@ + + + + + + + + + + + diff --git a/buildres/csl/csl-styles/apa-with-abstract.csl b/buildres/csl/csl-styles/apa-with-abstract.csl index ddff715c9d6..25d185866bf 100644 --- a/buildres/csl/csl-styles/apa-with-abstract.csl +++ b/buildres/csl/csl-styles/apa-with-abstract.csl @@ -320,8 +320,8 @@ @@ -389,7 +389,7 @@ - @@ -470,7 +470,7 @@ - @@ -529,7 +529,7 @@ - @@ -1294,7 +1294,7 @@ - @@ -1559,7 +1559,7 @@ - + @@ -1567,6 +1567,20 @@ + + + + + + + + + + + @@ -1574,7 +1588,7 @@ - diff --git a/buildres/csl/csl-styles/apa.csl b/buildres/csl/csl-styles/apa.csl index 09f4635a505..0fd6bd71a5e 100644 --- a/buildres/csl/csl-styles/apa.csl +++ b/buildres/csl/csl-styles/apa.csl @@ -320,8 +320,8 @@ @@ -389,7 +389,7 @@ - @@ -470,7 +470,7 @@ - @@ -530,7 +530,7 @@ - @@ -1295,7 +1295,7 @@ - @@ -1560,7 +1560,7 @@ - + @@ -1568,6 +1568,20 @@ + + + + + + + + + + + @@ -1575,7 +1589,7 @@ - diff --git a/buildres/csl/csl-styles/association-for-computational-linguistics.csl b/buildres/csl/csl-styles/association-for-computational-linguistics.csl index 63ea2e0f2a4..ec95b2a95b5 100644 --- a/buildres/csl/csl-styles/association-for-computational-linguistics.csl +++ b/buildres/csl/csl-styles/association-for-computational-linguistics.csl @@ -67,11 +67,6 @@ - - - - - @@ -199,7 +194,6 @@ - @@ -212,7 +206,6 @@ - @@ -236,7 +229,6 @@ - @@ -244,7 +236,6 @@ - @@ -262,18 +253,10 @@ - - - - - - - - - + @@ -282,7 +265,6 @@ - @@ -293,13 +275,11 @@ - - diff --git a/buildres/csl/csl-styles/beilstein-journal-of-organic-chemistry.csl b/buildres/csl/csl-styles/beilstein-journal-of-organic-chemistry.csl index 8487b04c1ee..2ab0fe83caf 100644 --- a/buildres/csl/csl-styles/beilstein-journal-of-organic-chemistry.csl +++ b/buildres/csl/csl-styles/beilstein-journal-of-organic-chemistry.csl @@ -3,7 +3,7 @@ Beilstein Journal of Organic Chemistry - Beilstein + BJOC http://www.zotero.org/styles/beilstein-journal-of-organic-chemistry @@ -154,7 +154,6 @@ - diff --git a/buildres/csl/csl-styles/dependent/liver-transplantation.csl b/buildres/csl/csl-styles/dependent/liver-transplantation.csl new file mode 100644 index 00000000000..fd358188785 --- /dev/null +++ b/buildres/csl/csl-styles/dependent/liver-transplantation.csl @@ -0,0 +1,16 @@ + + diff --git a/buildres/csl/csl-styles/food-and-agriculture-organization-of-the-united-nations.csl b/buildres/csl/csl-styles/food-and-agriculture-organization-of-the-united-nations.csl index f017c7c9f94..0941c0564b0 100644 --- a/buildres/csl/csl-styles/food-and-agriculture-organization-of-the-united-nations.csl +++ b/buildres/csl/csl-styles/food-and-agriculture-organization-of-the-united-nations.csl @@ -31,7 +31,7 @@ This style is created to meet the citation and bibliographical requirements of FAOSTYLE, and has been tested with Zotero and Mendeley. Last update: February 2022. - 2022-02-02T16:03:38+00:00 + 2022-08-02T08:28:31+00:00 This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License @@ -180,6 +180,20 @@ + + + + + + + + + + + @@ -298,7 +312,7 @@ - + @@ -310,7 +324,7 @@ - + diff --git a/buildres/csl/csl-styles/vancouver-author-date.csl b/buildres/csl/csl-styles/vancouver-author-date.csl index 13b1bc2d023..4bd51dd6b9d 100644 --- a/buildres/csl/csl-styles/vancouver-author-date.csl +++ b/buildres/csl/csl-styles/vancouver-author-date.csl @@ -7,6 +7,7 @@ + Charles Parnot charles.parnot@gmail.com @@ -15,7 +16,7 @@ Vancouver style as outlined by International Committee of Medical Journal Editors Uniform Requirements for Manuscripts Submitted to Biomedical Journals: Sample References - 2012-11-09T12:00:00+00:00 + 2022-07-18T16:49:43+00:00 This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License @@ -47,6 +48,16 @@ + + + + + + + + + + @@ -253,25 +264,29 @@ - - - - - + + + + + + - - - + + + + + + diff --git a/external-libraries.md b/external-libraries.md index e207b994cf5..1eb2b8eeb71 100644 --- a/external-libraries.md +++ b/external-libraries.md @@ -274,13 +274,6 @@ URL: https://github.com/jhalterman/typetools License: Apache-2.0 ``` -```yaml -Id: org.antlr:antlr-runtime -Project: ANTLR 3 -URL: http://www.antlr3.org/ -License: BSD-3-Clause -``` - ```yaml Id: org.antlr:antlr4-runtime Project: ANTLR 4 @@ -289,16 +282,9 @@ License: BSD-3-Clause ``` ```yaml -Id: org.apache.commons:commons-csv +Id: org.apache.commons:* Project: Apache Commons CSV -URL: https://commons.apache.org/proper/commons-csv/ -License: Apache-2.0 -``` - -```yaml -Id: org.apache.commons:commons-lang3 -Project: Apache Commons Lang -URL: https://commons.apache.org/proper/commons-lang/ +URL: https://commons.apache.org/ License: Apache-2.0 ``` @@ -574,7 +560,7 @@ de.saxsys:mvvmfx:1.8.0 de.undercouch:citeproc-java:3.0.0-alpha.6 eu.lestard:doc-annotations:0.2 info.debatty:java-string-similarity:2.0.0 -io.github.java-diff-utils:java-diff-utils:4.11 +io.github.java-diff-utils:java-diff-utils:4.12 jakarta.annotation:jakarta.annotation-api:1.3.5 jakarta.xml.bind:jakarta.xml.bind-api:3.0.1 net.harawata:appdirs:1.2.1 @@ -582,21 +568,22 @@ net.java.dev.jna:jna-platform:5.6.0 net.java.dev.jna:jna:5.6.0 net.jcip:jcip-annotations:1.0 net.jodah:typetools:0.6.1 -org.antlr:antlr-runtime:3.5.3 org.antlr:antlr4-runtime:4.9.3 org.apache.commons:commons-csv:1.9.0 org.apache.commons:commons-lang3:3.12.0 +org.apache.commons:commons-text:1.9 org.apache.httpcomponents:httpasyncclient:4.1.5 org.apache.httpcomponents:httpclient:4.5.13 org.apache.httpcomponents:httpcore-nio:4.4.13 org.apache.httpcomponents:httpcore:4.4.13 org.apache.httpcomponents:httpmime:4.5.13 -org.apache.lucene:lucene-analysis-common:9.2.0 -org.apache.lucene:lucene-core:9.2.0 -org.apache.lucene:lucene-highlighter:9.2.0 -org.apache.lucene:lucene-queries:9.2.0 -org.apache.lucene:lucene-queryparser:9.2.0 -org.apache.lucene:lucene-sandbox:9.2.0 +org.apache.lucene:lucene-analysis-common:9.3.0 +org.apache.lucene:lucene-core:9.3.0 +org.apache.lucene:lucene-highlighter:9.3.0 +org.apache.lucene:lucene-memory:9.3.0 +org.apache.lucene:lucene-queries:9.3.0 +org.apache.lucene:lucene-queryparser:9.3.0 +org.apache.lucene:lucene-sandbox:9.3.0 org.apache.pdfbox:fontbox:3.0.0-RC1 org.apache.pdfbox:pdfbox:3.0.0-RC1 org.apache.pdfbox:xmpbox:3.0.0-RC1 @@ -620,8 +607,8 @@ org.jsoup:jsoup:1.15.1 org.kordamp.ikonli:ikonli-core:12.3.1 org.kordamp.ikonli:ikonli-javafx:12.3.1 org.kordamp.ikonli:ikonli-materialdesign2-pack:12.3.1 -org.libreoffice:libreoffice:7.3.4 -org.libreoffice:unoloader:7.3.4 +org.libreoffice:libreoffice:7.3.5 +org.libreoffice:unoloader:7.3.5 org.mariadb.jdbc:mariadb-java-client:2.7.6 org.openjfx:javafx-base:18.0.1 org.openjfx:javafx-controls:18.0.1 diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 2cd46e36917..2d32b9f9627 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -57,7 +57,7 @@ environment: parts: jabref: plugin: dump - source: https://builds.jabref.org/main/JabRef-5.7-portable_linux.tar.gz + source: https://builds.jabref.org/main/JabRef-5.8-portable_linux.tar.gz stage-packages: - x11-utils override-build: | diff --git a/src/main/antlr3/org/jabref/bst/Bst.g b/src/main/antlr3/org/jabref/bst/Bst.g deleted file mode 100644 index 498621ad1a1..00000000000 --- a/src/main/antlr3/org/jabref/bst/Bst.g +++ /dev/null @@ -1,96 +0,0 @@ -grammar Bst; - -options { - output=AST; -} - -tokens { - IDLIST; - STACK; - ENTRY; - COMMANDS; -} - -// applies only to the parser: -@header {// Generated by ANTLR -package org.jabref.logic.bst;} - -// applies only to the lexer: -@lexer::header {// Generated by ANTLR -package org.jabref.logic.bst;} - -program : commands+ -> ^(COMMANDS commands+); - -commands - : STRINGS^ idList - | INTEGERS^ idList - | FUNCTION^ id stack - | MACRO^ id '{'! STRING '}'! - | READ^ - | EXECUTE^ '{'! function '}'! - | ITERATE^ '{'! function '}'! - | REVERSE^ '{'! function '}'! - | ENTRY^ idList0 idList0 idList0 - | SORT^; - -identifier - : IDENTIFIER; - -id - : '{'! identifier '}'!; - -idList - : '{' identifier+ '}' -> ^(IDLIST identifier+); - -idList0 - : '{' identifier* '}' -> ^(IDLIST identifier*); - -function - : '<' | '>' | '=' | '+' | '-' | ':=' | '*' | identifier; - -stack - : '{' stackitem+ '}' -> ^(STACK stackitem+); - -stackitem - : function - | STRING - | INTEGER - | QUOTED - | stack; - -STRINGS : 'STRINGS'; -INTEGERS : 'INTEGERS'; -FUNCTION : 'FUNCTION'; -EXECUTE : 'EXECUTE'; -SORT : 'SORT'; -ITERATE : 'ITERATE'; -REVERSE : 'REVERSE'; -ENTRY : 'ENTRY'; -READ : 'READ'; -MACRO : 'MACRO'; - -QUOTED - : '\'' IDENTIFIER; - -IDENTIFIER - : LETTER (LETTER|NUMERAL|'_')* ; - -fragment LETTER - : ('a'..'z'|'A'..'Z'|'.'|'$'); - -STRING - : '"' (~('"'))* '"'; - -INTEGER - : '#' ('+'|'-')? NUMERAL+ ; - -fragment NUMERAL - : ('0'..'9'); - -WS - : (' '|'\t'|'\n'|'\r')+ {_channel=99;} ; - -LINE_COMMENT - : '%' ~('\n'|'\r')* '\r'? '\n' {_channel=99;} - ; - diff --git a/src/main/antlr4/org/jabref/bst/Bst.g4 b/src/main/antlr4/org/jabref/bst/Bst.g4 new file mode 100644 index 00000000000..92b96ce18df --- /dev/null +++ b/src/main/antlr4/org/jabref/bst/Bst.g4 @@ -0,0 +1,85 @@ +grammar Bst; + +// Lexer + +STRINGS : 'STRINGS'; +INTEGERS : 'INTEGERS'; +FUNCTION : 'FUNCTION'; +EXECUTE : 'EXECUTE'; +SORT : 'SORT'; +ITERATE : 'ITERATE'; +REVERSE : 'REVERSE'; +ENTRY : 'ENTRY'; +READ : 'READ'; +MACRO : 'MACRO'; + +GT : '>'; +LT : '<'; +EQUAL : '='; +ASSIGN : ':='; +ADD : '+'; +SUB : '-'; +CONCAT : '*'; +LBRACE : '{'; +RBRACE : '}'; + +fragment LETTER : ('a'..'z'|'A'..'Z'|'.'|'$'); +fragment NUMERAL : ('0'..'9'); + +IDENTIFIER : LETTER (LETTER|NUMERAL|'_')*; +INTEGER : '#' ('+'|'-')? NUMERAL+; +QUOTED : '\'' IDENTIFIER; +STRING : '"' (~('"'))* '"'; + +WS: [ \r\n\t]+ -> skip; +LINE_COMMENT : '%' ~('\n'|'\r')* '\r'? '\n' -> skip; + +// Parser + +bstFile + : commands+ EOF + ; + +commands + : STRINGS ids=idListObl #stringsCommand + | INTEGERS ids=idListObl #integersCommand + | FUNCTION LBRACE id=identifier RBRACE function=stack #functionCommand + | MACRO LBRACE id=identifier RBRACE LBRACE repl=STRING RBRACE #macroCommand + | READ #readCommand + | EXECUTE LBRACE bstFunction RBRACE #executeCommand + | ITERATE LBRACE bstFunction RBRACE #iterateCommand + | REVERSE LBRACE bstFunction RBRACE #reverseCommand + | ENTRY idListOpt idListOpt idListOpt #entryCommand + | SORT #sortCommand + ; + +identifier + : IDENTIFIER + ; + +// Obligatory identifier list +idListObl + : LBRACE identifier+ RBRACE + ; + +// Optional identifier list +idListOpt + : LBRACE identifier* RBRACE + ; + +bstFunction + : LT | GT | EQUAL | ADD | SUB | ASSIGN | CONCAT + | identifier + ; + +stack + : LBRACE stackitem+ RBRACE + ; + +stackitem + : bstFunction + | STRING + | INTEGER + | QUOTED + | stack + ; diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 751863df3e1..c05e10bda2b 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -85,7 +85,6 @@ requires org.mariadb.jdbc; uses org.mariadb.jdbc.credential.CredentialPlugin; requires org.apache.commons.lang3; - requires antlr.runtime; requires org.antlr.antlr4.runtime; requires org.fxmisc.flowless; requires org.apache.tika.core; diff --git a/src/main/java/org/jabref/gui/Base.css b/src/main/java/org/jabref/gui/Base.css index 188182d5151..f1829b7cc7e 100644 --- a/src/main/java/org/jabref/gui/Base.css +++ b/src/main/java/org/jabref/gui/Base.css @@ -1305,3 +1305,16 @@ TextFlow * { .text-field:invalid { -fx-background-color: rgba(240, 128, 128, 0.5); } + +.rating { + -fx-skin: "org.jabref.gui.util.CustomRatingSkin"; + -fx-padding: 0.5em 0px 0px 0px; +} + +.rating > .container > .button { + -fx-icon-color: derive(-fx-text-base-color, 85%); +} + +.rating > .container > .button.strong { + -fx-icon-color: -fx-text-base-color; +} diff --git a/src/main/java/org/jabref/gui/ClipBoardManager.java b/src/main/java/org/jabref/gui/ClipBoardManager.java index 290cb774c72..a75dd1798d9 100644 --- a/src/main/java/org/jabref/gui/ClipBoardManager.java +++ b/src/main/java/org/jabref/gui/ClipBoardManager.java @@ -5,10 +5,7 @@ import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Collections; import java.util.List; import java.util.Optional; @@ -22,19 +19,8 @@ import org.jabref.architecture.AllowedToUseAwt; import org.jabref.logic.bibtex.BibEntryWriter; import org.jabref.logic.bibtex.FieldWriter; -import org.jabref.logic.importer.FetcherException; -import org.jabref.logic.importer.ImportException; -import org.jabref.logic.importer.ImportFormatReader; -import org.jabref.logic.importer.ImportFormatReader.UnknownFormatImport; -import org.jabref.logic.importer.ParseException; -import org.jabref.logic.importer.fetcher.ArXiv; -import org.jabref.logic.importer.fetcher.DoiFetcher; -import org.jabref.logic.importer.fileformat.BibtexParser; import org.jabref.model.database.BibDatabaseMode; import org.jabref.model.entry.BibEntry; -import org.jabref.model.entry.identifier.ArXivIdentifier; -import org.jabref.model.entry.identifier.DOI; -import org.jabref.model.util.OptionalUtil; import org.jabref.preferences.PreferencesService; import org.slf4j.Logger; @@ -49,18 +35,16 @@ public class ClipBoardManager { private static Clipboard clipboard; private static java.awt.datatransfer.Clipboard primary; - private static ImportFormatReader importFormatReader; + private final PreferencesService preferencesService; public ClipBoardManager(PreferencesService preferencesService) { - this(Clipboard.getSystemClipboard(), Toolkit.getDefaultToolkit().getSystemSelection(), Globals.IMPORT_FORMAT_READER, preferencesService); + this(Clipboard.getSystemClipboard(), Toolkit.getDefaultToolkit().getSystemSelection(), preferencesService); } - public ClipBoardManager(Clipboard clipboard, java.awt.datatransfer.Clipboard primary, ImportFormatReader importFormatReader, PreferencesService preferencesService) { + public ClipBoardManager(Clipboard clipboard, java.awt.datatransfer.Clipboard primary, PreferencesService preferencesService) { ClipBoardManager.clipboard = clipboard; ClipBoardManager.primary = primary; - ClipBoardManager.importFormatReader = importFormatReader; - this.preferencesService = preferencesService; } @@ -103,6 +87,10 @@ public static String getContents() { return result; } + public Optional getBibTeXEntriesFromClipbaord() { + return Optional.ofNullable(clipboard.getContent(DragAndDropDataFormats.ENTRIES)).map(String.class::cast); + } + /** * Get the String residing on the primary clipboard (if it exists). * @@ -167,71 +155,4 @@ public void setContent(List entries) throws IOException { clipboard.setContent(content); setPrimaryClipboardContent(content); } - - public List extractData() { - Object entries = clipboard.getContent(DragAndDropDataFormats.ENTRIES); - - if (entries == null) { - return handleStringData(clipboard.getString()); - } - return handleBibTeXData((String) entries); - } - - private List handleBibTeXData(String entries) { - BibtexParser parser = new BibtexParser(preferencesService.getImportFormatPreferences(), Globals.getFileUpdateMonitor()); - try { - return parser.parseEntries(new ByteArrayInputStream(entries.getBytes(StandardCharsets.UTF_8))); - } catch (ParseException ex) { - LOGGER.error("Could not paste", ex); - return Collections.emptyList(); - } - } - - private List handleStringData(String data) { - if ((data == null) || data.isEmpty()) { - return Collections.emptyList(); - } - - Optional doi = DOI.parse(data); - if (doi.isPresent()) { - return fetchByDOI(doi.get()); - } - Optional arXiv = ArXivIdentifier.parse(data); - if (arXiv.isPresent()) { - return fetchByArXiv(arXiv.get()); - } - - return tryImportFormats(data); - } - - private List tryImportFormats(String data) { - try { - UnknownFormatImport unknownFormatImport = importFormatReader.importUnknownFormat(data); - return unknownFormatImport.parserResult.getDatabase().getEntries(); - } catch (ImportException ignored) { - return Collections.emptyList(); - } - } - - private List fetchByDOI(DOI doi) { - LOGGER.info("Found DOI in clipboard"); - try { - Optional entry = new DoiFetcher(preferencesService.getImportFormatPreferences()).performSearchById(doi.getDOI()); - return OptionalUtil.toList(entry); - } catch (FetcherException ex) { - LOGGER.error("Error while fetching", ex); - return Collections.emptyList(); - } - } - - private List fetchByArXiv(ArXivIdentifier arXivIdentifier) { - LOGGER.info("Found arxiv identifier in clipboard"); - try { - Optional entry = new ArXiv(preferencesService.getImportFormatPreferences()).performSearchById(arXivIdentifier.getNormalizedWithoutVersion()); - return OptionalUtil.toList(entry); - } catch (FetcherException ex) { - LOGGER.error("Error while fetching", ex); - return Collections.emptyList(); - } - } } diff --git a/src/main/java/org/jabref/gui/Dark.css b/src/main/java/org/jabref/gui/Dark.css index adaa1cd1a83..0c23ea36773 100644 --- a/src/main/java/org/jabref/gui/Dark.css +++ b/src/main/java/org/jabref/gui/Dark.css @@ -132,3 +132,11 @@ .notification-bar > .pane { -fx-background-color: -fx-light-text-color; } + +.rating > .container > .button { + -fx-icon-color: derive(-fx-light-text-color, -50%); +} + +.rating > .container > .button.strong { + -fx-icon-color: -fx-light-text-color; +} diff --git a/src/main/java/org/jabref/gui/DefaultInjector.java b/src/main/java/org/jabref/gui/DefaultInjector.java index d198dbe1136..5318c3b95dc 100644 --- a/src/main/java/org/jabref/gui/DefaultInjector.java +++ b/src/main/java/org/jabref/gui/DefaultInjector.java @@ -7,6 +7,7 @@ import org.jabref.gui.keyboard.KeyBindingRepository; import org.jabref.gui.theme.ThemeManager; import org.jabref.gui.util.TaskExecutor; +import org.jabref.logic.importer.ImportFormatReader; import org.jabref.logic.journals.JournalAbbreviationRepository; import org.jabref.logic.protectedterms.ProtectedTermsLoader; import org.jabref.model.entry.BibEntryTypesManager; @@ -52,6 +53,8 @@ private static Object createDependency(Class> clazz) { return Globals.undoManager; } else if (clazz == BibEntryTypesManager.class) { return Globals.entryTypesManager; + } else if (clazz == ImportFormatReader.class) { + return Globals.IMPORT_FORMAT_READER; } else { try { return clazz.newInstance(); diff --git a/src/main/java/org/jabref/gui/EntryTypeView.java b/src/main/java/org/jabref/gui/EntryTypeView.java index 10a95f3a5d0..7db73467529 100644 --- a/src/main/java/org/jabref/gui/EntryTypeView.java +++ b/src/main/java/org/jabref/gui/EntryTypeView.java @@ -24,10 +24,12 @@ import org.jabref.gui.util.IconValidationDecorator; import org.jabref.gui.util.ViewModelListCellFactory; import org.jabref.logic.importer.IdBasedFetcher; +import org.jabref.logic.importer.ImportFormatReader; import org.jabref.logic.importer.WebFetcher; import org.jabref.logic.l10n.Localization; import org.jabref.model.database.BibDatabaseMode; import org.jabref.model.entry.BibEntryType; +import org.jabref.model.entry.types.BiblatexAPAEntryTypeDefinitions; import org.jabref.model.entry.types.BiblatexEntryTypeDefinitions; import org.jabref.model.entry.types.BiblatexSoftwareEntryTypeDefinitions; import org.jabref.model.entry.types.BibtexEntryTypeDefinitions; @@ -47,6 +49,7 @@ public class EntryTypeView extends BaseDialog { @Inject StateManager stateManager; + @Inject ImportFormatReader importFormatReader; @FXML private ButtonType generateButton; @FXML private TextField idTextField; @@ -118,7 +121,7 @@ private void addEntriesToPane(FlowPane pane, Collection extends BibEntryType> @FXML public void initialize() { visualizer.setDecoration(new IconValidationDecorator()); - viewModel = new EntryTypeViewModel(preferencesService, libraryTab, dialogService, stateManager); + viewModel = new EntryTypeViewModel(preferencesService, libraryTab, dialogService, stateManager, importFormatReader); idBasedFetchers.itemsProperty().bind(viewModel.fetcherItemsProperty()); idTextField.textProperty().bindBidirectional(viewModel.idTextProperty()); @@ -157,6 +160,7 @@ public void initialize() { .filter(e -> !recommendedEntries.contains(e)) .collect(Collectors.toList()); otherEntries.addAll(BiblatexSoftwareEntryTypeDefinitions.ALL); + otherEntries.addAll(BiblatexAPAEntryTypeDefinitions.ALL); } else { recommendedEntries = BibtexEntryTypeDefinitions.RECOMMENDED; otherEntries = BibtexEntryTypeDefinitions.ALL diff --git a/src/main/java/org/jabref/gui/EntryTypeViewModel.java b/src/main/java/org/jabref/gui/EntryTypeViewModel.java index 16782b19c00..4e18e4d87c0 100644 --- a/src/main/java/org/jabref/gui/EntryTypeViewModel.java +++ b/src/main/java/org/jabref/gui/EntryTypeViewModel.java @@ -17,8 +17,11 @@ import org.jabref.gui.externalfiles.ImportHandler; import org.jabref.gui.externalfiletype.ExternalFileTypes; import org.jabref.gui.importer.NewEntryAction; +import org.jabref.logic.importer.FetcherClientException; import org.jabref.logic.importer.FetcherException; +import org.jabref.logic.importer.FetcherServerException; import org.jabref.logic.importer.IdBasedFetcher; +import org.jabref.logic.importer.ImportFormatReader; import org.jabref.logic.importer.WebFetchers; import org.jabref.logic.importer.fetcher.DoiFetcher; import org.jabref.logic.l10n.Localization; @@ -50,15 +53,19 @@ public class EntryTypeViewModel { private final DialogService dialogService; private final Validator idFieldValidator; private final StateManager stateManager; + private final ImportFormatReader importFormatReader; public EntryTypeViewModel(PreferencesService preferences, LibraryTab libraryTab, DialogService dialogService, - StateManager stateManager) { + StateManager stateManager, + ImportFormatReader importFormatReader) { this.libraryTab = libraryTab; this.preferencesService = preferences; this.dialogService = dialogService; this.stateManager = stateManager; + this.importFormatReader = importFormatReader; + fetchers.addAll(WebFetchers.getIdBasedFetchers( preferences.getImportFormatPreferences(), preferences.getImporterPreferences())); @@ -140,15 +147,18 @@ public void runFetcherWorker() { String fetcherExceptionMessage = exception.getMessage(); String fetcher = selectedItemProperty().getValue().getName(); String searchId = idText.getValue(); - if (exception instanceof FetcherException) { - dialogService.showErrorDialogAndWait(Localization.lang("Error"), Localization.lang("Error while fetching from %0", fetcher + "." + "\n" + fetcherExceptionMessage)); + + if (exception instanceof FetcherClientException) { + dialogService.showInformationDialogAndWait(Localization.lang("Failed to import by ID"), Localization.lang("Bibliographic data not found. Cause is likely the client side. Please check connection and identifier for correctness.") + "\n" + fetcherExceptionMessage); + } else if (exception instanceof FetcherServerException) { + dialogService.showInformationDialogAndWait(Localization.lang("Failed to import by ID"), Localization.lang("Bibliographic data not found. Cause is likely the server side. Please try agan later.") + "\n" + fetcherExceptionMessage); } else { - dialogService.showErrorDialogAndWait(Localization.lang("No files found.", Localization.lang("Fetcher '%0' did not find an entry for id '%1'.", fetcher, searchId) + "\n" + fetcherExceptionMessage)); + dialogService.showInformationDialogAndWait(Localization.lang("Failed to import by ID"), Localization.lang("Error message %0", fetcherExceptionMessage)); } + LOGGER.error(String.format("Exception during fetching when using fetcher '%s' with entry id '%s'.", searchId, fetcher), exception); searchingProperty.set(false); - fetcherWorker = new FetcherWorker(); }); @@ -164,7 +174,8 @@ public void runFetcherWorker() { Globals.getFileUpdateMonitor(), libraryTab.getUndoManager(), stateManager, - dialogService); + dialogService, + importFormatReader); handler.importEntryWithDuplicateCheck(libraryTab.getBibDatabaseContext(), entry); searchSuccesfulProperty.set(true); @@ -177,7 +188,7 @@ public void runFetcherWorker() { String searchId = idText.getValue(); // When DOI ID is not found, allow the user to either return to the dialog or add entry manually - boolean addEntryFlag = dialogService.showConfirmationDialogAndWait(Localization.lang("DOI not found"), + boolean addEntryFlag = dialogService.showConfirmationDialogAndWait(Localization.lang("Identifier not found"), Localization.lang("Fetcher '%0' did not find an entry for id '%1'.", fetcher, searchId), Localization.lang("Add entry manually"), Localization.lang("Return to dialog")); diff --git a/src/main/java/org/jabref/gui/JabRefFrame.java b/src/main/java/org/jabref/gui/JabRefFrame.java index ba86aa068b8..440dc84dec6 100644 --- a/src/main/java/org/jabref/gui/JabRefFrame.java +++ b/src/main/java/org/jabref/gui/JabRefFrame.java @@ -127,6 +127,7 @@ import org.jabref.logic.help.HelpFile; import org.jabref.logic.importer.IdFetcher; import org.jabref.logic.importer.ImportCleanup; +import org.jabref.logic.importer.ImportFormatReader; import org.jabref.logic.importer.ParserResult; import org.jabref.logic.importer.WebFetchers; import org.jabref.logic.l10n.Localization; @@ -183,6 +184,7 @@ public class JabRefFrame extends BorderPane { private Subscription dividerSubscription; private final TaskExecutor taskExecutor; + private final ImportFormatReader importFormatReader; public JabRefFrame(Stage mainStage) { this.mainStage = mainStage; @@ -194,6 +196,7 @@ public JabRefFrame(Stage mainStage) { this.globalSearchBar = new GlobalSearchBar(this, stateManager, prefs, undoManager); this.fileHistory = new FileHistoryMenu(prefs, dialogService, getOpenDatabaseAction()); this.taskExecutor = Globals.TASK_EXECUTOR; + this.importFormatReader = Globals.IMPORT_FORMAT_READER; this.setOnKeyTyped(key -> { if (this.fileHistory.isShowing()) { if (this.fileHistory.openFileByKey(key)) { @@ -232,8 +235,8 @@ private void initDragAndDrop() { this.getScene().setOnDragEntered(event -> { // It is necessary to setOnDragOver for newly opened tabs // drag'n'drop on tabs covered dnd on tabbedPane, so dnd on tabs should contain all dnds on tabbedPane - tabbedPane.lookupAll(".tab").forEach(t -> { - t.setOnDragOver(tabDragEvent -> { + tabbedPane.lookupAll(".tab").forEach(tab -> { + tab.setOnDragOver(tabDragEvent -> { if (DragAndDropHelper.hasBibFiles(tabDragEvent.getDragboard())) { tabDragEvent.acceptTransferModes(TransferMode.ANY); if (!tabbedPane.getTabs().contains(dndIndicator)) { @@ -249,8 +252,8 @@ private void initDragAndDrop() { tabDragEvent.consume(); } }); - t.setOnDragExited(event1 -> tabbedPane.getTabs().remove(dndIndicator)); - t.setOnDragDropped(tabDragEvent -> { + tab.setOnDragExited(event1 -> tabbedPane.getTabs().remove(dndIndicator)); + tab.setOnDragDropped(tabDragEvent -> { if (DragAndDropHelper.hasBibFiles(tabDragEvent.getDragboard())) { tabbedPane.getTabs().remove(dndIndicator); List bibFiles = DragAndDropHelper.getBibFiles(tabDragEvent.getDragboard()); @@ -260,7 +263,7 @@ private void initDragAndDrop() { tabDragEvent.consume(); } else { for (Tab libraryTab : tabbedPane.getTabs()) { - if (libraryTab.getId().equals(t.getId()) && + if (libraryTab.getId().equals(tab.getId()) && !tabbedPane.getSelectionModel().getSelectedItem().equals(libraryTab)) { ((LibraryTab) libraryTab).dropEntry(stateManager.getLocalDragboard().getBibEntries()); } @@ -1140,7 +1143,7 @@ private void trackOpenNewDatabase(LibraryTab libraryTab) { public LibraryTab addTab(BibDatabaseContext databaseContext, boolean raisePanel) { Objects.requireNonNull(databaseContext); - LibraryTab libraryTab = new LibraryTab(this, prefs, stateManager, themeManager, databaseContext, ExternalFileTypes.getInstance()); + LibraryTab libraryTab = new LibraryTab(this, prefs, stateManager, themeManager, databaseContext, ExternalFileTypes.getInstance(), importFormatReader); addTab(libraryTab, raisePanel); return libraryTab; } diff --git a/src/main/java/org/jabref/gui/JabRefMain.java b/src/main/java/org/jabref/gui/JabRefMain.java index 32de8b6f0d9..e26b1286c9b 100644 --- a/src/main/java/org/jabref/gui/JabRefMain.java +++ b/src/main/java/org/jabref/gui/JabRefMain.java @@ -163,7 +163,7 @@ private static void applyPreferences(PreferencesService preferences) { // Build list of Import and Export formats Globals.IMPORT_FORMAT_READER.resetImportFormats(preferences.getImporterPreferences(), - preferences.getGeneralPreferences(), preferences.getImportFormatPreferences(), + preferences.getImportFormatPreferences(), preferences.getXmpPreferences(), Globals.getFileUpdateMonitor()); Globals.entryTypesManager.addCustomOrModifiedTypes(preferences.getBibEntryTypes(BibDatabaseMode.BIBTEX), preferences.getBibEntryTypes(BibDatabaseMode.BIBLATEX)); diff --git a/src/main/java/org/jabref/gui/LibraryTab.java b/src/main/java/org/jabref/gui/LibraryTab.java index 139cdf31fd0..f037135f6d9 100644 --- a/src/main/java/org/jabref/gui/LibraryTab.java +++ b/src/main/java/org/jabref/gui/LibraryTab.java @@ -44,6 +44,7 @@ import org.jabref.logic.autosaveandbackup.AutosaveManager; import org.jabref.logic.autosaveandbackup.BackupManager; import org.jabref.logic.citationstyle.CitationStyleCache; +import org.jabref.logic.importer.ImportFormatReader; import org.jabref.logic.importer.ParserResult; import org.jabref.logic.importer.util.FileFieldParser; import org.jabref.logic.l10n.Localization; @@ -114,13 +115,15 @@ public class LibraryTab extends Tab { private BackgroundTask dataLoadingTask = BackgroundTask.wrap(() -> null); private final IndexingTaskManager indexingTaskManager = new IndexingTaskManager(Globals.TASK_EXECUTOR); + private final ImportFormatReader importFormatReader; public LibraryTab(JabRefFrame frame, PreferencesService preferencesService, StateManager stateManager, ThemeManager themeManager, BibDatabaseContext bibDatabaseContext, - ExternalFileTypes externalFileTypes) { + ExternalFileTypes externalFileTypes, + ImportFormatReader importFormatReader) { this.frame = Objects.requireNonNull(frame); this.bibDatabaseContext = Objects.requireNonNull(bibDatabaseContext); this.externalFileTypes = Objects.requireNonNull(externalFileTypes); @@ -129,6 +132,7 @@ public LibraryTab(JabRefFrame frame, this.preferencesService = Objects.requireNonNull(preferencesService); this.stateManager = Objects.requireNonNull(stateManager); this.themeManager = Objects.requireNonNull(themeManager); + this.importFormatReader = importFormatReader; bibDatabaseContext.getDatabase().registerListener(this); bibDatabaseContext.getMetaData().registerListener(this); @@ -485,7 +489,9 @@ private void createMainTable() { dialogService, stateManager, externalFileTypes, - Globals.getKeyPrefs()); + Globals.getKeyPrefs(), + Globals.getClipboardManager(), + Globals.IMPORT_FORMAT_READER); // Add the listener that binds selection to state manager (TODO: should be replaced by proper JavaFX binding as soon as table is implemented in JavaFX) mainTable.addSelectionListener(listEvent -> stateManager.setSelectedEntries(mainTable.getSelectedEntries())); @@ -798,11 +804,11 @@ public void resetChangedProperties() { } public static class Factory { - public LibraryTab createLibraryTab(JabRefFrame frame, PreferencesService preferencesService, StateManager stateManager, ThemeManager themeManager, Path file, BackgroundTask dataLoadingTask) { + public LibraryTab createLibraryTab(JabRefFrame frame, PreferencesService preferencesService, StateManager stateManager, ThemeManager themeManager, Path file, BackgroundTask dataLoadingTask, ImportFormatReader importFormatReader) { BibDatabaseContext context = new BibDatabaseContext(); context.setDatabasePath(file); - LibraryTab newTab = new LibraryTab(frame, preferencesService, stateManager, themeManager, context, ExternalFileTypes.getInstance()); + LibraryTab newTab = new LibraryTab(frame, preferencesService, stateManager, themeManager, context, ExternalFileTypes.getInstance(), importFormatReader); newTab.setDataLoadingTask(dataLoadingTask); dataLoadingTask.onRunning(newTab::onDatabaseLoadingStarted) @@ -938,7 +944,7 @@ public void notify(Node graphic, String text, List actions, Duration dur this.setText(text); this.getActions().setAll(actions); this.show(); - if (duration != null && !duration.equals(Duration.ZERO)) { + if ((duration != null) && !duration.equals(Duration.ZERO)) { PauseTransition delay = new PauseTransition(duration); delay.setOnFinished(e -> this.hide()); delay.play(); diff --git a/src/main/java/org/jabref/gui/StateManager.java b/src/main/java/org/jabref/gui/StateManager.java index a8d93152de9..cc6ab8ae8bd 100644 --- a/src/main/java/org/jabref/gui/StateManager.java +++ b/src/main/java/org/jabref/gui/StateManager.java @@ -8,9 +8,11 @@ import javafx.beans.Observable; import javafx.beans.binding.Bindings; import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyListProperty; import javafx.beans.property.ReadOnlyListWrapper; import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.ObservableMap; @@ -18,6 +20,7 @@ import javafx.scene.Node; import javafx.util.Pair; +import org.jabref.gui.edit.automaticfiededitor.LastAutomaticFieldEditorEdit; import org.jabref.gui.sidepane.SidePaneType; import org.jabref.gui.util.BackgroundTask; import org.jabref.gui.util.CustomLocalDragboard; @@ -62,6 +65,8 @@ public class StateManager { private final ObservableMap dialogWindowStates = FXCollections.observableHashMap(); private final ObservableList visibleSidePanes = FXCollections.observableArrayList(); + private final ObjectProperty lastAutomaticFieldEditorEdit = new SimpleObjectProperty<>(); + public StateManager() { activeGroups.bind(Bindings.valueAt(selectedGroups, activeDatabase.orElse(null))); } @@ -172,4 +177,16 @@ public DialogWindowState getDialogWindowState(String className) { public void setDialogWindowState(String className, DialogWindowState state) { dialogWindowStates.put(className, state); } + + public ObjectProperty lastAutomaticFieldEditorEditProperty() { + return lastAutomaticFieldEditorEdit; + } + + public LastAutomaticFieldEditorEdit getLastAutomaticFieldEditorEdit() { + return lastAutomaticFieldEditorEditProperty().get(); + } + + public void setLastAutomaticFieldEditorEdit(LastAutomaticFieldEditorEdit automaticFieldEditorEdit) { + lastAutomaticFieldEditorEditProperty().set(automaticFieldEditorEdit); + } } diff --git a/src/main/java/org/jabref/gui/bibtexextractor/BibtexExtractorViewModel.java b/src/main/java/org/jabref/gui/bibtexextractor/BibtexExtractorViewModel.java index 3472e69d466..8224873ad70 100644 --- a/src/main/java/org/jabref/gui/bibtexextractor/BibtexExtractorViewModel.java +++ b/src/main/java/org/jabref/gui/bibtexextractor/BibtexExtractorViewModel.java @@ -17,6 +17,7 @@ import org.jabref.gui.util.BackgroundTask; import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.importer.FetcherException; +import org.jabref.logic.importer.ImportFormatReader; import org.jabref.logic.importer.fetcher.GrobidCitationFetcher; import org.jabref.logic.l10n.Localization; import org.jabref.model.database.BibDatabaseContext; @@ -43,7 +44,8 @@ public BibtexExtractorViewModel(BibDatabaseContext bibdatabaseContext, FileUpdateMonitor fileUpdateMonitor, TaskExecutor taskExecutor, UndoManager undoManager, - StateManager stateManager) { + StateManager stateManager, + ImportFormatReader importFormatReader) { this.dialogService = dialogService; this.preferencesService = preferencesService; @@ -55,7 +57,8 @@ public BibtexExtractorViewModel(BibDatabaseContext bibdatabaseContext, fileUpdateMonitor, undoManager, stateManager, - dialogService); + dialogService, + importFormatReader); } public StringProperty inputTextProperty() { diff --git a/src/main/java/org/jabref/gui/bibtexextractor/ExtractBibtexDialog.java b/src/main/java/org/jabref/gui/bibtexextractor/ExtractBibtexDialog.java index 04affccbf1a..3a5d0ec7a3f 100644 --- a/src/main/java/org/jabref/gui/bibtexextractor/ExtractBibtexDialog.java +++ b/src/main/java/org/jabref/gui/bibtexextractor/ExtractBibtexDialog.java @@ -13,6 +13,7 @@ import org.jabref.gui.StateManager; import org.jabref.gui.util.BaseDialog; import org.jabref.gui.util.TaskExecutor; +import org.jabref.logic.importer.ImportFormatReader; import org.jabref.logic.l10n.Localization; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.util.FileUpdateMonitor; @@ -35,6 +36,7 @@ public class ExtractBibtexDialog extends BaseDialog { @Inject private TaskExecutor taskExecutor; @Inject private UndoManager undoManager; @Inject private PreferencesService preferencesService; + @Inject private ImportFormatReader importFormatReader; public ExtractBibtexDialog() { ViewLoader.view(this) @@ -53,7 +55,7 @@ public ExtractBibtexDialog() { @FXML private void initialize() { BibDatabaseContext database = stateManager.getActiveDatabase().orElseThrow(() -> new NullPointerException("Database null")); - this.viewModel = new BibtexExtractorViewModel(database, dialogService, preferencesService, fileUpdateMonitor, taskExecutor, undoManager, stateManager); + this.viewModel = new BibtexExtractorViewModel(database, dialogService, preferencesService, fileUpdateMonitor, taskExecutor, undoManager, stateManager, importFormatReader); input.textProperty().bindBidirectional(viewModel.inputTextProperty()); } } diff --git a/src/main/java/org/jabref/gui/collab/MetaDataChangeViewModel.java b/src/main/java/org/jabref/gui/collab/MetaDataChangeViewModel.java index 83b6351fd78..a60bccaa6e9 100644 --- a/src/main/java/org/jabref/gui/collab/MetaDataChangeViewModel.java +++ b/src/main/java/org/jabref/gui/collab/MetaDataChangeViewModel.java @@ -39,5 +39,8 @@ public Node description() { @Override public void makeChange(BibDatabaseContext database, NamedCompound undoEdit) { database.setMetaData(metaDataDiff.getNewMetaData()); + // group change is handled by GroupChangeViewModel, so we set the groups root to the original value + // to prevent any inconsistency + metaDataDiff.getGroupDifferences().ifPresent(groupDiff -> database.getMetaData().setGroups(groupDiff.getOriginalGroupRoot())); } } diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/AbstractAutomaticFieldEditorTabViewModel.java b/src/main/java/org/jabref/gui/edit/automaticfiededitor/AbstractAutomaticFieldEditorTabViewModel.java new file mode 100644 index 00000000000..fe6e60fee01 --- /dev/null +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/AbstractAutomaticFieldEditorTabViewModel.java @@ -0,0 +1,48 @@ +package org.jabref.gui.edit.automaticfiededitor; + +import java.util.Collection; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import org.jabref.gui.AbstractViewModel; +import org.jabref.gui.StateManager; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.StandardField; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class AbstractAutomaticFieldEditorTabViewModel extends AbstractViewModel { + public static final Logger LOGGER = LoggerFactory.getLogger(AbstractAutomaticFieldEditorTabViewModel.class); + + protected final StateManager stateManager; + + private final ObservableList allFields = FXCollections.observableArrayList(); + + public AbstractAutomaticFieldEditorTabViewModel(BibDatabase bibDatabase, StateManager stateManager) { + Objects.requireNonNull(bibDatabase); + Objects.requireNonNull(stateManager); + this.stateManager = stateManager; + + addFields(EnumSet.allOf(StandardField.class)); + addFields(bibDatabase.getAllVisibleFields()); + allFields.sort(Comparator.comparing(Field::getName)); + } + + public ObservableList getAllFields() { + return allFields; + } + + private void addFields(Collection extends Field> fields) { + Set fieldsSet = new HashSet<>(allFields); + fieldsSet.addAll(fields); + allFields.setAll(fieldsSet); + } +} diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorAction.java b/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorAction.java index a56af9dfa78..904762ca02a 100644 --- a/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorAction.java +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorAction.java @@ -20,7 +20,6 @@ public AutomaticFieldEditorAction(StateManager stateManager, DialogService dialo @Override public void execute() { - dialogService.showCustomDialogAndWait(new AutomaticFieldEditorDialog(stateManager.getSelectedEntries(), - stateManager.getActiveDatabase().orElseThrow())); + dialogService.showCustomDialogAndWait(new AutomaticFieldEditorDialog(stateManager)); } } diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorDialog.fxml b/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorDialog.fxml index f79462369a1..a909b45d522 100644 --- a/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorDialog.fxml +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorDialog.fxml @@ -9,7 +9,6 @@ - - + + diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorDialog.java b/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorDialog.java index 5ecbf06b275..9f6a4663d9d 100644 --- a/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorDialog.java +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorDialog.java @@ -1,39 +1,50 @@ package org.jabref.gui.edit.automaticfiededitor; +import java.util.ArrayList; import java.util.List; +import javax.swing.undo.CannotUndoException; import javax.swing.undo.UndoManager; import javafx.fxml.FXML; import javafx.scene.control.ButtonBar; -import javafx.scene.control.ButtonType; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import org.jabref.gui.Globals; +import org.jabref.gui.StateManager; import org.jabref.gui.util.BaseDialog; -import org.jabref.gui.util.ControlHelper; import org.jabref.logic.l10n.Localization; -import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.database.BibDatabase; import org.jabref.model.entry.BibEntry; import com.airhacks.afterburner.views.ViewLoader; +import com.tobiasdiez.easybind.EasyBind; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AutomaticFieldEditorDialog extends BaseDialog { + + private static final Logger LOGGER = LoggerFactory.getLogger(AutomaticFieldEditorDialog.class); -public class AutomaticFieldEditorDialog extends BaseDialog { - @FXML public ButtonType saveButton; - @FXML public ButtonType cancelButton; @FXML private TabPane tabPane; private final UndoManager undoManager; - private final BibDatabaseContext databaseContext; + private final BibDatabase database; private final List selectedEntries; + + private final StateManager stateManager; + private AutomaticFieldEditorViewModel viewModel; - public AutomaticFieldEditorDialog(List selectedEntries, BibDatabaseContext databaseContext) { - this.selectedEntries = selectedEntries; - this.databaseContext = databaseContext; + private List notificationPanes = new ArrayList<>(); + + public AutomaticFieldEditorDialog(StateManager stateManager) { + this.selectedEntries = stateManager.getSelectedEntries(); + this.database = stateManager.getActiveDatabase().orElseThrow().getDatabase(); + this.stateManager = stateManager; this.undoManager = Globals.undoManager; this.setTitle(Localization.lang("Automatic field editor")); @@ -42,8 +53,14 @@ public AutomaticFieldEditorDialog(List selectedEntries, BibDatabaseCon .load() .setAsDialogPane(this); - ControlHelper.setAction(saveButton, getDialogPane(), event -> saveChangesAndCloseDialog()); - ControlHelper.setAction(cancelButton, getDialogPane(), event -> cancelChangesAndCloseDialog()); + setResultConverter(buttonType -> { + if (buttonType != null && buttonType.getButtonData() == ButtonBar.ButtonData.OK_DONE) { + saveChanges(); + } else { + cancelChanges(); + } + return ""; + }); // This will prevent all dialog buttons from having the same size // Read more: https://stackoverflow.com/questions/45866249/javafx-8-alert-different-button-sizes @@ -54,20 +71,30 @@ public AutomaticFieldEditorDialog(List selectedEntries, BibDatabaseCon @FXML public void initialize() { - viewModel = new AutomaticFieldEditorViewModel(selectedEntries, databaseContext, undoManager); + viewModel = new AutomaticFieldEditorViewModel(selectedEntries, database, undoManager, stateManager); for (AutomaticFieldEditorTab tabModel : viewModel.getFieldEditorTabs()) { - tabPane.getTabs().add(new Tab(tabModel.getTabName(), tabModel.getContent())); + NotificationPaneAdapter notificationPane = new NotificationPaneAdapter(tabModel.getContent()); + notificationPanes.add(notificationPane); + tabPane.getTabs().add(new Tab(tabModel.getTabName(), notificationPane)); } + + EasyBind.listen(stateManager.lastAutomaticFieldEditorEditProperty(), (obs, old, lastEdit) -> { + viewModel.getDialogEdits().addEdit(lastEdit.getEdit()); + notificationPanes.get(lastEdit.getTabIndex()) + .notify(lastEdit.getAffectedEntries(), selectedEntries.size()); + }); } - private void saveChangesAndCloseDialog() { + private void saveChanges() { viewModel.saveChanges(); - close(); } - private void cancelChangesAndCloseDialog() { - viewModel.cancelChanges(); - close(); + private void cancelChanges() { + try { + viewModel.cancelChanges(); + } catch (CannotUndoException e) { + LOGGER.info("Could not undo", e); + } } } diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorViewModel.java b/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorViewModel.java index 0c334ddf6fa..3f517a1dabd 100644 --- a/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorViewModel.java +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/AutomaticFieldEditorViewModel.java @@ -8,11 +8,12 @@ import javafx.collections.ObservableList; import org.jabref.gui.AbstractViewModel; -import org.jabref.gui.edit.automaticfiededitor.editfieldvalue.EditFieldValueTabView; +import org.jabref.gui.StateManager; +import org.jabref.gui.edit.automaticfiededitor.copyormovecontent.CopyOrMoveFieldContentTabView; +import org.jabref.gui.edit.automaticfiededitor.editfieldcontent.EditFieldContentTabView; import org.jabref.gui.edit.automaticfiededitor.renamefield.RenameFieldTabView; -import org.jabref.gui.edit.automaticfiededitor.twofields.TwoFieldsTabView; import org.jabref.gui.undo.NamedCompound; -import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.database.BibDatabase; import org.jabref.model.entry.BibEntry; public class AutomaticFieldEditorViewModel extends AbstractViewModel { @@ -20,17 +21,19 @@ public class AutomaticFieldEditorViewModel extends AbstractViewModel { private final ObservableList fieldEditorTabs = FXCollections.observableArrayList(); private final NamedCompound dialogEdits = new NamedCompound(NAMED_COMPOUND_EDITS); - private final BibDatabaseContext databaseContext; private final UndoManager undoManager; - public AutomaticFieldEditorViewModel(List selectedEntries, BibDatabaseContext databaseContext, UndoManager undoManager) { + public AutomaticFieldEditorViewModel(List selectedEntries, BibDatabase database, UndoManager undoManager, StateManager stateManager) { + this.undoManager = undoManager; fieldEditorTabs.addAll( - new EditFieldValueTabView(selectedEntries, databaseContext, dialogEdits), - new TwoFieldsTabView(selectedEntries, databaseContext, dialogEdits), - new RenameFieldTabView(selectedEntries, databaseContext, dialogEdits) + new EditFieldContentTabView(database, stateManager), + new CopyOrMoveFieldContentTabView(database, stateManager), + new RenameFieldTabView(database, stateManager) ); - this.databaseContext = databaseContext; - this.undoManager = undoManager; + } + + public NamedCompound getDialogEdits() { + return dialogEdits; } public ObservableList getFieldEditorTabs() { diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/LastAutomaticFieldEditorEdit.java b/src/main/java/org/jabref/gui/edit/automaticfiededitor/LastAutomaticFieldEditorEdit.java new file mode 100644 index 00000000000..a1263576608 --- /dev/null +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/LastAutomaticFieldEditorEdit.java @@ -0,0 +1,44 @@ +package org.jabref.gui.edit.automaticfiededitor; + +import javax.swing.undo.AbstractUndoableEdit; +import javax.swing.undo.CannotRedoException; +import javax.swing.undo.CannotUndoException; + +import org.jabref.gui.undo.NamedCompound; + +public class LastAutomaticFieldEditorEdit extends AbstractUndoableEdit { + private final Integer affectedEntries; + private final NamedCompound edit; + + private final Integer tabIndex; + + public LastAutomaticFieldEditorEdit(Integer affectedEntries, Integer tabIndex, NamedCompound edit) { + this.affectedEntries = affectedEntries; + this.edit = edit; + this.tabIndex = tabIndex; + } + + public Integer getAffectedEntries() { + return affectedEntries; + } + + public NamedCompound getEdit() { + return edit; + } + + public Integer getTabIndex() { + return tabIndex; + } + + @Override + public void undo() throws CannotUndoException { + super.undo(); + edit.undo(); + } + + @Override + public void redo() throws CannotRedoException { + super.redo(); + edit.redo(); + } +} diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/MoveFieldValueAction.java b/src/main/java/org/jabref/gui/edit/automaticfiededitor/MoveFieldValueAction.java index 8863d17de78..660c2f8454b 100644 --- a/src/main/java/org/jabref/gui/edit/automaticfiededitor/MoveFieldValueAction.java +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/MoveFieldValueAction.java @@ -16,27 +16,48 @@ public class MoveFieldValueAction extends SimpleCommand { private final NamedCompound edits; - public MoveFieldValueAction(Field fromField, Field toField, List entries, NamedCompound edits) { + private int affectedEntriesCount; + + private final boolean overwriteToFieldContent; + + public MoveFieldValueAction(Field fromField, Field toField, List entries, NamedCompound edits, boolean overwriteToFieldContent) { this.fromField = fromField; this.toField = toField; this.entries = entries; this.edits = edits; + this.overwriteToFieldContent = overwriteToFieldContent; + } + + public MoveFieldValueAction(Field fromField, Field toField, List entries, NamedCompound edits) { + this(fromField, toField, entries, edits, true); } @Override public void execute() { + affectedEntriesCount = 0; for (BibEntry entry : entries) { String fromFieldValue = entry.getField(fromField).orElse(""); String toFieldValue = entry.getField(toField).orElse(""); - if (StringUtil.isNotBlank(fromFieldValue)) { - entry.setField(toField, fromFieldValue); - entry.setField(fromField, ""); - - edits.addEdit(new UndoableFieldChange(entry, fromField, fromFieldValue, null)); - edits.addEdit(new UndoableFieldChange(entry, toField, toFieldValue, fromFieldValue)); + if (overwriteToFieldContent || toFieldValue.isEmpty()) { + entry.setField(toField, fromFieldValue); + entry.setField(fromField, ""); + + edits.addEdit(new UndoableFieldChange(entry, fromField, fromFieldValue, null)); + edits.addEdit(new UndoableFieldChange(entry, toField, toFieldValue, fromFieldValue)); + affectedEntriesCount++; + } } } + edits.end(); } + + /** + * @return the number of affected entries + * */ + public int executeAndGetAffectedEntriesCount() { + execute(); + return affectedEntriesCount; + } } diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/NotificationPaneAdapter.java b/src/main/java/org/jabref/gui/edit/automaticfiededitor/NotificationPaneAdapter.java new file mode 100644 index 00000000000..4fc8b9d8b63 --- /dev/null +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/NotificationPaneAdapter.java @@ -0,0 +1,23 @@ +package org.jabref.gui.edit.automaticfiededitor; + +import java.util.Collections; + +import javafx.scene.Node; +import javafx.util.Duration; + +import org.jabref.gui.LibraryTab; +import org.jabref.gui.icon.IconTheme; + +public class NotificationPaneAdapter extends LibraryTab.DatabaseNotification { + + public NotificationPaneAdapter(Node content) { + super(content); + } + + public void notify(int affectedEntries, int totalEntries) { + String notificationMessage = String.format("%d/%d affected entries", affectedEntries, totalEntries); + Node notificationGraphic = IconTheme.JabRefIcons.INTEGRITY_INFO.getGraphicNode(); + + notify(notificationGraphic, notificationMessage, Collections.emptyList(), Duration.seconds(2)); + } +} diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/twofields/TwoFieldsTab.fxml b/src/main/java/org/jabref/gui/edit/automaticfiededitor/copyormovecontent/CopyOrMoveFieldContentTab.fxml similarity index 68% rename from src/main/java/org/jabref/gui/edit/automaticfiededitor/twofields/TwoFieldsTab.fxml rename to src/main/java/org/jabref/gui/edit/automaticfiededitor/copyormovecontent/CopyOrMoveFieldContentTab.fxml index 7ac475e6cd7..1302a550b17 100644 --- a/src/main/java/org/jabref/gui/edit/automaticfiededitor/twofields/TwoFieldsTab.fxml +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/copyormovecontent/CopyOrMoveFieldContentTab.fxml @@ -11,7 +11,7 @@ - + @@ -28,21 +28,25 @@ - - - - - + + + + + - - - + + + - + diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/copyormovecontent/CopyOrMoveFieldContentTabView.java b/src/main/java/org/jabref/gui/edit/automaticfiededitor/copyormovecontent/CopyOrMoveFieldContentTabView.java new file mode 100644 index 00000000000..af742e708c4 --- /dev/null +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/copyormovecontent/CopyOrMoveFieldContentTabView.java @@ -0,0 +1,109 @@ +package org.jabref.gui.edit.automaticfiededitor.copyormovecontent; + +import java.util.ArrayList; +import java.util.List; + +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; + +import org.jabref.gui.StateManager; +import org.jabref.gui.edit.automaticfiededitor.AbstractAutomaticFieldEditorTabView; +import org.jabref.gui.edit.automaticfiededitor.AutomaticFieldEditorTab; +import org.jabref.logic.l10n.Localization; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.Field; + +import com.airhacks.afterburner.views.ViewLoader; +import com.tobiasdiez.easybind.EasyBind; +import de.saxsys.mvvmfx.utils.validation.visualization.ControlsFxVisualizer; + +import static org.jabref.gui.customentrytypes.CustomEntryTypeDialogViewModel.FIELD_STRING_CONVERTER; + +public class CopyOrMoveFieldContentTabView extends AbstractAutomaticFieldEditorTabView implements AutomaticFieldEditorTab { + public Button copyContentButton; + @FXML + private Button moveContentButton; + + @FXML + private Button swapContentButton; + + @FXML + private ComboBox fromFieldComboBox; + @FXML + private ComboBox toFieldComboBox; + + @FXML + private CheckBox overwriteFieldContentCheckBox; + + private CopyOrMoveFieldContentTabViewModel viewModel; + private final List selectedEntries; + private final BibDatabase database; + private final StateManager stateManager; + + private final ControlsFxVisualizer visualizer = new ControlsFxVisualizer(); + + public CopyOrMoveFieldContentTabView(BibDatabase database, StateManager stateManager) { + this.selectedEntries = new ArrayList<>(stateManager.getSelectedEntries()); + this.database = database; + this.stateManager = stateManager; + + ViewLoader.view(this) + .root(this) + .load(); + } + + public void initialize() { + viewModel = new CopyOrMoveFieldContentTabViewModel(selectedEntries, database, stateManager); + initializeFromAndToComboBox(); + + viewModel.overwriteFieldContentProperty().bindBidirectional(overwriteFieldContentCheckBox.selectedProperty()); + + moveContentButton.disableProperty().bind(viewModel.canMoveProperty().not()); + swapContentButton.disableProperty().bind(viewModel.canSwapProperty().not()); + copyContentButton.disableProperty().bind(viewModel.toFieldValidationStatus().validProperty().not()); + overwriteFieldContentCheckBox.disableProperty().bind(viewModel.toFieldValidationStatus().validProperty().not()); + + Platform.runLater(() -> { + visualizer.initVisualization(viewModel.toFieldValidationStatus(), toFieldComboBox, true); + }); + } + + private void initializeFromAndToComboBox() { + fromFieldComboBox.getItems().setAll(viewModel.getAllFields()); + toFieldComboBox.getItems().setAll(viewModel.getAllFields()); + + fromFieldComboBox.setConverter(FIELD_STRING_CONVERTER); + + toFieldComboBox.setConverter(FIELD_STRING_CONVERTER); + + fromFieldComboBox.valueProperty().bindBidirectional(viewModel.fromFieldProperty()); + toFieldComboBox.valueProperty().bindBidirectional(viewModel.toFieldProperty()); + + EasyBind.listen(fromFieldComboBox.getEditor().textProperty(), observable -> fromFieldComboBox.commitValue()); + EasyBind.listen(toFieldComboBox.getEditor().textProperty(), observable -> toFieldComboBox.commitValue()); + } + + @Override + public String getTabName() { + return Localization.lang("Copy or Move content"); + } + + @FXML + void copyContent() { + viewModel.copyValue(); + } + + @FXML + void moveContent() { + viewModel.moveValue(); + } + + @FXML + void swapContent() { + viewModel.swapValues(); + } +} diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/copyormovecontent/CopyOrMoveFieldContentTabViewModel.java b/src/main/java/org/jabref/gui/edit/automaticfiededitor/copyormovecontent/CopyOrMoveFieldContentTabViewModel.java new file mode 100644 index 00000000000..3ed3a1b38ee --- /dev/null +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/copyormovecontent/CopyOrMoveFieldContentTabViewModel.java @@ -0,0 +1,183 @@ +package org.jabref.gui.edit.automaticfiededitor.copyormovecontent; + +import java.util.ArrayList; +import java.util.List; + +import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; + +import org.jabref.gui.StateManager; +import org.jabref.gui.edit.automaticfiededitor.AbstractAutomaticFieldEditorTabViewModel; +import org.jabref.gui.edit.automaticfiededitor.LastAutomaticFieldEditorEdit; +import org.jabref.gui.edit.automaticfiededitor.MoveFieldValueAction; +import org.jabref.gui.undo.NamedCompound; +import org.jabref.gui.undo.UndoableFieldChange; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.strings.StringUtil; + +import de.saxsys.mvvmfx.utils.validation.FunctionBasedValidator; +import de.saxsys.mvvmfx.utils.validation.ValidationMessage; +import de.saxsys.mvvmfx.utils.validation.ValidationStatus; +import de.saxsys.mvvmfx.utils.validation.Validator; + +public class CopyOrMoveFieldContentTabViewModel extends AbstractAutomaticFieldEditorTabViewModel { + public static final int TAB_INDEX = 1; + private final ObjectProperty fromField = new SimpleObjectProperty<>(StandardField.ABSTRACT); + + private final ObjectProperty toField = new SimpleObjectProperty<>(StandardField.AUTHOR); + + private final BooleanProperty overwriteFieldContent = new SimpleBooleanProperty(Boolean.FALSE); + private final List selectedEntries; + + private final Validator toFieldValidator; + + private final BooleanBinding canMove; + + private final BooleanBinding canSwap; + + public CopyOrMoveFieldContentTabViewModel(List selectedEntries, BibDatabase bibDatabase, StateManager stateManager) { + super(bibDatabase, stateManager); + this.selectedEntries = new ArrayList<>(selectedEntries); + + toFieldValidator = new FunctionBasedValidator<>(toField, field -> { + if (StringUtil.isBlank(field.getName())) { + return ValidationMessage.error("Field name cannot be empty"); + } else if (StringUtil.containsWhitespace(field.getName())) { + return ValidationMessage.error("Field name cannot have whitespace characters"); + } + return null; + }); + + canMove = BooleanBinding.booleanExpression(toFieldValidationStatus().validProperty()) + .and(overwriteFieldContentProperty()); + + canSwap = BooleanBinding.booleanExpression(toFieldValidationStatus().validProperty()) + .and(overwriteFieldContentProperty()); + } + + public ValidationStatus toFieldValidationStatus() { + return toFieldValidator.getValidationStatus(); + } + + public BooleanBinding canMoveProperty() { + return canMove; + } + + public BooleanBinding canSwapProperty() { + return canSwap; + } + + public Field getFromField() { + return fromField.get(); + } + + public ObjectProperty fromFieldProperty() { + return fromField; + } + + public Field getToField() { + return toField.get(); + } + + public ObjectProperty toFieldProperty() { + return toField; + } + + public boolean isOverwriteFieldContent() { + return overwriteFieldContent.get(); + } + + public BooleanProperty overwriteFieldContentProperty() { + return overwriteFieldContent; + } + + public void copyValue() { + NamedCompound copyFieldValueEdit = new NamedCompound("COPY_FIELD_VALUE"); + int affectedEntriesCount = 0; + for (BibEntry entry : selectedEntries) { + String fromFieldValue = entry.getField(fromField.get()).orElse(""); + String toFieldValue = entry.getField(toField.get()).orElse(""); + + if (overwriteFieldContent.get() || StringUtil.isBlank(toFieldValue)) { + if (StringUtil.isNotBlank(fromFieldValue)) { + entry.setField(toField.get(), fromFieldValue); + copyFieldValueEdit.addEdit(new UndoableFieldChange(entry, + toField.get(), + toFieldValue, + fromFieldValue)); + affectedEntriesCount++; + } + } + } + if (copyFieldValueEdit.hasEdits()) { + copyFieldValueEdit.end(); + } + stateManager.setLastAutomaticFieldEditorEdit(new LastAutomaticFieldEditorEdit( + affectedEntriesCount, TAB_INDEX, copyFieldValueEdit + )); + } + + public void moveValue() { + NamedCompound moveEdit = new NamedCompound("MOVE_EDIT"); + int affectedEntriesCount = 0; + if (overwriteFieldContent.get()) { + affectedEntriesCount = new MoveFieldValueAction(fromField.get(), + toField.get(), + selectedEntries, + moveEdit).executeAndGetAffectedEntriesCount(); + + if (moveEdit.hasEdits()) { + moveEdit.end(); + } + } + stateManager.setLastAutomaticFieldEditorEdit(new LastAutomaticFieldEditorEdit( + affectedEntriesCount, TAB_INDEX, moveEdit + )); + } + + public void swapValues() { + NamedCompound swapFieldValuesEdit = new NamedCompound("SWAP_FIELD_VALUES"); + int affectedEntriesCount = 0; + for (BibEntry entry : selectedEntries) { + String fromFieldValue = entry.getField(fromField.get()).orElse(""); + String toFieldValue = entry.getField(toField.get()).orElse(""); + + if (overwriteFieldContent.get() && StringUtil.isNotBlank(fromFieldValue) && StringUtil.isNotBlank(toFieldValue)) { + entry.setField(toField.get(), fromFieldValue); + entry.setField(fromField.get(), toFieldValue); + + swapFieldValuesEdit.addEdit(new UndoableFieldChange( + entry, + toField.get(), + toFieldValue, + fromFieldValue + )); + + swapFieldValuesEdit.addEdit(new UndoableFieldChange( + entry, + fromField.get(), + fromFieldValue, + toFieldValue + )); + affectedEntriesCount++; + } + } + + if (swapFieldValuesEdit.hasEdits()) { + swapFieldValuesEdit.end(); + } + stateManager.setLastAutomaticFieldEditorEdit(new LastAutomaticFieldEditorEdit( + affectedEntriesCount, TAB_INDEX, swapFieldValuesEdit + )); + } + + public List getSelectedEntries() { + return selectedEntries; + } +} diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/editfieldvalue/EditFieldValueTab.fxml b/src/main/java/org/jabref/gui/edit/automaticfiededitor/editfieldcontent/EditFieldContentTab.fxml similarity index 85% rename from src/main/java/org/jabref/gui/edit/automaticfiededitor/editfieldvalue/EditFieldValueTab.fxml rename to src/main/java/org/jabref/gui/edit/automaticfiededitor/editfieldcontent/EditFieldContentTab.fxml index 4cf8a44016e..1beac33dd48 100644 --- a/src/main/java/org/jabref/gui/edit/automaticfiededitor/editfieldvalue/EditFieldValueTab.fxml +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/editfieldcontent/EditFieldContentTab.fxml @@ -15,7 +15,7 @@ - + @@ -32,7 +32,8 @@ - + @@ -42,7 +43,7 @@ - + @@ -51,13 +52,13 @@ - - + + - + diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/editfieldcontent/EditFieldContentTabView.java b/src/main/java/org/jabref/gui/edit/automaticfiededitor/editfieldcontent/EditFieldContentTabView.java new file mode 100644 index 00000000000..af1b4f9d9c1 --- /dev/null +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/editfieldcontent/EditFieldContentTabView.java @@ -0,0 +1,100 @@ +package org.jabref.gui.edit.automaticfiededitor.editfieldcontent; + +import java.util.List; + +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.TextField; + +import org.jabref.gui.StateManager; +import org.jabref.gui.edit.automaticfiededitor.AbstractAutomaticFieldEditorTabView; +import org.jabref.logic.l10n.Localization; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.Field; + +import com.airhacks.afterburner.views.ViewLoader; +import com.tobiasdiez.easybind.EasyBind; +import de.saxsys.mvvmfx.utils.validation.visualization.ControlsFxVisualizer; + +import static org.jabref.gui.customentrytypes.CustomEntryTypeDialogViewModel.FIELD_STRING_CONVERTER; + +public class EditFieldContentTabView extends AbstractAutomaticFieldEditorTabView { + public Button appendValueButton; + public Button clearFieldButton; + public Button setValueButton; + @FXML + private ComboBox fieldComboBox; + + @FXML + private TextField fieldValueTextField; + + @FXML + private CheckBox overwriteFieldContentCheckBox; + + private final List selectedEntries; + private final BibDatabase database; + + private EditFieldContentViewModel viewModel; + + private final StateManager stateManager; + + private final ControlsFxVisualizer visualizer = new ControlsFxVisualizer(); + + public EditFieldContentTabView(BibDatabase database, StateManager stateManager) { + this.selectedEntries = stateManager.getSelectedEntries(); + this.database = database; + this.stateManager = stateManager; + + ViewLoader.view(this) + .root(this) + .load(); + } + + @FXML + public void initialize() { + viewModel = new EditFieldContentViewModel(database, selectedEntries, stateManager); + fieldComboBox.setConverter(FIELD_STRING_CONVERTER); + + fieldComboBox.getItems().setAll(viewModel.getAllFields()); + + fieldComboBox.getSelectionModel().selectFirst(); + + fieldComboBox.valueProperty().bindBidirectional(viewModel.selectedFieldProperty()); + EasyBind.listen(fieldComboBox.getEditor().textProperty(), observable -> fieldComboBox.commitValue()); + + fieldValueTextField.textProperty().bindBidirectional(viewModel.fieldValueProperty()); + + overwriteFieldContentCheckBox.selectedProperty().bindBidirectional(viewModel.overwriteFieldContentProperty()); + + appendValueButton.disableProperty().bind(viewModel.canAppendProperty().not()); + setValueButton.disableProperty().bind(viewModel.fieldValidationStatus().validProperty().not()); + clearFieldButton.disableProperty().bind(viewModel.fieldValidationStatus().validProperty().not()); + overwriteFieldContentCheckBox.disableProperty().bind(viewModel.fieldValidationStatus().validProperty().not()); + + Platform.runLater(() -> visualizer.initVisualization(viewModel.fieldValidationStatus(), fieldComboBox, true)); + } + + @Override + public String getTabName() { + return Localization.lang("Edit content"); + } + + @FXML + void appendToFieldValue() { + viewModel.appendToFieldValue(); + } + + @FXML + void clearField() { + viewModel.clearSelectedField(); + } + + @FXML + void setFieldValue() { + viewModel.setFieldValue(); + } +} diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/editfieldcontent/EditFieldContentViewModel.java b/src/main/java/org/jabref/gui/edit/automaticfiededitor/editfieldcontent/EditFieldContentViewModel.java new file mode 100644 index 00000000000..933e1d4f87b --- /dev/null +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/editfieldcontent/EditFieldContentViewModel.java @@ -0,0 +1,164 @@ +package org.jabref.gui.edit.automaticfiededitor.editfieldcontent; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +import org.jabref.gui.StateManager; +import org.jabref.gui.edit.automaticfiededitor.AbstractAutomaticFieldEditorTabViewModel; +import org.jabref.gui.edit.automaticfiededitor.LastAutomaticFieldEditorEdit; +import org.jabref.gui.undo.NamedCompound; +import org.jabref.gui.undo.UndoableFieldChange; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.strings.StringUtil; + +import de.saxsys.mvvmfx.utils.validation.FunctionBasedValidator; +import de.saxsys.mvvmfx.utils.validation.ValidationMessage; +import de.saxsys.mvvmfx.utils.validation.ValidationStatus; +import de.saxsys.mvvmfx.utils.validation.Validator; + +public class EditFieldContentViewModel extends AbstractAutomaticFieldEditorTabViewModel { + public static final int TAB_INDEX = 0; + + private final List selectedEntries; + + private final StringProperty fieldValue = new SimpleStringProperty(""); + + private final ObjectProperty selectedField = new SimpleObjectProperty<>(StandardField.AUTHOR); + + private final BooleanProperty overwriteFieldContent = new SimpleBooleanProperty(Boolean.FALSE); + + private final Validator fieldValidator; + private final BooleanBinding canAppend; + + public EditFieldContentViewModel(BibDatabase database, List selectedEntries, StateManager stateManager) { + super(database, stateManager); + this.selectedEntries = new ArrayList<>(selectedEntries); + + fieldValidator = new FunctionBasedValidator<>(selectedField, field -> { + if (StringUtil.isBlank(field.getName())) { + return ValidationMessage.error("Field name cannot be empty"); + } else if (StringUtil.containsWhitespace(field.getName())) { + return ValidationMessage.error("Field name cannot have whitespace characters"); + } + return null; + }); + + canAppend = Bindings.and(overwriteFieldContentProperty(), fieldValidationStatus().validProperty()); + } + + public ValidationStatus fieldValidationStatus() { + return fieldValidator.getValidationStatus(); + } + + public BooleanBinding canAppendProperty() { + return canAppend; + } + + public void clearSelectedField() { + NamedCompound clearFieldEdit = new NamedCompound("CLEAR_SELECTED_FIELD"); + int affectedEntriesCount = 0; + for (BibEntry entry : selectedEntries) { + Optional oldFieldValue = entry.getField(selectedField.get()); + if (oldFieldValue.isPresent()) { + entry.clearField(selectedField.get()) + .ifPresent(fieldChange -> clearFieldEdit.addEdit(new UndoableFieldChange(fieldChange))); + affectedEntriesCount++; + } + } + + if (clearFieldEdit.hasEdits()) { + clearFieldEdit.end(); + } + stateManager.setLastAutomaticFieldEditorEdit(new LastAutomaticFieldEditorEdit( + affectedEntriesCount, + TAB_INDEX, + clearFieldEdit + )); + } + + public void setFieldValue() { + NamedCompound setFieldEdit = new NamedCompound("CHANGE_SELECTED_FIELD"); + String toSetFieldValue = fieldValue.getValue(); + int affectedEntriesCount = 0; + for (BibEntry entry : selectedEntries) { + Optional oldFieldValue = entry.getField(selectedField.get()); + if (oldFieldValue.isEmpty() || overwriteFieldContent.get()) { + entry.setField(selectedField.get(), toSetFieldValue) + .ifPresent(fieldChange -> setFieldEdit.addEdit(new UndoableFieldChange(fieldChange))); + fieldValue.set(""); + // TODO: increment affected entries only when UndoableFieldChange.isPresent() + affectedEntriesCount++; + } + } + + if (setFieldEdit.hasEdits()) { + setFieldEdit.end(); + } + stateManager.setLastAutomaticFieldEditorEdit(new LastAutomaticFieldEditorEdit( + affectedEntriesCount, + TAB_INDEX, + setFieldEdit + )); + } + + public void appendToFieldValue() { + NamedCompound appendToFieldEdit = new NamedCompound("APPEND_TO_SELECTED_FIELD"); + String toAppendFieldValue = fieldValue.getValue(); + int affectedEntriesCount = 0; + for (BibEntry entry : selectedEntries) { + Optional oldFieldValue = entry.getField(selectedField.get()); + // Append button should be disabled if 'overwriteNonEmptyFields' is false + if (overwriteFieldContent.get()) { + String newFieldValue = oldFieldValue.orElse("").concat(toAppendFieldValue); + + entry.setField(selectedField.get(), newFieldValue) + .ifPresent(fieldChange -> appendToFieldEdit.addEdit(new UndoableFieldChange(fieldChange))); + + fieldValue.set(""); + affectedEntriesCount++; + } + } + + if (appendToFieldEdit.hasEdits()) { + appendToFieldEdit.end(); + } + stateManager.setLastAutomaticFieldEditorEdit(new LastAutomaticFieldEditorEdit( + affectedEntriesCount, + TAB_INDEX, + appendToFieldEdit + )); + } + + public ObjectProperty selectedFieldProperty() { + return selectedField; + } + + public Field getSelectedField() { + return selectedFieldProperty().get(); + } + + public String getFieldValue() { + return fieldValue.get(); + } + + public StringProperty fieldValueProperty() { + return fieldValue; + } + + public BooleanProperty overwriteFieldContentProperty() { + return overwriteFieldContent; + } +} diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/editfieldvalue/EditFieldValueTabView.java b/src/main/java/org/jabref/gui/edit/automaticfiededitor/editfieldvalue/EditFieldValueTabView.java deleted file mode 100644 index 3bd9ea57a09..00000000000 --- a/src/main/java/org/jabref/gui/edit/automaticfiededitor/editfieldvalue/EditFieldValueTabView.java +++ /dev/null @@ -1,95 +0,0 @@ -package org.jabref.gui.edit.automaticfiededitor.editfieldvalue; - -import java.util.Comparator; -import java.util.List; - -import javafx.fxml.FXML; -import javafx.scene.control.Button; -import javafx.scene.control.CheckBox; -import javafx.scene.control.ComboBox; -import javafx.scene.control.TextField; -import javafx.util.StringConverter; - -import org.jabref.gui.edit.automaticfiededitor.AbstractAutomaticFieldEditorTabView; -import org.jabref.gui.undo.NamedCompound; -import org.jabref.logic.l10n.Localization; -import org.jabref.model.database.BibDatabaseContext; -import org.jabref.model.entry.BibEntry; -import org.jabref.model.entry.field.Field; -import org.jabref.model.entry.field.FieldFactory; - -import com.airhacks.afterburner.views.ViewLoader; - -public class EditFieldValueTabView extends AbstractAutomaticFieldEditorTabView { - public Button appendValueButton; - @FXML - private ComboBox fieldComboBox; - - @FXML - private TextField fieldValueTextField; - - @FXML - private CheckBox overwriteNonEmptyFieldsCheckBox; - - private final List selectedEntries; - private final BibDatabaseContext databaseContext; - - private EditFieldValueViewModel viewModel; - - private final NamedCompound dialogEdits; - - public EditFieldValueTabView(List selectedEntries, BibDatabaseContext databaseContext, NamedCompound dialogEdits) { - this.selectedEntries = selectedEntries; - this.databaseContext = databaseContext; - this.dialogEdits = dialogEdits; - - ViewLoader.view(this) - .root(this) - .load(); - } - - @FXML - public void initialize() { - viewModel = new EditFieldValueViewModel(databaseContext, selectedEntries, dialogEdits); - fieldComboBox.setConverter(new StringConverter<>() { - @Override - public String toString(Field field) { - return field.getName(); - } - - @Override - public Field fromString(String name) { - return FieldFactory.parseField(name); - } - }); - fieldComboBox.getItems().addAll(viewModel.getAllFields().sorted(Comparator.comparing(Field::getName))); - fieldComboBox.getSelectionModel().selectFirst(); - viewModel.selectedFieldProperty().bindBidirectional(fieldComboBox.valueProperty()); - - viewModel.fieldValueProperty().bindBidirectional(fieldValueTextField.textProperty()); - - viewModel.overwriteNonEmptyFieldsProperty().bindBidirectional(overwriteNonEmptyFieldsCheckBox.selectedProperty()); - - appendValueButton.disableProperty().bind(overwriteNonEmptyFieldsCheckBox.selectedProperty().not()); - } - - @Override - public String getTabName() { - return Localization.lang("Edit field value"); - } - - @FXML - void appendToFieldValue() { - viewModel.appendToFieldValue(); - } - - @FXML - void clearField() { - viewModel.clearSelectedField(); - } - - @FXML - void setFieldValue() { - viewModel.setFieldValue(); - } -} diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/editfieldvalue/EditFieldValueViewModel.java b/src/main/java/org/jabref/gui/edit/automaticfiededitor/editfieldvalue/EditFieldValueViewModel.java deleted file mode 100644 index d11928ebc53..00000000000 --- a/src/main/java/org/jabref/gui/edit/automaticfiededitor/editfieldvalue/EditFieldValueViewModel.java +++ /dev/null @@ -1,135 +0,0 @@ -package org.jabref.gui.edit.automaticfiededitor.editfieldvalue; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; - -import org.jabref.gui.AbstractViewModel; -import org.jabref.gui.undo.NamedCompound; -import org.jabref.gui.undo.UndoableFieldChange; -import org.jabref.model.database.BibDatabaseContext; -import org.jabref.model.entry.BibEntry; -import org.jabref.model.entry.field.Field; - -public class EditFieldValueViewModel extends AbstractViewModel { - private final BibDatabaseContext databaseContext; - private final List selectedEntries; - - private final StringProperty fieldValue = new SimpleStringProperty(); - - private final ObjectProperty selectedField = new SimpleObjectProperty<>(); - - private final BooleanProperty overwriteNonEmptyFields = new SimpleBooleanProperty(); - - private final ObservableList allFields = FXCollections.observableArrayList(); - - private final NamedCompound dialogEdits; - - public EditFieldValueViewModel(BibDatabaseContext databaseContext, List selectedEntries, NamedCompound dialogEdits) { - this.databaseContext = databaseContext; - this.selectedEntries = new ArrayList<>(selectedEntries); - this.dialogEdits = dialogEdits; - - allFields.addAll(databaseContext.getDatabase().getAllVisibleFields().stream().toList()); - } - - public void clearSelectedField() { - NamedCompound clearFieldEdit = new NamedCompound("CLEAR_SELECTED_FIELD"); - - for (BibEntry entry : selectedEntries) { - Optional oldFieldValue = entry.getField(selectedField.get()); - if (oldFieldValue.isPresent()) { - entry.setField(selectedField.get(), ""); - - clearFieldEdit.addEdit(new UndoableFieldChange(entry, - selectedField.get(), - oldFieldValue.orElse(null), - fieldValue.get())); - } - } - - if (clearFieldEdit.hasEdits()) { - clearFieldEdit.end(); - dialogEdits.addEdit(clearFieldEdit); - } - } - - public void setFieldValue() { - NamedCompound setFieldEdit = new NamedCompound("CHANGE_SELECTED_FIELD"); - String toSetFieldValue = fieldValue.getValue(); - - for (BibEntry entry : selectedEntries) { - Optional oldFieldValue = entry.getField(selectedField.get()); - if (oldFieldValue.isEmpty() || overwriteNonEmptyFields.get()) { - entry.setField(selectedField.get(), toSetFieldValue); - - setFieldEdit.addEdit(new UndoableFieldChange(entry, - selectedField.get(), - oldFieldValue.orElse(null), - toSetFieldValue)); - fieldValue.set(""); - } - } - - if (setFieldEdit.hasEdits()) { - setFieldEdit.end(); - dialogEdits.addEdit(setFieldEdit); - } - } - - public void appendToFieldValue() { - NamedCompound appendToFieldEdit = new NamedCompound("APPEND_TO_SELECTED_FIELD"); - String toAppendFieldValue = fieldValue.getValue(); - - for (BibEntry entry : selectedEntries) { - Optional oldFieldValue = entry.getField(selectedField.get()); - // Append button should be disabled if 'overwriteNonEmptyFields' is false - if (overwriteNonEmptyFields.get()) { - String newFieldValue = oldFieldValue.orElse("").concat(toAppendFieldValue); - - entry.setField(selectedField.get(), newFieldValue); - appendToFieldEdit.addEdit(new UndoableFieldChange(entry, - selectedField.get(), - oldFieldValue.orElse(null), - newFieldValue) - ); - - fieldValue.set(""); - } - } - - if (appendToFieldEdit.hasEdits()) { - appendToFieldEdit.end(); - dialogEdits.addEdit(appendToFieldEdit); - } - } - - public ObservableList getAllFields() { - return allFields; - } - - public ObjectProperty selectedFieldProperty() { - return selectedField; - } - - public String getFieldValue() { - return fieldValue.get(); - } - - public StringProperty fieldValueProperty() { - return fieldValue; - } - - public BooleanProperty overwriteNonEmptyFieldsProperty() { - return overwriteNonEmptyFields; - } -} diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/renamefield/RenameFieldTab.fxml b/src/main/java/org/jabref/gui/edit/automaticfiededitor/renamefield/RenameFieldTab.fxml index 388f1f567c0..9e4b5b54269 100644 --- a/src/main/java/org/jabref/gui/edit/automaticfiededitor/renamefield/RenameFieldTab.fxml +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/renamefield/RenameFieldTab.fxml @@ -46,7 +46,7 @@ - + diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/renamefield/RenameFieldTabView.java b/src/main/java/org/jabref/gui/edit/automaticfiededitor/renamefield/RenameFieldTabView.java index 8029cfc7827..f39ae2efc86 100644 --- a/src/main/java/org/jabref/gui/edit/automaticfiededitor/renamefield/RenameFieldTabView.java +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/renamefield/RenameFieldTabView.java @@ -1,38 +1,45 @@ package org.jabref.gui.edit.automaticfiededitor.renamefield; -import java.util.Comparator; import java.util.List; +import javafx.application.Platform; import javafx.fxml.FXML; +import javafx.scene.control.Button; import javafx.scene.control.ComboBox; import javafx.scene.control.TextField; -import javafx.util.StringConverter; +import org.jabref.gui.StateManager; import org.jabref.gui.edit.automaticfiededitor.AbstractAutomaticFieldEditorTabView; import org.jabref.gui.edit.automaticfiededitor.AutomaticFieldEditorTab; -import org.jabref.gui.undo.NamedCompound; import org.jabref.logic.l10n.Localization; -import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.database.BibDatabase; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.Field; -import org.jabref.model.entry.field.FieldFactory; import com.airhacks.afterburner.views.ViewLoader; +import com.tobiasdiez.easybind.EasyBind; +import de.saxsys.mvvmfx.utils.validation.visualization.ControlsFxVisualizer; + +import static org.jabref.gui.customentrytypes.CustomEntryTypeDialogViewModel.FIELD_STRING_CONVERTER; public class RenameFieldTabView extends AbstractAutomaticFieldEditorTabView implements AutomaticFieldEditorTab { + @FXML + private Button renameButton; @FXML private ComboBox fieldComboBox; @FXML private TextField newFieldNameTextField; private final List selectedEntries; - private final BibDatabaseContext databaseContext; - private final NamedCompound dialogEdits; + private final BibDatabase database; + private final StateManager stateManager; private RenameFieldViewModel viewModel; - public RenameFieldTabView(List selectedEntries, BibDatabaseContext databaseContext, NamedCompound dialogEdits) { - this.selectedEntries = selectedEntries; - this.databaseContext = databaseContext; - this.dialogEdits = dialogEdits; + private final ControlsFxVisualizer visualizer = new ControlsFxVisualizer(); + + public RenameFieldTabView(BibDatabase database, StateManager stateManager) { + this.selectedEntries = stateManager.getSelectedEntries(); + this.database = database; + this.stateManager = stateManager; ViewLoader.view(this) .root(this) @@ -41,25 +48,23 @@ public RenameFieldTabView(List selectedEntries, BibDatabaseContext dat @FXML public void initialize() { - viewModel = new RenameFieldViewModel(selectedEntries, databaseContext, dialogEdits); - - fieldComboBox.getItems().addAll(viewModel.getAllFields().sorted(Comparator.comparing(Field::getName))); - fieldComboBox.setConverter(new StringConverter<>() { - @Override - public String toString(Field field) { - return field.getName(); - } - - @Override - public Field fromString(String name) { - return FieldFactory.parseField(name); - } - }); + viewModel = new RenameFieldViewModel(selectedEntries, database, stateManager); + fieldComboBox.getItems().setAll(viewModel.getAllFields()); fieldComboBox.getSelectionModel().selectFirst(); - viewModel.selectedFieldProperty().bindBidirectional(fieldComboBox.valueProperty()); - viewModel.newFieldNameProperty().bindBidirectional(newFieldNameTextField.textProperty()); + fieldComboBox.setConverter(FIELD_STRING_CONVERTER); + + fieldComboBox.valueProperty().bindBidirectional(viewModel.selectedFieldProperty()); + EasyBind.listen(fieldComboBox.getEditor().textProperty(), observable -> fieldComboBox.commitValue()); + + renameButton.disableProperty().bind(viewModel.canRenameProperty().not()); + + newFieldNameTextField.textProperty().bindBidirectional(viewModel.newFieldNameProperty()); + + Platform.runLater(() -> { + visualizer.initVisualization(viewModel.fieldNameValidationStatus(), newFieldNameTextField, true); + }); } @Override diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/renamefield/RenameFieldViewModel.java b/src/main/java/org/jabref/gui/edit/automaticfiededitor/renamefield/RenameFieldViewModel.java index f8e430ccc51..6f22623d466 100644 --- a/src/main/java/org/jabref/gui/edit/automaticfiededitor/renamefield/RenameFieldViewModel.java +++ b/src/main/java/org/jabref/gui/edit/automaticfiededitor/renamefield/RenameFieldViewModel.java @@ -2,37 +2,70 @@ import java.util.List; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import org.jabref.gui.AbstractViewModel; +import org.jabref.gui.StateManager; +import org.jabref.gui.edit.automaticfiededitor.AbstractAutomaticFieldEditorTabViewModel; +import org.jabref.gui.edit.automaticfiededitor.LastAutomaticFieldEditorEdit; import org.jabref.gui.edit.automaticfiededitor.MoveFieldValueAction; import org.jabref.gui.undo.NamedCompound; -import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.database.BibDatabase; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.Field; import org.jabref.model.entry.field.FieldFactory; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.strings.StringUtil; + +import de.saxsys.mvvmfx.utils.validation.FunctionBasedValidator; +import de.saxsys.mvvmfx.utils.validation.ValidationMessage; +import de.saxsys.mvvmfx.utils.validation.ValidationStatus; +import de.saxsys.mvvmfx.utils.validation.Validator; + +public class RenameFieldViewModel extends AbstractAutomaticFieldEditorTabViewModel { + public static final int TAB_INDEX = 2; + private final StringProperty newFieldName = new SimpleStringProperty(""); + private final ObjectProperty selectedField = new SimpleObjectProperty<>(StandardField.AUTHOR); + private final List selectedEntries; -public class RenameFieldViewModel extends AbstractViewModel { + private final Validator fieldValidator; - private final StringProperty newFieldName = new SimpleStringProperty(); - private final ObjectProperty selectedField = new SimpleObjectProperty<>(); + private final Validator fieldNameValidator; - private final ObservableList allFields = FXCollections.observableArrayList(); - private final List selectedEntries; - private final BibDatabaseContext databaseContext; - private final NamedCompound dialogEdits; + private final BooleanBinding canRename; - public RenameFieldViewModel(List selectedEntries, BibDatabaseContext databaseContext, NamedCompound dialogEdits) { + public RenameFieldViewModel(List selectedEntries, BibDatabase database, StateManager stateManager) { + super(database, stateManager); this.selectedEntries = selectedEntries; - this.databaseContext = databaseContext; - this.dialogEdits = dialogEdits; - allFields.addAll(databaseContext.getDatabase().getAllVisibleFields()); + fieldValidator = new FunctionBasedValidator<>(selectedField, field -> StringUtil.isNotBlank(field.getName()), + ValidationMessage.error("Field cannot be empty")); + fieldNameValidator = new FunctionBasedValidator<>(newFieldName, fieldName -> { + if (StringUtil.isBlank(fieldName)) { + return ValidationMessage.error("Field name cannot be empty"); + } else if (StringUtil.containsWhitespace(fieldName)) { + return ValidationMessage.error("Field name cannot have whitespace characters"); + } + return null; + }); + + canRename = Bindings.and(fieldValidationStatus().validProperty(), fieldNameValidationStatus().validProperty()); + } + + public ValidationStatus fieldValidationStatus() { + return fieldValidator.getValidationStatus(); + } + + public ValidationStatus fieldNameValidationStatus() { + return fieldNameValidator.getValidationStatus(); + } + + public BooleanBinding canRenameProperty() { + return canRename; } public String getNewFieldName() { @@ -43,6 +76,10 @@ public StringProperty newFieldNameProperty() { return newFieldName; } + public void setNewFieldName(String newName) { + newFieldNameProperty().set(newName); + } + public Field getSelectedField() { return selectedField.get(); } @@ -51,21 +88,27 @@ public ObjectProperty selectedFieldProperty() { return selectedField; } - public ObservableList getAllFields() { - return allFields; + public void selectField(Field field) { + selectedFieldProperty().set(field); } public void renameField() { NamedCompound renameEdit = new NamedCompound("RENAME_EDIT"); - - new MoveFieldValueAction(selectedField.get(), - FieldFactory.parseField(newFieldName.get()), - selectedEntries, - renameEdit).execute(); - - if (renameEdit.hasEdits()) { - renameEdit.end(); - dialogEdits.addEdit(renameEdit); + int affectedEntriesCount = 0; + if (fieldNameValidationStatus().isValid()) { + affectedEntriesCount = new MoveFieldValueAction(selectedField.get(), + FieldFactory.parseField(newFieldName.get()), + selectedEntries, + renameEdit, + false).executeAndGetAffectedEntriesCount(); + + if (renameEdit.hasEdits()) { + renameEdit.end(); + } } + + stateManager.setLastAutomaticFieldEditorEdit(new LastAutomaticFieldEditorEdit( + affectedEntriesCount, TAB_INDEX, renameEdit + )); } } diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/twofields/TwoFieldsTabView.java b/src/main/java/org/jabref/gui/edit/automaticfiededitor/twofields/TwoFieldsTabView.java deleted file mode 100644 index baf000ce3cb..00000000000 --- a/src/main/java/org/jabref/gui/edit/automaticfiededitor/twofields/TwoFieldsTabView.java +++ /dev/null @@ -1,118 +0,0 @@ -package org.jabref.gui.edit.automaticfiededitor.twofields; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; - -import javafx.fxml.FXML; -import javafx.scene.control.Button; -import javafx.scene.control.CheckBox; -import javafx.scene.control.ComboBox; -import javafx.util.StringConverter; - -import org.jabref.gui.edit.automaticfiededitor.AbstractAutomaticFieldEditorTabView; -import org.jabref.gui.edit.automaticfiededitor.AutomaticFieldEditorTab; -import org.jabref.gui.undo.NamedCompound; -import org.jabref.logic.l10n.Localization; -import org.jabref.model.database.BibDatabaseContext; -import org.jabref.model.entry.BibEntry; -import org.jabref.model.entry.field.Field; -import org.jabref.model.entry.field.FieldFactory; - -import com.airhacks.afterburner.views.ViewLoader; - -public class TwoFieldsTabView extends AbstractAutomaticFieldEditorTabView implements AutomaticFieldEditorTab { - @FXML - private Button moveValueButton; - - @FXML - private Button swapValuesButton; - - @FXML - private ComboBox fromFieldComboBox; - @FXML - private ComboBox toFieldComboBox; - - @FXML - private CheckBox overwriteNonEmptyFields; - - private TwoFieldsViewModel viewModel; - private final List selectedEntries; - private final BibDatabaseContext databaseContext; - private final NamedCompound dialogEdits; - - public TwoFieldsTabView(List selectedEntries, BibDatabaseContext databaseContext, NamedCompound dialogEdits) { - this.selectedEntries = new ArrayList<>(selectedEntries); - this.databaseContext = databaseContext; - this.dialogEdits = dialogEdits; - - ViewLoader.view(this) - .root(this) - .load(); - } - - public void initialize() { - viewModel = new TwoFieldsViewModel(selectedEntries, databaseContext, dialogEdits); - initializeFromAndToComboBox(); - - viewModel.overwriteNonEmptyFieldsProperty().bindBidirectional(overwriteNonEmptyFields.selectedProperty()); - - moveValueButton.disableProperty().bind(viewModel.overwriteNonEmptyFieldsProperty().not()); - swapValuesButton.disableProperty().bind(viewModel.overwriteNonEmptyFieldsProperty().not()); - } - - private void initializeFromAndToComboBox() { - fromFieldComboBox.getItems().addAll(viewModel.getAllFields().sorted(Comparator.comparing(Field::getName))); - toFieldComboBox.getItems().addAll(viewModel.getAllFields().sorted(Comparator.comparing(Field::getName))); - - fromFieldComboBox.setConverter(new StringConverter<>() { - @Override - public String toString(Field field) { - return field.getName(); - } - - @Override - public Field fromString(String name) { - return FieldFactory.parseField(name); - } - }); - - toFieldComboBox.setConverter(new StringConverter<>() { - @Override - public String toString(Field field) { - return field.getName(); - } - - @Override - public Field fromString(String name) { - return FieldFactory.parseField(name); - } - }); - - fromFieldComboBox.getSelectionModel().selectFirst(); - toFieldComboBox.getSelectionModel().selectLast(); - - viewModel.fromFieldProperty().bindBidirectional(fromFieldComboBox.valueProperty()); - viewModel.toFieldProperty().bindBidirectional(toFieldComboBox.valueProperty()); - } - - @Override - public String getTabName() { - return Localization.lang("Two fields"); - } - - @FXML - void copyValue() { - viewModel.copyValue(); - } - - @FXML - void moveValue() { - viewModel.moveValue(); - } - - @FXML - void swapValues() { - viewModel.swapValues(); - } -} diff --git a/src/main/java/org/jabref/gui/edit/automaticfiededitor/twofields/TwoFieldsViewModel.java b/src/main/java/org/jabref/gui/edit/automaticfiededitor/twofields/TwoFieldsViewModel.java deleted file mode 100644 index cf184630db9..00000000000 --- a/src/main/java/org/jabref/gui/edit/automaticfiededitor/twofields/TwoFieldsViewModel.java +++ /dev/null @@ -1,141 +0,0 @@ -package org.jabref.gui.edit.automaticfiededitor.twofields; - -import java.util.ArrayList; -import java.util.List; - -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; - -import org.jabref.gui.AbstractViewModel; -import org.jabref.gui.edit.automaticfiededitor.MoveFieldValueAction; -import org.jabref.gui.undo.NamedCompound; -import org.jabref.gui.undo.UndoableFieldChange; -import org.jabref.model.database.BibDatabaseContext; -import org.jabref.model.entry.BibEntry; -import org.jabref.model.entry.field.Field; -import org.jabref.model.strings.StringUtil; - -public class TwoFieldsViewModel extends AbstractViewModel { - private final ObjectProperty fromField = new SimpleObjectProperty<>(); - - private final ObjectProperty toField = new SimpleObjectProperty<>(); - - private final BooleanProperty overwriteNonEmptyFields = new SimpleBooleanProperty(); - private final ObservableList allFields = FXCollections.observableArrayList(); - // TODO: Create an abstraction where selectedEntries, databaseContext and dialogEdits dependencies are shared across - // all automatic field editors tab view models - private final List selectedEntries; - private final BibDatabaseContext databaseContext; - private final NamedCompound dialogEdits; - - public TwoFieldsViewModel(List selectedEntries, BibDatabaseContext databaseContext, NamedCompound dialogEdits) { - this.selectedEntries = new ArrayList<>(selectedEntries); - this.databaseContext = databaseContext; - this.dialogEdits = dialogEdits; - - allFields.addAll(databaseContext.getDatabase().getAllVisibleFields()); - } - - public ObservableList getAllFields() { - return allFields; - } - - public Field getFromField() { - return fromField.get(); - } - - public ObjectProperty fromFieldProperty() { - return fromField; - } - - public Field getToField() { - return toField.get(); - } - - public ObjectProperty toFieldProperty() { - return toField; - } - - public boolean getOverwriteNonEmptyFields() { - return overwriteNonEmptyFields.get(); - } - - public BooleanProperty overwriteNonEmptyFieldsProperty() { - return overwriteNonEmptyFields; - } - - public void copyValue() { - NamedCompound copyFieldValueEdit = new NamedCompound("COPY_FIELD_VALUE"); - - for (BibEntry entry : selectedEntries) { - String fromFieldValue = entry.getField(fromField.get()).orElse(""); - String toFieldValue = entry.getField(toField.get()).orElse(""); - - if (overwriteNonEmptyFields.get() || StringUtil.isBlank(toFieldValue)) { - entry.setField(toField.get(), fromFieldValue); - - copyFieldValueEdit.addEdit(new UndoableFieldChange(entry, - toField.get(), - toFieldValue, - fromFieldValue)); - } - } - - if (copyFieldValueEdit.hasEdits()) { - copyFieldValueEdit.end(); - dialogEdits.addEdit(copyFieldValueEdit); - } - } - - public void moveValue() { - NamedCompound moveEdit = new NamedCompound("MOVE_EDIT"); - if (overwriteNonEmptyFields.get()) { - new MoveFieldValueAction(fromField.get(), - toField.get(), - selectedEntries, - moveEdit).execute(); - - if (moveEdit.hasEdits()) { - moveEdit.end(); - dialogEdits.addEdit(moveEdit); - } - } - } - - public void swapValues() { - NamedCompound swapFieldValuesEdit = new NamedCompound("SWAP_FIELD_VALUES"); - - for (BibEntry entry : selectedEntries) { - String fromFieldValue = entry.getField(fromField.get()).orElse(""); - String toFieldValue = entry.getField(toField.get()).orElse(""); - - if (overwriteNonEmptyFields.get() && StringUtil.isNotBlank(fromFieldValue) && StringUtil.isNotBlank(toFieldValue)) { - entry.setField(toField.get(), fromFieldValue); - entry.setField(fromField.get(), toFieldValue); - - swapFieldValuesEdit.addEdit(new UndoableFieldChange( - entry, - toField.get(), - toFieldValue, - fromFieldValue - )); - - swapFieldValuesEdit.addEdit(new UndoableFieldChange( - entry, - fromField.get(), - fromFieldValue, - toFieldValue - )); - } - } - - if (swapFieldValuesEdit.hasEdits()) { - swapFieldValuesEdit.end(); - dialogEdits.addEdit(swapFieldValuesEdit); - } - } -} diff --git a/src/main/java/org/jabref/gui/errorconsole/ErrorConsoleViewModel.java b/src/main/java/org/jabref/gui/errorconsole/ErrorConsoleViewModel.java index 3d2752a42ef..bd3bb87da8c 100644 --- a/src/main/java/org/jabref/gui/errorconsole/ErrorConsoleViewModel.java +++ b/src/main/java/org/jabref/gui/errorconsole/ErrorConsoleViewModel.java @@ -2,9 +2,6 @@ import java.io.IOException; import java.net.URISyntaxException; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Date; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -30,8 +27,6 @@ public class ErrorConsoleViewModel extends AbstractViewModel { private static final Logger LOGGER = LoggerFactory.getLogger(ErrorConsoleViewModel.class); - private final DateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss"); - private final Date date = new Date(); private final DialogService dialogService; private final ClipBoardManager clipBoardManager; private final BuildInfo buildInfo; diff --git a/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java b/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java index 7de10cf7b35..61cc7afd2e8 100644 --- a/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java +++ b/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java @@ -1,6 +1,8 @@ package org.jabref.gui.externalfiles; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; @@ -22,7 +24,15 @@ import org.jabref.logic.citationkeypattern.CitationKeyGenerator; import org.jabref.logic.database.DuplicateCheck; import org.jabref.logic.externalfiles.ExternalFilesContentImporter; +import org.jabref.logic.importer.FetcherException; import org.jabref.logic.importer.ImportCleanup; +import org.jabref.logic.importer.ImportException; +import org.jabref.logic.importer.ImportFormatReader; +import org.jabref.logic.importer.ImportFormatReader.UnknownFormatImport; +import org.jabref.logic.importer.ParseException; +import org.jabref.logic.importer.fetcher.ArXiv; +import org.jabref.logic.importer.fetcher.DoiFetcher; +import org.jabref.logic.importer.fileformat.BibtexParser; import org.jabref.logic.l10n.Localization; import org.jabref.logic.util.UpdateField; import org.jabref.logic.util.io.FileUtil; @@ -30,9 +40,12 @@ import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.identifier.ArXivIdentifier; +import org.jabref.model.entry.identifier.DOI; import org.jabref.model.groups.GroupEntryChanger; import org.jabref.model.groups.GroupTreeNode; import org.jabref.model.util.FileUpdateMonitor; +import org.jabref.model.util.OptionalUtil; import org.jabref.preferences.PreferencesService; import org.slf4j.Logger; @@ -49,6 +62,7 @@ public class ImportHandler { private final UndoManager undoManager; private final StateManager stateManager; private final DialogService dialogService; + private final ImportFormatReader importFormatReader; public ImportHandler(BibDatabaseContext database, ExternalFileTypes externalFileTypes, @@ -56,13 +70,15 @@ public ImportHandler(BibDatabaseContext database, FileUpdateMonitor fileupdateMonitor, UndoManager undoManager, StateManager stateManager, - DialogService dialogService) { + DialogService dialogService, + ImportFormatReader importFormatReader) { this.bibDatabaseContext = database; this.preferencesService = preferencesService; this.fileUpdateMonitor = fileupdateMonitor; this.stateManager = stateManager; this.dialogService = dialogService; + this.importFormatReader = importFormatReader; this.linker = new ExternalFilesEntryLinker(externalFileTypes, preferencesService.getFilePreferences(), database); this.contentImporter = new ExternalFilesContentImporter( @@ -245,4 +261,52 @@ private void generateKeys(List entries) { keyGenerator.generateAndSetKey(entry); } } + + public List handleBibTeXData(String entries) { + BibtexParser parser = new BibtexParser(preferencesService.getImportFormatPreferences(), Globals.getFileUpdateMonitor()); + try { + return parser.parseEntries(new ByteArrayInputStream(entries.getBytes(StandardCharsets.UTF_8))); + } catch (ParseException ex) { + LOGGER.error("Could not paste", ex); + return Collections.emptyList(); + } + } + + public List handleStringData(String data) throws FetcherException { + if ((data == null) || data.isEmpty()) { + return Collections.emptyList(); + } + + Optional doi = DOI.parse(data); + if (doi.isPresent()) { + return fetchByDOI(doi.get()); + } + Optional arXiv = ArXivIdentifier.parse(data); + if (arXiv.isPresent()) { + return fetchByArXiv(arXiv.get()); + } + + return tryImportFormats(data); + } + + private List tryImportFormats(String data) { + try { + UnknownFormatImport unknownFormatImport = importFormatReader.importUnknownFormat(data); + return unknownFormatImport.parserResult.getDatabase().getEntries(); + } catch (ImportException ignored) { + return Collections.emptyList(); + } + } + + private List fetchByDOI(DOI doi) throws FetcherException { + LOGGER.info("Found DOI in clipboard"); + Optional entry = new DoiFetcher(preferencesService.getImportFormatPreferences()).performSearchById(doi.getDOI()); + return OptionalUtil.toList(entry); + } + + private List fetchByArXiv(ArXivIdentifier arXivIdentifier) throws FetcherException { + LOGGER.info("Found arxiv identifier in clipboard"); + Optional entry = new ArXiv(preferencesService.getImportFormatPreferences()).performSearchById(arXivIdentifier.getNormalizedWithoutVersion()); + return OptionalUtil.toList(entry); + } } diff --git a/src/main/java/org/jabref/gui/externalfiles/UnlinkedFilesDialogView.java b/src/main/java/org/jabref/gui/externalfiles/UnlinkedFilesDialogView.java index 2d1af899d12..0805c8a1f22 100644 --- a/src/main/java/org/jabref/gui/externalfiles/UnlinkedFilesDialogView.java +++ b/src/main/java/org/jabref/gui/externalfiles/UnlinkedFilesDialogView.java @@ -42,6 +42,7 @@ import org.jabref.gui.util.ValueTableCellFactory; import org.jabref.gui.util.ViewModelListCellFactory; import org.jabref.gui.util.ViewModelTreeCellFactory; +import org.jabref.logic.importer.ImportFormatReader; import org.jabref.logic.l10n.Localization; import org.jabref.model.util.FileUpdateMonitor; import org.jabref.preferences.PreferencesService; @@ -81,6 +82,7 @@ public class UnlinkedFilesDialogView extends BaseDialog { @Inject private TaskExecutor taskExecutor; @Inject private FileUpdateMonitor fileUpdateMonitor; @Inject private ThemeManager themeManager; + @Inject private ImportFormatReader importFormatReader; private final ControlsFxVisualizer validationVisualizer; private UnlinkedFilesDialogViewModel viewModel; @@ -106,7 +108,7 @@ public UnlinkedFilesDialogView() { @FXML private void initialize() { - viewModel = new UnlinkedFilesDialogViewModel(dialogService, ExternalFileTypes.getInstance(), undoManager, fileUpdateMonitor, preferencesService, stateManager, taskExecutor); + viewModel = new UnlinkedFilesDialogViewModel(dialogService, ExternalFileTypes.getInstance(), undoManager, fileUpdateMonitor, preferencesService, stateManager, taskExecutor, importFormatReader); progressDisplay.progressProperty().bind(viewModel.progressValueProperty()); progressText.textProperty().bind(viewModel.progressTextProperty()); diff --git a/src/main/java/org/jabref/gui/externalfiles/UnlinkedFilesDialogViewModel.java b/src/main/java/org/jabref/gui/externalfiles/UnlinkedFilesDialogViewModel.java index 7d19b2bdc98..fd942a61508 100644 --- a/src/main/java/org/jabref/gui/externalfiles/UnlinkedFilesDialogViewModel.java +++ b/src/main/java/org/jabref/gui/externalfiles/UnlinkedFilesDialogViewModel.java @@ -36,6 +36,7 @@ import org.jabref.gui.util.FileDialogConfiguration; import org.jabref.gui.util.FileNodeViewModel; import org.jabref.gui.util.TaskExecutor; +import org.jabref.logic.importer.ImportFormatReader; import org.jabref.logic.l10n.Localization; import org.jabref.logic.util.StandardFileType; import org.jabref.model.database.BibDatabaseContext; @@ -86,7 +87,8 @@ public UnlinkedFilesDialogViewModel(DialogService dialogService, FileUpdateMonitor fileUpdateMonitor, PreferencesService preferences, StateManager stateManager, - TaskExecutor taskExecutor) { + TaskExecutor taskExecutor, + ImportFormatReader importFormatReader) { this.preferences = preferences; this.dialogService = dialogService; this.taskExecutor = taskExecutor; @@ -98,7 +100,8 @@ public UnlinkedFilesDialogViewModel(DialogService dialogService, fileUpdateMonitor, undoManager, stateManager, - dialogService); + dialogService, + importFormatReader); this.fileFilterList = FXCollections.observableArrayList( new FileExtensionViewModel(StandardFileType.ANY_FILE, externalFileTypes), diff --git a/src/main/java/org/jabref/gui/fieldeditors/IdentifierEditorViewModel.java b/src/main/java/org/jabref/gui/fieldeditors/IdentifierEditorViewModel.java index 04025e60be7..5d69547367d 100644 --- a/src/main/java/org/jabref/gui/fieldeditors/IdentifierEditorViewModel.java +++ b/src/main/java/org/jabref/gui/fieldeditors/IdentifierEditorViewModel.java @@ -15,6 +15,8 @@ import org.jabref.gui.mergeentries.FetchAndMergeEntry; import org.jabref.gui.util.BackgroundTask; import org.jabref.gui.util.TaskExecutor; +import org.jabref.logic.importer.FetcherClientException; +import org.jabref.logic.importer.FetcherServerException; import org.jabref.logic.importer.WebFetchers; import org.jabref.logic.importer.util.IdentifierParser; import org.jabref.logic.integrity.FieldCheckers; @@ -27,8 +29,13 @@ import org.jabref.preferences.PreferencesService; import com.tobiasdiez.easybind.EasyBind; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class IdentifierEditorViewModel extends AbstractEditorViewModel { + + private static final Logger LOGGER = LoggerFactory.getLogger(IdentifierEditorViewModel.class); + private final BooleanProperty validIdentifierIsNotPresent = new SimpleBooleanProperty(true); private final BooleanProperty identifierLookupInProgress = new SimpleBooleanProperty(false); private final BooleanProperty idFetcherAvailable = new SimpleBooleanProperty(true); @@ -119,8 +126,19 @@ public void lookupIdentifier(BibEntry entry) { dialogService.notify(Localization.lang("No %0 found", field.getDisplayName())); } }) - .onFailure(dialogService::showErrorDialogAndWait) - .executeWith(taskExecutor); + .onFailure(exception -> { + LOGGER.error("Error while fetching bibliographic information", exception); + if (exception instanceof FetcherClientException) { + dialogService.showInformationDialogAndWait(Localization.lang("Look up %0", idFetcher.getName()), Localization.lang("No data was found for the identifier")); + } else if (exception instanceof FetcherServerException) { + dialogService.showInformationDialogAndWait(Localization.lang("Look up %0", idFetcher.getName()), Localization.lang("Server not available")); + } else if (exception.getCause() != null) { + dialogService.showWarningDialogAndWait(Localization.lang("Look up %0", idFetcher.getName()), Localization.lang("Error occured %0", exception.getCause().getMessage())); + } else { + dialogService.showWarningDialogAndWait(Localization.lang("Look up %0", idFetcher.getName()), Localization.lang("Error occured %0", exception.getCause().getMessage())); + } + }) + .executeWith(taskExecutor); }); } } diff --git a/src/main/java/org/jabref/gui/importer/GenerateEntryFromIdAction.java b/src/main/java/org/jabref/gui/importer/GenerateEntryFromIdAction.java index 4c5c8a5747f..619ec5a8e35 100644 --- a/src/main/java/org/jabref/gui/importer/GenerateEntryFromIdAction.java +++ b/src/main/java/org/jabref/gui/importer/GenerateEntryFromIdAction.java @@ -11,9 +11,10 @@ import org.jabref.gui.externalfiletype.ExternalFileTypes; import org.jabref.gui.util.BackgroundTask; import org.jabref.gui.util.TaskExecutor; -import org.jabref.logic.JabRefException; import org.jabref.logic.importer.CompositeIdFetcher; +import org.jabref.logic.importer.FetcherClientException; import org.jabref.logic.importer.FetcherException; +import org.jabref.logic.importer.FetcherServerException; import org.jabref.logic.l10n.Localization; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.types.StandardEntryType; @@ -47,11 +48,17 @@ public void execute() { backgroundTask.titleProperty().set(Localization.lang("Import by ID")); backgroundTask.showToUser(true); backgroundTask.onRunning(() -> dialogService.notify("%s".formatted(backgroundTask.messageProperty().get()))); - backgroundTask.onFailure((e) -> { - // When unable to import by ID, present the user options to cancel or add entry manually - boolean addEntryFlag = dialogService.showConfirmationDialogAndWait(Localization.lang("Failed to import by ID"), - e.getMessage(), - Localization.lang("Add entry manually")); + backgroundTask.onFailure((exception) -> { + String fetcherExceptionMessage = exception.getMessage(); + + boolean addEntryFlag; + if (exception instanceof FetcherClientException) { + addEntryFlag = dialogService.showConfirmationDialogAndWait(Localization.lang("Failed to import by ID"), Localization.lang("Bibliographic data not found. Cause is likely the client side. Please check connection and identifier for correctness.") + "\n" + fetcherExceptionMessage, Localization.lang("Add entry manually")); + } else if (exception instanceof FetcherServerException) { + addEntryFlag = dialogService.showConfirmationDialogAndWait(Localization.lang("Failed to import by ID"), Localization.lang("Bibliographic data not found. Cause is likely the server side. Please try agan later.") + "\n" + fetcherExceptionMessage, Localization.lang("Add entry manually")); + } else { + addEntryFlag = dialogService.showConfirmationDialogAndWait(Localization.lang("Failed to import by ID"), Localization.lang("Error message %0", fetcherExceptionMessage), Localization.lang("Add entry manually")); + } if (addEntryFlag) { // add entry manually new NewEntryAction(libraryTab.frame(), StandardEntryType.Article, dialogService, @@ -62,7 +69,7 @@ public void execute() { Optional result = bibEntry; if (result.isPresent()) { final BibEntry entry = result.get(); - ImportHandler handler = new ImportHandler(libraryTab.getBibDatabaseContext(), ExternalFileTypes.getInstance(), preferencesService, Globals.getFileUpdateMonitor(), libraryTab.getUndoManager(), stateManager, dialogService); + ImportHandler handler = new ImportHandler(libraryTab.getBibDatabaseContext(), ExternalFileTypes.getInstance(), preferencesService, Globals.getFileUpdateMonitor(), libraryTab.getUndoManager(), stateManager, dialogService, null); handler.importEntryWithDuplicateCheck(libraryTab.getBibDatabaseContext(), entry); } else { dialogService.notify("No entry found or import canceled"); @@ -76,17 +83,12 @@ public void execute() { private BackgroundTask> searchAndImportEntryInBackground() { return new BackgroundTask<>() { @Override - protected Optional call() throws JabRefException { + protected Optional call() throws FetcherException { if (isCanceled()) { return Optional.empty(); } - updateMessage(Localization.lang("Searching...")); - try { - return new CompositeIdFetcher(preferencesService.getImportFormatPreferences()).performSearchById(identifier); - } catch (FetcherException fetcherException) { - throw new JabRefException("Fetcher error: %s".formatted(fetcherException.getMessage())); - } + return new CompositeIdFetcher(preferencesService.getImportFormatPreferences()).performSearchById(identifier); } }; } diff --git a/src/main/java/org/jabref/gui/importer/ImportAction.java b/src/main/java/org/jabref/gui/importer/ImportAction.java index 37566e8dc83..3b08767886d 100644 --- a/src/main/java/org/jabref/gui/importer/ImportAction.java +++ b/src/main/java/org/jabref/gui/importer/ImportAction.java @@ -122,7 +122,7 @@ private List doImport(List files) // Unknown format: DefaultTaskExecutor.runAndWaitInJavaFXThread(() -> { if (fileIsPdf(filename) && GrobidOptInDialogHelper.showAndWaitIfUserIsUndecided(frame.getDialogService(), prefs.getImporterPreferences())) { - Globals.IMPORT_FORMAT_READER.resetImportFormats(prefs.getImporterPreferences(), prefs.getGeneralPreferences(), prefs.getImportFormatPreferences(), prefs.getXmpPreferences(), Globals.getFileUpdateMonitor()); + Globals.IMPORT_FORMAT_READER.resetImportFormats(prefs.getImporterPreferences(), prefs.getImportFormatPreferences(), prefs.getXmpPreferences(), Globals.getFileUpdateMonitor()); } frame.getDialogService().notify(Localization.lang("Importing in unknown format") + "..."); }); @@ -130,8 +130,8 @@ private List doImport(List files) imports.add(Globals.IMPORT_FORMAT_READER.importUnknownFormat(filename, Globals.getFileUpdateMonitor())); } else { DefaultTaskExecutor.runAndWaitInJavaFXThread(() -> { - if (importer.get() instanceof PdfGrobidImporter || importer.get() instanceof PdfMergeMetadataImporter && GrobidOptInDialogHelper.showAndWaitIfUserIsUndecided(frame.getDialogService(), prefs.getImporterPreferences())) { - Globals.IMPORT_FORMAT_READER.resetImportFormats(prefs.getImporterPreferences(), prefs.getGeneralPreferences(), prefs.getImportFormatPreferences(), prefs.getXmpPreferences(), Globals.getFileUpdateMonitor()); + if ((importer.get() instanceof PdfGrobidImporter) || ((importer.get() instanceof PdfMergeMetadataImporter) && GrobidOptInDialogHelper.showAndWaitIfUserIsUndecided(frame.getDialogService(), prefs.getImporterPreferences()))) { + Globals.IMPORT_FORMAT_READER.resetImportFormats(prefs.getImporterPreferences(), prefs.getImportFormatPreferences(), prefs.getXmpPreferences(), Globals.getFileUpdateMonitor()); } frame.getDialogService().notify(Localization.lang("Importing in %0 format", importer.get().getName()) + "..."); }); diff --git a/src/main/java/org/jabref/gui/importer/ImportEntriesViewModel.java b/src/main/java/org/jabref/gui/importer/ImportEntriesViewModel.java index b139a4573c4..877a677234c 100644 --- a/src/main/java/org/jabref/gui/importer/ImportEntriesViewModel.java +++ b/src/main/java/org/jabref/gui/importer/ImportEntriesViewModel.java @@ -12,6 +12,7 @@ import org.jabref.gui.AbstractViewModel; import org.jabref.gui.DialogService; +import org.jabref.gui.Globals; import org.jabref.gui.JabRefGUI; import org.jabref.gui.StateManager; import org.jabref.gui.duplicationFinder.DuplicateResolverDialog; @@ -170,7 +171,8 @@ private void buildImportHandlerThenImportEntries(List entriesToImport) fileUpdateMonitor, undoManager, stateManager, - dialogService); + dialogService, + Globals.IMPORT_FORMAT_READER); importHandler.importEntries(entriesToImport); dialogService.notify(Localization.lang("Number of entries successfully imported") + ": " + entriesToImport.size()); } diff --git a/src/main/java/org/jabref/gui/importer/actions/OpenDatabaseAction.java b/src/main/java/org/jabref/gui/importer/actions/OpenDatabaseAction.java index 02c38ad9a34..052dba2ccac 100644 --- a/src/main/java/org/jabref/gui/importer/actions/OpenDatabaseAction.java +++ b/src/main/java/org/jabref/gui/importer/actions/OpenDatabaseAction.java @@ -180,7 +180,7 @@ private void openTheFile(Path file, boolean raisePanel) { BackgroundTask backgroundTask = BackgroundTask.wrap(() -> loadDatabase(file)); LibraryTab.Factory libraryTabFactory = new LibraryTab.Factory(); - LibraryTab newTab = libraryTabFactory.createLibraryTab(frame, preferencesService, stateManager, themeManager, file, backgroundTask); + LibraryTab newTab = libraryTabFactory.createLibraryTab(frame, preferencesService, stateManager, themeManager, file, backgroundTask, Globals.IMPORT_FORMAT_READER); backgroundTask.onFinished(() -> trackOpenNewDatabase(newTab)); } diff --git a/src/main/java/org/jabref/gui/maintable/MainTable.java b/src/main/java/org/jabref/gui/maintable/MainTable.java index 32a80afb754..d37f3bb09c4 100644 --- a/src/main/java/org/jabref/gui/maintable/MainTable.java +++ b/src/main/java/org/jabref/gui/maintable/MainTable.java @@ -3,6 +3,7 @@ import java.io.File; import java.io.IOException; import java.nio.file.Path; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -24,6 +25,7 @@ import javafx.scene.input.MouseEvent; import javafx.scene.input.TransferMode; +import org.jabref.gui.ClipBoardManager; import org.jabref.gui.DialogService; import org.jabref.gui.DragAndDropDataFormats; import org.jabref.gui.Globals; @@ -41,6 +43,10 @@ import org.jabref.gui.util.CustomLocalDragboard; import org.jabref.gui.util.DefaultTaskExecutor; import org.jabref.gui.util.ViewModelTableRowFactory; +import org.jabref.logic.importer.FetcherClientException; +import org.jabref.logic.importer.FetcherException; +import org.jabref.logic.importer.FetcherServerException; +import org.jabref.logic.importer.ImportFormatReader; import org.jabref.logic.l10n.Localization; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.database.event.EntriesAddedEvent; @@ -63,7 +69,7 @@ public class MainTable extends TableView { private final ImportHandler importHandler; private final CustomLocalDragboard localDragboard; - + private final ClipBoardManager clipBoardManager; private long lastKeyPressTime; private String columnSearchTerm; @@ -74,7 +80,9 @@ public MainTable(MainTableDataModel model, DialogService dialogService, StateManager stateManager, ExternalFileTypes externalFileTypes, - KeyBindingRepository keyBindingRepository) { + KeyBindingRepository keyBindingRepository, + ClipBoardManager clipBoardManager, + ImportFormatReader importFormatReader) { super(); this.libraryTab = libraryTab; @@ -82,6 +90,7 @@ public MainTable(MainTableDataModel model, this.stateManager = stateManager; this.database = Objects.requireNonNull(database); this.model = model; + this.clipBoardManager = clipBoardManager; UndoManager undoManager = libraryTab.getUndoManager(); MainTablePreferences mainTablePreferences = preferencesService.getMainTablePreferences(); @@ -91,7 +100,8 @@ public MainTable(MainTableDataModel model, Globals.getFileUpdateMonitor(), undoManager, stateManager, - dialogService); + dialogService, + importFormatReader); localDragboard = stateManager.getLocalDragboard(); @@ -304,8 +314,10 @@ private void clearAndSelectLast() { } public void paste() { - // Find entries in clipboard - List entriesToAdd = Globals.getClipboardManager().extractData(); + List entriesToAdd = new ArrayList<>(); + entriesToAdd = this.clipBoardManager.getBibTeXEntriesFromClipbaord() + .map(importHandler::handleBibTeXData) + .orElseGet(this::handleNonBibteXStringData); for (BibEntry entry : entriesToAdd) { importHandler.importEntryWithDuplicateCheck(database, entry); @@ -315,6 +327,23 @@ public void paste() { } } + private List handleNonBibteXStringData() { + String data = this.clipBoardManager.getContents(); + List entries = new ArrayList<>(); + try { + entries = this.importHandler.handleStringData(data); + } catch (FetcherException exception) { + if (exception instanceof FetcherClientException) { + dialogService.showInformationDialogAndWait(Localization.lang("Look up identifier"), Localization.lang("No data was found for the identifier")); + } else if (exception instanceof FetcherServerException) { + dialogService.showInformationDialogAndWait(Localization.lang("Look up identifier"), Localization.lang("Server not available")); + } else { + dialogService.showErrorDialogAndWait(exception); + } + } + return entries; + } + public void dropEntry(List entriesToAdd) { for (BibEntry entry : entriesToAdd) { importHandler.importEntryWithDuplicateCheck(database, (BibEntry) entry.clone()); diff --git a/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java b/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java index 5210613e4de..6ef5e500218 100644 --- a/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java +++ b/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java @@ -47,7 +47,10 @@ public MainTableDataModel(BibDatabaseContext context, PreferencesService prefere entriesFiltered = new FilteredList<>(entriesViewModel); entriesFiltered.predicateProperty().bind( - EasyBind.combine(stateManager.activeGroupProperty(), stateManager.activeSearchQueryProperty(), (groups, query) -> entry -> isMatched(groups, query, entry)) + EasyBind.combine(stateManager.activeGroupProperty(), + stateManager.activeSearchQueryProperty(), + groupsPreferences.groupViewModeProperty(), + (groups, query, groupViewMode) -> entry -> isMatched(groups, query, entry)) ); IntegerProperty resultSize = new SimpleIntegerProperty(); diff --git a/src/main/java/org/jabref/gui/maintable/columns/SpecialFieldColumn.java b/src/main/java/org/jabref/gui/maintable/columns/SpecialFieldColumn.java index 271a7abe4cb..971bb5e0f69 100644 --- a/src/main/java/org/jabref/gui/maintable/columns/SpecialFieldColumn.java +++ b/src/main/java/org/jabref/gui/maintable/columns/SpecialFieldColumn.java @@ -9,6 +9,7 @@ import javafx.scene.control.MenuItem; import javafx.scene.control.Tooltip; import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; import org.jabref.gui.icon.JabRefIcon; import org.jabref.gui.maintable.BibEntryTableViewModel; @@ -55,7 +56,7 @@ public SpecialFieldColumn(MainTableColumnModel model, PreferencesService prefere MainTableColumnFactory.setExactWidth(this, SpecialFieldsPreferences.COLUMN_RANKING_WIDTH); this.setResizable(false); new OptionalValueTableCellFactory() - .withGraphicIfPresent(this::createSpecialRating) + .withGraphic(this::createSpecialRating) .install(this); } else { MainTableColumnFactory.setExactWidth(this, ColumnPreferences.ICON_COLUMN_WIDTH); @@ -89,9 +90,24 @@ public SpecialFieldColumn(MainTableColumnModel model, PreferencesService prefere this.setSortable(true); } - private Rating createSpecialRating(BibEntryTableViewModel entry, SpecialFieldValueViewModel value) { + private Rating createSpecialRating(BibEntryTableViewModel entry, Optional value) { Rating ranking = new Rating(); - ranking.setRating(value.getValue().toRating()); + + if (value.isPresent()) { + ranking.setRating(value.get().getValue().toRating()); + } else { + ranking.setRating(0); + } + + ranking.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> { + if (event.getButton().equals(MouseButton.PRIMARY) && event.getClickCount() == 2) { + ranking.setRating(0); + event.consume(); + } else if (event.getButton().equals(MouseButton.SECONDARY)) { + event.consume(); + } + }); + EasyBind.subscribe(ranking.ratingProperty(), rating -> new SpecialFieldViewModel(SpecialField.RANKING, preferencesService, undoManager) .setSpecialFieldValue(entry.getEntry(), SpecialFieldValue.getRating(rating.intValue()))); diff --git a/src/main/java/org/jabref/gui/mergeentries/FetchAndMergeEntry.java b/src/main/java/org/jabref/gui/mergeentries/FetchAndMergeEntry.java index 7c1a9e127ba..ed7220b532e 100644 --- a/src/main/java/org/jabref/gui/mergeentries/FetchAndMergeEntry.java +++ b/src/main/java/org/jabref/gui/mergeentries/FetchAndMergeEntry.java @@ -17,6 +17,8 @@ import org.jabref.gui.util.BackgroundTask; import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.importer.EntryBasedFetcher; +import org.jabref.logic.importer.FetcherClientException; +import org.jabref.logic.importer.FetcherServerException; import org.jabref.logic.importer.IdBasedFetcher; import org.jabref.logic.importer.ImportCleanup; import org.jabref.logic.importer.WebFetcher; @@ -76,7 +78,13 @@ public void fetchAndMerge(BibEntry entry, List fields) { }) .onFailure(exception -> { LOGGER.error("Error while fetching bibliographic information", exception); - dialogService.showErrorDialogAndWait(exception); + if (exception instanceof FetcherClientException) { + dialogService.showInformationDialogAndWait(Localization.lang("Fetching information using %0", fetcher.get().getName()), Localization.lang("No data was found for the identifier")); + } else if (exception instanceof FetcherServerException) { + dialogService.showInformationDialogAndWait(Localization.lang("Fetching information using %0", fetcher.get().getName()), Localization.lang("Server not available")); + } else { + dialogService.showInformationDialogAndWait(Localization.lang("Fetching information using %0", fetcher.get().getName()), Localization.lang("Error occured %0", exception.getMessage())); + } }) .executeWith(Globals.TASK_EXECUTOR); } @@ -158,7 +166,7 @@ public void fetchAndMerge(BibEntry entry, EntryBasedFetcher fetcher) { } }) .onFailure(exception -> { - LOGGER.error("Error while fetching entry with " + fetcher.getName(), exception); + LOGGER.error("Error while fetching entry with {} ", fetcher.getName(), exception); dialogService.showErrorDialogAndWait(Localization.lang("Error while fetching from %0", fetcher.getName()), exception); }) .executeWith(taskExecutor); diff --git a/src/main/java/org/jabref/gui/openoffice/OpenOfficePanel.java b/src/main/java/org/jabref/gui/openoffice/OpenOfficePanel.java index 68f274e1a2d..298c0149309 100644 --- a/src/main/java/org/jabref/gui/openoffice/OpenOfficePanel.java +++ b/src/main/java/org/jabref/gui/openoffice/OpenOfficePanel.java @@ -381,6 +381,7 @@ protected OOBibBase call() throws Exception { connectTask.setOnFailed(value -> { Throwable ex = connectTask.getException(); + LOGGER.error("autodetect failed", ex); if (ex instanceof UnsatisfiedLinkError) { LOGGER.warn("Could not connect to running OpenOffice/LibreOffice", ex); @@ -395,6 +396,9 @@ protected OOBibBase call() throws Exception { + Localization.lang("Make sure you have installed OpenOffice/LibreOffice with Java support.") + "\n" + Localization.lang("If connecting manually, please verify program and library paths.") + "\n" + "\n" + Localization.lang("Error message:"), ex); + } else if (ex instanceof BootstrapException bootstrapEx) { + LOGGER.error("Exception boostrap cause", bootstrapEx.getTargetException()); + dialogService.showErrorDialogAndWait("Bootstrap error", bootstrapEx.getTargetException()); } else { dialogService.showErrorDialogAndWait(Localization.lang("Autodetection failed"), Localization.lang("Autodetection failed"), ex); } diff --git a/src/main/java/org/jabref/gui/preferences/customimporter/CustomImporterTabViewModel.java b/src/main/java/org/jabref/gui/preferences/customimporter/CustomImporterTabViewModel.java index 218c76d940a..08012693858 100644 --- a/src/main/java/org/jabref/gui/preferences/customimporter/CustomImporterTabViewModel.java +++ b/src/main/java/org/jabref/gui/preferences/customimporter/CustomImporterTabViewModel.java @@ -55,7 +55,6 @@ public void storeSettings() { .collect(Collectors.toSet())); Globals.IMPORT_FORMAT_READER.resetImportFormats( preferences.getImporterPreferences(), - preferences.getGeneralPreferences(), preferences.getImportFormatPreferences(), preferences.getXmpPreferences(), Globals.getFileUpdateMonitor()); diff --git a/src/main/java/org/jabref/gui/shared/SharedDatabaseLoginDialogViewModel.java b/src/main/java/org/jabref/gui/shared/SharedDatabaseLoginDialogViewModel.java index 43bf6f86dac..970d36db98e 100644 --- a/src/main/java/org/jabref/gui/shared/SharedDatabaseLoginDialogViewModel.java +++ b/src/main/java/org/jabref/gui/shared/SharedDatabaseLoginDialogViewModel.java @@ -212,6 +212,9 @@ private void setPreferences() { } sharedDatabasePreferences.setRememberPassword(rememberPassword.get()); + + sharedDatabasePreferences.setFolder(folder.getValue()); + sharedDatabasePreferences.setAutosave(autosave.get()); } /** @@ -225,6 +228,8 @@ private void applyPreferences() { Optional sharedDatabaseUser = sharedDatabasePreferences.getUser(); Optional sharedDatabasePassword = sharedDatabasePreferences.getPassword(); boolean sharedDatabaseRememberPassword = sharedDatabasePreferences.getRememberPassword(); + Optional sharedDatabaseFolder = sharedDatabasePreferences.getFolder(); + boolean sharedDatabaseAutosave = sharedDatabasePreferences.getAutosave(); Optional sharedDatabaseKeystoreFile = sharedDatabasePreferences.getKeyStoreFile(); if (sharedDatabaseType.isPresent()) { @@ -248,6 +253,9 @@ private void applyPreferences() { } rememberPassword.set(sharedDatabaseRememberPassword); + + sharedDatabaseFolder.ifPresent(folder::set); + autosave.set(sharedDatabaseAutosave); } private boolean isSharedDatabaseAlreadyPresent(DBMSConnectionProperties connectionProperties) { diff --git a/src/main/java/org/jabref/gui/util/CustomRatingSkin.java b/src/main/java/org/jabref/gui/util/CustomRatingSkin.java new file mode 100644 index 00000000000..96e6e8d75d5 --- /dev/null +++ b/src/main/java/org/jabref/gui/util/CustomRatingSkin.java @@ -0,0 +1,21 @@ +package org.jabref.gui.util; + +import javafx.scene.Node; + +import org.jabref.gui.icon.IconTheme; + +import impl.org.controlsfx.skin.RatingSkin; +import org.controlsfx.control.Rating; + +public class CustomRatingSkin extends RatingSkin { + public CustomRatingSkin(Rating control) { + super(control); + + consumeMouseEvents(false); + } + + @Override + protected Node createButtonNode() { + return IconTheme.JabRefIcons.RANKING.getGraphicNode(); + } +} diff --git a/src/main/java/org/jabref/logic/bibtex/comparator/GroupDiff.java b/src/main/java/org/jabref/logic/bibtex/comparator/GroupDiff.java index 4914ed9642d..35c2e5a1338 100644 --- a/src/main/java/org/jabref/logic/bibtex/comparator/GroupDiff.java +++ b/src/main/java/org/jabref/logic/bibtex/comparator/GroupDiff.java @@ -23,7 +23,7 @@ public static Optional compare(MetaData originalMetaData, MetaData ne final Optional newGroups = newMetaData.getGroups(); if (!originalGroups.equals(newGroups)) { - return Optional.of(new GroupDiff(newGroups.orElse(null), originalGroups.orElse(null))); + return Optional.of(new GroupDiff(originalGroups.orElse(null), newGroups.orElse(null))); } else { return Optional.empty(); } diff --git a/src/main/java/org/jabref/logic/bst/BibtexWidth.java b/src/main/java/org/jabref/logic/bst/BibtexWidth.java deleted file mode 100644 index 61e0b099f20..00000000000 --- a/src/main/java/org/jabref/logic/bst/BibtexWidth.java +++ /dev/null @@ -1,241 +0,0 @@ -package org.jabref.logic.bst; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * - * The |built_in| function {\.{purify\$}} pops the top (string) literal, removes - * nonalphanumeric characters except for |white_space| and |sep_char| characters - * (these get converted to a |space|) and removes certain alphabetic characters - * contained in the control sequences associated with a special character, and - * pushes the resulting string. If the literal isn't a string, it complains and - * pushes the null string. - * - */ -public class BibtexWidth { - - private static final Logger LOGGER = LoggerFactory.getLogger(BibtexWidth.class); - - /* - * Quoted from Bibtex: - * - * Now we initialize the system-dependent |char_width| array, for which - * |space| is the only |white_space| character given a nonzero printing - * width. The widths here are taken from Stanford's June~'87 $cmr10$~font - * and represent hundredths of a point (rounded), but since they're used - * only for relative comparisons, the units have no meaning. - */ - - private static int[] widths; - - static { - if (BibtexWidth.widths == null) { - BibtexWidth.widths = new int[128]; - - for (int i = 0; i < 128; i++) { - BibtexWidth.widths[i] = 0; - } - BibtexWidth.widths[32] = 278; - BibtexWidth.widths[33] = 278; - BibtexWidth.widths[34] = 500; - BibtexWidth.widths[35] = 833; - BibtexWidth.widths[36] = 500; - BibtexWidth.widths[37] = 833; - BibtexWidth.widths[38] = 778; - BibtexWidth.widths[39] = 278; - BibtexWidth.widths[40] = 389; - BibtexWidth.widths[41] = 389; - BibtexWidth.widths[42] = 500; - BibtexWidth.widths[43] = 778; - BibtexWidth.widths[44] = 278; - BibtexWidth.widths[45] = 333; - BibtexWidth.widths[46] = 278; - BibtexWidth.widths[47] = 500; - BibtexWidth.widths[48] = 500; - BibtexWidth.widths[49] = 500; - BibtexWidth.widths[50] = 500; - BibtexWidth.widths[51] = 500; - BibtexWidth.widths[52] = 500; - BibtexWidth.widths[53] = 500; - BibtexWidth.widths[54] = 500; - BibtexWidth.widths[55] = 500; - BibtexWidth.widths[56] = 500; - BibtexWidth.widths[57] = 500; - BibtexWidth.widths[58] = 278; - BibtexWidth.widths[59] = 278; - BibtexWidth.widths[60] = 278; - BibtexWidth.widths[61] = 778; - BibtexWidth.widths[62] = 472; - BibtexWidth.widths[63] = 472; - BibtexWidth.widths[64] = 778; - BibtexWidth.widths[65] = 750; - BibtexWidth.widths[66] = 708; - BibtexWidth.widths[67] = 722; - BibtexWidth.widths[68] = 764; - BibtexWidth.widths[69] = 681; - BibtexWidth.widths[70] = 653; - BibtexWidth.widths[71] = 785; - BibtexWidth.widths[72] = 750; - BibtexWidth.widths[73] = 361; - BibtexWidth.widths[74] = 514; - BibtexWidth.widths[75] = 778; - BibtexWidth.widths[76] = 625; - BibtexWidth.widths[77] = 917; - BibtexWidth.widths[78] = 750; - BibtexWidth.widths[79] = 778; - BibtexWidth.widths[80] = 681; - BibtexWidth.widths[81] = 778; - BibtexWidth.widths[82] = 736; - BibtexWidth.widths[83] = 556; - BibtexWidth.widths[84] = 722; - BibtexWidth.widths[85] = 750; - BibtexWidth.widths[86] = 750; - BibtexWidth.widths[87] = 1028; - BibtexWidth.widths[88] = 750; - BibtexWidth.widths[89] = 750; - BibtexWidth.widths[90] = 611; - BibtexWidth.widths[91] = 278; - BibtexWidth.widths[92] = 500; - BibtexWidth.widths[93] = 278; - BibtexWidth.widths[94] = 500; - BibtexWidth.widths[95] = 278; - BibtexWidth.widths[96] = 278; - BibtexWidth.widths[97] = 500; - BibtexWidth.widths[98] = 556; - BibtexWidth.widths[99] = 444; - BibtexWidth.widths[100] = 556; - BibtexWidth.widths[101] = 444; - BibtexWidth.widths[102] = 306; - BibtexWidth.widths[103] = 500; - BibtexWidth.widths[104] = 556; - BibtexWidth.widths[105] = 278; - BibtexWidth.widths[106] = 306; - BibtexWidth.widths[107] = 528; - BibtexWidth.widths[108] = 278; - BibtexWidth.widths[109] = 833; - BibtexWidth.widths[110] = 556; - BibtexWidth.widths[111] = 500; - BibtexWidth.widths[112] = 556; - BibtexWidth.widths[113] = 528; - BibtexWidth.widths[114] = 392; - BibtexWidth.widths[115] = 394; - BibtexWidth.widths[116] = 389; - BibtexWidth.widths[117] = 556; - BibtexWidth.widths[118] = 528; - BibtexWidth.widths[119] = 722; - BibtexWidth.widths[120] = 528; - BibtexWidth.widths[121] = 528; - BibtexWidth.widths[122] = 444; - BibtexWidth.widths[123] = 500; - BibtexWidth.widths[124] = 1000; - BibtexWidth.widths[125] = 500; - BibtexWidth.widths[126] = 500; - } - } - - private BibtexWidth() { - } - - private static int getSpecialCharWidth(char[] c, int pos) { - if ((pos + 1) < c.length) { - if ((c[pos] == 'o') && (c[pos + 1] == 'e')) { - return 778; - } - if ((c[pos] == 'O') && (c[pos + 1] == 'E')) { - return 1014; - } - if ((c[pos] == 'a') && (c[pos + 1] == 'e')) { - return 722; - } - if ((c[pos] == 'A') && (c[pos + 1] == 'E')) { - return 903; - } - if ((c[pos] == 's') && (c[pos + 1] == 's')) { - return 500; - } - } - return BibtexWidth.getCharWidth(c[pos]); - } - - public static int getCharWidth(char c) { - if ((c >= 0) && (c < 128)) { - return BibtexWidth.widths[c]; - } else { - return 0; - } - } - - public static int width(String toMeasure) { - /* - * From Bibtex: We use the natural width for all but special characters, - * and we complain if the string isn't brace-balanced. - */ - - int i = 0; - int n = toMeasure.length(); - int braceLevel = 0; - char[] c = toMeasure.toCharArray(); - int result = 0; - - /* - * From Bibtex: - * - * We use the natural widths of all characters except that some - * characters have no width: braces, control sequences (except for the - * usual 13 accented and foreign characters, whose widths are given in - * the next module), and |white_space| following control sequences (even - * a null control sequence). - * - */ - while (i < n) { - if (c[i] == '{') { - braceLevel++; - if ((braceLevel == 1) && ((i + 1) < n) && (c[i + 1] == '\\')) { - i++; // skip brace - while ((i < n) && (braceLevel > 0)) { - i++; // skip backslash - - int afterBackslash = i; - while ((i < n) && Character.isLetter(c[i])) { - i++; - } - if ((i < n) && (i == afterBackslash)) { - i++; // Skip non-alpha control seq - } else { - if (BibtexCaseChanger.findSpecialChar(c, afterBackslash).isPresent()) { - result += BibtexWidth.getSpecialCharWidth(c, afterBackslash); - } - } - while ((i < n) && Character.isWhitespace(c[i])) { - i++; - } - while ((i < n) && (braceLevel > 0) && (c[i] != '\\')) { - if (c[i] == '}') { - braceLevel--; - } else if (c[i] == '{') { - braceLevel++; - } else { - result += BibtexWidth.getCharWidth(c[i]); - } - i++; - } - } - continue; - } - } else if (c[i] == '}') { - if (braceLevel > 0) { - braceLevel--; - } else { - LOGGER.warn("Too many closing braces in string: " + toMeasure); - } - } - result += BibtexWidth.getCharWidth(c[i]); - i++; - } - if (braceLevel > 0) { - LOGGER.warn("No enough closing braces in string: " + toMeasure); - } - return result; - } -} diff --git a/src/main/java/org/jabref/logic/bst/BstEntry.java b/src/main/java/org/jabref/logic/bst/BstEntry.java new file mode 100644 index 00000000000..89571de8046 --- /dev/null +++ b/src/main/java/org/jabref/logic/bst/BstEntry.java @@ -0,0 +1,21 @@ +package org.jabref.logic.bst; + +import java.util.HashMap; +import java.util.Map; + +import org.jabref.model.entry.BibEntry; + +public class BstEntry { + + public final BibEntry entry; + + public final Map localStrings = new HashMap<>(); + + public final Map fields = new HashMap<>(); + + public final Map localIntegers = new HashMap<>(); + + public BstEntry(BibEntry e) { + this.entry = e; + } +} diff --git a/src/main/java/org/jabref/logic/bst/BstFunctions.java b/src/main/java/org/jabref/logic/bst/BstFunctions.java new file mode 100644 index 00000000000..6689f83b2a5 --- /dev/null +++ b/src/main/java/org/jabref/logic/bst/BstFunctions.java @@ -0,0 +1,931 @@ +package org.jabref.logic.bst; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Stack; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.jabref.logic.bst.util.BstCaseChanger; +import org.jabref.logic.bst.util.BstNameFormatter; +import org.jabref.logic.bst.util.BstPurifier; +import org.jabref.logic.bst.util.BstTextPrefixer; +import org.jabref.logic.bst.util.BstWidthCalculator; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.Author; +import org.jabref.model.entry.AuthorList; + +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.tree.ParseTree; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BstFunctions { + private static final Logger LOGGER = LoggerFactory.getLogger(BstFunctions.class); + private static final Pattern ADD_PERIOD_PATTERN = Pattern.compile("([^.?!}\\s])(}|\\s)*$"); + + private final Map strings; + private final Map integers; + private final Map functions; + private final String preamble; + + private final Stack stack; + private final StringBuilder bbl; + + private int bstWarning = 0; + + @FunctionalInterface + public interface BstFunction { + + void execute(BstVMVisitor visitor, ParserRuleContext ctx); + + default void execute(BstVMVisitor visitor, ParserRuleContext ctx, BstEntry bstEntryContext) { + this.execute(visitor, ctx); + } + } + + public BstFunctions(BstVMContext bstVMContext, + StringBuilder bbl) { + this.strings = bstVMContext.strings(); + this.integers = bstVMContext.integers(); + this.functions = bstVMContext.functions(); + this.preamble = Optional.ofNullable(bstVMContext.bibDatabase()).flatMap(BibDatabase::getPreamble).orElse(""); + this.stack = bstVMContext.stack(); + + this.bbl = bbl; + } + + protected Map getBuiltInFunctions() { + Map builtInFunctions = new HashMap<>(); + + builtInFunctions.put(">", this::bstIsGreaterThan); + builtInFunctions.put("<", this::bstIsLowerThan); + builtInFunctions.put("=", this::bstEquals); + builtInFunctions.put("+", this::bstAdd); + builtInFunctions.put("-", this::bstSubtract); + builtInFunctions.put("*", this::bstConcat); + builtInFunctions.put(":=", new BstAssignFunction()); + builtInFunctions.put("add.period$", this::bstAddPeriod); + builtInFunctions.put("call.type$", new BstCallTypeFunction()); + builtInFunctions.put("change.case$", this::bstChangeCase); + builtInFunctions.put("chr.to.int$", this::bstChrToInt); + builtInFunctions.put("cite$", new BstCiteFunction()); + builtInFunctions.put("duplicate$", this::bstDuplicate); + builtInFunctions.put("empty$", this::bstEmpty); + builtInFunctions.put("format.name$", this::bstFormatName); + builtInFunctions.put("if$", this::bstIf); + builtInFunctions.put("int.to.chr$", this::bstIntToChr); + builtInFunctions.put("int.to.str$", this::bstIntToStr); + builtInFunctions.put("missing$", this::bstMissing); + builtInFunctions.put("newline$", this::bstNewLine); + builtInFunctions.put("num.names$", this::bstNumNames); + builtInFunctions.put("pop$", this::bstPop); + builtInFunctions.put("preamble$", this::bstPreamble); + builtInFunctions.put("purify$", this::bstPurify); + builtInFunctions.put("quote$", this::bstQuote); + builtInFunctions.put("skip$", this::bstSkip); + builtInFunctions.put("stack$", this::bstStack); + builtInFunctions.put("substring$", this::bstSubstring); + builtInFunctions.put("swap$", this::bstSwap); + builtInFunctions.put("text.length$", this::bstTextLength); + builtInFunctions.put("text.prefix$", this::bstTextPrefix); + builtInFunctions.put("top$", this::bstTop); + builtInFunctions.put("type$", new BstTypeFunction()); + builtInFunctions.put("warning$", this::bstWarning); + builtInFunctions.put("while$", this::bstWhile); + builtInFunctions.put("width$", this::bstWidth); + builtInFunctions.put("write$", this::bstWrite); + + return builtInFunctions; + } + + /** + * Pops the top two (integer) literals, compares them, and pushes + * the integer 1 if the second is greater than the first, 0 + * otherwise. + */ + private void bstIsGreaterThan(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.size() < 2) { + throw new BstVMException("Not enough operands on stack for operation > (line %d)".formatted(ctx.start.getLine())); + } + Object o2 = stack.pop(); + Object o1 = stack.pop(); + + if (!((o1 instanceof Integer) && (o2 instanceof Integer))) { + throw new BstVMException("Can only compare two integers with >"); + } + + stack.push(((Integer) o1).compareTo((Integer) o2) > 0 ? BstVM.TRUE : BstVM.FALSE); + } + + /** + * Pops the top two (integer) literals, compares them, and pushes + * the integer 1 if the second is lower than the first, 0 + * otherwise. + */ + private void bstIsLowerThan(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.size() < 2) { + throw new BstVMException("Not enough operands on stack for operation <"); + } + Object o2 = stack.pop(); + Object o1 = stack.pop(); + + if (!((o1 instanceof Integer) && (o2 instanceof Integer))) { + throw new BstVMException("Can only compare two integers with < (line %d)".formatted(ctx.start.getLine())); + } + + stack.push(((Integer) o1).compareTo((Integer) o2) < 0 ? BstVM.TRUE : BstVM.FALSE); + } + + /** + * Pops the top two (both integer or both string) literals, compares + * them, and pushes the integer 1 if they're equal, 0 otherwise. + */ + private void bstEquals(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.size() < 2) { + throw new BstVMException("Not enough operands on stack for operation = (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + Object o2 = stack.pop(); + + if ((o1 == null) ^ (o2 == null)) { + stack.push(BstVM.FALSE); + return; + } + + if ((o1 == null) && (o2 == null)) { + stack.push(BstVM.TRUE); + return; + } + + stack.push(o1.equals(o2) ? BstVM.TRUE : BstVM.FALSE); + } + + /** + * Pops the top two (integer) literals and pushes their sum. + */ + private void bstAdd(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.size() < 2) { + throw new BstVMException("Not enough operands on stack for operation + (line %d)".formatted(ctx.start.getLine())); + } + Object o2 = stack.pop(); + Object o1 = stack.pop(); + + if (!((o1 instanceof Integer) && (o2 instanceof Integer))) { + throw new BstVMException("Can only compare two integers with + (line %d)".formatted(ctx.start.getLine())); + } + + stack.push((Integer) o1 + (Integer) o2); + } + + /** + * Pops the top two (integer) literals and pushes their difference + * (the first subtracted from the second). + */ + private void bstSubtract(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.size() < 2) { + throw new BstVMException("Not enough operands on stack for operation - (line %d)".formatted(ctx.start.getLine())); + } + Object o2 = stack.pop(); + Object o1 = stack.pop(); + + if (!((o1 instanceof Integer) && (o2 instanceof Integer))) { + throw new BstVMException("Can only subtract two integers with - (line %d)".formatted(ctx.start.getLine())); + } + + stack.push((Integer) o1 - (Integer) o2); + } + + /** + * Pops the top two (string) literals, concatenates them (in reverse + * order, that is, the order in which pushed), and pushes the + * resulting string. + */ + private void bstConcat(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.size() < 2) { + throw new BstVMException("Not enough operands on stack for operation * (line %d)".formatted(ctx.start.getLine())); + } + Object o2 = stack.pop(); + Object o1 = stack.pop(); + + if (o1 == null) { + o1 = ""; + } + if (o2 == null) { + o2 = ""; + } + + if (!((o1 instanceof String) && (o2 instanceof String))) { + LOGGER.error("o1: {} ({})", o1, o1.getClass()); + LOGGER.error("o2: {} ({})", o2, o2.getClass()); + throw new BstVMException("Can only concatenate two String with * (line %d)".formatted(ctx.start.getLine())); + } + + stack.push(o1.toString() + o2); + } + + /** + * Pops the top two literals and assigns to the first (which must be + * a global or entry variable) the value of the second. + */ + public class BstAssignFunction implements BstFunction { + + @Override + public void execute(BstVMVisitor visitor, ParserRuleContext ctx) { + this.execute(visitor, ctx, null); + } + + @Override + public void execute(BstVMVisitor visitor, ParserRuleContext ctx, BstEntry bstEntry) { + if (stack.size() < 2) { + throw new BstVMException("Not enough operands on stack for operation := (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + Object o2 = stack.pop(); + + if (!(o1 instanceof BstVMVisitor.Identifier identifier)) { + throw new BstVMException("Invalid parameters (line %d)".formatted(ctx.start.getLine())); + } + String name = identifier.name(); + + if (o2 instanceof String value) { + if ((bstEntry != null) && bstEntry.localStrings.containsKey(name)) { + bstEntry.localStrings.put(name, value); + return; + } + + if (strings.containsKey(name)) { + strings.put(name, value); + } + } else if (o2 instanceof Integer value) { + if ((bstEntry != null) && bstEntry.localIntegers.containsKey(name)) { + bstEntry.localIntegers.put(name, value); + return; + } + + if (integers.containsKey(name)) { + integers.put(name, value); + } + } else { + throw new BstVMException("Invalid parameters (line %d)".formatted(ctx.start.getLine())); + } + } + } + + /** + * Pops the top (string) literal, adds a `.' to it if the last non + * '}' character isn't a `.', `?', or `!', and pushes this resulting + * string. + */ + private void bstAddPeriod(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.isEmpty()) { + throw new BstVMException("Not enough operands on stack for operation add.period$ (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + + if (!(o1 instanceof String s)) { + throw new BstVMException("Can only add a period to a string for add.period$ (line %d)".formatted(ctx.start.getLine())); + } + + Matcher m = ADD_PERIOD_PATTERN.matcher(s); + + if (m.find()) { + StringBuilder sb = new StringBuilder(); + m.appendReplacement(sb, m.group(1)); + sb.append('.'); + String group2 = m.group(2); + if (group2 != null) { + sb.append(m.group(2)); + } + stack.push(sb.toString()); + } else { + stack.push(s); + } + } + + /** + * Executes the function whose name is the entry type of an entry. + * For example if an entry is of type book, this function executes + * the book function. When given as an argument to the ITERATE + * command, call.type$ actually produces the output for the entries. + * For an entry with an unknown type, it executes the function + * default.type. Thus you should define (before the READ command) + * one function for each standard entry type as well as a + * default.type function. + */ + public class BstCallTypeFunction implements BstFunction { + @Override + public void execute(BstVMVisitor visitor, ParserRuleContext ctx) { + throw new BstVMException("Call.type$ can only be called from within a context (ITERATE or REVERSE). (line %d)".formatted(ctx.start.getLine())); + } + + @Override + public void execute(BstVMVisitor visitor, ParserRuleContext ctx, BstEntry bstEntry) { + if (bstEntry == null) { + this.execute(visitor, ctx); // Throw error + } else { + functions.get(bstEntry.entry.getType().getName()).execute(visitor, ctx, bstEntry); + } + } + } + + /** + * Pops the top two (string) literals; it changes the case of the second + * according to the specifications of the first, as follows. (Note: The word + * `letters' in the next sentence refers only to those at brace-level 0, the + * top-most brace level; no other characters are changed, except perhaps for + * \special characters", described in Section 4.) If the first literal is the + * string `t', it converts to lower case all letters except the very first + * character in the string, which it leaves alone, and except the first + * character following any colon and then nonnull white space, which it also + * leaves alone; if it's the string `l', it converts all letters to lower case; + * and if it's the string `u', it converts all letters to upper case. It then + * pushes this resulting string. If either type is incorrect, it complains and + * pushes the null string; however, if both types are correct but the + * specification string (i.e., the first string) isn't one of the legal ones, it + * merely pushes the second back onto the stack, after complaining. (Another + * note: It ignores case differences in the specification string; for example, + * the strings t and T are equivalent for the purposes of this built-in + * function.) + */ + private void bstChangeCase(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.size() < 2) { + throw new BstVMException("Not enough operands on stack for operation change.case$ (line %d)".formatted(ctx.start.getLine())); + } + + Object o1 = stack.pop(); + if (!((o1 instanceof String format) && (format.length() == 1))) { + throw new BstVMException("A format string of length 1 is needed for change.case$ (line %d)".formatted(ctx.start.getLine())); + } + + Object o2 = stack.pop(); + if (!(o2 instanceof String toChange)) { + throw new BstVMException("A string is needed as second parameter for change.case$ (line %d)".formatted(ctx.start.getLine())); + } + + stack.push(BstCaseChanger.changeCase(toChange, BstCaseChanger.FormatMode.of(format))); + } + + /** + * Pops the top (string) literal, makes sure it's a single + * character, converts it to the corresponding ASCII integer, and + * pushes this integer. + */ + private void bstChrToInt(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.isEmpty()) { + throw new BstVMException("Not enough operands on stack for operation chr.to.int$ (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + + if (!((o1 instanceof String s) && (((String) o1).length() == 1))) { + throw new BstVMException("Can only perform chr.to.int$ on string with length 1 (line %d)".formatted(ctx.start.getLine())); + } + + stack.push((int) s.charAt(0)); + } + + /** + * Pushes the string that was the \cite-command argument for this + * entry. + */ + public class BstCiteFunction implements BstFunction { + @Override + public void execute(BstVMVisitor visitor, ParserRuleContext ctx) { + throw new BstVMException("Must have an entry to cite$ (line %d)".formatted(ctx.start.getLine())); + } + + @Override + public void execute(BstVMVisitor visitor, ParserRuleContext ctx, BstEntry bstEntryContext) { + if (bstEntryContext == null) { + execute(visitor, ctx); + return; + } + + stack.push(bstEntryContext.entry.getCitationKey().orElse(null)); + } + } + + /** + * Pops the top literal from the stack and pushes two copies of it. + */ + private void bstDuplicate(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.isEmpty()) { + throw new BstVMException("Not enough operands on stack for operation duplicate$ (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + + stack.push(o1); + stack.push(o1); + } + + /** + * Pops the top literal and pushes the integer 1 if it's a missing + * field or a string having no non-white-space characters, 0 + * otherwise. + */ + private void bstEmpty(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.isEmpty()) { + throw new BstVMException("Not enough operands on stack for operation empty$ (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + + if (o1 == null) { + stack.push(BstVM.TRUE); + return; + } + + if (!(o1 instanceof String s)) { + throw new BstVMException("Operand does not match function empty$ (line %d)".formatted(ctx.start.getLine())); + } + + stack.push("".equals(s.trim()) ? BstVM.TRUE : BstVM.FALSE); + } + + /** + * The |built_in| function {\.{format.name\$}} pops the + * top three literals (they are a string, an integer, and a string + * literal, in that order). The last string literal represents a + * name list (each name corresponding to a person), the integer + * literal specifies which name to pick from this list, and the + * first string literal specifies how to format this name, as + * described in the \BibTeX\ documentation. Finally, this function + * pushes the formatted name. If any of the types is incorrect, it + * complains and pushes the null string. + */ + private void bstFormatName(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.size() < 3) { + throw new BstVMException("Not enough operands on stack for operation format.name$ (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + Object o2 = stack.pop(); + Object o3 = stack.pop(); + + if (!(o1 instanceof String) && !(o2 instanceof Integer) && !(o3 instanceof String)) { + // warning("A string is needed for change.case$"); + stack.push(""); + return; + } + + String format = (String) o1; + Integer name = (Integer) o2; + String names = (String) o3; + + if (names == null) { + stack.push(""); + } else { + AuthorList a = AuthorList.parse(names); + if (name > a.getNumberOfAuthors()) { + throw new BstVMException("Author Out of Bounds. Number %d invalid for %s (line %d)".formatted(name, names, ctx.start.getLine())); + } + Author author = a.getAuthor(name - 1); + + stack.push(BstNameFormatter.formatName(author, format)); + } + } + + /** + * Pops the top three literals (they are two function literals and + * an integer literal, in that order); if the integer is greater + * than 0, it executes the second literal, else it executes the + * first. + */ + private void bstIf(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.size() < 3) { + throw new BstVMException("Not enough operands on stack for if$ (line %d)".formatted(ctx.start.getLine())); + } + + Object f1 = stack.pop(); + Object f2 = stack.pop(); + Object i = stack.pop(); + + if (!((f1 instanceof BstVMVisitor.Identifier) || (f1 instanceof ParseTree)) + && ((f2 instanceof BstVMVisitor.Identifier) || (f2 instanceof ParseTree)) + && (i instanceof Integer)) { + throw new BstVMException("Expecting two functions and an integer for if$ (line %d)".formatted(ctx.start.getLine())); + } + + if (((Integer) i) > 0) { + callIdentifierOrTree(f2, visitor, ctx); + } else { + callIdentifierOrTree(f1, visitor, ctx); + } + } + + private void callIdentifierOrTree(Object f, BstVMVisitor visitor, ParserRuleContext ctx) { + if (f instanceof ParseTree tree) { + visitor.visit(tree); + } else if (f instanceof BstVMVisitor.Identifier identifier) { + visitor.resolveIdentifier(identifier.name(), ctx); + } else { + stack.push(f); + } + } + + /** + * Pops the top (integer) literal, interpreted as the ASCII integer + * value of a single character, converts it to the corresponding + * single-character string, and pushes this string. + */ + private void bstIntToChr(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.isEmpty()) { + throw new BstVMException("Not enough operands on stack for operation int.to.chr$ (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + + if (!(o1 instanceof Integer i)) { + throw new BstVMException("Can only perform operation int.to.chr$ on an Integer (line %d)".formatted(ctx.start.getLine())); + } + + stack.push(String.valueOf((char) i.intValue())); + } + + /** + * Pops the top (integer) literal, converts it to its (unique) + * string equivalent, and pushes this string. + */ + private void bstIntToStr(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.isEmpty()) { + throw new BstVMException("Not enough operands on stack for operation int.to.str$ (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + + if (!(o1 instanceof Integer)) { + throw new BstVMException("Can only transform an integer to an string using int.to.str$ (line %d)".formatted(ctx.start.getLine())); + } + + stack.push(o1.toString()); + } + + /** + * Pops the top literal and pushes the integer 1 if it's a missing + * field, 0 otherwise. + */ + private void bstMissing(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.isEmpty()) { + throw new BstVMException("Not enough operands on stack for operation missing$ (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + + if (o1 == null) { + stack.push(BstVM.TRUE); + return; + } + + if (!(o1 instanceof String)) { + LOGGER.warn("Not a string or missing field in operation missing$ (line %d)".formatted(ctx.start.getLine())); + stack.push(BstVM.TRUE); + return; + } + + stack.push(BstVM.FALSE); + } + + /** + * Writes onto the bbl file what is accumulated in the output buffer. + * It writes a blank line if and only if the output buffer is empty. + * Since write$ does reasonable line breaking, you should use this + * function only when you want a blank line or an explicit line + * break. + */ + private void bstNewLine(BstVMVisitor visitor, ParserRuleContext ctx) { + this.bbl.append('\n'); + } + + /** + * Pops the top (string) literal and pushes the number of names the + * string represents one plus the number of occurrences of the + * substring "and" (ignoring case differences) surrounded by + * non-null white-space at the top brace level. + */ + private void bstNumNames(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.isEmpty()) { + throw new BstVMException("Not enough operands on stack for operation num.names$ (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + + if (!(o1 instanceof String s)) { + throw new BstVMException("Need a string at the top of the stack for num.names$ (line %d)".formatted(ctx.start.getLine())); + } + + stack.push(AuthorList.parse(s).getNumberOfAuthors()); + } + + /** + * Pops the top of the stack but doesn't print it; this gets rid of + * an unwanted stack literal. + */ + private void bstPop(BstVMVisitor visitor, ParserRuleContext ctx) { + stack.pop(); + } + + /** + * The |built_in| function {\.{preamble\$}} pushes onto the stack + * the concatenation of all the \.{preamble} strings read from the + * database files. (or the empty string if there were none) + * '@PREAMBLE' strings are read from the database files. + */ + private void bstPreamble(BstVMVisitor visitor, ParserRuleContext ctx) { + stack.push(preamble); + } + + /** + * Pops the top (string) literal, removes nonalphanumeric characters + * except for white-space characters and hyphens and ties (these all get + * converted to a space), removes certain alphabetic characters + * contained in the control sequences associated with a \special + * character", and pushes the resulting string. + */ + private void bstPurify(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.isEmpty()) { + throw new BstVMException("Not enough operands on stack for operation purify$ (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + + if (!(o1 instanceof String)) { + LOGGER.warn("A string is needed for purify$"); + stack.push(""); + return; + } + + stack.push(BstPurifier.purify((String) o1)); + } + + /** + * Pushes the string consisting of the double-quote character. + */ + private void bstQuote(BstVMVisitor visitor, ParserRuleContext ctx) { + stack.push("\""); + } + + /** + * Does nothing. + */ + private void bstSkip(BstVMVisitor visitor, ParserRuleContext ctx) { + // no-op + } + + /** + * Pops and prints the whole stack; it's meant to be used for style + * designers while debugging. + */ + private void bstStack(BstVMVisitor visitor, ParserRuleContext ctx) { + while (!stack.empty()) { + LOGGER.debug("Stack entry {}", stack.pop()); + } + } + + /** + * Pops the top three literals (they are the two integers literals + * len and start, and a string literal, in that order). It pushes + * the substring of the (at most) len consecutive characters + * starting at the startth character (assuming 1-based indexing) if + * start is positive, and ending at the start-th character + * (including) from the end if start is negative (where the first + * character from the end is the last character). + */ + private void bstSubstring(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.size() < 3) { + throw new BstVMException("Not enough operands on stack for operation substring$ (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + Object o2 = stack.pop(); + Object o3 = stack.pop(); + + if (!((o1 instanceof Integer len) && (o2 instanceof Integer start) && (o3 instanceof String s))) { + throw new BstVMException("Expecting two integers and a string for substring$ (line %d)".formatted(ctx.start.getLine())); + } + + int lenI = len; + int startI = start; + + if (lenI > (Integer.MAX_VALUE / 2)) { + lenI = Integer.MAX_VALUE / 2; + } + + if (startI > (Integer.MAX_VALUE / 2)) { + startI = Integer.MAX_VALUE / 2; + } + + if (startI < (Integer.MIN_VALUE / 2)) { + startI = -Integer.MIN_VALUE / 2; + } + + if (startI < 0) { + startI += s.length() + 1; + startI = Math.max(1, (startI + 1) - lenI); + } + stack.push(s.substring(startI - 1, Math.min((startI - 1) + lenI, s.length()))); + } + + /** + * Swaps the top two literals on the stack. text.length$ Pops the + * top (string) literal, and pushes the number of text characters + * it contains, where an accented character (more precisely, a + * \special character", defined in Section 4) counts as a single + * text character, even if it's missing its matching right brace, + * and where braces don't count as text characters. + */ + private void bstSwap(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.size() < 2) { + throw new BstVMException("Not enough operands on stack for operation swap$ (line %d)".formatted(ctx.start.getLine())); + } + Object f1 = stack.pop(); + Object f2 = stack.pop(); + + stack.push(f1); + stack.push(f2); + } + + /** + * text.length$ Pops the top (string) literal, and pushes the number + * of text characters it contains, where an accented character (more + * precisely, a "special character", defined in Section 4) counts as + * a single text character, even if it's missing its matching right + * brace, and where braces don't count as text characters. + * + * From BibTeXing: For the purposes of counting letters in labels, + * BibTEX considers everything contained inside the braces as a + * single letter. + */ + private void bstTextLength(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.isEmpty()) { + throw new BstVMException("Not enough operands on stack for operation text.length$ (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + + if (!(o1 instanceof String s)) { + throw new BstVMException("Can only perform operation on a string text.length$ (line %d)".formatted(ctx.start.getLine())); + } + + char[] c = s.toCharArray(); + int result = 0; + int i = 0; + int n = s.length(); + int braceLevel = 0; + + while (i < n) { + i++; + if (c[i - 1] == '{') { + braceLevel++; + if ((braceLevel == 1) && (i < n)) { + if (c[i] == '\\') { + i++; // skip over backslash + while ((i < n) && (braceLevel > 0)) { + if (c[i] == '}') { + braceLevel--; + } else if (c[i] == '{') { + braceLevel++; + } + i++; + } + result++; + } + } + } else if (c[i - 1] == '}') { + if (braceLevel > 0) { + braceLevel--; + } + } else { + result++; + } + } + stack.push(result); + } + + /** + * Pops the top two literals (the integer literal len and a string + * literal, in that order). It pushes the substring of the (at most) len + * consecutive text characters starting from the beginning of the + * string. This function is similar to substring$, but this one + * considers a \special character", even if it's missing its matching + * right brace, to be a single text character (rather than however many + * ASCII characters it actually comprises), and this function doesn't + * consider braces to be text characters; furthermore, this function + * appends any needed matching right braces. + */ + private void bstTextPrefix(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.size() < 2) { + throw new BstVMException("Not enough operands on stack for operation text.prefix$ (line %d)".formatted(ctx.start.getLine())); + } + + Object o1 = stack.pop(); + if (!(o1 instanceof Integer)) { + LOGGER.warn("An integer is needed as first parameter to text.prefix$ (line {})", ctx.start.getLine()); + stack.push(""); + return; + } + + Object o2 = stack.pop(); + if (!(o2 instanceof String)) { + LOGGER.warn("A string is needed as second parameter to text.prefix$ (line {})", ctx.start.getLine()); + stack.push(""); + return; + } + + stack.push(BstTextPrefixer.textPrefix((Integer) o1, (String) o2)); + } + + /** + * Pops and prints the top of the stack to the log file. It's useful for debugging. + */ + private void bstTop(BstVMVisitor visitor, ParserRuleContext ctx) { + LOGGER.debug("Stack entry {} (line {})", stack.pop(), ctx.start.getLine()); + } + + /** + * Pushes the current entry's type (book, article, etc.), but pushes + * the null string if the type is either unknown or undefined. + */ + public class BstTypeFunction implements BstFunction { + @Override + public void execute(BstVMVisitor visitor, ParserRuleContext ctx) { + throw new BstVMException("type$ need a context (line %d)".formatted(ctx.start.getLine())); + } + + @Override + public void execute(BstVMVisitor visitor, ParserRuleContext ctx, BstEntry bstEntryContext) { + if (bstEntryContext == null) { + this.execute(visitor, ctx); + return; + } + + stack.push(bstEntryContext.entry.getType().getName()); + } + } + + /** + * Pops the top (string) literal and prints it following a warning + * message. This also increments a count of the number of warning + * messages issued. + */ + private void bstWarning(BstVMVisitor visitor, ParserRuleContext ctx) { + LOGGER.warn("Warning (#{}): {}", bstWarning++, stack.pop()); + } + + /** + * Pops the top two (function) literals, and keeps executing the + * second as long as the (integer) literal left on the stack by + * executing the first is greater than 0. + */ + private void bstWhile(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.size() < 2) { + throw new BstVMException("Not enough operands on stack for operation while$ (line %d)".formatted(ctx.start.getLine())); + } + Object f2 = stack.pop(); + Object f1 = stack.pop(); + + if (!((f1 instanceof BstVMVisitor.Identifier) || (f1 instanceof ParseTree)) + && ((f2 instanceof BstVMVisitor.Identifier) || (f2 instanceof ParseTree))) { + throw new BstVMException("Expecting two functions for while$ (line %d)".formatted(ctx.start.getLine())); + } + + do { + visitor.visit((ParseTree) f1); + + Object i = stack.pop(); + if (!(i instanceof Integer)) { + throw new BstVMException("First parameter to while has to return an integer but was %s (line %d)" + .formatted(i.toString(), ctx.start.getLine())); + } + if ((Integer) i <= 0) { + break; + } + visitor.visit((ParseTree) f2); + } while (true); + } + + /** + * The |built_in| function {\.{width\$}} pops the top (string) literal and + * pushes the integer that represents its width in units specified by the + * |char_width| array. This function takes the literal literally; that is, it + * assumes each character in the string is to be printed as is, regardless of + * whether the character has a special meaning to \TeX, except that special + * characters (even without their |right_brace|s) are handled specially. If the + * literal isn't a string, it complains and pushes~0. + */ + private void bstWidth(BstVMVisitor visitor, ParserRuleContext ctx) { + if (stack.isEmpty()) { + throw new BstVMException("Not enough operands on stack for operation width$ (line %d)".formatted(ctx.start.getLine())); + } + Object o1 = stack.pop(); + + if (!(o1 instanceof String)) { + LOGGER.warn("A string is needed for width$"); + stack.push(0); + return; + } + + stack.push(BstWidthCalculator.width((String) o1)); + } + + /** + * Pops the top (string) literal and writes it on the output buffer + * (which will result in stuff being written onto the bbl file when + * the buffer fills up). + */ + private void bstWrite(BstVMVisitor visitor, ParserRuleContext ctx) { + String s = (String) stack.pop(); + bbl.append(s); + } +} diff --git a/src/main/java/org/jabref/logic/bst/BstPreviewLayout.java b/src/main/java/org/jabref/logic/bst/BstPreviewLayout.java index d0c040842fd..a002d388e60 100644 --- a/src/main/java/org/jabref/logic/bst/BstPreviewLayout.java +++ b/src/main/java/org/jabref/logic/bst/BstPreviewLayout.java @@ -23,7 +23,7 @@ public class BstPreviewLayout implements PreviewLayout { private final String name; - private VM vm; + private BstVM bstVM; private String error; public BstPreviewLayout(Path path) { @@ -34,7 +34,7 @@ public BstPreviewLayout(Path path) { return; } try { - vm = new VM(path.toFile()); + bstVM = new BstVM(path); } catch (Exception e) { LOGGER.error("Could not read {}.", path.toAbsolutePath(), e); error = Localization.lang("Error opening file '%0'.", path.toString()); @@ -49,7 +49,7 @@ public String generatePreview(BibEntry originalEntry, BibDatabaseContext databas // ensure that the entry is of BibTeX format (and do not modify the original entry) BibEntry entry = (BibEntry) originalEntry.clone(); new ConvertToBibtexCleanup().cleanup(entry); - String result = vm.run(List.of(entry)); + String result = bstVM.render(List.of(entry)); // Remove all comments result = result.replaceAll("%.*", ""); // Remove all LaTeX comments diff --git a/src/main/java/org/jabref/logic/bst/BstVM.java b/src/main/java/org/jabref/logic/bst/BstVM.java new file mode 100644 index 00000000000..a2428d683d9 --- /dev/null +++ b/src/main/java/org/jabref/logic/bst/BstVM.java @@ -0,0 +1,114 @@ +package org.jabref.logic.bst; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Stack; + +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; + +import org.antlr.v4.runtime.BailErrorStrategy; +import org.antlr.v4.runtime.BaseErrorListener; +import org.antlr.v4.runtime.CharStream; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Recognizer; +import org.antlr.v4.runtime.misc.ParseCancellationException; +import org.antlr.v4.runtime.tree.ParseTree; + +public class BstVM { + + protected static final Integer FALSE = 0; + protected static final Integer TRUE = 1; + + protected final ParseTree tree; + protected BstVMContext latestContext; // for testing + + private Path path = null; + + public BstVM(Path path) throws RecognitionException, IOException { + this(CharStreams.fromPath(path)); + this.path = path; + } + + public BstVM(String s) throws RecognitionException { + this(CharStreams.fromString(s)); + } + + protected BstVM(CharStream bst) throws RecognitionException { + this(charStream2CommonTree(bst)); + } + + private BstVM(ParseTree tree) { + this.tree = tree; + } + + private static ParseTree charStream2CommonTree(CharStream query) { + BstLexer lexer = new BstLexer(query); + lexer.removeErrorListeners(); + lexer.addErrorListener(ThrowingErrorListener.INSTANCE); + BstParser parser = new BstParser(new CommonTokenStream(lexer)); + parser.removeErrorListeners(); + parser.addErrorListener(ThrowingErrorListener.INSTANCE); + parser.setErrorHandler(new BailErrorStrategy()); + return parser.bstFile(); + } + + /** + * Transforms the given list of BibEntries to a rendered list of references using the parsed bst program + * + * @param bibEntries list of entries to convert + * @param bibDatabase (may be null) the bibDatabase used for resolving strings / crossref + * @return list of references in plain text form + */ + public String render(Collection bibEntries, BibDatabase bibDatabase) { + Objects.requireNonNull(bibEntries); + + List entries = new ArrayList<>(bibEntries.size()); + for (BibEntry entry : bibEntries) { + entries.add(new BstEntry(entry)); + } + + StringBuilder resultBuffer = new StringBuilder(); + + BstVMContext bstVMContext = new BstVMContext(entries, bibDatabase, path); + bstVMContext.functions().putAll(new BstFunctions(bstVMContext, resultBuffer).getBuiltInFunctions()); + bstVMContext.integers().put("entry.max$", Integer.MAX_VALUE); + bstVMContext.integers().put("global.max$", Integer.MAX_VALUE); + + BstVMVisitor bstVMVisitor = new BstVMVisitor(bstVMContext, resultBuffer); + bstVMVisitor.visit(tree); + + latestContext = bstVMContext; + + return resultBuffer.toString(); + } + + public String render(Collection bibEntries) { + return render(bibEntries, null); + } + + protected Stack getStack() { + if (latestContext != null) { + return latestContext.stack(); + } else { + throw new BstVMException("BstVM must have rendered at least once to provide the latest stack"); + } + } + + private static class ThrowingErrorListener extends BaseErrorListener { + public static final ThrowingErrorListener INSTANCE = new ThrowingErrorListener(); + + @Override + public void syntaxError(Recognizer, ?> recognizer, Object offendingSymbol, + int line, int charPositionInLine, String msg, RecognitionException e) + throws ParseCancellationException { + throw new ParseCancellationException("line " + line + ":" + charPositionInLine + " " + msg); + } + } +} diff --git a/src/main/java/org/jabref/logic/bst/BstVMContext.java b/src/main/java/org/jabref/logic/bst/BstVMContext.java new file mode 100644 index 00000000000..8a98f605dd8 --- /dev/null +++ b/src/main/java/org/jabref/logic/bst/BstVMContext.java @@ -0,0 +1,22 @@ +package org.jabref.logic.bst; + +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Stack; + +import org.jabref.model.database.BibDatabase; + +public record BstVMContext(List entries, + Map strings, + Map integers, + Map functions, + Stack stack, + BibDatabase bibDatabase, + Optional path) { + public BstVMContext(List entries, BibDatabase bibDatabase, Path path) { + this(entries, new HashMap<>(), new HashMap<>(), new HashMap<>(), new Stack<>(), bibDatabase, Optional.ofNullable(path)); + } +} diff --git a/src/main/java/org/jabref/logic/bst/BstVMException.java b/src/main/java/org/jabref/logic/bst/BstVMException.java new file mode 100644 index 00000000000..2851166d094 --- /dev/null +++ b/src/main/java/org/jabref/logic/bst/BstVMException.java @@ -0,0 +1,7 @@ +package org.jabref.logic.bst; + +public class BstVMException extends RuntimeException { + public BstVMException(String string) { + super(string); + } +} diff --git a/src/main/java/org/jabref/logic/bst/BstVMVisitor.java b/src/main/java/org/jabref/logic/bst/BstVMVisitor.java new file mode 100644 index 00000000000..a815480e506 --- /dev/null +++ b/src/main/java/org/jabref/logic/bst/BstVMVisitor.java @@ -0,0 +1,263 @@ +package org.jabref.logic.bst; + +import java.util.Comparator; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; + +import org.jabref.logic.bibtex.FieldContentFormatterPreferences; +import org.jabref.logic.bibtex.FieldWriter; +import org.jabref.logic.bibtex.FieldWriterPreferences; +import org.jabref.logic.bibtex.InvalidFieldValueException; +import org.jabref.model.entry.Month; +import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.FieldFactory; +import org.jabref.model.entry.field.StandardField; + +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.TerminalNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class BstVMVisitor extends BstBaseVisitor { + private static final Logger LOGGER = LoggerFactory.getLogger(BstVMVisitor.class); + + private final BstVMContext bstVMContext; + private final StringBuilder bbl; + + private BstEntry selectedBstEntry = null; + + public record Identifier(String name) { + } + + public BstVMVisitor(BstVMContext bstVMContext, StringBuilder bbl) { + this.bstVMContext = bstVMContext; + this.bbl = bbl; + } + + @Override + public Integer visitStringsCommand(BstParser.StringsCommandContext ctx) { + if (ctx.ids.identifier().size() > 20) { + throw new BstVMException("Strings limit reached"); + } + + for (BstParser.IdentifierContext identifierContext : ctx.ids.identifier()) { + bstVMContext.strings().put(identifierContext.getText(), null); + } + return BstVM.TRUE; + } + + @Override + public Integer visitIntegersCommand(BstParser.IntegersCommandContext ctx) { + for (BstParser.IdentifierContext identifierContext : ctx.ids.identifier()) { + bstVMContext.integers().put(identifierContext.getText(), 0); + } + return BstVM.TRUE; + } + + @Override + public Integer visitFunctionCommand(BstParser.FunctionCommandContext ctx) { + bstVMContext.functions().put(ctx.id.getText(), + (visitor, functionContext) -> visitor.visit(ctx.function)); + return BstVM.TRUE; + } + + @Override + public Integer visitMacroCommand(BstParser.MacroCommandContext ctx) { + String replacement = ctx.repl.getText().substring(1, ctx.repl.getText().length() - 1); + bstVMContext.functions().put(ctx.id.getText(), + (visitor, functionContext) -> bstVMContext.stack().push(replacement)); + return BstVM.TRUE; + } + + @Override + public Integer visitReadCommand(BstParser.ReadCommandContext ctx) { + FieldWriter fieldWriter = new FieldWriter(new FieldWriterPreferences(true, List.of(StandardField.MONTH), new FieldContentFormatterPreferences())); + for (BstEntry e : bstVMContext.entries()) { + for (Map.Entry mEntry : e.fields.entrySet()) { + Field field = FieldFactory.parseField(mEntry.getKey()); + String fieldValue = e.entry.getResolvedFieldOrAlias(field, bstVMContext.bibDatabase()) + .map(content -> { + try { + String result = fieldWriter.write(field, content); + if (result.startsWith("{")) { + // Strip enclosing {} from the output + return result.substring(1, result.length() - 1); + } + if (field == StandardField.MONTH) { + // We don't have the internal BibTeX strings at hand. + // Thus, we look up the full month name in the generic table. + return Month.parse(result) + .map(Month::getFullName) + .orElse(result); + } + return result; + } catch ( + InvalidFieldValueException invalidFieldValueException) { + // in case there is something wrong with the content, just return the content itself + return content; + } + }) + .orElse(null); + mEntry.setValue(fieldValue); + } + } + + for (BstEntry e : bstVMContext.entries()) { + if (!e.fields.containsKey(StandardField.CROSSREF.getName())) { + e.fields.put(StandardField.CROSSREF.getName(), null); + } + } + + return BstVM.TRUE; + } + + @Override + public Integer visitExecuteCommand(BstParser.ExecuteCommandContext ctx) { + this.selectedBstEntry = null; + visit(ctx.bstFunction()); + + return BstVM.TRUE; + } + + @Override + public Integer visitIterateCommand(BstParser.IterateCommandContext ctx) { + for (BstEntry entry : bstVMContext.entries()) { + this.selectedBstEntry = entry; + visit(ctx.bstFunction()); + } + + return BstVM.TRUE; + } + + @Override + public Integer visitReverseCommand(BstParser.ReverseCommandContext ctx) { + ListIterator i = bstVMContext.entries().listIterator(bstVMContext.entries().size()); + while (i.hasPrevious()) { + this.selectedBstEntry = i.previous(); + visit(ctx.bstFunction()); + } + + return BstVM.TRUE; + } + + @Override + public Integer visitEntryCommand(BstParser.EntryCommandContext ctx) { + // ENTRY command contains 3 optionally filled identifier lists: + // Fields, Integers and Strings + + BstParser.IdListOptContext entryFields = ctx.idListOpt(0); + for (BstParser.IdentifierContext identifierContext : entryFields.identifier()) { + for (BstEntry entry : bstVMContext.entries()) { + entry.fields.put(identifierContext.getText(), null); + } + } + + BstParser.IdListOptContext entryIntegers = ctx.idListOpt(1); + for (BstParser.IdentifierContext identifierContext : entryIntegers.identifier()) { + for (BstEntry entry : bstVMContext.entries()) { + entry.localIntegers.put(identifierContext.getText(), 0); + } + } + + BstParser.IdListOptContext entryStrings = ctx.idListOpt(2); + for (BstParser.IdentifierContext identifierContext : entryStrings.identifier()) { + for (BstEntry entry : bstVMContext.entries()) { + entry.localStrings.put(identifierContext.getText(), null); + } + } + + for (BstEntry entry : bstVMContext.entries()) { + entry.localStrings.put("sort.key$", null); + } + + return BstVM.TRUE; + } + + @Override + public Integer visitSortCommand(BstParser.SortCommandContext ctx) { + bstVMContext.entries().sort(Comparator.comparing(o -> (o.localStrings.get("sort.key$")))); + return BstVM.TRUE; + } + + @Override + public Integer visitIdentifier(BstParser.IdentifierContext ctx) { + resolveIdentifier(ctx.IDENTIFIER().getText(), ctx); + return BstVM.TRUE; + } + + protected void resolveIdentifier(String name, ParserRuleContext ctx) { + if (selectedBstEntry != null) { + if (selectedBstEntry.fields.containsKey(name)) { + bstVMContext.stack().push(selectedBstEntry.fields.get(name)); + return; + } + if (selectedBstEntry.localStrings.containsKey(name)) { + bstVMContext.stack().push(selectedBstEntry.localStrings.get(name)); + return; + } + if (selectedBstEntry.localIntegers.containsKey(name)) { + bstVMContext.stack().push(selectedBstEntry.localIntegers.get(name)); + return; + } + } + + if (bstVMContext.strings().containsKey(name)) { + bstVMContext.stack().push(bstVMContext.strings().get(name)); + return; + } + if (bstVMContext.integers().containsKey(name)) { + bstVMContext.stack().push(bstVMContext.integers().get(name)); + return; + } + if (bstVMContext.functions().containsKey(name)) { + bstVMContext.functions().get(name).execute(this, ctx); + return; + } + + throw new BstVMException("No matching identifier found: " + name); + } + + @Override + public Integer visitBstFunction(BstParser.BstFunctionContext ctx) { + String name = ctx.getChild(0).getText(); + if (bstVMContext.functions().containsKey(name)) { + bstVMContext.functions().get(name).execute(this, ctx, selectedBstEntry); + } else { + visit(ctx.getChild(0)); + } + + return BstVM.TRUE; + } + + @Override + public Integer visitStackitem(BstParser.StackitemContext ctx) { + for (ParseTree childNode : ctx.children) { + try { + if (childNode instanceof TerminalNode token) { + switch (token.getSymbol().getType()) { + case BstParser.STRING -> { + String s = token.getText(); + bstVMContext.stack().push(s.substring(1, s.length() - 1)); + } + case BstParser.INTEGER -> + bstVMContext.stack().push(Integer.parseInt(token.getText().substring(1))); + case BstParser.QUOTED -> + bstVMContext.stack().push(new Identifier(token.getText().substring(1))); + } + } else if (childNode instanceof BstParser.StackContext) { + bstVMContext.stack().push(childNode); + } else { + this.visit(childNode); + } + } catch (BstVMException e) { + bstVMContext.path().ifPresentOrElse( + (path) -> LOGGER.error("{} ({})", e.getMessage(), path), + () -> LOGGER.error(e.getMessage())); + throw e; + } + } + return BstVM.TRUE; + } +} diff --git a/src/main/java/org/jabref/logic/bst/ChangeCaseFunction.java b/src/main/java/org/jabref/logic/bst/ChangeCaseFunction.java deleted file mode 100644 index 65f4f64c619..00000000000 --- a/src/main/java/org/jabref/logic/bst/ChangeCaseFunction.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.jabref.logic.bst; - -import java.util.Locale; -import java.util.Stack; - -import org.jabref.logic.bst.BibtexCaseChanger.FORMAT_MODE; -import org.jabref.logic.bst.VM.BstEntry; -import org.jabref.logic.bst.VM.BstFunction; - -/** - * From the Bibtex manual: - * - * Pops the top two (string) literals; it changes the case of the second - * according to the specifications of the first, as follows. (Note: The word - * `letters' in the next sentence refers only to those at brace-level 0, the - * top-most brace level; no other characters are changed, except perhaps for - * \special characters", described in Section 4.) If the first literal is the - * string `t', it converts to lower case all letters except the very first - * character in the string, which it leaves alone, and except the first - * character following any colon and then nonnull white space, which it also - * leaves alone; if it's the string `l', it converts all letters to lower case; - * and if it's the string `u', it converts all letters to upper case. It then - * pushes this resulting string. If either type is incorrect, it complains and - * pushes the null string; however, if both types are correct but the - * specification string (i.e., the first string) isn't one of the legal ones, it - * merely pushes the second back onto the stack, after complaining. (Another - * note: It ignores case differences in the specification string; for example, - * the strings t and T are equivalent for the purposes of this built-in - * function.) - * - * Christopher: I think this should be another grammar! This parser is horrible. - * - */ -public class ChangeCaseFunction implements BstFunction { - - private final VM vm; - - public ChangeCaseFunction(VM vm) { - this.vm = vm; - } - - @Override - public void execute(BstEntry context) { - Stack stack = vm.getStack(); - - if (stack.size() < 2) { - throw new VMException("Not enough operands on stack for operation change.case$"); - } - - Object o1 = stack.pop(); - if (!((o1 instanceof String) && (((String) o1).length() == 1))) { - throw new VMException("A format string of length 1 is needed for change.case$"); - } - - Object o2 = stack.pop(); - if (!(o2 instanceof String)) { - throw new VMException("A string is needed as second parameter for change.case$"); - } - - char format = ((String) o1).toLowerCase(Locale.ROOT).charAt(0); - String s = (String) o2; - - stack.push(BibtexCaseChanger.changeCase(s, FORMAT_MODE.getFormatModeForBSTFormat(format))); - } -} diff --git a/src/main/java/org/jabref/logic/bst/FormatNameFunction.java b/src/main/java/org/jabref/logic/bst/FormatNameFunction.java deleted file mode 100644 index b2845c89be7..00000000000 --- a/src/main/java/org/jabref/logic/bst/FormatNameFunction.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.jabref.logic.bst; - -import java.util.Stack; - -import org.jabref.logic.bst.VM.BstEntry; -import org.jabref.logic.bst.VM.BstFunction; -import org.jabref.model.entry.Author; -import org.jabref.model.entry.AuthorList; - -/** - * From Bibtex: - * - * "The |built_in| function {\.{format.name\$}} pops the - * top three literals (they are a string, an integer, and a string - * literal, in that order). The last string literal represents a - * name list (each name corresponding to a person), the integer - * literal specifies which name to pick from this list, and the - * first string literal specifies how to format this name, as - * described in the \BibTeX\ documentation. Finally, this function - * pushes the formatted name. If any of the types is incorrect, it - * complains and pushes the null string." - * - * All the pain is encapsulated in BibtexNameFormatter. :-) - * - */ -public class FormatNameFunction implements BstFunction { - - private final VM vm; - - public FormatNameFunction(VM vm) { - this.vm = vm; - } - - @Override - public void execute(BstEntry context) { - Stack stack = vm.getStack(); - - if (stack.size() < 3) { - throw new VMException("Not enough operands on stack for operation format.name$"); - } - Object o1 = stack.pop(); - Object o2 = stack.pop(); - Object o3 = stack.pop(); - - if (!(o1 instanceof String) && !(o2 instanceof Integer) && !(o3 instanceof String)) { - // warning("A string is needed for change.case$"); - stack.push(""); - return; - } - - String format = (String) o1; - Integer name = (Integer) o2; - String names = (String) o3; - - if (names == null) { - stack.push(""); - } else { - AuthorList a = AuthorList.parse(names); - if (name > a.getNumberOfAuthors()) { - throw new VMException("Author Out of Bounds. Number " + name + " invalid for " + names); - } - Author author = a.getAuthor(name - 1); - - stack.push(BibtexNameFormatter.formatName(author, format, vm)); - } - } -} diff --git a/src/main/java/org/jabref/logic/bst/PurifyFunction.java b/src/main/java/org/jabref/logic/bst/PurifyFunction.java deleted file mode 100644 index b0df9df3735..00000000000 --- a/src/main/java/org/jabref/logic/bst/PurifyFunction.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.jabref.logic.bst; - -import java.util.Stack; - -import org.jabref.logic.bst.VM.BstEntry; -import org.jabref.logic.bst.VM.BstFunction; - -/** - * - * The |built_in| function {\.{purify\$}} pops the top (string) literal, removes - * nonalphanumeric characters except for |white_space| and |sep_char| characters - * (these get converted to a |space|) and removes certain alphabetic characters - * contained in the control sequences associated with a special character, and - * pushes the resulting string. If the literal isn't a string, it complains and - * pushes the null string. - * - */ -public class PurifyFunction implements BstFunction { - - private final VM vm; - - public PurifyFunction(VM vm) { - this.vm = vm; - } - - @Override - public void execute(BstEntry context) { - Stack stack = vm.getStack(); - - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation purify$"); - } - Object o1 = stack.pop(); - - if (!(o1 instanceof String)) { - vm.warn("A string is needed for purify$"); - stack.push(""); - return; - } - - stack.push(BibtexPurify.purify((String) o1, vm)); - } -} diff --git a/src/main/java/org/jabref/logic/bst/TextPrefixFunction.java b/src/main/java/org/jabref/logic/bst/TextPrefixFunction.java deleted file mode 100644 index 415ee97498b..00000000000 --- a/src/main/java/org/jabref/logic/bst/TextPrefixFunction.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.jabref.logic.bst; - -import java.util.Stack; - -import org.jabref.logic.bst.VM.BstEntry; -import org.jabref.logic.bst.VM.BstFunction; - -/** - * The |built_in| function {\.{text.prefix\$}} pops the top two literals - * (the integer literal |pop_lit1| and a string literal, in that order). - * It pushes the substring of the (at most) |pop_lit1| consecutive text - * characters starting from the beginning of the string. This function - * is similar to {\.{substring\$}}, but this one considers an accented - * character (or more precisely, a ``special character''$\!$, even if - * it's missing its matching |right_brace|) to be a single text character - * (rather than however many |ASCII_code| characters it actually - * comprises), and this function doesn't consider braces to be text - * characters; furthermore, this function appends any needed matching - * |right_brace|s. If any of the types is incorrect, it complains and - * pushes the null string. - */ -public class TextPrefixFunction implements BstFunction { - - private final VM vm; - - public TextPrefixFunction(VM vm) { - this.vm = vm; - } - - @Override - public void execute(BstEntry context) { - Stack stack = vm.getStack(); - - if (stack.size() < 2) { - throw new VMException("Not enough operands on stack for operation text.prefix$"); - } - - Object o1 = stack.pop(); - if (!(o1 instanceof Integer)) { - vm.warn("An integer is needed as first parameter to text.prefix$"); - stack.push(""); - return; - } - - Object o2 = stack.pop(); - if (!(o2 instanceof String)) { - vm.warn("A string is needed as second parameter to text.prefix$"); - stack.push(""); - return; - } - - stack.push(BibtexTextPrefix.textPrefix((Integer) o1, (String) o2, vm)); - } -} diff --git a/src/main/java/org/jabref/logic/bst/VM.java b/src/main/java/org/jabref/logic/bst/VM.java deleted file mode 100644 index caaf9078f64..00000000000 --- a/src/main/java/org/jabref/logic/bst/VM.java +++ /dev/null @@ -1,1245 +0,0 @@ -package org.jabref.logic.bst; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.ListIterator; -import java.util.Map; -import java.util.Objects; -import java.util.Stack; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.jabref.logic.bibtex.FieldContentFormatterPreferences; -import org.jabref.logic.bibtex.FieldWriter; -import org.jabref.logic.bibtex.FieldWriterPreferences; -import org.jabref.logic.bibtex.InvalidFieldValueException; -import org.jabref.model.database.BibDatabase; -import org.jabref.model.entry.AuthorList; -import org.jabref.model.entry.BibEntry; -import org.jabref.model.entry.Month; -import org.jabref.model.entry.field.Field; -import org.jabref.model.entry.field.FieldFactory; -import org.jabref.model.entry.field.StandardField; - -import org.antlr.runtime.ANTLRFileStream; -import org.antlr.runtime.ANTLRStringStream; -import org.antlr.runtime.CharStream; -import org.antlr.runtime.CommonTokenStream; -import org.antlr.runtime.RecognitionException; -import org.antlr.runtime.tree.CommonTree; -import org.antlr.runtime.tree.Tree; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A BibTeX Virtual machine that can execute .bst files. - * - * Documentation can be found in the original bibtex distribution: - * - * https://www.ctan.org/pkg/bibtex - */ -public class VM implements Warn { - - public static final Integer FALSE = 0; - - public static final Integer TRUE = 1; - - private static final Pattern ADD_PERIOD_PATTERN = Pattern.compile("([^\\.\\?\\!\\}\\s])(\\}|\\s)*$"); - - private static final Logger LOGGER = LoggerFactory.getLogger(VM.class); - - private List entries; - - private Map strings = new HashMap<>(); - - private Map integers = new HashMap<>(); - - private Map functions = new HashMap<>(); - - private Stack stack = new Stack<>(); - - private final Map buildInFunctions; - - private File file; - - private final CommonTree tree; - - private StringBuilder bbl; - - private String preamble = ""; - - public static class Identifier { - - public final String name; - - public Identifier(String name) { - this.name = name; - } - - public String getName() { - return name; - } - } - - public static class Variable { - - public final String name; - - public Variable(String name) { - this.name = name; - } - - public String getName() { - return name; - } - } - - @FunctionalInterface - public interface BstFunction { - void execute(BstEntry context); - } - - public VM(File f) throws RecognitionException, IOException { - this(new ANTLRFileStream(f.getPath())); - this.file = f; - } - - public VM(String s) throws RecognitionException { - this(new ANTLRStringStream(s)); - } - - private VM(CharStream bst) throws RecognitionException { - this(VM.charStream2CommonTree(bst)); - } - - private VM(CommonTree tree) { - this.tree = tree; - - this.buildInFunctions = new HashMap<>(37); - - /* - * Pops the top two (integer) literals, compares them, and pushes - * the integer 1 if the second is greater than the first, 0 - * otherwise. - */ - buildInFunctions.put(">", context -> { - if (stack.size() < 2) { - throw new VMException("Not enough operands on stack for operation >"); - } - Object o2 = stack.pop(); - Object o1 = stack.pop(); - - if (!((o1 instanceof Integer) && (o2 instanceof Integer))) { - throw new VMException("Can only compare two integers with >"); - } - - stack.push(((Integer) o1).compareTo((Integer) o2) > 0 ? VM.TRUE : VM.FALSE); - }); - - /* Analogous to >. */ - buildInFunctions.put("<", context -> { - if (stack.size() < 2) { - throw new VMException("Not enough operands on stack for operation <"); - } - Object o2 = stack.pop(); - Object o1 = stack.pop(); - - if (!((o1 instanceof Integer) && (o2 instanceof Integer))) { - throw new VMException("Can only compare two integers with <"); - } - - stack.push(((Integer) o1).compareTo((Integer) o2) < 0 ? VM.TRUE : VM.FALSE); - }); - - /* - * Pops the top two (both integer or both string) literals, compares - * them, and pushes the integer 1 if they're equal, 0 otherwise. - */ - buildInFunctions.put("=", context -> { - if (stack.size() < 2) { - throw new VMException("Not enough operands on stack for operation ="); - } - Object o1 = stack.pop(); - Object o2 = stack.pop(); - - if ((o1 == null) ^ (o2 == null)) { - stack.push(VM.FALSE); - return; - } - - if ((o1 == null) && (o2 == null)) { - stack.push(VM.TRUE); - return; - } - - stack.push(o1.equals(o2) ? VM.TRUE : VM.FALSE); - }); - - /* Pops the top two (integer) literals and pushes their sum. */ - buildInFunctions.put("+", context -> { - if (stack.size() < 2) { - throw new VMException("Not enough operands on stack for operation +"); - } - Object o2 = stack.pop(); - Object o1 = stack.pop(); - - if (!((o1 instanceof Integer) && (o2 instanceof Integer))) { - throw new VMException("Can only compare two integers with +"); - } - - stack.push((Integer) o1 + (Integer) o2); - }); - - /* - * Pops the top two (integer) literals and pushes their difference - * (the first subtracted from the second). - */ - buildInFunctions.put("-", context -> { - if (stack.size() < 2) { - throw new VMException("Not enough operands on stack for operation -"); - } - Object o2 = stack.pop(); - Object o1 = stack.pop(); - - if (!((o1 instanceof Integer) && (o2 instanceof Integer))) { - throw new VMException("Can only subtract two integers with -"); - } - - stack.push((Integer) o1 - (Integer) o2); - }); - - /* - * Pops the top two (string) literals, concatenates them (in reverse - * order, that is, the order in which pushed), and pushes the - * resulting string. - */ - buildInFunctions.put("*", context -> { - if (stack.size() < 2) { - throw new VMException("Not enough operands on stack for operation *"); - } - Object o2 = stack.pop(); - Object o1 = stack.pop(); - - if (o1 == null) { - o1 = ""; - } - if (o2 == null) { - o2 = ""; - } - - if (!((o1 instanceof String) && (o2 instanceof String))) { - LOGGER.error("o1: {} ({})", o1, o1.getClass()); - LOGGER.error("o2: {} ({})", o2, o2.getClass()); - throw new VMException("Can only concatenate two String with *"); - } - - stack.push(o1.toString() + o2); - }); - - /* - * Pops the top two literals and assigns to the first (which must be - * a global or entry variable) the value of the second. - */ - buildInFunctions.put(":=", context -> { - if (stack.size() < 2) { - throw new VMException("Invalid call to operation :="); - } - Object o1 = stack.pop(); - Object o2 = stack.pop(); - assign(context, o1, o2); - }); - - /* - * Pops the top (string) literal, adds a `.' to it if the last non - * '}' character isn't a `.', `?', or `!', and pushes this resulting - * string. - */ - buildInFunctions.put("add.period$", context -> addPeriodFunction()); - - /* - * Executes the function whose name is the entry type of an entry. - * For example if an entry is of type book, this function executes - * the book function. When given as an argument to the ITERATE - * command, call.type$ actually produces the output for the entries. - * For an entry with an unknown type, it executes the function - * default.type. Thus you should define (before the READ command) - * one function for each standard entry type as well as a - * default.type function. - */ - buildInFunctions.put("call.type$", context -> { - if (context == null) { - throw new VMException("Call.type$ can only be called from within a context (ITERATE or REVERSE)."); - } - VM.this.execute(context.entry.getType().getName(), context); - }); - - buildInFunctions.put("change.case$", new ChangeCaseFunction(this)); - - /* - * Pops the top (string) literal, makes sure it's a single - * character, converts it to the corresponding ASCII integer, and - * pushes this integer. - */ - buildInFunctions.put("chr.to.int$", context -> { - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation chr.to.int$"); - } - Object o1 = stack.pop(); - - if (!((o1 instanceof String) && (((String) o1).length() == 1))) { - throw new VMException("Can only perform chr.to.int$ on string with length 1"); - } - - String s = (String) o1; - - stack.push((int) s.charAt(0)); - }); - - /* - * Pushes the string that was the \cite-command argument for this - * entry. - */ - buildInFunctions.put("cite$", context -> { - if (context == null) { - throw new VMException("Must have an entry to cite$"); - } - stack.push(context.entry.getCitationKey().orElse(null)); - }); - - /* - * Pops the top literal from the stack and pushes two copies of it. - */ - buildInFunctions.put("duplicate$", context -> { - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation duplicate$"); - } - Object o1 = stack.pop(); - - stack.push(o1); - stack.push(o1); - }); - - /* - * Pops the top literal and pushes the integer 1 if it's a missing - * field or a string having no non-white-space characters, 0 - * otherwise. - */ - buildInFunctions.put("empty$", context -> { - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation empty$"); - } - Object o1 = stack.pop(); - - if (o1 == null) { - stack.push(VM.TRUE); - return; - } - - if (!(o1 instanceof String)) { - throw new VMException("Operand does not match function empty$"); - } - - String s = (String) o1; - - stack.push("".equals(s.trim()) ? VM.TRUE : VM.FALSE); - }); - - buildInFunctions.put("format.name$", new FormatNameFunction(this)); - - /* - * Pops the top three literals (they are two function literals and - * an integer literal, in that order); if the integer is greater - * than 0, it executes the second literal, else it executes the - * first. - */ - buildInFunctions.put("if$", context -> { - if (stack.size() < 3) { - throw new VMException("Not enough operands on stack for operation ="); - } - Object f1 = stack.pop(); - Object f2 = stack.pop(); - Object i = stack.pop(); - - if (!((f1 instanceof Identifier) || (f1 instanceof Tree)) - && ((f2 instanceof Identifier) || (f2 instanceof Tree)) && (i instanceof Integer)) { - throw new VMException("Expecting two functions and an integer for if$."); - } - - if ((Integer) i > 0) { - VM.this.executeInContext(f2, context); - } else { - VM.this.executeInContext(f1, context); - } - }); - - /* - * Pops the top (integer) literal, interpreted as the ASCII integer - * value of a single character, converts it to the corresponding - * single-character string, and pushes this string. - */ - buildInFunctions.put("int.to.chr$", context -> { - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation int.to.chr$"); - } - Object o1 = stack.pop(); - - if (!(o1 instanceof Integer)) { - throw new VMException("Can only perform operation int.to.chr$ on an Integer"); - } - - Integer i = (Integer) o1; - - stack.push(String.valueOf((char) i.intValue())); - }); - - /* - * Pops the top (integer) literal, converts it to its (unique) - * string equivalent, and pushes this string. - */ - buildInFunctions.put("int.to.str$", context -> { - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation int.to.str$"); - } - Object o1 = stack.pop(); - - if (!(o1 instanceof Integer)) { - throw new VMException("Can only transform an integer to an string using int.to.str$"); - } - - stack.push(o1.toString()); - }); - - /* - * Pops the top literal and pushes the integer 1 if it's a missing - * field, 0 otherwise. - */ - buildInFunctions.put("missing$", context -> { - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation missing$"); - } - Object o1 = stack.pop(); - - if (o1 == null) { - stack.push(VM.TRUE); - return; - } - - if (!(o1 instanceof String)) { - warn("Not a string or missing field in operation missing$"); - stack.push(VM.TRUE); - return; - } - - stack.push(VM.FALSE); - }); - - /* - * Writes onto the bbl file what is accumulated in the output buffer. - * It writes a blank line if and only if the output buffer is empty. - * Since write$ does reasonable line breaking, you should use this - * function only when you want a blank line or an explicit line - * break. - */ - buildInFunctions.put("newline$", context -> VM.this.bbl.append('\n')); - - /* - * Pops the top (string) literal and pushes the number of names the - * string represents one plus the number of occurrences of the - * substring "and" (ignoring case differences) surrounded by - * non-null white-space at the top brace level. - */ - buildInFunctions.put("num.names$", context -> { - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation num.names$"); - } - Object o1 = stack.pop(); - - if (!(o1 instanceof String)) { - throw new VMException("Need a string at the top of the stack for num.names$"); - } - String s = (String) o1; - - stack.push(AuthorList.parse(s).getNumberOfAuthors()); - }); - - /* - * Pops the top of the stack but doesn't print it; this gets rid of - * an unwanted stack literal. - */ - buildInFunctions.put("pop$", context -> stack.pop()); - - /* - * The |built_in| function {\.{preamble\$}} pushes onto the stack - * the concatenation of all the \.{preamble} strings read from the - * database files. (or the empty string if there where none) - * - * @PREAMBLE strings read from the database files. - */ - buildInFunctions.put("preamble$", context -> { - stack.push(preamble); - }); - - /* - * Pops the top (string) literal, removes nonalphanumeric characters - * except for white-space characters and hyphens and ties (these all get - * converted to a space), removes certain alphabetic characters - * contained in the control sequences associated with a \special - * character", and pushes the resulting string. - */ - buildInFunctions.put("purify$", new PurifyFunction(this)); - - /* - * Pushes the string consisting of the double-quote character. - */ - buildInFunctions.put("quote$", context -> stack.push("\"")); - - /* - * Is a no-op. - */ - buildInFunctions.put("skip$", context -> { - // Nothing to do! Yeah! - }); - - /* - * Pops and prints the whole stack; it's meant to be used for style - * designers while debugging. - */ - buildInFunctions.put("stack$", context -> { - while (!stack.empty()) { - LOGGER.debug("Stack entry {}", stack.pop()); - } - }); - - /* - * Pops the top three literals (they are the two integers literals - * len and start, and a string literal, in that order). It pushes - * the substring of the (at most) len consecutive characters - * starting at the startth character (assuming 1-based indexing) if - * start is positive, and ending at the start-th character - * (including) from the end if start is negative (where the first - * character from the end is the last character). - */ - buildInFunctions.put("substring$", context -> substringFunction()); - - /* - * Swaps the top two literals on the stack. text.length$ Pops the - * top (string) literal, and pushes the number of text characters - * it contains, where an accented character (more precisely, a - * \special character", defined in Section 4) counts as a single - * text character, even if it's missing its matching right brace, - * and where braces don't count as text characters. - */ - buildInFunctions.put("swap$", context -> { - if (stack.size() < 2) { - throw new VMException("Not enough operands on stack for operation swap$"); - } - Object f1 = stack.pop(); - Object f2 = stack.pop(); - - stack.push(f1); - stack.push(f2); - }); - - /* - * text.length$ Pops the top (string) literal, and pushes the number - * of text characters it contains, where an accented character (more - * precisely, a "special character", defined in Section 4) counts as - * a single text character, even if it's missing its matching right - * brace, and where braces don't count as text characters. - * - * From BibTeXing: For the purposes of counting letters in labels, - * BibTEX considers everything contained inside the braces as a - * single letter. - */ - buildInFunctions.put("text.length$", context -> textLengthFunction()); - - /* - * Pops the top two literals (the integer literal len and a string - * literal, in that order). It pushes the substring of the (at most) len - * consecutive text characters starting from the beginning of the - * string. This function is similar to substring$, but this one - * considers a \special character", even if it's missing its matching - * right brace, to be a single text character (rather than however many - * ASCII characters it actually comprises), and this function doesn't - * consider braces to be text characters; furthermore, this function - * appends any needed matching right braces. - */ - buildInFunctions.put("text.prefix$", new TextPrefixFunction(this)); - - /* - * Pops and prints the top of the stack to the log file. It's useful for debugging. - */ - buildInFunctions.put("top$", context -> LOGGER.debug("Stack entry {}", stack.pop())); - - /* - * Pushes the current entry's type (book, article, etc.), but pushes - * the null string if the type is either unknown or undefined. - */ - buildInFunctions.put("type$", context -> { - if (context == null) { - throw new VMException("type$ need a context."); - } - - stack.push(context.entry.getType().getName()); - }); - - /* - * Pops the top (string) literal and prints it following a warning - * message. This also increments a count of the number of warning - * messages issued. - */ - buildInFunctions.put("warning$", new BstFunction() { - int warning = 1; - - @Override - public void execute(BstEntry context) { - LOGGER.warn("Warning (#" + (warning++) + "): " + stack.pop()); - } - }); - - /* - * Pops the top two (function) literals, and keeps executing the - * second as long as the (integer) literal left on the stack by - * executing the first is greater than 0. - */ - buildInFunctions.put("while$", this::whileFunction); - - buildInFunctions.put("width$", new WidthFunction(this)); - - /* - * Pops the top (string) literal and writes it on the output buffer - * (which will result in stuff being written onto the bbl file when - * the buffer fills up). - */ - buildInFunctions.put("write$", context -> { - String s = (String) stack.pop(); - VM.this.bbl.append(s); - }); - } - - private void textLengthFunction() { - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation text.length$"); - } - Object o1 = stack.pop(); - - if (!(o1 instanceof String)) { - throw new VMException("Can only perform operation on a string text.length$"); - } - - String s = (String) o1; - char[] c = s.toCharArray(); - int result = 0; - - // Comments from bibtex.web: - - // sp_ptr := str_start[pop_lit1]; - int i = 0; - - // sp_end := str_start[pop_lit1+1]; - int n = s.length(); - - // sp_brace_level := 0; - int braceLevel = 0; - - // while (sp_ptr < sp_end) do begin - while (i < n) { - // incr(sp_ptr); - i++; - // if (str_pool[sp_ptr-1] = left_brace) then - // begin - if (c[i - 1] == '{') { - // incr(sp_brace_level); - braceLevel++; - // if ((sp_brace_level = 1) and (sp_ptr < sp_end)) then - if ((braceLevel == 1) && (i < n)) { - // if (str_pool[sp_ptr] = backslash) then - // begin - if (c[i] == '\\') { - // incr(sp_ptr); {skip over the |backslash|} - i++; // skip over backslash - // while ((sp_ptr < sp_end) and (sp_brace_level - // > 0)) do begin - while ((i < n) && (braceLevel > 0)) { - // if (str_pool[sp_ptr] = right_brace) then - if (c[i] == '}') { - // decr(sp_brace_level) - braceLevel--; - } else if (c[i] == '{') { - // incr(sp_brace_level); - braceLevel++; - } - // incr(sp_ptr); - i++; - // end; - } - // incr(num_text_chars); - result++; - // end; - } - // end - } - - // else if (str_pool[sp_ptr-1] = right_brace) then - // begin - } else if (c[i - 1] == '}') { - // if (sp_brace_level > 0) then - if (braceLevel > 0) { - // decr(sp_brace_level); - braceLevel--; - // end - } - } else { // else - // incr(num_text_chars); - result++; - } - } - stack.push(result); - } - - private void whileFunction(BstEntry context) { - if (stack.size() < 2) { - throw new VMException("Not enough operands on stack for operation while$"); - } - Object f2 = stack.pop(); - Object f1 = stack.pop(); - - if (!((f1 instanceof Identifier) || (f1 instanceof Tree)) - && ((f2 instanceof Identifier) || (f2 instanceof Tree))) { - throw new VMException("Expecting two functions for while$."); - } - - do { - VM.this.executeInContext(f1, context); - - Object i = stack.pop(); - if (!(i instanceof Integer)) { - throw new VMException("First parameter to while has to return an integer but was " + i); - } - if ((Integer) i <= 0) { - break; - } - VM.this.executeInContext(f2, context); - } while (true); - } - - private void substringFunction() { - if (stack.size() < 3) { - throw new VMException("Not enough operands on stack for operation substring$"); - } - Object o1 = stack.pop(); - Object o2 = stack.pop(); - Object o3 = stack.pop(); - - if (!((o1 instanceof Integer) && (o2 instanceof Integer) && (o3 instanceof String))) { - throw new VMException("Expecting two integers and a string for substring$"); - } - - Integer len = (Integer) o1; - Integer start = (Integer) o2; - - int lenI = len; - int startI = start; - - if (lenI > (Integer.MAX_VALUE / 2)) { - lenI = Integer.MAX_VALUE / 2; - } - - if (startI > (Integer.MAX_VALUE / 2)) { - startI = Integer.MAX_VALUE / 2; - } - - if (startI < (Integer.MIN_VALUE / 2)) { - startI = -Integer.MIN_VALUE / 2; - } - - String s = (String) o3; - - if (startI < 0) { - startI += s.length() + 1; - startI = Math.max(1, (startI + 1) - lenI); - } - stack.push(s.substring(startI - 1, Math.min((startI - 1) + lenI, s.length()))); - } - - private void addPeriodFunction() { - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation add.period$"); - } - Object o1 = stack.pop(); - - if (!(o1 instanceof String)) { - throw new VMException("Can only add a period to a string for add.period$"); - } - - String s = (String) o1; - Matcher m = ADD_PERIOD_PATTERN.matcher(s); - - if (m.find()) { - StringBuilder sb = new StringBuilder(); - m.appendReplacement(sb, m.group(1)); - sb.append('.'); - String group2 = m.group(2); - if (group2 != null) { - sb.append(m.group(2)); - } - stack.push(sb.toString()); - } else { - stack.push(s); - } - } - - private static CommonTree charStream2CommonTree(CharStream bst) throws RecognitionException { - BstLexer lex = new BstLexer(bst); - CommonTokenStream tokens = new CommonTokenStream(lex); - BstParser parser = new BstParser(tokens); - BstParser.program_return r = parser.program(); - return (CommonTree) r.getTree(); - } - - private boolean assign(BstEntry context, Object o1, Object o2) { - if (!(o1 instanceof Identifier) || !((o2 instanceof String) || (o2 instanceof Integer))) { - throw new VMException("Invalid parameters"); - } - - String name = ((Identifier) o1).getName(); - - if (o2 instanceof String) { - if ((context != null) && context.localStrings.containsKey(name)) { - context.localStrings.put(name, (String) o2); - return true; - } - - if (strings.containsKey(name)) { - strings.put(name, (String) o2); - return true; - } - return false; - } - - if ((context != null) && context.localIntegers.containsKey(name)) { - context.localIntegers.put(name, (Integer) o2); - return true; - } - - if (integers.containsKey(name)) { - integers.put(name, (Integer) o2); - return true; - } - return false; - } - - public String run(BibDatabase db) { - preamble = db.getPreamble().orElse(""); - return run(db.getEntries()); - } - - public String run(Collection bibtex) { - return this.run(bibtex, null); - } - - /** - * Transforms the given list of BibEntries to a rendered list of references using the underlying bst file - * - * @param bibEntries list of entries to convert - * @param bibDatabase (may be null) the bibDatabase used for resolving strings / crossref - * @return list of references in plain text form - */ - public String run(Collection bibEntries, BibDatabase bibDatabase) { - Objects.requireNonNull(bibEntries); - - // Reset - bbl = new StringBuilder(); - - strings = new HashMap<>(); - - integers = new HashMap<>(); - integers.put("entry.max$", Integer.MAX_VALUE); - integers.put("global.max$", Integer.MAX_VALUE); - - functions = new HashMap<>(); - functions.putAll(buildInFunctions); - - stack = new Stack<>(); - - // Create entries - entries = new ArrayList<>(bibEntries.size()); - for (BibEntry entry : bibEntries) { - entries.add(new BstEntry(entry)); - } - - // Go - for (int i = 0; i < tree.getChildCount(); i++) { - Tree child = tree.getChild(i); - switch (child.getType()) { - case BstParser.STRINGS: - strings(child); - break; - case BstParser.INTEGERS: - integers(child); - break; - case BstParser.FUNCTION: - function(child); - break; - case BstParser.EXECUTE: - execute(child); - break; - case BstParser.SORT: - sort(); - break; - case BstParser.ITERATE: - iterate(child); - break; - case BstParser.REVERSE: - reverse(child); - break; - case BstParser.ENTRY: - entry(child); - break; - case BstParser.READ: - read(bibDatabase); - break; - case BstParser.MACRO: - macro(child); - break; - default: - LOGGER.info("Unknown type: {}", child.getType()); - break; - } - } - - return bbl.toString(); - } - - /** - * Dredges up from the database file the field values for each entry in the list. It has no arguments. If a database - * entry doesn't have a value for a field (and probably no database entry will have a value for every field), that - * field variable is marked as missing for the entry. - * - * We use null for the missing entry designator. - */ - private void read(BibDatabase bibDatabase) { - FieldWriter fieldWriter = new FieldWriter(new FieldWriterPreferences(true, List.of(StandardField.MONTH), new FieldContentFormatterPreferences())); - for (BstEntry e : entries) { - for (Map.Entry mEntry : e.fields.entrySet()) { - Field field = FieldFactory.parseField(mEntry.getKey()); - String fieldValue = e.entry.getResolvedFieldOrAlias(field, bibDatabase) - .map(content -> { - try { - String result = fieldWriter.write(field, content); - if (result.startsWith("{")) { - // Strip enclosing {} from the output - return result.substring(1, result.length() - 1); - } - if (field == StandardField.MONTH) { - // We don't have the internal BibTeX strings at hand. - // We nevertheless want to have the full month name. - // Thus, we lookup the full month name here. - return Month.parse(result) - .map(month -> month.getFullName()) - .orElse(result); - } - return result; - } catch (InvalidFieldValueException invalidFieldValueException) { - // in case there is something wrong with the content, just return the content itself - return content; - } - }) - .orElse(null); - mEntry.setValue(fieldValue); - } - } - - for (BstEntry e : entries) { - if (!e.fields.containsKey(StandardField.CROSSREF.getName())) { - e.fields.put(StandardField.CROSSREF.getName(), null); - } - } - } - - /** - * Defines a string macro. It has two arguments; the first is the macro's name, which is treated like any other - * variable or function name, and the second is its definition, which must be double-quote-delimited. You must have - * one for each three-letter month abbreviation; in addition, you should have one for common journal names. The - * user's database may override any definition you define using this command. If you want to define a string the - * user can't touch, use the FUNCTION command, which has a compatible syntax. - */ - private void macro(Tree child) { - String name = child.getChild(0).getText(); - String replacement = child.getChild(1).getText(); - functions.put(name, new MacroFunction(replacement)); - } - - public class MacroFunction implements BstFunction { - - private final String replacement; - - public MacroFunction(String replacement) { - this.replacement = replacement; - } - - @Override - public void execute(BstEntry context) { - VM.this.push(replacement); - } - } - - /** - * Declares the fields and entry variables. It has three arguments, each a (possibly empty) list of variable names. - * The three lists are of: fields, integer entry variables, and string entry variables. There is an additional field - * that BibTEX automatically declares, crossref, used for cross referencing. And there is an additional string entry - * variable automatically declared, sort.key$, used by the SORT command. Each of these variables has a value for - * each entry on the list. - */ - private void entry(Tree child) { - // Fields first - Tree t = child.getChild(0); - - for (int i = 0; i < t.getChildCount(); i++) { - String name = t.getChild(i).getText(); - - for (BstEntry entry : entries) { - entry.fields.put(name, null); - } - } - - // Integers - t = child.getChild(1); - - for (int i = 0; i < t.getChildCount(); i++) { - String name = t.getChild(i).getText(); - - for (BstEntry entry : entries) { - entry.localIntegers.put(name, 0); - } - } - // Strings - t = child.getChild(2); - - for (int i = 0; i < t.getChildCount(); i++) { - String name = t.getChild(i).getText(); - for (BstEntry entry : entries) { - entry.localStrings.put(name, null); - } - } - for (BstEntry entry : entries) { - entry.localStrings.put("sort.key$", null); - } - } - - private void reverse(Tree child) { - BstFunction f = functions.get(child.getChild(0).getText()); - - ListIterator i = entries.listIterator(entries.size()); - while (i.hasPrevious()) { - f.execute(i.previous()); - } - } - - private void iterate(Tree child) { - BstFunction f = functions.get(child.getChild(0).getText()); - - for (BstEntry entry : entries) { - f.execute(entry); - } - } - - /** - * Sorts the entry list using the values of the string entry variable sort.key$. It has no arguments. - */ - private void sort() { - entries.sort(Comparator.comparing(o -> (o.localStrings.get("sort.key$")))); - } - - private void executeInContext(Object o, BstEntry context) { - if (o instanceof Tree) { - Tree t = (Tree) o; - new StackFunction(t).execute(context); - } else if (o instanceof Identifier) { - execute(((Identifier) o).getName(), context); - } - } - - private void execute(Tree child) { - execute(child.getChild(0).getText(), null); - } - - public class StackFunction implements BstFunction { - - private final Tree localTree; - - public StackFunction(Tree stack) { - localTree = stack; - } - - public Tree getTree() { - return localTree; - } - - @Override - public void execute(BstEntry context) { - for (int i = 0; i < localTree.getChildCount(); i++) { - Tree c = localTree.getChild(i); - try { - - switch (c.getType()) { - case BstParser.STRING: - String s = c.getText(); - push(s.substring(1, s.length() - 1)); - break; - case BstParser.INTEGER: - push(Integer.parseInt(c.getText().substring(1))); - break; - case BstParser.QUOTED: - push(new Identifier(c.getText().substring(1))); - break; - case BstParser.STACK: - push(c); - break; - default: - VM.this.execute(c.getText(), context); - break; - } - } catch (VMException e) { - if (file == null) { - LOGGER.error("ERROR " + e.getMessage() + " (" + c.getLine() + ")"); - } else { - LOGGER.error("ERROR " + e.getMessage() + " (" + file.getPath() + ":" - + c.getLine() + ")"); - } - throw e; - } - } - } - } - - private void push(Tree t) { - stack.push(t); - } - - private void execute(String name, BstEntry context) { - if (context != null) { - if (context.fields.containsKey(name)) { - stack.push(context.fields.get(name)); - return; - } - if (context.localStrings.containsKey(name)) { - stack.push(context.localStrings.get(name)); - return; - } - if (context.localIntegers.containsKey(name)) { - stack.push(context.localIntegers.get(name)); - return; - } - } - if (strings.containsKey(name)) { - stack.push(strings.get(name)); - return; - } - if (integers.containsKey(name)) { - stack.push(integers.get(name)); - return; - } - - if (functions.containsKey(name)) { - // OK to have a null context - functions.get(name).execute(context); - return; - } - - throw new VMException("No matching identifier found: " + name); - } - - private void function(Tree child) { - String name = child.getChild(0).getText(); - Tree localStack = child.getChild(1); - functions.put(name, new StackFunction(localStack)); - } - - /** - * Declares global integer variables. It has one argument, a list of variable names. There are two such - * automatically-declared variables, entry.max$ and global.max$, used for limiting the lengths of string vari- - * ables. You may have any number of these commands, but a variable's declaration must precede its use. - */ - private void integers(Tree child) { - Tree t = child.getChild(0); - - for (int i = 0; i < t.getChildCount(); i++) { - String name = t.getChild(i).getText(); - integers.put(name, 0); - } - } - - /** - * Declares global string variables. It has one argument, a list of variable names. You may have any number of these - * commands, but a variable's declaration must precede its use. - * - * @param child - */ - private void strings(Tree child) { - Tree t = child.getChild(0); - - for (int i = 0; i < t.getChildCount(); i++) { - String name = t.getChild(i).getText(); - strings.put(name, null); - } - } - - public static class BstEntry { - - public final BibEntry entry; - - public final Map localStrings = new HashMap<>(); - - // keys filled by org.jabref.logic.bst.VM.entry based on the contents of the bst file - public final Map fields = new HashMap<>(); - - public final Map localIntegers = new HashMap<>(); - - public BstEntry(BibEntry e) { - this.entry = e; - } - } - - private void push(Integer integer) { - stack.push(integer); - } - - private void push(String string) { - stack.push(string); - } - - private void push(Identifier identifier) { - stack.push(identifier); - } - - public Map getStrings() { - return strings; - } - - public Map getIntegers() { - return integers; - } - - public List getEntries() { - return entries; - } - - public Map getFunctions() { - return functions; - } - - public Stack getStack() { - return stack; - } - - @Override - public void warn(String string) { - LOGGER.warn(string); - } -} diff --git a/src/main/java/org/jabref/logic/bst/VMException.java b/src/main/java/org/jabref/logic/bst/VMException.java deleted file mode 100644 index f46c5cd5277..00000000000 --- a/src/main/java/org/jabref/logic/bst/VMException.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.jabref.logic.bst; - -public class VMException extends RuntimeException { - - public VMException(String string) { - super(string); - } -} diff --git a/src/main/java/org/jabref/logic/bst/Warn.java b/src/main/java/org/jabref/logic/bst/Warn.java deleted file mode 100644 index 7a524ad9834..00000000000 --- a/src/main/java/org/jabref/logic/bst/Warn.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.jabref.logic.bst; - -@FunctionalInterface -public interface Warn { - - void warn(String s); -} diff --git a/src/main/java/org/jabref/logic/bst/WidthFunction.java b/src/main/java/org/jabref/logic/bst/WidthFunction.java deleted file mode 100644 index 06784267bf0..00000000000 --- a/src/main/java/org/jabref/logic/bst/WidthFunction.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.jabref.logic.bst; - -import java.util.Stack; - -import org.jabref.logic.bst.VM.BstEntry; -import org.jabref.logic.bst.VM.BstFunction; - -/** - * The |built_in| function {\.{width\$}} pops the top (string) literal and - * pushes the integer that represents its width in units specified by the - * |char_width| array. This function takes the literal literally; that is, it - * assumes each character in the string is to be printed as is, regardless of - * whether the character has a special meaning to \TeX, except that special - * characters (even without their |right_brace|s) are handled specially. If the - * literal isn't a string, it complains and pushes~0. - * - */ -public class WidthFunction implements BstFunction { - - private final VM vm; - - public WidthFunction(VM vm) { - this.vm = vm; - } - - @Override - public void execute(BstEntry context) { - Stack stack = vm.getStack(); - - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation width$"); - } - Object o1 = stack.pop(); - - if (!(o1 instanceof String)) { - vm.warn("A string is needed for change.case$"); - stack.push(0); - return; - } - - stack.push(BibtexWidth.width((String) o1)); - } -} diff --git a/src/main/java/org/jabref/logic/bst/BibtexCaseChanger.java b/src/main/java/org/jabref/logic/bst/util/BstCaseChanger.java similarity index 83% rename from src/main/java/org/jabref/logic/bst/BibtexCaseChanger.java rename to src/main/java/org/jabref/logic/bst/util/BstCaseChanger.java index e05ce998e80..dd3e7c7b05c 100644 --- a/src/main/java/org/jabref/logic/bst/BibtexCaseChanger.java +++ b/src/main/java/org/jabref/logic/bst/util/BstCaseChanger.java @@ -1,4 +1,4 @@ -package org.jabref.logic.bst; +package org.jabref.logic.bst.util; import java.util.Locale; import java.util.Optional; @@ -6,9 +6,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public final class BibtexCaseChanger { +public final class BstCaseChanger { - private static final Logger LOGGER = LoggerFactory.getLogger(BibtexCaseChanger.class); + private static final Logger LOGGER = LoggerFactory.getLogger(BstCaseChanger.class); // stores whether the char before the current char was a colon private boolean prevColon = true; @@ -16,7 +16,7 @@ public final class BibtexCaseChanger { // global variable to store the current brace level private int braceLevel; - public enum FORMAT_MODE { + public enum FormatMode { // First character and character after a ":" as upper case - everything else in lower case. Obey {}. TITLE_LOWERS('t'), @@ -40,7 +40,7 @@ public enum FORMAT_MODE { private final char asChar; - FORMAT_MODE(char asChar) { + FormatMode(char asChar) { this.asChar = asChar; } @@ -53,17 +53,21 @@ public char asChar() { * * @throws IllegalArgumentException if char is not 't', 'l', 'u' */ - public static FORMAT_MODE getFormatModeForBSTFormat(final char bstFormat) { - for (FORMAT_MODE mode : FORMAT_MODE.values()) { + public static FormatMode of(final char bstFormat) { + for (FormatMode mode : FormatMode.values()) { if (mode.asChar == bstFormat) { return mode; } } throw new IllegalArgumentException(); } + + public static FormatMode of(final String bstFormat) { + return of(bstFormat.toLowerCase(Locale.ROOT).charAt(0)); + } } - private BibtexCaseChanger() { + private BstCaseChanger() { } /** @@ -72,11 +76,11 @@ private BibtexCaseChanger() { * @param s the string to handle * @param format the format */ - public static String changeCase(String s, FORMAT_MODE format) { - return (new BibtexCaseChanger()).doChangeCase(s, format); + public static String changeCase(String s, FormatMode format) { + return (new BstCaseChanger()).doChangeCase(s, format); } - private String doChangeCase(String s, FORMAT_MODE format) { + private String doChangeCase(String s, FormatMode format) { char[] c = s.toCharArray(); StringBuilder sb = new StringBuilder(); @@ -93,7 +97,7 @@ private String doChangeCase(String s, FORMAT_MODE format) { i++; continue; } - if ((format == FORMAT_MODE.TITLE_LOWERS) && ((i == 0) || (prevColon && Character.isWhitespace(c[i - 1])))) { + if ((format == FormatMode.TITLE_LOWERS) && ((i == 0) || (prevColon && Character.isWhitespace(c[i - 1])))) { sb.append('{'); i++; prevColon = false; @@ -136,12 +140,9 @@ private String doChangeCase(String s, FORMAT_MODE format) { * is other stuff, too, between braces, but it doesn't try to do anything * special with |colon|s. * - * @param c * @param start the current position. It points to the opening brace - * @param format - * @return */ - private int convertSpecialChar(StringBuilder sb, char[] c, int start, FORMAT_MODE format) { + private int convertSpecialChar(StringBuilder sb, char[] c, int start, FormatMode format) { int i = start; sb.append(c[i]); @@ -152,7 +153,7 @@ private int convertSpecialChar(StringBuilder sb, char[] c, int start, FORMAT_MOD i++; // skip over the |backslash| - Optional s = BibtexCaseChanger.findSpecialChar(c, i); + Optional s = BstCaseChanger.findSpecialChar(c, i); if (s.isPresent()) { i = convertAccented(c, i, s.get(), sb, format); } @@ -174,14 +175,9 @@ private int convertSpecialChar(StringBuilder sb, char[] c, int start, FORMAT_MOD * up) and append the result to the stringBuffer, return the updated * position. * - * @param c - * @param start - * @param s - * @param sb - * @param format * @return the new position */ - private int convertAccented(char[] c, int start, String s, StringBuilder sb, FORMAT_MODE format) { + private int convertAccented(char[] c, int start, String s, StringBuilder sb, FormatMode format) { int pos = start; pos += s.length(); @@ -214,29 +210,27 @@ private int convertAccented(char[] c, int start, String s, StringBuilder sb, FOR return pos; } - private int convertNonControl(char[] c, int start, StringBuilder sb, FORMAT_MODE format) { + private int convertNonControl(char[] c, int start, StringBuilder sb, FormatMode format) { int pos = start; switch (format) { - case TITLE_LOWERS: - case ALL_LOWERS: + case TITLE_LOWERS, ALL_LOWERS -> { sb.append(Character.toLowerCase(c[pos])); pos++; - break; - case ALL_UPPERS: + } + case ALL_UPPERS -> { sb.append(Character.toUpperCase(c[pos])); pos++; - break; - default: - LOGGER.info("convertNonControl - Unknown format: " + format); - break; + } + default -> + LOGGER.info("convertNonControl - Unknown format: " + format); } return pos; } - private int convertCharIfBraceLevelIsZero(char[] c, int start, StringBuilder sb, FORMAT_MODE format) { + private int convertCharIfBraceLevelIsZero(char[] c, int start, StringBuilder sb, FormatMode format) { int i = start; switch (format) { - case TITLE_LOWERS: + case TITLE_LOWERS -> { if ((i == 0) || (prevColon && Character.isWhitespace(c[i - 1]))) { sb.append(c[i]); } else { @@ -247,16 +241,13 @@ private int convertCharIfBraceLevelIsZero(char[] c, int start, StringBuilder sb, } else if (!Character.isWhitespace(c[i])) { prevColon = false; } - break; - case ALL_LOWERS: - sb.append(Character.toLowerCase(c[i])); - break; - case ALL_UPPERS: - sb.append(Character.toUpperCase(c[i])); - break; - default: - LOGGER.info("convertCharIfBraceLevelIsZero - Unknown format: " + format); - break; + } + case ALL_LOWERS -> + sb.append(Character.toLowerCase(c[i])); + case ALL_UPPERS -> + sb.append(Character.toUpperCase(c[i])); + default -> + LOGGER.info("convertCharIfBraceLevelIsZero - Unknown format: " + format); } i++; return i; diff --git a/src/main/java/org/jabref/logic/bst/BibtexNameFormatter.java b/src/main/java/org/jabref/logic/bst/util/BstNameFormatter.java similarity index 76% rename from src/main/java/org/jabref/logic/bst/BibtexNameFormatter.java rename to src/main/java/org/jabref/logic/bst/util/BstNameFormatter.java index aecf571bdf4..c4986dc12dd 100644 --- a/src/main/java/org/jabref/logic/bst/BibtexNameFormatter.java +++ b/src/main/java/org/jabref/logic/bst/util/BstNameFormatter.java @@ -1,13 +1,17 @@ -package org.jabref.logic.bst; +package org.jabref.logic.bst.util; import java.util.Arrays; import java.util.Locale; import java.util.Optional; import java.util.stream.Collectors; +import org.jabref.logic.bst.BstVMException; import org.jabref.model.entry.Author; import org.jabref.model.entry.AuthorList; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * From Bibtex: * @@ -24,9 +28,10 @@ * Sounds easy - is a nightmare... X-( * */ -public class BibtexNameFormatter { +public class BstNameFormatter { + private static final Logger LOGGER = LoggerFactory.getLogger(BstNameFormatter.class); - private BibtexNameFormatter() { + private BstNameFormatter() { } /** @@ -35,23 +40,18 @@ private BibtexNameFormatter() { * @param authorsNameList The string from an author field * @param whichName index of the list, starting with 1 * @param formatString TODO - * @param warn collects the warnings, may-be-null - * @return */ - public static String formatName(String authorsNameList, int whichName, String formatString, Warn warn) { + public static String formatName(String authorsNameList, int whichName, String formatString) { AuthorList al = AuthorList.parse(authorsNameList); if ((whichName < 1) && (whichName > al.getNumberOfAuthors())) { - warn.warn("AuthorList " + authorsNameList + " does not contain an author with number " + whichName); + LOGGER.warn("AuthorList {} does not contain an author with number {}", authorsNameList, whichName); return ""; } - return BibtexNameFormatter.formatName(al.getAuthor(whichName - 1), formatString, warn); + return BstNameFormatter.formatName(al.getAuthor(whichName - 1), formatString); } - /** - * @param warn collects the warnings, may-be-null - */ - public static String formatName(Author author, String format, Warn warn) { + public static String formatName(Author author, String format) { StringBuilder sb = new StringBuilder(); char[] c = format.toCharArray(); @@ -81,11 +81,7 @@ public static String formatName(Author author, String format, Warn warn) { } if ((braceLevel == 1) && Character.isLetter(c[i])) { if ("fvlj".indexOf(c[i]) == -1) { - if (warn != null) { - warn.warn( - "Format string in format.name$ may only contain fvlj on brace level 1 in group " - + group + ": " + format); - } + LOGGER.warn("Format string in format.name$ may only contain fvlj on brace level 1 in group {}: {}", group, format); } else { level1Chars.append(c[i]); } @@ -99,31 +95,26 @@ public static String formatName(Author author, String format, Warn warn) { continue; } - if ((control.length() > 2) && (warn != null)) { - warn.warn("Format string in format.name$ may only be one or two character long on brace level 1 in group " + group + ": " + format); + if ((control.length() > 2)) { + LOGGER.warn("Format string in format.name$ may only be one or two character long on brace level 1 in group {}: {}", group, format); } char type = control.charAt(0); - Optional tokenS; - switch (type) { - case 'f': - tokenS = author.getFirst(); - break; - case 'v': - tokenS = author.getVon(); - break; - case 'l': - tokenS = author.getLast(); - break; - case 'j': - tokenS = author.getJr(); - break; - default: - throw new VMException("Internal error"); - } - - if (!tokenS.isPresent()) { + Optional tokenS = switch (type) { + case 'f' -> + author.getFirst(); + case 'v' -> + author.getVon(); + case 'l' -> + author.getLast(); + case 'j' -> + author.getJr(); + default -> + throw new BstVMException("Internal error"); + }; + + if (tokenS.isEmpty()) { i++; continue; } @@ -135,9 +126,7 @@ public static String formatName(Author author, String format, Warn warn) { if (control.charAt(1) == control.charAt(0)) { abbreviateThatIsSingleLetter = false; } else { - if (warn != null) { - warn.warn("Format string in format.name$ may only contain one type of vlfj on brace level 1 in group " + group + ": " + format); - } + LOGGER.warn("Format string in format.name$ may only contain one type of vlfj on brace level 1 in group {}: {}", group, format); } } @@ -162,7 +151,7 @@ public static String formatName(Author author, String format, Warn warn) { } if (((j + 1) < d.length) && (d[j + 1] == '{')) { StringBuilder interTokenSb = new StringBuilder(); - j = BibtexNameFormatter.consumeToMatchingBrace(interTokenSb, d, j + 1); + j = BstNameFormatter.consumeToMatchingBrace(interTokenSb, d, j + 1); interToken = interTokenSb.substring(1, interTokenSb.length() - 1); } @@ -171,7 +160,7 @@ public static String formatName(Author author, String format, Warn warn) { if (abbreviateThatIsSingleLetter) { String[] dashes = token.split("-"); - token = Arrays.asList(dashes).stream().map(BibtexNameFormatter::getFirstCharOfString) + token = Arrays.stream(dashes).map(BstNameFormatter::getFirstCharOfString) .collect(Collectors.joining(".-")); } @@ -187,7 +176,7 @@ public static String formatName(Author author, String format, Warn warn) { // No clue what this means (What the hell are tokens anyway??? // if (lex_class[name_sep_char[cur_token]] = sep_char) then // append_ex_buf_char_and_check (name_sep_char[cur_token]) - if ((k == (tokens.length - 2)) || (BibtexNameFormatter.numberOfChars(sb.substring(groupStart, sb.length()), 3) < 3)) { + if ((k == (tokens.length - 2)) || (BstNameFormatter.numberOfChars(sb.substring(groupStart, sb.length()), 3) < 3)) { sb.append('~'); } else { sb.append(' '); @@ -212,7 +201,7 @@ public static String formatName(Author author, String format, Warn warn) { if (sb.length() > 0) { boolean noDisTie = false; if ((sb.charAt(sb.length() - 1) == '~') && - ((BibtexNameFormatter.numberOfChars(sb.substring(groupStart, sb.length()), 4) >= 4) || + ((BstNameFormatter.numberOfChars(sb.substring(groupStart, sb.length()), 4) >= 4) || ((sb.length() > 1) && (noDisTie = sb.charAt(sb.length() - 2) == '~')))) { sb.deleteCharAt(sb.length() - 1); if (!noDisTie) { @@ -221,16 +210,14 @@ public static String formatName(Author author, String format, Warn warn) { } } } else if (c[i] == '}') { - if (warn != null) { - warn.warn("Unmatched brace in format string: " + format); - } + LOGGER.warn("Unmatched brace in format string: {}", format); } else { sb.append(c[i]); // verbatim } i++; } - if ((braceLevel != 0) && (warn != null)) { - warn.warn("Unbalanced brace in format string for nameFormat: " + format); + if ((braceLevel != 0)) { + LOGGER.warn("Unbalanced brace in format string for nameFormat: {}", format); } return sb.toString(); @@ -268,7 +255,7 @@ public static String getFirstCharOfString(String s) { } if ((c[i] == '{') && ((i + 1) < c.length) && (c[i + 1] == '\\')) { StringBuilder sb = new StringBuilder(); - BibtexNameFormatter.consumeToMatchingBrace(sb, c, i); + BstNameFormatter.consumeToMatchingBrace(sb, c, i); return sb.toString(); } } diff --git a/src/main/java/org/jabref/logic/bst/BibtexPurify.java b/src/main/java/org/jabref/logic/bst/util/BstPurifier.java similarity index 77% rename from src/main/java/org/jabref/logic/bst/BibtexPurify.java rename to src/main/java/org/jabref/logic/bst/util/BstPurifier.java index 08cb14db207..90818a0749f 100644 --- a/src/main/java/org/jabref/logic/bst/BibtexPurify.java +++ b/src/main/java/org/jabref/logic/bst/util/BstPurifier.java @@ -1,4 +1,7 @@ -package org.jabref.logic.bst; +package org.jabref.logic.bst.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * @@ -10,17 +13,13 @@ * pushes the null string. * */ -public class BibtexPurify { +public class BstPurifier { + private static final Logger LOGGER = LoggerFactory.getLogger(BstPurifier.class); - private BibtexPurify() { + private BstPurifier() { } - /** - * @param toPurify - * @param warn may-be-null - * @return - */ - public static String purify(String toPurify, Warn warn) { + public static String purify(String toPurify) { StringBuilder sb = new StringBuilder(); char[] cs = toPurify.toCharArray(); @@ -41,7 +40,7 @@ public static String purify(String toPurify, Warn warn) { i++; // skip brace while ((i < n) && (braceLevel > 0)) { i++; // skip backslash - BibtexCaseChanger.findSpecialChar(cs, i).ifPresent(sb::append); + BstCaseChanger.findSpecialChar(cs, i).ifPresent(sb::append); while ((i < n) && Character.isLetter(cs[i])) { i++; @@ -63,15 +62,13 @@ public static String purify(String toPurify, Warn warn) { if (braceLevel > 0) { braceLevel--; } else { - if (warn != null) { - warn.warn("Unbalanced brace in string for purify$: " + toPurify); - } + LOGGER.warn("Unbalanced brace in string for purify$: {}", toPurify); } } i++; } - if ((braceLevel != 0) && (warn != null)) { - warn.warn("Unbalanced brace in string for purify$: " + toPurify); + if ((braceLevel != 0)) { + LOGGER.warn("Unbalanced brace in string for purify$: {}", toPurify); } return sb.toString(); diff --git a/src/main/java/org/jabref/logic/bst/BibtexTextPrefix.java b/src/main/java/org/jabref/logic/bst/util/BstTextPrefixer.java similarity index 84% rename from src/main/java/org/jabref/logic/bst/BibtexTextPrefix.java rename to src/main/java/org/jabref/logic/bst/util/BstTextPrefixer.java index 721838e29fe..31cbc71de9b 100644 --- a/src/main/java/org/jabref/logic/bst/BibtexTextPrefix.java +++ b/src/main/java/org/jabref/logic/bst/util/BstTextPrefixer.java @@ -1,4 +1,7 @@ -package org.jabref.logic.bst; +package org.jabref.logic.bst.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The |built_in| function {\.{text.prefix\$}} pops the top two literals (the @@ -14,15 +17,13 @@ * complains and pushes the null string. * */ -public class BibtexTextPrefix { +public class BstTextPrefixer { + private static final Logger LOGGER = LoggerFactory.getLogger(BstTextPrefixer.class); - private BibtexTextPrefix() { + private BstTextPrefixer() { } - /** - * @param warn may-be-null - */ - public static String textPrefix(int inNumOfChars, String toPrefix, Warn warn) { + public static String textPrefix(int inNumOfChars, String toPrefix) { int numOfChars = inNumOfChars; StringBuilder sb = new StringBuilder(); @@ -53,15 +54,13 @@ public static String textPrefix(int inNumOfChars, String toPrefix, Warn warn) { if (braceLevel > 0) { braceLevel--; } else { - if (warn != null) { - warn.warn("Unbalanced brace in string for purify$: " + toPrefix); - } + LOGGER.warn("Unbalanced brace in string for purify$: {}", toPrefix); } } else { numOfChars--; } } - sb.append(toPrefix.substring(0, i)); + sb.append(toPrefix, 0, i); while (braceLevel > 0) { sb.append('}'); braceLevel--; diff --git a/src/main/java/org/jabref/logic/bst/util/BstWidthCalculator.java b/src/main/java/org/jabref/logic/bst/util/BstWidthCalculator.java new file mode 100644 index 00000000000..c286fe1497b --- /dev/null +++ b/src/main/java/org/jabref/logic/bst/util/BstWidthCalculator.java @@ -0,0 +1,241 @@ +package org.jabref.logic.bst.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * The |built_in| function {\.{purify\$}} pops the top (string) literal, removes + * nonalphanumeric characters except for |white_space| and |sep_char| characters + * (these get converted to a |space|) and removes certain alphabetic characters + * contained in the control sequences associated with a special character, and + * pushes the resulting string. If the literal isn't a string, it complains and + * pushes the null string. + * + */ +public class BstWidthCalculator { + + private static final Logger LOGGER = LoggerFactory.getLogger(BstWidthCalculator.class); + + /* + * Quoted from Bibtex: + * + * Now we initialize the system-dependent |char_width| array, for which + * |space| is the only |white_space| character given a nonzero printing + * width. The widths here are taken from Stanford's June~'87 $cmr10$~font + * and represent hundredths of a point (rounded), but since they're used + * only for relative comparisons, the units have no meaning. + */ + + private static int[] widths; + + static { + if (BstWidthCalculator.widths == null) { + BstWidthCalculator.widths = new int[128]; + + for (int i = 0; i < 128; i++) { + BstWidthCalculator.widths[i] = 0; + } + BstWidthCalculator.widths[32] = 278; + BstWidthCalculator.widths[33] = 278; + BstWidthCalculator.widths[34] = 500; + BstWidthCalculator.widths[35] = 833; + BstWidthCalculator.widths[36] = 500; + BstWidthCalculator.widths[37] = 833; + BstWidthCalculator.widths[38] = 778; + BstWidthCalculator.widths[39] = 278; + BstWidthCalculator.widths[40] = 389; + BstWidthCalculator.widths[41] = 389; + BstWidthCalculator.widths[42] = 500; + BstWidthCalculator.widths[43] = 778; + BstWidthCalculator.widths[44] = 278; + BstWidthCalculator.widths[45] = 333; + BstWidthCalculator.widths[46] = 278; + BstWidthCalculator.widths[47] = 500; + BstWidthCalculator.widths[48] = 500; + BstWidthCalculator.widths[49] = 500; + BstWidthCalculator.widths[50] = 500; + BstWidthCalculator.widths[51] = 500; + BstWidthCalculator.widths[52] = 500; + BstWidthCalculator.widths[53] = 500; + BstWidthCalculator.widths[54] = 500; + BstWidthCalculator.widths[55] = 500; + BstWidthCalculator.widths[56] = 500; + BstWidthCalculator.widths[57] = 500; + BstWidthCalculator.widths[58] = 278; + BstWidthCalculator.widths[59] = 278; + BstWidthCalculator.widths[60] = 278; + BstWidthCalculator.widths[61] = 778; + BstWidthCalculator.widths[62] = 472; + BstWidthCalculator.widths[63] = 472; + BstWidthCalculator.widths[64] = 778; + BstWidthCalculator.widths[65] = 750; + BstWidthCalculator.widths[66] = 708; + BstWidthCalculator.widths[67] = 722; + BstWidthCalculator.widths[68] = 764; + BstWidthCalculator.widths[69] = 681; + BstWidthCalculator.widths[70] = 653; + BstWidthCalculator.widths[71] = 785; + BstWidthCalculator.widths[72] = 750; + BstWidthCalculator.widths[73] = 361; + BstWidthCalculator.widths[74] = 514; + BstWidthCalculator.widths[75] = 778; + BstWidthCalculator.widths[76] = 625; + BstWidthCalculator.widths[77] = 917; + BstWidthCalculator.widths[78] = 750; + BstWidthCalculator.widths[79] = 778; + BstWidthCalculator.widths[80] = 681; + BstWidthCalculator.widths[81] = 778; + BstWidthCalculator.widths[82] = 736; + BstWidthCalculator.widths[83] = 556; + BstWidthCalculator.widths[84] = 722; + BstWidthCalculator.widths[85] = 750; + BstWidthCalculator.widths[86] = 750; + BstWidthCalculator.widths[87] = 1028; + BstWidthCalculator.widths[88] = 750; + BstWidthCalculator.widths[89] = 750; + BstWidthCalculator.widths[90] = 611; + BstWidthCalculator.widths[91] = 278; + BstWidthCalculator.widths[92] = 500; + BstWidthCalculator.widths[93] = 278; + BstWidthCalculator.widths[94] = 500; + BstWidthCalculator.widths[95] = 278; + BstWidthCalculator.widths[96] = 278; + BstWidthCalculator.widths[97] = 500; + BstWidthCalculator.widths[98] = 556; + BstWidthCalculator.widths[99] = 444; + BstWidthCalculator.widths[100] = 556; + BstWidthCalculator.widths[101] = 444; + BstWidthCalculator.widths[102] = 306; + BstWidthCalculator.widths[103] = 500; + BstWidthCalculator.widths[104] = 556; + BstWidthCalculator.widths[105] = 278; + BstWidthCalculator.widths[106] = 306; + BstWidthCalculator.widths[107] = 528; + BstWidthCalculator.widths[108] = 278; + BstWidthCalculator.widths[109] = 833; + BstWidthCalculator.widths[110] = 556; + BstWidthCalculator.widths[111] = 500; + BstWidthCalculator.widths[112] = 556; + BstWidthCalculator.widths[113] = 528; + BstWidthCalculator.widths[114] = 392; + BstWidthCalculator.widths[115] = 394; + BstWidthCalculator.widths[116] = 389; + BstWidthCalculator.widths[117] = 556; + BstWidthCalculator.widths[118] = 528; + BstWidthCalculator.widths[119] = 722; + BstWidthCalculator.widths[120] = 528; + BstWidthCalculator.widths[121] = 528; + BstWidthCalculator.widths[122] = 444; + BstWidthCalculator.widths[123] = 500; + BstWidthCalculator.widths[124] = 1000; + BstWidthCalculator.widths[125] = 500; + BstWidthCalculator.widths[126] = 500; + } + } + + private BstWidthCalculator() { + } + + private static int getSpecialCharWidth(char[] c, int pos) { + if ((pos + 1) < c.length) { + if ((c[pos] == 'o') && (c[pos + 1] == 'e')) { + return 778; + } + if ((c[pos] == 'O') && (c[pos + 1] == 'E')) { + return 1014; + } + if ((c[pos] == 'a') && (c[pos + 1] == 'e')) { + return 722; + } + if ((c[pos] == 'A') && (c[pos + 1] == 'E')) { + return 903; + } + if ((c[pos] == 's') && (c[pos + 1] == 's')) { + return 500; + } + } + return BstWidthCalculator.getCharWidth(c[pos]); + } + + public static int getCharWidth(char c) { + if ((c >= 0) && (c < 128)) { + return BstWidthCalculator.widths[c]; + } else { + return 0; + } + } + + public static int width(String toMeasure) { + /* + * From Bibtex: We use the natural width for all but special characters, + * and we complain if the string isn't brace-balanced. + */ + + int i = 0; + int n = toMeasure.length(); + int braceLevel = 0; + char[] c = toMeasure.toCharArray(); + int result = 0; + + /* + * From Bibtex: + * + * We use the natural widths of all characters except that some + * characters have no width: braces, control sequences (except for the + * usual 13 accented and foreign characters, whose widths are given in + * the next module), and |white_space| following control sequences (even + * a null control sequence). + * + */ + while (i < n) { + if (c[i] == '{') { + braceLevel++; + if ((braceLevel == 1) && ((i + 1) < n) && (c[i + 1] == '\\')) { + i++; // skip brace + while ((i < n) && (braceLevel > 0)) { + i++; // skip backslash + + int afterBackslash = i; + while ((i < n) && Character.isLetter(c[i])) { + i++; + } + if ((i < n) && (i == afterBackslash)) { + i++; // Skip non-alpha control seq + } else { + if (BstCaseChanger.findSpecialChar(c, afterBackslash).isPresent()) { + result += BstWidthCalculator.getSpecialCharWidth(c, afterBackslash); + } + } + while ((i < n) && Character.isWhitespace(c[i])) { + i++; + } + while ((i < n) && (braceLevel > 0) && (c[i] != '\\')) { + if (c[i] == '}') { + braceLevel--; + } else if (c[i] == '{') { + braceLevel++; + } else { + result += BstWidthCalculator.getCharWidth(c[i]); + } + i++; + } + } + continue; + } + } else if (c[i] == '}') { + if (braceLevel > 0) { + braceLevel--; + } else { + LOGGER.warn("Too many closing braces in string: " + toMeasure); + } + } + result += BstWidthCalculator.getCharWidth(c[i]); + i++; + } + if (braceLevel > 0) { + LOGGER.warn("No enough closing braces in string: " + toMeasure); + } + return result; + } +} diff --git a/src/main/java/org/jabref/logic/importer/FetcherClientException.java b/src/main/java/org/jabref/logic/importer/FetcherClientException.java new file mode 100644 index 00000000000..8de9d0d6114 --- /dev/null +++ b/src/main/java/org/jabref/logic/importer/FetcherClientException.java @@ -0,0 +1,19 @@ +package org.jabref.logic.importer; + +/** + * Should be thrown when you encounter a http status code error >= 400 and < 500 + */ +public class FetcherClientException extends FetcherException { + + public FetcherClientException(String errorMessage, Throwable cause) { + super(errorMessage, cause); + } + + public FetcherClientException(String errorMessage) { + super(errorMessage); + } + + public FetcherClientException(String errorMessage, String localizedMessage, Throwable cause) { + super(errorMessage, localizedMessage, cause); + } +} diff --git a/src/main/java/org/jabref/logic/importer/FetcherServerException.java b/src/main/java/org/jabref/logic/importer/FetcherServerException.java new file mode 100644 index 00000000000..537d71ff530 --- /dev/null +++ b/src/main/java/org/jabref/logic/importer/FetcherServerException.java @@ -0,0 +1,18 @@ +package org.jabref.logic.importer; +/** + * Should be thrown when you encounter a http status code error >= 500 + */ +public class FetcherServerException extends FetcherException { + + public FetcherServerException(String errorMessage, Throwable cause) { + super(errorMessage, cause); + } + + public FetcherServerException(String errorMessage) { + super(errorMessage); + } + + public FetcherServerException(String errorMessage, String localizedMessage, Throwable cause) { + super(errorMessage, localizedMessage, cause); + } +} diff --git a/src/main/java/org/jabref/logic/importer/IdBasedParserFetcher.java b/src/main/java/org/jabref/logic/importer/IdBasedParserFetcher.java index 6be4a8dddfd..e348b69b8d8 100644 --- a/src/main/java/org/jabref/logic/importer/IdBasedParserFetcher.java +++ b/src/main/java/org/jabref/logic/importer/IdBasedParserFetcher.java @@ -81,7 +81,10 @@ default Optional performSearchById(String identifier) throws FetcherEx } catch (URISyntaxException e) { throw new FetcherException("Search URI is malformed", e); } catch (IOException e) { - // TODO: Catch HTTP Response 401 errors and report that user has no rights to access resource. It might be that there is an UnknownHostException (eutils.ncbi.nlm.nih.gov cannot be resolved). + // check for the case where we already have a FetcherException from UrlDownload + if (e.getCause() instanceof FetcherException fe) { + throw fe; + } throw new FetcherException("A network error occurred", e); } catch (ParseException e) { throw new FetcherException("An internal parser error occurred", e); diff --git a/src/main/java/org/jabref/logic/importer/IdParserFetcher.java b/src/main/java/org/jabref/logic/importer/IdParserFetcher.java index bf4b2bb32b9..b9b5ed92e44 100644 --- a/src/main/java/org/jabref/logic/importer/IdParserFetcher.java +++ b/src/main/java/org/jabref/logic/importer/IdParserFetcher.java @@ -88,8 +88,10 @@ default Optional findIdentifier(BibEntry entry) throws FetcherException { LOGGER.debug("Id not found"); return Optional.empty(); } catch (IOException e) { - // TODO: Catch HTTP Response 401 errors and report that user has no rights to access resource - // TODO catch 503 service unavailable and alert user + // check for the case where we already have a FetcherException from UrlDownload + if (e.getCause() instanceof FetcherException fe) { + throw fe; + } throw new FetcherException("An I/O exception occurred", e); } catch (ParseException e) { throw new FetcherException("An internal parser error occurred", e); diff --git a/src/main/java/org/jabref/logic/importer/ImportFormatReader.java b/src/main/java/org/jabref/logic/importer/ImportFormatReader.java index f3b8826171d..b9b6f4b850c 100644 --- a/src/main/java/org/jabref/logic/importer/ImportFormatReader.java +++ b/src/main/java/org/jabref/logic/importer/ImportFormatReader.java @@ -39,7 +39,6 @@ import org.jabref.model.entry.BibEntry; import org.jabref.model.strings.StringUtil; import org.jabref.model.util.FileUpdateMonitor; -import org.jabref.preferences.GeneralPreferences; public class ImportFormatReader { @@ -51,11 +50,9 @@ public class ImportFormatReader { */ private final List formats = new ArrayList<>(); - private GeneralPreferences generalPreferences; private ImportFormatPreferences importFormatPreferences; - public void resetImportFormats(ImporterPreferences importerPreferences, GeneralPreferences generalPreferences, ImportFormatPreferences newImportFormatPreferences, XmpPreferences xmpPreferences, FileUpdateMonitor fileMonitor) { - this.generalPreferences = generalPreferences; + public void resetImportFormats(ImporterPreferences importerPreferences, ImportFormatPreferences newImportFormatPreferences, XmpPreferences xmpPreferences, FileUpdateMonitor fileMonitor) { this.importFormatPreferences = newImportFormatPreferences; formats.clear(); diff --git a/src/main/java/org/jabref/logic/importer/PagedSearchBasedFetcher.java b/src/main/java/org/jabref/logic/importer/PagedSearchBasedFetcher.java index 1b8d31a064b..bafae176485 100644 --- a/src/main/java/org/jabref/logic/importer/PagedSearchBasedFetcher.java +++ b/src/main/java/org/jabref/logic/importer/PagedSearchBasedFetcher.java @@ -52,6 +52,7 @@ default int getPageSize() { * @param luceneQuery the root node of the lucene query * @return a list of {@link BibEntry}, which are matched by the query (may be empty) */ + @Override default List performSearch(QueryNode luceneQuery) throws FetcherException { return new ArrayList<>(performSearchPaged(luceneQuery, 0).getContent()); } diff --git a/src/main/java/org/jabref/logic/importer/fetcher/DoiFetcher.java b/src/main/java/org/jabref/logic/importer/fetcher/DoiFetcher.java index 41afea25707..4f0927ef468 100644 --- a/src/main/java/org/jabref/logic/importer/fetcher/DoiFetcher.java +++ b/src/main/java/org/jabref/logic/importer/fetcher/DoiFetcher.java @@ -72,7 +72,6 @@ public Optional performSearchById(String identifier) throws FetcherExc return new Medra().performSearchById(identifier); } URL doiURL = new URL(doi.get().getURIAsASCIIString()); - // BibTeX data URLDownload download = getUrlDownload(doiURL); download.addHeader("Accept", MediaTypes.APPLICATION_BIBTEX); @@ -80,8 +79,11 @@ public Optional performSearchById(String identifier) throws FetcherExc try { bibtexString = download.asString(); } catch (IOException e) { - // an IOException will be thrown if download is unable to download from the doiURL - throw new FetcherException(Localization.lang("No DOI data exists"), e); + // an IOException with a nested FetcherException will be thrown when you encounter a 400x or 500x http status code + if (e.getCause() instanceof FetcherException fe) { + throw fe; + } + throw e; } // BibTeX entry diff --git a/src/main/java/org/jabref/logic/importer/fetcher/IsbnFetcher.java b/src/main/java/org/jabref/logic/importer/fetcher/IsbnFetcher.java index c97184bd36b..f4dedae3e7c 100644 --- a/src/main/java/org/jabref/logic/importer/fetcher/IsbnFetcher.java +++ b/src/main/java/org/jabref/logic/importer/fetcher/IsbnFetcher.java @@ -50,13 +50,19 @@ public Optional performSearchById(String identifier) throws FetcherExc identifier = NEWLINE_SPACE_PATTERN.matcher(identifier).replaceAll(""); OpenLibraryFetcher openLibraryFetcher = new OpenLibraryFetcher(importFormatPreferences); - Optional bibEntry = openLibraryFetcher.performSearchById(identifier); - // nothing found at OpenLibrary: try ebook.de - if (!bibEntry.isPresent()) { - LOGGER.debug("No entry found at OpenLibrary; trying ebook.de"); - IsbnViaEbookDeFetcher isbnViaEbookDeFetcher = new IsbnViaEbookDeFetcher(importFormatPreferences); - bibEntry = isbnViaEbookDeFetcher.performSearchById(identifier); + Optional bibEntry = Optional.empty(); + try { + bibEntry = openLibraryFetcher.performSearchById(identifier); + } catch (FetcherException ex) { + LOGGER.debug("Got a fetcher exception for IBSN search", ex); + } finally { + // nothing found at OpenLibrary: try ebook.de + if (!bibEntry.isPresent()) { + LOGGER.debug("No entry found at OpenLibrary; trying ebook.de"); + IsbnViaEbookDeFetcher isbnViaEbookDeFetcher = new IsbnViaEbookDeFetcher(importFormatPreferences); + bibEntry = isbnViaEbookDeFetcher.performSearchById(identifier); + } } return bibEntry; diff --git a/src/main/java/org/jabref/logic/importer/fileformat/BibTeXMLImporter.java b/src/main/java/org/jabref/logic/importer/fileformat/BibTeXMLImporter.java index 5fb3d8ed900..65301e2a7a1 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/BibTeXMLImporter.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/BibTeXMLImporter.java @@ -184,7 +184,7 @@ private void parse(T entryType, Map fields) { } else if (isMethodToIgnore(method.getName())) { continue; } else if (method.getName().startsWith("get")) { - putIfValueNotNull(fields, FieldFactory.parseField(method.getName().replace("get", "")), (String) method.invoke(entryType)); + putIfValueNotNull(fields, FieldFactory.parseField(entryType, method.getName().replace("get", "")), (String) method.invoke(entryType)); } } catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException e) { LOGGER.error("Could not invoke method", e); diff --git a/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java b/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java index f3e6e7462f5..666ae01a7fb 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java @@ -387,8 +387,8 @@ private String purge(String context, String stringToPurge) { } // strip empty lines while ((runningIndex < indexOfAt) && - (context.charAt(runningIndex) == '\r' || - context.charAt(runningIndex) == '\n')) { + ((context.charAt(runningIndex) == '\r') || + (context.charAt(runningIndex) == '\n'))) { runningIndex++; } return context.substring(runningIndex); diff --git a/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java b/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java index 2ff451e6dca..afbe5af657d 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java @@ -14,6 +14,7 @@ import org.jabref.model.entry.Author; import org.jabref.model.entry.AuthorList; import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.BiblatexSoftwareField; import org.jabref.model.entry.field.Field; import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.field.UnknownField; @@ -95,7 +96,7 @@ public ParserResult importDatabase(BufferedReader reader) throws IOException { StandardEntryType entryType = StandardEntryType.Software; // Map CFF fields to JabRef Fields - HashMap fieldMap = getFieldMappings(); + HashMap fieldMap = getFieldMappings(); for (Map.Entry property : citation.values.entrySet()) { if (fieldMap.containsKey(property.getKey())) { entryMap.put(fieldMap.get(property.getKey()), property.getValue()); @@ -120,7 +121,7 @@ public ParserResult importDatabase(BufferedReader reader) throws IOException { entryMap.put(StandardField.AUTHOR, authorStr); // Select DOI to keep - if (entryMap.get(StandardField.DOI) == null && citation.ids != null) { + if ((entryMap.get(StandardField.DOI) == null) && (citation.ids != null)) { List doiIds = citation.ids.stream() .filter(id -> id.type.equals("doi")) .collect(Collectors.toList()); @@ -137,14 +138,14 @@ public ParserResult importDatabase(BufferedReader reader) throws IOException { .collect(Collectors.toList()); if (swhIds.size() == 1) { - entryMap.put(StandardField.SWHID, swhIds.get(0)); + entryMap.put(BiblatexSoftwareField.SWHID, swhIds.get(0)); } else if (swhIds.size() > 1) { List relSwhIds = swhIds.stream() .filter(id -> id.split(":").length > 3) // quick filter for invalid swhids .filter(id -> id.split(":")[2].equals("rel")) .collect(Collectors.toList()); if (relSwhIds.size() == 1) { - entryMap.put(StandardField.SWHID, relSwhIds.get(0)); + entryMap.put(BiblatexSoftwareField.SWHID, relSwhIds.get(0)); } } } @@ -166,19 +167,19 @@ public boolean isRecognizedFormat(BufferedReader reader) throws IOException { try { citation = mapper.readValue(reader, CffFormat.class); - return citation != null && citation.values.get("title") != null; + return (citation != null) && (citation.values.get("title") != null); } catch (IOException e) { return false; } } - private HashMap getFieldMappings() { - HashMap fieldMappings = new HashMap<>(); + private HashMap getFieldMappings() { + HashMap fieldMappings = new HashMap<>(); fieldMappings.put("title", StandardField.TITLE); fieldMappings.put("version", StandardField.VERSION); fieldMappings.put("doi", StandardField.DOI); - fieldMappings.put("license", StandardField.LICENSE); - fieldMappings.put("repository", StandardField.REPOSITORY); + fieldMappings.put("license", BiblatexSoftwareField.LICENSE); + fieldMappings.put("repository", BiblatexSoftwareField.REPOSITORY); fieldMappings.put("url", StandardField.URL); fieldMappings.put("abstract", StandardField.ABSTRACT); fieldMappings.put("message", StandardField.COMMENT); diff --git a/src/main/java/org/jabref/logic/importer/fileformat/CopacImporter.java b/src/main/java/org/jabref/logic/importer/fileformat/CopacImporter.java index ab857becd0a..ab638ae4cf3 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/CopacImporter.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/CopacImporter.java @@ -137,7 +137,7 @@ public ParserResult importDatabase(BufferedReader reader) throws IOException { } else if ("DT- ".equals(code)) { setOrAppend(b, new UnknownField("documenttype"), line.substring(4).trim(), ", "); } else { - setOrAppend(b, FieldFactory.parseField(code.substring(0, 2)), line.substring(4).trim(), ", "); + setOrAppend(b, FieldFactory.parseField(StandardEntryType.Book, line.substring(0, 2)), line.substring(4).trim(), ", "); } } results.add(b); diff --git a/src/main/java/org/jabref/logic/importer/fileformat/IsiImporter.java b/src/main/java/org/jabref/logic/importer/fileformat/IsiImporter.java index b502977f716..2845756b770 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/IsiImporter.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/IsiImporter.java @@ -281,7 +281,7 @@ public ParserResult importDatabase(BufferedReader reader) throws IOException { if ("ER".equals(beg) || "EF".equals(beg) || "VR".equals(beg) || "FN".equals(beg)) { continue; } - hm.put(FieldFactory.parseField(beg), value); + hm.put(FieldFactory.parseField(type, beg), value); } } diff --git a/src/main/java/org/jabref/logic/importer/fileformat/MedlineImporter.java b/src/main/java/org/jabref/logic/importer/fileformat/MedlineImporter.java index 392fad9772b..b9e9eac39d4 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/MedlineImporter.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/MedlineImporter.java @@ -425,7 +425,7 @@ private void addArticleIdList(Map fields, ArticleIdList articleId if ("pubmed".equals(id.getIdType())) { fields.put(StandardField.PMID, id.getContent()); } else { - fields.put(FieldFactory.parseField(id.getIdType()), id.getContent()); + fields.put(FieldFactory.parseField(StandardEntryType.Article, id.getIdType()), id.getContent()); } } } @@ -499,7 +499,7 @@ private void addKeyWords(Map fields, List allKeyword private void addOtherId(Map fields, List otherID) { for (OtherID id : otherID) { if ((id.getSource() != null) && (id.getContent() != null)) { - fields.put(FieldFactory.parseField(id.getSource()), id.getContent()); + fields.put(FieldFactory.parseField(StandardEntryType.Article, id.getSource()), id.getContent()); } } } diff --git a/src/main/java/org/jabref/logic/importer/util/MetaDataParser.java b/src/main/java/org/jabref/logic/importer/util/MetaDataParser.java index eb1e905adca..5f3d5e29c52 100644 --- a/src/main/java/org/jabref/logic/importer/util/MetaDataParser.java +++ b/src/main/java/org/jabref/logic/importer/util/MetaDataParser.java @@ -65,6 +65,7 @@ public MetaData parse(MetaData metaData, Map data, Character key String user = entry.getKey().substring(MetaData.FILE_DIRECTORY.length() + 1); metaData.setUserFileDirectory(user, getSingleItem(value)); } else if (entry.getKey().startsWith(MetaData.SELECTOR_META_PREFIX)) { + // edge case, it might be one special field e.g. article from biblatex-apa, but we can't distinguish this from any other field and rather prefer to handle it as UnknownField metaData.addContentSelector(ContentSelectors.parse(FieldFactory.parseField(entry.getKey().substring(MetaData.SELECTOR_META_PREFIX.length())), StringUtil.unquote(entry.getValue(), MetaData.ESCAPE_CHARACTER))); } else if (entry.getKey().startsWith(MetaData.FILE_DIRECTORY + "Latex-")) { // The user name comes directly after "FILE_DIRECTORYLatex-" diff --git a/src/main/java/org/jabref/logic/layout/format/NameFormatter.java b/src/main/java/org/jabref/logic/layout/format/NameFormatter.java index 919ff4df2d5..e8812235435 100644 --- a/src/main/java/org/jabref/logic/layout/format/NameFormatter.java +++ b/src/main/java/org/jabref/logic/layout/format/NameFormatter.java @@ -5,7 +5,7 @@ import java.util.Map; import java.util.Objects; -import org.jabref.logic.bst.BibtexNameFormatter; +import org.jabref.logic.bst.util.BstNameFormatter; import org.jabref.logic.layout.LayoutFormatter; import org.jabref.model.entry.AuthorList; @@ -86,7 +86,7 @@ private static String format(String toFormat, AuthorList al, String[] formats) { for (int i = 1; i <= al.getNumberOfAuthors(); i++) { for (int j = 1; j < formats.length; j += 2) { if ("*".equals(formats[j])) { - sb.append(BibtexNameFormatter.formatName(toFormat, i, formats[j + 1], null)); + sb.append(BstNameFormatter.formatName(toFormat, i, formats[j + 1])); break; } else { String[] range = formats[j].split("\\.\\."); @@ -112,7 +112,7 @@ private static String format(String toFormat, AuthorList al, String[] formats) { } if ((s <= i) && (i <= e)) { - sb.append(BibtexNameFormatter.formatName(toFormat, i, formats[j + 1], null)); + sb.append(BstNameFormatter.formatName(toFormat, i, formats[j + 1])); break; } } diff --git a/src/main/java/org/jabref/logic/net/URLDownload.java b/src/main/java/org/jabref/logic/net/URLDownload.java index 77ac1ff3093..c499dcf3635 100644 --- a/src/main/java/org/jabref/logic/net/URLDownload.java +++ b/src/main/java/org/jabref/logic/net/URLDownload.java @@ -40,6 +40,8 @@ import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; +import org.jabref.logic.importer.FetcherClientException; +import org.jabref.logic.importer.FetcherServerException; import org.jabref.logic.util.io.FileUtil; import org.jabref.model.util.FileHelper; @@ -350,21 +352,23 @@ private URLConnection openConnection() throws IOException { if (connection instanceof HttpURLConnection) { // normally, 3xx is redirect int status = ((HttpURLConnection) connection).getResponseCode(); - if (status != HttpURLConnection.HTTP_OK) { - if ((status == HttpURLConnection.HTTP_MOVED_TEMP) - || (status == HttpURLConnection.HTTP_MOVED_PERM) - || (status == HttpURLConnection.HTTP_SEE_OTHER)) { - // get redirect url from "location" header field - String newUrl = connection.getHeaderField("location"); - // open the new connnection again - connection = new URLDownload(newUrl).openConnection(); - } + + if ((status == HttpURLConnection.HTTP_MOVED_TEMP) + || (status == HttpURLConnection.HTTP_MOVED_PERM) + || (status == HttpURLConnection.HTTP_SEE_OTHER)) { + // get redirect url from "location" header field + String newUrl = connection.getHeaderField("location"); + // open the new connection again + connection = new URLDownload(newUrl).openConnection(); + } + if ((status >= 400) && (status < 500)) { + throw new IOException(new FetcherClientException("Encountered HTTP Status code " + status)); + } + if (status >= 500) { + throw new IOException(new FetcherServerException("Encountered HTTP Status Code " + status)); } } - // this does network i/o: GET + read returned headers - connection.connect(); - return connection; } diff --git a/src/main/java/org/jabref/logic/shared/prefs/SharedDatabasePreferences.java b/src/main/java/org/jabref/logic/shared/prefs/SharedDatabasePreferences.java index 260ec297c24..4b1a14fe1c9 100644 --- a/src/main/java/org/jabref/logic/shared/prefs/SharedDatabasePreferences.java +++ b/src/main/java/org/jabref/logic/shared/prefs/SharedDatabasePreferences.java @@ -25,6 +25,8 @@ public class SharedDatabasePreferences { private static final String SHARED_DATABASE_NAME = "sharedDatabaseName"; private static final String SHARED_DATABASE_USER = "sharedDatabaseUser"; private static final String SHARED_DATABASE_PASSWORD = "sharedDatabasePassword"; + private static final String SHARED_DATABASE_FOLDER = "sharedDatabaseFolder"; + private static final String SHARED_DATABASE_AUTOSAVE = "sharedDatabaseAutosave"; private static final String SHARED_DATABASE_REMEMBER_PASSWORD = "sharedDatabaseRememberPassword"; private static final String SHARED_DATABASE_USE_SSL = "sharedDatabaseUseSSL"; private static final String SHARED_DATABASE_KEYSTORE_FILE = "sharedDatabaseKeyStoreFile"; @@ -77,6 +79,14 @@ public boolean getRememberPassword() { return internalPrefs.getBoolean(SHARED_DATABASE_REMEMBER_PASSWORD, false); } + public Optional getFolder() { + return getOptionalValue(SHARED_DATABASE_FOLDER); + } + + public boolean getAutosave() { + return internalPrefs.getBoolean(SHARED_DATABASE_AUTOSAVE, false); + } + public boolean isUseSSL() { return internalPrefs.getBoolean(SHARED_DATABASE_USE_SSL, false); } @@ -109,6 +119,14 @@ public void setRememberPassword(boolean rememberPassword) { internalPrefs.putBoolean(SHARED_DATABASE_REMEMBER_PASSWORD, rememberPassword); } + public void setFolder(String folder) { + internalPrefs.put(SHARED_DATABASE_FOLDER, folder); + } + + public void setAutosave(boolean autosave) { + internalPrefs.putBoolean(SHARED_DATABASE_AUTOSAVE, autosave); + } + public void setUseSSL(boolean useSSL) { internalPrefs.putBoolean(SHARED_DATABASE_USE_SSL, useSSL); } diff --git a/src/main/java/org/jabref/model/database/BibDatabase.java b/src/main/java/org/jabref/model/database/BibDatabase.java index 72a559ff663..52e4e841ce9 100644 --- a/src/main/java/org/jabref/model/database/BibDatabase.java +++ b/src/main/java/org/jabref/model/database/BibDatabase.java @@ -20,6 +20,7 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import javafx.collections.ObservableSet; import org.jabref.logic.bibtex.FieldWriter; import org.jabref.model.database.event.EntriesAddedEvent; @@ -29,6 +30,7 @@ import org.jabref.model.entry.Month; import org.jabref.model.entry.event.EntriesEventSource; import org.jabref.model.entry.event.EntryChangedEvent; +import org.jabref.model.entry.event.FieldAddedOrRemovedEvent; import org.jabref.model.entry.event.FieldChangedEvent; import org.jabref.model.entry.field.Field; import org.jabref.model.entry.field.FieldFactory; @@ -52,6 +54,8 @@ public class BibDatabase { * State attributes */ private final ObservableList entries = FXCollections.synchronizedObservableList(FXCollections.observableArrayList(BibEntry::getObservables)); + + private final ObservableSet visibleFields = FXCollections.observableSet(); private Map bibtexStrings = new ConcurrentHashMap<>(); private final EventBus eventBus = new EventBus(); @@ -136,13 +140,8 @@ public ObservableList getEntries() { * * @return set of fieldnames, that are visible */ - public Set getAllVisibleFields() { - Set allFields = new TreeSet<>(Comparator.comparing(Field::getName)); - for (BibEntry e : getEntries()) { - allFields.addAll(e.getFields()); - } - return allFields.stream().filter(field -> !FieldFactory.isInternalField(field)) - .collect(Collectors.toSet()); + public ObservableSet getAllVisibleFields() { + return visibleFields; } /** @@ -214,6 +213,8 @@ public synchronized void insertEntries(List newEntries, EntriesEventSo eventBus.post(new EntriesAddedEvent(newEntries, newEntries.get(0), eventSource)); } entries.addAll(newEntries); + + updateVisibleFields(); } public synchronized void removeEntry(BibEntry bibEntry) { @@ -251,6 +252,7 @@ public synchronized void removeEntries(List toBeDeleted, EntriesEventS boolean anyRemoved = entries.removeIf(entry -> ids.contains(entry.getId())); if (anyRemoved) { eventBus.post(new EntriesRemovedEvent(toBeDeleted, eventSource)); + updateVisibleFields(); } } @@ -584,6 +586,30 @@ private void relayEntryChangeEvent(FieldChangedEvent event) { eventBus.post(event); } + @Subscribe + private void listen(FieldAddedOrRemovedEvent event) { + // When a field is removed from an entry we can't tell if it's + // still present in other entries, and thus we can't remove it + // from the set of visible fields. However, when a new field is added + // to any entry, we can simply add it to the set because we're + // going to add it whether other entries have it or not + boolean isAdded = visibleFields.add(event.getField()); + if (!isAdded) { + updateVisibleFields(); + } + } + + private void updateVisibleFields() { + visibleFields.clear(); + Set allFields = new TreeSet<>(Comparator.comparing(Field::getName)); + for (BibEntry e : getEntries()) { + allFields.addAll(e.getFields()); + } + visibleFields.addAll(allFields.stream().filter(field -> !FieldFactory.isInternalField(field)) + .filter(field -> StringUtil.isNotBlank(field.getName())) + .collect(Collectors.toSet())); + } + public Optional getReferencedEntry(BibEntry entry) { return entry.getField(StandardField.CROSSREF).flatMap(this::getEntryByCitationKey); } diff --git a/src/main/java/org/jabref/model/entry/BibEntryTypesManager.java b/src/main/java/org/jabref/model/entry/BibEntryTypesManager.java index b737596abd6..836cd39dac8 100644 --- a/src/main/java/org/jabref/model/entry/BibEntryTypesManager.java +++ b/src/main/java/org/jabref/model/entry/BibEntryTypesManager.java @@ -11,6 +11,7 @@ import org.jabref.model.database.BibDatabaseMode; import org.jabref.model.entry.field.BibField; import org.jabref.model.entry.field.FieldFactory; +import org.jabref.model.entry.types.BiblatexAPAEntryTypeDefinitions; import org.jabref.model.entry.types.BiblatexEntryTypeDefinitions; import org.jabref.model.entry.types.BiblatexSoftwareEntryTypeDefinitions; import org.jabref.model.entry.types.BibtexEntryTypeDefinitions; @@ -21,7 +22,7 @@ public class BibEntryTypesManager { public static final String ENTRYTYPE_FLAG = "jabref-entrytype: "; private final InternalEntryTypes BIBTEX = new InternalEntryTypes(Stream.concat(BibtexEntryTypeDefinitions.ALL.stream(), IEEETranEntryTypeDefinitions.ALL.stream()).collect(Collectors.toList())); - private final InternalEntryTypes BIBLATEX = new InternalEntryTypes(Stream.concat(BiblatexEntryTypeDefinitions.ALL.stream(), BiblatexSoftwareEntryTypeDefinitions.ALL.stream()).collect(Collectors.toList())); + private final InternalEntryTypes BIBLATEX = new InternalEntryTypes(Stream.concat(BiblatexEntryTypeDefinitions.ALL.stream(), Stream.concat(BiblatexSoftwareEntryTypeDefinitions.ALL.stream(), BiblatexAPAEntryTypeDefinitions.ALL.stream())).collect(Collectors.toList())); public BibEntryTypesManager() { } @@ -99,6 +100,7 @@ public List getAllCustomTypes(BibDatabaseMode mode) { return customizedTypes.stream() .filter(entryType -> BiblatexEntryTypeDefinitions.ALL.stream().noneMatch(biblatexType -> biblatexType.getType().equals(entryType.getType()))) .filter(entryType -> BiblatexSoftwareEntryTypeDefinitions.ALL.stream().noneMatch(biblatexSoftware -> biblatexSoftware.getType().equals(entryType.getType()))) + .filter(entryType -> BiblatexAPAEntryTypeDefinitions.ALL.stream().noneMatch(biblatexAPA -> biblatexAPA.getType().equals(entryType.getType()))) .collect(Collectors.toList()); } } diff --git a/src/main/java/org/jabref/model/entry/field/BiblatexApaField.java b/src/main/java/org/jabref/model/entry/field/BiblatexApaField.java new file mode 100644 index 00000000000..b2938963efe --- /dev/null +++ b/src/main/java/org/jabref/model/entry/field/BiblatexApaField.java @@ -0,0 +1,82 @@ +package org.jabref.model.entry.field; + +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Optional; +import java.util.Set; + +import org.jabref.model.entry.types.BiblatexApaEntryType; + +public enum BiblatexApaField implements Field { + + AMENDMENT("amendment"), + ARTICLE("article"), + CITATION("citation"), + CITATION_CITEORG("citation_citeorg"), + CITATION_CITEDATE("citation_citedate", FieldProperty.DATE), + CITATION_CITEINFO("citation_citeinfo"), + SECTION("section", FieldProperty.NUMERIC), + SOURCE("source"); + + private final String name; + private final String displayName; + private final Set properties; + + BiblatexApaField(String name) { + this.name = name; + this.displayName = null; + this.properties = EnumSet.noneOf(FieldProperty.class); + } + + BiblatexApaField(String name, String displayName) { + this.name = name; + this.displayName = displayName; + this.properties = EnumSet.noneOf(FieldProperty.class); + } + + BiblatexApaField(String name, String displayName, FieldProperty first, FieldProperty... rest) { + this.name = name; + this.displayName = displayName; + this.properties = EnumSet.of(first, rest); + } + + BiblatexApaField(String name, FieldProperty first, FieldProperty... rest) { + this.name = name; + this.displayName = null; + this.properties = EnumSet.of(first, rest); + } + + public static Optional fromName(T type, String name) { + if (!(type instanceof BiblatexApaEntryType)) { + return Optional.empty(); + } + return Arrays.stream(BiblatexApaField.values()) + .filter(field -> field.getName().equalsIgnoreCase(name)) + .findAny(); + } + + @Override + public Set getProperties() { + return Collections.unmodifiableSet(properties); + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean isStandardField() { + return false; + } + + @Override + public String getDisplayName() { + if (displayName == null) { + return Field.super.getDisplayName(); + } else { + return displayName; + } + } +} diff --git a/src/main/java/org/jabref/model/entry/field/BiblatexSoftwareField.java b/src/main/java/org/jabref/model/entry/field/BiblatexSoftwareField.java new file mode 100644 index 00000000000..fc3bb4dcc48 --- /dev/null +++ b/src/main/java/org/jabref/model/entry/field/BiblatexSoftwareField.java @@ -0,0 +1,82 @@ +package org.jabref.model.entry.field; + +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Optional; +import java.util.Set; + +import org.jabref.model.entry.types.BiblatexSoftwareEntryType; + +public enum BiblatexSoftwareField implements Field { + + HALID("hal_id"), + HALVERSION("hal_version"), + INTRODUCEDIN("introducedin"), + LICENSE("license"), + RELATEDTYPE("relatedtype"), + RELATEDSTRING("relatedstring"), + REPOSITORY("repository"), + SWHID("swhid"); + + private final String name; + private final String displayName; + private final Set properties; + + BiblatexSoftwareField(String name) { + this.name = name; + this.displayName = null; + this.properties = EnumSet.noneOf(FieldProperty.class); + } + + BiblatexSoftwareField(String name, String displayName) { + this.name = name; + this.displayName = displayName; + this.properties = EnumSet.noneOf(FieldProperty.class); + } + + BiblatexSoftwareField(String name, String displayName, FieldProperty first, FieldProperty... rest) { + this.name = name; + this.displayName = displayName; + this.properties = EnumSet.of(first, rest); + } + + BiblatexSoftwareField(String name, FieldProperty first, FieldProperty... rest) { + this.name = name; + this.displayName = null; + this.properties = EnumSet.of(first, rest); + } + + public static Optional fromName(T type, String name) { + if (!(type instanceof BiblatexSoftwareEntryType)) { + return Optional.empty(); + } + return Arrays.stream(BiblatexSoftwareField.values()) + .filter(field -> field.getName().equalsIgnoreCase(name)) + .findAny(); + } + + @Override + public Set getProperties() { + return Collections.unmodifiableSet(properties); + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean isStandardField() { + return false; + } + + @Override + public String getDisplayName() { + if (displayName == null) { + return Field.super.getDisplayName(); + } else { + return displayName; + } + } +} diff --git a/src/main/java/org/jabref/model/entry/field/FieldFactory.java b/src/main/java/org/jabref/model/entry/field/FieldFactory.java index b1c2f3b99b9..07a34b039d3 100644 --- a/src/main/java/org/jabref/model/entry/field/FieldFactory.java +++ b/src/main/java/org/jabref/model/entry/field/FieldFactory.java @@ -73,13 +73,23 @@ public static String serializeFieldsList(Collection fields) { .collect(Collectors.joining(DELIMITER)); } + public static Field parseField(T type, String fieldName) { + return OptionalUtil.orElse( + OptionalUtil.orElse( + OptionalUtil.orElse( + OptionalUtil.orElse( + OptionalUtil.orElse( + InternalField.fromName(fieldName), + StandardField.fromName(fieldName)), + SpecialField.fromName(fieldName)), + IEEEField.fromName(fieldName)), + BiblatexSoftwareField.fromName(type, fieldName)), + BiblatexApaField.fromName(type, fieldName)) + .orElse(new UnknownField(fieldName)); + } + public static Field parseField(String fieldName) { - return OptionalUtil.orElse(OptionalUtil.orElse(OptionalUtil.orElse( - InternalField.fromName(fieldName), - StandardField.fromName(fieldName)), - SpecialField.fromName(fieldName)), - IEEEField.fromName(fieldName)) - .orElse(new UnknownField(fieldName)); + return parseField(null, fieldName); } public static Set getKeyFields() { @@ -138,6 +148,8 @@ private static Set getFieldsFiltered(Predicate selector) { private static Set getAllFields() { Set fields = new HashSet<>(); + fields.addAll(EnumSet.allOf(BiblatexApaField.class)); + fields.addAll(EnumSet.allOf(BiblatexSoftwareField.class)); fields.addAll(EnumSet.allOf(IEEEField.class)); fields.addAll(EnumSet.allOf(InternalField.class)); fields.addAll(EnumSet.allOf(SpecialField.class)); diff --git a/src/main/java/org/jabref/model/entry/field/IEEEField.java b/src/main/java/org/jabref/model/entry/field/IEEEField.java index d00bec02eee..f56ce79ea99 100644 --- a/src/main/java/org/jabref/model/entry/field/IEEEField.java +++ b/src/main/java/org/jabref/model/entry/field/IEEEField.java @@ -31,7 +31,7 @@ public enum IEEEField implements Field { this.properties = EnumSet.of(first, rest); } - public static Optional fromName(String name) { + public static Optional fromName(String name) { return Arrays.stream(IEEEField.values()) .filter(field -> field.getName().equalsIgnoreCase(name)) .findAny(); diff --git a/src/main/java/org/jabref/model/entry/field/SpecialFieldValue.java b/src/main/java/org/jabref/model/entry/field/SpecialFieldValue.java index d4c1f8df7d5..6c3337b41f6 100644 --- a/src/main/java/org/jabref/model/entry/field/SpecialFieldValue.java +++ b/src/main/java/org/jabref/model/entry/field/SpecialFieldValue.java @@ -39,6 +39,7 @@ public enum SpecialFieldValue { public static SpecialFieldValue getRating(int ranking) { return switch (ranking) { + case 0 -> CLEAR_RANK; case 1 -> RANK_1; case 2 -> RANK_2; case 3 -> RANK_3; @@ -58,6 +59,7 @@ public Optional getFieldValue() { public int toRating() { return switch (this) { + case CLEAR_RANK -> 0; case RANK_1 -> 1; case RANK_2 -> 2; case RANK_3 -> 3; diff --git a/src/main/java/org/jabref/model/entry/field/StandardField.java b/src/main/java/org/jabref/model/entry/field/StandardField.java index ef3646802e0..bcf9b710b7d 100644 --- a/src/main/java/org/jabref/model/entry/field/StandardField.java +++ b/src/main/java/org/jabref/model/entry/field/StandardField.java @@ -59,14 +59,11 @@ public enum StandardField implements Field { FOREWORD("foreword", FieldProperty.PERSON_NAMES), FOLDER("folder"), GENDER("gender", FieldProperty.GENDER), - HALID("hal_id"), - HALVERSION("hal_version"), HOLDER("holder", FieldProperty.PERSON_NAMES), HOWPUBLISHED("howpublished"), IDS("ids", FieldProperty.MULTIPLE_ENTRY_LINK), INSTITUTION("institution"), INTRODUCTION("introduction", FieldProperty.PERSON_NAMES), - INTRODUCEDIN("introducedin"), ISBN("isbn", "ISBN", FieldProperty.ISBN), ISRN("isrn", "ISRN"), ISSN("issn", "ISSN"), @@ -81,7 +78,6 @@ public enum StandardField implements Field { LANGUAGE("language", FieldProperty.LANGUAGE), LABEL("label"), LIBRARY("library"), - LICENSE("license"), LOCATION("location"), MAINSUBTITLE("mainsubtitle", FieldProperty.BOOK_NAME), MAINTITLE("maintitle", FieldProperty.BOOK_NAME), @@ -106,10 +102,7 @@ public enum StandardField implements Field { PUBSTATE("pubstate", FieldProperty.PUBLICATION_STATE), PRIMARYCLASS("primaryclass"), RELATED("related", FieldProperty.MULTIPLE_ENTRY_LINK), - RELATEDTYPE("relatedtype"), - RELATEDSTRING("relatedstring"), REPORTNO("reportno"), - REPOSITORY("repository"), REVIEW("review"), REVISION("revision"), SCHOOL("school"), @@ -120,7 +113,6 @@ public enum StandardField implements Field { SORTKEY("sortkey"), SORTNAME("sortname", FieldProperty.PERSON_NAMES), SUBTITLE("subtitle"), - SWHID("swhid"), TITLE("title"), TITLEADDON("titleaddon"), TRANSLATOR("translator", FieldProperty.PERSON_NAMES), diff --git a/src/main/java/org/jabref/model/entry/types/BiblatexAPAEntryTypeDefinitions.java b/src/main/java/org/jabref/model/entry/types/BiblatexAPAEntryTypeDefinitions.java new file mode 100644 index 00000000000..f5c0b89b0be --- /dev/null +++ b/src/main/java/org/jabref/model/entry/types/BiblatexAPAEntryTypeDefinitions.java @@ -0,0 +1,43 @@ +package org.jabref.model.entry.types; + +import java.util.Arrays; +import java.util.List; + +import org.jabref.model.entry.BibEntryType; +import org.jabref.model.entry.BibEntryTypeBuilder; +import org.jabref.model.entry.field.BiblatexApaField; +import org.jabref.model.entry.field.StandardField; + +public class BiblatexAPAEntryTypeDefinitions { + + private static final BibEntryType JURISDICTION = new BibEntryTypeBuilder() + .withType(BiblatexApaEntryType.Jurisdiction) + .withImportantFields(StandardField.ORGANIZATION, BiblatexApaField.CITATION_CITEORG, BiblatexApaField.CITATION_CITEDATE, BiblatexApaField.CITATION_CITEDATE, StandardField.ORIGDATE) + .withRequiredFields(StandardField.TITLE, BiblatexApaField.CITATION, BiblatexApaField.CITATION_CITEINFO, StandardField.URL, StandardField.DATE) + .build(); + + private static final BibEntryType LEGISLATION = new BibEntryTypeBuilder() + .withType(BiblatexApaEntryType.Legislation) + .withImportantFields(StandardField.TITLEADDON, StandardField.ORIGDATE) + .withRequiredFields(StandardField.TITLE, StandardField.LOCATION, StandardField.URL, StandardField.DATE) + .build(); + + private static final BibEntryType LEGADMINMATERIAL = new BibEntryTypeBuilder() + .withType(BiblatexApaEntryType.Legadminmaterial) + .withImportantFields(StandardField.NUMBER, StandardField.SHORTTITLE, StandardField.NOTE, StandardField.KEYWORDS) + .withRequiredFields(StandardField.TITLE, BiblatexApaField.CITATION, StandardField.URL, StandardField.DATE) + .build(); + + private static final BibEntryType CONSTITUTION = new BibEntryTypeBuilder() + .withType(BiblatexApaEntryType.Constitution) + .withImportantFields(BiblatexApaField.ARTICLE, BiblatexApaField.AMENDMENT, StandardField.EVENTDATE, StandardField.KEYWORDS, StandardField.PART, BiblatexApaField.SECTION) + .withRequiredFields(BiblatexApaField.SOURCE, StandardField.TYPE) + .build(); + + private static final BibEntryType LEGAL = new BibEntryTypeBuilder() + .withType(BiblatexApaEntryType.Legal) + .withRequiredFields(StandardField.TITLE, StandardField.DATE, StandardField.URI, StandardField.KEYWORDS, StandardField.PART, BiblatexApaField.SECTION) + .build(); + + public static final List ALL = Arrays.asList(JURISDICTION, LEGISLATION, LEGADMINMATERIAL, CONSTITUTION, LEGAL); +} diff --git a/src/main/java/org/jabref/model/entry/types/BiblatexApaEntryType.java b/src/main/java/org/jabref/model/entry/types/BiblatexApaEntryType.java new file mode 100644 index 00000000000..6a1d8d3abe2 --- /dev/null +++ b/src/main/java/org/jabref/model/entry/types/BiblatexApaEntryType.java @@ -0,0 +1,36 @@ +package org.jabref.model.entry.types; + +import java.util.Arrays; +import java.util.Locale; +import java.util.Optional; + +public enum BiblatexApaEntryType implements EntryType { + + Legislation("Legislation"), + Legadminmaterial("Legadminmaterial"), + Jurisdiction("Jurisdiction"), + Constitution("Constitution"), + Legal("Legal"); + + private final String displayName; + + BiblatexApaEntryType(String displayName) { + this.displayName = displayName; + } + + @Override + public String getName() { + return displayName.toLowerCase(Locale.ENGLISH); + } + + @Override + public String getDisplayName() { + return displayName; + } + + public static Optional fromName(String name) { + return Arrays.stream(BiblatexApaEntryType.values()) + .filter(field -> field.getName().equalsIgnoreCase(name)) + .findAny(); + } +} diff --git a/src/main/java/org/jabref/model/entry/types/BiblatexSoftwareEntryType.java b/src/main/java/org/jabref/model/entry/types/BiblatexSoftwareEntryType.java new file mode 100644 index 00000000000..041c48e592a --- /dev/null +++ b/src/main/java/org/jabref/model/entry/types/BiblatexSoftwareEntryType.java @@ -0,0 +1,35 @@ +package org.jabref.model.entry.types; + +import java.util.Arrays; +import java.util.Locale; +import java.util.Optional; + +public enum BiblatexSoftwareEntryType implements EntryType { + + Dataset("Dataset"), + SoftwareVersion("SoftwareVersion"), + SoftwareModule("SoftwareModule"), + CodeFragment("CodeFragment"); + + private final String displayName; + + BiblatexSoftwareEntryType(String displayName) { + this.displayName = displayName; + } + + public static Optional fromName(String name) { + return Arrays.stream(BiblatexSoftwareEntryType.values()) + .filter(field -> field.getName().equalsIgnoreCase(name)) + .findAny(); + } + + @Override + public String getName() { + return displayName.toLowerCase(Locale.ENGLISH); + } + + @Override + public String getDisplayName() { + return displayName; + } +} diff --git a/src/main/java/org/jabref/model/entry/types/BiblatexSoftwareEntryTypeDefinitions.java b/src/main/java/org/jabref/model/entry/types/BiblatexSoftwareEntryTypeDefinitions.java index 0fc48e6d904..6a2e7f0851e 100644 --- a/src/main/java/org/jabref/model/entry/types/BiblatexSoftwareEntryTypeDefinitions.java +++ b/src/main/java/org/jabref/model/entry/types/BiblatexSoftwareEntryTypeDefinitions.java @@ -5,6 +5,7 @@ import org.jabref.model.entry.BibEntryType; import org.jabref.model.entry.BibEntryTypeBuilder; +import org.jabref.model.entry.field.BiblatexSoftwareField; import org.jabref.model.entry.field.OrFields; import org.jabref.model.entry.field.StandardField; @@ -12,40 +13,40 @@ public class BiblatexSoftwareEntryTypeDefinitions { private static final BibEntryType SOFTWARE = new BibEntryTypeBuilder() .withType(StandardEntryType.Software) .withImportantFields(StandardField.DATE, StandardField.DOI, StandardField.EPRINTTYPE, StandardField.EPRINTCLASS, StandardField.EPRINT, - StandardField.EDITOR, StandardField.FILE, StandardField.HALID, StandardField.HALVERSION, StandardField.INSTITUTION, StandardField.INTRODUCEDIN, - StandardField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, StandardField.PUBLISHER, StandardField.RELATED, - StandardField.RELATEDSTRING, StandardField.REPOSITORY, StandardField.SWHID, StandardField.URLDATE, StandardField.VERSION) + StandardField.EDITOR, StandardField.FILE, BiblatexSoftwareField.HALID, BiblatexSoftwareField.HALVERSION, StandardField.INSTITUTION, BiblatexSoftwareField.INTRODUCEDIN, + BiblatexSoftwareField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, StandardField.PUBLISHER, StandardField.RELATED, + BiblatexSoftwareField.RELATEDSTRING, BiblatexSoftwareField.REPOSITORY, BiblatexSoftwareField.SWHID, StandardField.URLDATE, StandardField.VERSION) .withRequiredFields(new OrFields(StandardField.AUTHOR, StandardField.EDITOR), StandardField.TITLE, StandardField.URL, StandardField.VERSION, StandardField.YEAR) .build(); private static final BibEntryType SOFTWAREVERSION = new BibEntryTypeBuilder() - .withType(StandardEntryType.SoftwareVersion) - .withImportantFields(StandardField.DATE, StandardField.EPRINTCLASS, StandardField.EPRINTTYPE, StandardField.HALID, StandardField.HALVERSION, - StandardField.INSTITUTION, StandardField.INTRODUCEDIN, StandardField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, - StandardField.PUBLISHER, StandardField.RELATED, StandardField.RELATEDTYPE, StandardField.RELATEDSTRING, - StandardField.REPOSITORY, StandardField.SWHID, StandardField.SUBTITLE, StandardField.URLDATE) + .withType(BiblatexSoftwareEntryType.SoftwareVersion) + .withImportantFields(StandardField.DATE, StandardField.EPRINTCLASS, StandardField.EPRINTTYPE, BiblatexSoftwareField.HALID, BiblatexSoftwareField.HALVERSION, + StandardField.INSTITUTION, BiblatexSoftwareField.INTRODUCEDIN, BiblatexSoftwareField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, + StandardField.PUBLISHER, StandardField.RELATED, BiblatexSoftwareField.RELATEDTYPE, BiblatexSoftwareField.RELATEDSTRING, + BiblatexSoftwareField.REPOSITORY, BiblatexSoftwareField.SWHID, StandardField.SUBTITLE, StandardField.URLDATE) .withRequiredFields(new OrFields(StandardField.AUTHOR, StandardField.EDITOR), StandardField.TITLE, StandardField.URL, StandardField.YEAR, StandardField.VERSION) - .withDetailFields(StandardField.DATE, StandardField.EPRINTCLASS, StandardField.EPRINTTYPE, StandardField.HALID, StandardField.HALVERSION, - StandardField.INSTITUTION, StandardField.INTRODUCEDIN, StandardField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, - StandardField.PUBLISHER, StandardField.RELATED, StandardField.RELATEDTYPE, StandardField.RELATEDSTRING, - StandardField.REPOSITORY, StandardField.SWHID, StandardField.SUBTITLE, StandardField.URLDATE) + .withDetailFields(StandardField.DATE, StandardField.EPRINTCLASS, StandardField.EPRINTTYPE, BiblatexSoftwareField.HALID, BiblatexSoftwareField.HALVERSION, + StandardField.INSTITUTION, BiblatexSoftwareField.INTRODUCEDIN, BiblatexSoftwareField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, + StandardField.PUBLISHER, StandardField.RELATED, BiblatexSoftwareField.RELATEDTYPE, BiblatexSoftwareField.RELATEDSTRING, + BiblatexSoftwareField.REPOSITORY, BiblatexSoftwareField.SWHID, StandardField.SUBTITLE, StandardField.URLDATE) .withRequiredFields(new OrFields(StandardField.AUTHOR, StandardField.EDITOR), StandardField.TITLE, StandardField.URL, StandardField.YEAR) .build(); private static final BibEntryType SOFTWAREMODULE = new BibEntryTypeBuilder() - .withType(StandardEntryType.SoftwareModule) + .withType(BiblatexSoftwareEntryType.SoftwareModule) .withImportantFields(StandardField.DATE, StandardField.DOI, StandardField.EPRINTTYPE, StandardField.EPRINTCLASS, StandardField.EPRINT, - StandardField.EDITOR, StandardField.FILE, StandardField.HALID, StandardField.HALVERSION, StandardField.INSTITUTION, StandardField.INTRODUCEDIN, - StandardField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, StandardField.PUBLISHER, StandardField.RELATED, - StandardField.RELATEDSTRING, StandardField.REPOSITORY, StandardField.SWHID, StandardField.URLDATE, StandardField.VERSION) + StandardField.EDITOR, StandardField.FILE, BiblatexSoftwareField.HALID, BiblatexSoftwareField.HALVERSION, StandardField.INSTITUTION, BiblatexSoftwareField.INTRODUCEDIN, + BiblatexSoftwareField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, StandardField.PUBLISHER, StandardField.RELATED, + BiblatexSoftwareField.RELATEDSTRING, BiblatexSoftwareField.REPOSITORY, BiblatexSoftwareField.SWHID, StandardField.URLDATE, StandardField.VERSION) .withRequiredFields(StandardField.AUTHOR, StandardField.SUBTITLE, StandardField.URL, StandardField.YEAR) .build(); private static final BibEntryType CODEFRAGMENT = new BibEntryTypeBuilder() - .withType(StandardEntryType.CodeFragment) + .withType(BiblatexSoftwareEntryType.CodeFragment) .withImportantFields(StandardField.DATE, StandardField.DOI, StandardField.EPRINTTYPE, StandardField.EPRINTCLASS, StandardField.EPRINT, - StandardField.EDITOR, StandardField.FILE, StandardField.HALID, StandardField.HALVERSION, StandardField.INSTITUTION, StandardField.INTRODUCEDIN, - StandardField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, StandardField.PUBLISHER, StandardField.RELATED, - StandardField.RELATEDSTRING, StandardField.REPOSITORY, StandardField.SWHID, StandardField.URLDATE, StandardField.VERSION) + StandardField.EDITOR, StandardField.FILE, BiblatexSoftwareField.HALID, BiblatexSoftwareField.HALVERSION, StandardField.INSTITUTION, BiblatexSoftwareField.INTRODUCEDIN, + BiblatexSoftwareField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, StandardField.PUBLISHER, StandardField.RELATED, + BiblatexSoftwareField.RELATEDSTRING, BiblatexSoftwareField.REPOSITORY, BiblatexSoftwareField.SWHID, StandardField.URLDATE, StandardField.VERSION) .withRequiredFields(StandardField.URL) .build(); diff --git a/src/main/java/org/jabref/model/entry/types/EntryTypeFactory.java b/src/main/java/org/jabref/model/entry/types/EntryTypeFactory.java index 1c94f6dd9a0..6b40a56c4f4 100644 --- a/src/main/java/org/jabref/model/entry/types/EntryTypeFactory.java +++ b/src/main/java/org/jabref/model/entry/types/EntryTypeFactory.java @@ -49,6 +49,8 @@ private static boolean isBiblatex(EntryType type) { public static EntryType parse(String typeName) { List types = new ArrayList<>(Arrays.asList(StandardEntryType.values())); types.addAll(Arrays.asList(IEEETranEntryType.values())); + types.addAll(Arrays.asList(BiblatexSoftwareEntryType.values())); + types.addAll(Arrays.asList(BiblatexApaEntryType.values())); types.addAll(Arrays.asList(SystematicLiteratureReviewStudyEntryType.values())); return types.stream().filter(type -> type.getName().equals(typeName.toLowerCase(Locale.ENGLISH))).findFirst().orElse(new UnknownEntryType(typeName)); diff --git a/src/main/java/org/jabref/model/entry/types/StandardEntryType.java b/src/main/java/org/jabref/model/entry/types/StandardEntryType.java index fb2922e61a0..6f6f91ac6a5 100644 --- a/src/main/java/org/jabref/model/entry/types/StandardEntryType.java +++ b/src/main/java/org/jabref/model/entry/types/StandardEntryType.java @@ -36,10 +36,7 @@ public enum StandardEntryType implements EntryType { Thesis("Thesis"), WWW("WWW"), Software("Software"), - Dataset("Dataset"), - SoftwareVersion("SoftwareVersion"), - SoftwareModule("SoftwareModule"), - CodeFragment("CodeFragment"); + Dataset("Dataset"); private final String displayName; diff --git a/src/main/java/org/jabref/model/strings/StringUtil.java b/src/main/java/org/jabref/model/strings/StringUtil.java index 19f0b7a4542..2a443306e4d 100644 --- a/src/main/java/org/jabref/model/strings/StringUtil.java +++ b/src/main/java/org/jabref/model/strings/StringUtil.java @@ -750,4 +750,15 @@ public static String quoteStringIfSpaceIsContained(String string) { return string; } } + + /** + * Checks if the given string contains any whitespace characters. The supported whitespace characters + * are the set of characters matched by {@code \s} in regular expressions, which are {@code [ \t\n\x0B\f\r]}. + * + * @param s The string to check + * @return {@code True} if the given string does contain at least one whitespace character, {@code False} otherwise + * */ + public static boolean containsWhitespace(String s) { + return s.chars().anyMatch(Character::isWhitespace); + } } diff --git a/src/main/resources/journals/journalList.mv b/src/main/resources/journals/journalList.mv index 52541a1a095..b921eef3b19 100644 Binary files a/src/main/resources/journals/journalList.mv and b/src/main/resources/journals/journalList.mv differ diff --git a/src/main/resources/l10n/JabRef_de.properties b/src/main/resources/l10n/JabRef_de.properties index 72df848e1f7..901ac722dc2 100644 --- a/src/main/resources/l10n/JabRef_de.properties +++ b/src/main/resources/l10n/JabRef_de.properties @@ -2496,21 +2496,24 @@ Success\!\ Finished\ writing\ metadata.=Erfolgreich\! Das Schreiben der Metadate Custom\ API\ key=Eigener API-Schlüssel Check\ %0\ API\ Key\ Setting=%0 API-Schlüsseleinstellungen überprüfen -Edit\ field\ value=Feldinhalt bearbeiten -Two\ fields=Zwei Felder -Overwrite\ Non\ empty\ fields=Nicht leere Felder überschreiben +Edit\ content=Inhalt ändern +Copy\ or\ Move\ content=Inhalt kopieren oder verschieben +Overwrite\ field\ content=Inhalt des Feldes überschreiben Set=Festlegen Append=Anfügen -Clear\ field=Feld leeren -Field\ value=Feldinhalt -Edit\ field\ value\ of\ selected\ entries=Feldinhalt der ausgewählten Einträge bearbeiten +Clear\ field\ content=Feldinhalt löschen +Set\ or\ append\ content=Setze oder füge Inhalt an +Edit\ field\ content\ for\ selected\ entries=Feldinhalt der ausgewählten Einträge bearbeiten Rename=Umbenennen New\ field\ name=Neuer Feldname -Copy\ value=Wert kopieren -Move\ value=Inhalt verschieben -Swap\ values=Werte tauschen -Copy\ or\ move\ the\ value\ of\ one\ field\ to\ another=Den Inhalt eines Feldes in ein anderes kopieren oder verschieben +Copy\ content=Inhalt kopieren +Move\ content=Inhalt verschieben +Swap\ content=Inhalte vertauschen +Copy\ or\ move\ the\ content\ of\ one\ field\ to\ another=Inhalt eines Feldes kopieren oder in ein anderes Feld verschieben Automatic\ field\ editor=Automatischer Feldeditor +From=Von +Keep\ Modifications=Änderungen akzeptieren +To=Nach (Note\:\ If\ original\ entries\ lack\ keywords\ to\ qualify\ for\ the\ new\ group\ configuration,\ confirming\ here\ will\ add\ them)=(Hinweis\: Wenn es den ursprünglichen Einträgen an Schlüsselwörtern fehlt, die sich für die neue Gruppenkonfiguration qualifizieren, wird die Bestätigung hier diese hinzufügen) Assign=Zuweisen diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties index 133fd9e7f2b..4f355612618 100644 --- a/src/main/resources/l10n/JabRef_en.properties +++ b/src/main/resources/l10n/JabRef_en.properties @@ -224,8 +224,6 @@ cut\ entries=cut entries cut\ entry\ %0=cut entry %0 -DOI\ not\ found=DOI not found - Library\ encoding=Library encoding Library\ properties=Library properties @@ -578,8 +576,6 @@ No\ journal\ names\ could\ be\ abbreviated.=No journal names could be abbreviate No\ journal\ names\ could\ be\ unabbreviated.=No journal names could be unabbreviated. -No\ DOI\ data\ exists=No DOI data exists - not=not not\ found=not found @@ -2489,6 +2485,17 @@ Version=Version Error\ downloading=Error downloading +No\ data\ was\ found\ for\ the\ identifier=No data was found for the identifier +Server\ not\ available=Server not available +Fetching\ information\ using\ %0=Fetching information using %0 +Look\ up\ identifier=Look up identifier + +Bibliographic\ data\ not\ found.\ Cause\ is\ likely\ the\ client\ side.\ Please\ check\ connection\ and\ identifier\ for\ correctness.=Bibliographic data not found. Cause is likely the client side. Please check connection and identifier for correctness. +Bibliographic\ data\ not\ found.\ Cause\ is\ likely\ the\ server\ side.\ Please\ try\ agan\ later.=Bibliographic data not found. Cause is likely the server side. Please try agan later. +Error\ message\ %0=Error message %0 +Identifier\ not\ found=Identifier not found + + Error\ while\ writing\ metadata.\ See\ the\ error\ log\ for\ details.=Error while writing metadata. See the error log for details. Failed\ to\ write\ metadata,\ file\ %1\ not\ found.=Failed to write metadata, file %1 not found. Success\!\ Finished\ writing\ metadata.=Success! Finished writing metadata. @@ -2496,22 +2503,27 @@ Success\!\ Finished\ writing\ metadata.=Success! Finished writing metadata. Custom\ API\ key=Custom API key Check\ %0\ API\ Key\ Setting=Check %0 API Key Setting -Edit\ field\ value=Edit field value -Two\ fields=Two fields -Overwrite\ Non\ empty\ fields=Overwrite Non empty fields +Edit\ content=Edit content +Copy\ or\ Move\ content=Copy or Move content +Overwrite\ field\ content=Overwrite field content Set=Set Append=Append -Clear\ field=Clear field -Field\ value=Field value -Edit\ field\ value\ of\ selected\ entries=Edit field value of selected entries +Clear\ field\ content=Clear field content +Set\ or\ append\ content=Set or append content +Edit\ field\ content\ for\ selected\ entries=Edit field content for selected entries Rename=Rename New\ field\ name=New field name -Copy\ value=Copy value -Move\ value=Move value -Swap\ values=Swap values -Copy\ or\ move\ the\ value\ of\ one\ field\ to\ another=Copy or move the value of one field to another +Copy\ content=Copy content +Move\ content=Move content +Swap\ content=Swap content +Copy\ or\ move\ the\ content\ of\ one\ field\ to\ another=Copy or move the content of one field to another Automatic\ field\ editor=Automatic field editor +From=From +Keep\ Modifications=Keep Modifications +To=To (Note\:\ If\ original\ entries\ lack\ keywords\ to\ qualify\ for\ the\ new\ group\ configuration,\ confirming\ here\ will\ add\ them)=(Note: If original entries lack keywords to qualify for the new group configuration, confirming here will add them) Assign=Assign Do\ not\ assign=Do not assign + +Error\ occured\ %0=Error occured %0 diff --git a/src/main/resources/l10n/JabRef_es.properties b/src/main/resources/l10n/JabRef_es.properties index 5667fb916b0..60e35d98ad0 100644 --- a/src/main/resources/l10n/JabRef_es.properties +++ b/src/main/resources/l10n/JabRef_es.properties @@ -1,7 +1,12 @@ +Could\ not\ delete\ empty\ entries.=No se pudieron eliminar entradas vacías. +Delete\ empty\ entries=Eliminar entradas vacías +Empty\ entries=Vaciar entradas +Keep\ empty\ entries=Mantener entradas vacías +Library\ '%0'\ has\ empty\ entries.\ Do\ you\ want\ to\ delete\ them?=La biblioteca '%0' tiene entradas vacías. ¿Quieres eliminarlas? Unable\ to\ monitor\ file\ changes.\ Please\ close\ files\ and\ processes\ and\ restart.\ You\ may\ encounter\ errors\ if\ you\ continue\ with\ this\ session.=No es posible supervisar los cambios en los archivos. Cierre los archivos y procesos y reinicie. Puede que se produzcan errores si continúa con esta sesión. %0\ contains\ the\ regular\ expression\ %1=%0 contiene la expresión regular %1 @@ -15,6 +20,7 @@ Unable\ to\ monitor\ file\ changes.\ Please\ close\ files\ and\ processes\ and\ %0/%1\ entries=%0/%1 entradas +Reveal\ in\ File\ Explorer=Mostrar en Explorador de Archivos %0\ matches\ the\ regular\ expression\ %1=%0 coincidencias con la Expresión Regular %1 @@ -49,6 +55,7 @@ The\ path\ need\ not\ be\ on\ the\ classpath\ of\ JabRef.=La ruta no debe estar Add\ a\ regular\ expression\ for\ the\ key\ pattern.=Añadir una expresión regular para el patrón clave. +Add\ entry\ manually=Añadir entrada manualmente Add\ selected\ entries\ to\ this\ group=Añadir entradas seleccionadas a este grupo @@ -166,6 +173,8 @@ Copy=Copiar Copy\ title=Copiar título Copy\ \\cite{citation\ key}=Copiar \\cite{citation key} +Copy\ citation\ (html)=Copiar cita (html) +Copy\ citation\ (text)=Copiar cita (texto) Copy\ citation\ key=Copiar clave de cita Copy\ citation\ key\ and\ link=Copiar clave de cita y enlace Copy\ citation\ key\ and\ title=Copiar clave y título de cita @@ -214,13 +223,17 @@ cut\ entries=Cortar entradas cut\ entry\ %0=corte de entrada %0 +DOI\ not\ found=DOI no encontrado Library\ encoding=Codificación de la biblioteca Library\ properties=Propiedades de la biblioteca +%0\ -\ Library\ properties=%0 - Propiedades de la biblioteca Default=Por defecto +Character\ encoding\ UTF-8\ is\ not\ supported.=La codificación de caracteres UTF-8 no restá soportada. +UTF-8\ could\ not\ be\ used\ to\ encode\ the\ following\ characters\:\ %0=UTF-8 no se pudo utilizar para codificar los siguientes caracteres\: %0 The\ chosen\ encoding\ '%0'\ could\ not\ encode\ the\ following\ characters\:=La codificación de caracteres '%' no puede codificar los siguientes caracteres\: Downloading=Descargando @@ -269,6 +282,7 @@ Downloaded\ website\ as\ an\ HTML\ file.=Se descargó el sitio web como un archi duplicate\ removal=eliminación de duplicados +Duplicate\ fields=Campos duplicados Duplicate\ string\ name=Nombre de cadena duplicado @@ -330,6 +344,7 @@ External\ file\ links=Enlaces a archivos externos External\ programs=Programas externos +Failed\ to\ import\ by\ ID=Error al importar por ID Field=Campo @@ -354,6 +369,7 @@ Filter=Filtro Filter\ groups=Filtros +Finished\ writing\ metadata\ for\ %0\ file\ (%1\ skipped,\ %2\ errors).=Escritura de metadatos para el archivo %0 finalizada (%1 omitidos, %2 errores). First\ select\ the\ entries\ you\ want\ keys\ to\ be\ generated\ for.=En primer lugar, seleccione las entradas para las que desea generar claves @@ -370,6 +386,7 @@ Formatter\ name=Nombre del formateador found\ in\ AUX\ file=encontrado en archivo AUX +Fulltext\ search=Búsqueda de texto completo Fulltext\ for=Texto completo de @@ -448,6 +465,9 @@ Include\ subgroups\:\ When\ selected,\ view\ entries\ contained\ in\ this\ group Independent\ group\:\ When\ selected,\ view\ only\ this\ group's\ entries=Grupo independiente\: ver sólo las entradas de este grupo cuando esté seleccionado. I\ Agree=Acepto +Indexing\ pdf\ files=Indexando archivos pdf +Indexing\ for\ %0=Indexando para %0 +%0\ of\ %1\ linked\ files\ added\ to\ the\ index=%0 de %1 archivos enlazados añadidos al índice Invalid\ citation\ key=La clave de cita no es válida @@ -461,6 +481,8 @@ JabRef\ requests\ recommendations\ from\ Mr.\ DLib,\ which\ is\ an\ external\ se JabRef\ Version\ (Required\ to\ ensure\ backwards\ compatibility\ with\ Mr.\ DLib's\ Web\ Service)=Versión JabRef (necesaria para asegurar la compatibilidad con el servicio web de Mr. DLib) Journal\ abbreviations=Abreviaturas de publicaciones +Journal\ lists\:=Listados de revistas\: +Remove\ journal\ '%0'=Eliminar revista '%0' Keep\ both=Mantener ambos @@ -496,6 +518,7 @@ Main\ file\ directory=Carpeta del archivo principal Manage\ custom\ exports=Administrar exportaciones personalizadas Manage\ custom\ imports=Administrar importaciones personalizadas +External\ file\ types=Tipos de archivos externos Mark\ new\ entries\ with\ owner\ name=Marcar nuevas entradas con nombre de propietario @@ -518,7 +541,9 @@ Moved\ group\ "%0".=Se ha movido el grupo "%0". Mr.\ DLib\ Privacy\ settings=Mr. DLib Configuración de la privacidad +No\ database\ is\ open=No hay ninguna base de datos abierta +We\ need\ a\ database\ to\ export\ from.\ Open\ one.=Necesitamos una base de datos desde la que exportar. Abrir una. No\ recommendations\ received\ from\ Mr.\ DLib\ for\ this\ entry.=No se han recibido recomendaciones del Mr. DLib para esta entrada. @@ -552,6 +577,7 @@ No\ journal\ names\ could\ be\ abbreviated.=No se pudieron abreviar nombres de r No\ journal\ names\ could\ be\ unabbreviated.=No se pudieron expandir nombres de revistas. +No\ DOI\ data\ exists=No existen datos DOI not=no @@ -678,16 +704,23 @@ Remove\ group=Eliminar grupo Remove\ group\ and\ subgroups=Eliminar grupo y subgrupos +Remove\ groups\ and\ subgroups=Eliminar grupos y subgrupos +Remove\ all\ selected\ groups\ and\ keep\ their\ subgroups?=¿Eliminar todos los grupos seleccionados y mantener sus subgrupos? +Remove\ group\ "%0"\ and\ keep\ its\ subgroups?=¿Eliminar grupo "%0" y mantener sus subgrupos? +Remove\ groups=Eliminar grupos +Removed\ all\ selected\ groups.=Eliminados todos los grupos seleccionados. Remove\ group\ "%0"\ and\ its\ subgroups?=¿Eliminar el grupo "%0" y sus subgrupos? Removed\ group\ "%0"\ and\ its\ subgroups.=Se ha eliminado el grupo "%0" y sus subgrupos. +Remove\ all\ selected\ groups\ and\ their\ subgroups?=¿Eliminar todos los grupos seleccionados y sus subgrupos? +Removed\ all\ selected\ groups\ and\ their\ subgroups.=Eliminados todos los grupos seleccionados y sus subgrupos. Remove\ link=Eliminar enlace @@ -719,6 +752,8 @@ Replaces\ Unicode\ ligatures\ with\ their\ expanded\ form=Reemplaza las ligadura Required\ fields=Campos requeridos +Do\ not\ resolve\ BibTeX\ strings=No resolver cadenas BibTeX +Resolve\ BibTeX\ strings\ for\ the\ following\ fields=Resolver las cadenas BibTeX para los siguientes campos resolved=resuelto @@ -726,11 +761,13 @@ Restart=Reiniciar Restart\ required=Reinicio requerido +Return\ to\ dialog=Volver al diálogo Review=Revisar Review\ changes=Revisar cambios Review\ Field\ Migration=Revisar campo de migración +Loading=Cargando Save=Guardar Save\ all\ finished.=Guardar todos los finalizados @@ -744,6 +781,7 @@ Save\ library\ as...=Guardar biblioteca como... Saving=Guardando Saving\ all\ libraries...=Guardando todas las bibliotecas... Saving\ library=Guardando biblioteca +Library\ saved=Biblioteca guardada Saved\ selected\ to\ '%0'.=Selección guardada en '%0'. Search=Buscar @@ -795,6 +833,12 @@ Size=Tamaño Skipped\ -\ No\ PDF\ linked=Omitido - No se enlazó PDF Skipped\ -\ PDF\ does\ not\ exist=Omitido - No existe el PDF +JabRef\ skipped\ the\ entry.=JabRef omitió la entrada. +Import\ error=Error al importar +Open\ library\ error=Error al abrir la biblioteca +Please\ check\ your\ library\ file\ for\ wrong\ syntax.=Por favor, comprueba errores de sintaxis en tu archivo de biblioteca. +SourceTab\ error=Error de SourceTab +User\ input\ via\ entry-editor\ in\ `{}bibtex\ source`\ tab\ led\ to\ failure.=Entrada de usuario a través del editor de entrada en la pestaña `{}bibtex source` condujo al error. Sort\ subgroups=Ordenar subgrupos @@ -883,13 +927,18 @@ Warning=Advertencia Warnings=Advertencias +Warning\:\ You\ added\ field\ "%0"\ twice.\ Only\ one\ will\ be\ kept.=Advertencia\: Añadiste el campo "%0" dos veces. Solo uno será utilizado. web\ link=enlace a web What\ do\ you\ want\ to\ do?=¿Qué desea hacer? Whatever\ option\ you\ choose,\ Mr.\ DLib\ may\ share\ its\ data\ with\ research\ partners\ to\ further\ improve\ recommendation\ quality\ as\ part\ of\ a\ 'living\ lab'.\ Mr.\ DLib\ may\ also\ release\ public\ datasets\ that\ may\ contain\ anonymized\ information\ about\ you\ and\ the\ recommendations\ (sensitive\ information\ such\ as\ metadata\ of\ your\ articles\ will\ be\ anonymised\ through\ e.g.\ hashing).\ Research\ partners\ are\ obliged\ to\ adhere\ to\ the\ same\ strict\ data\ protection\ policy\ as\ Mr.\ DLib.=Sea cual sea la opción que elija, Mr. DLib puede compartir sus datos con socios de investigación para mejorar aún más la calidad de las recomendaciones como parte de un "laboratorio vivo". Mr. DLib también puede publicar conjuntos de datos públicos que pueden contener información anónima sobre usted y las recomendaciones (la información confidencial como los metadatos de sus artículos será anonimizada a través de, por ejemplo, hashing). Los socios de investigación están obligados a adherirse a la misma estricta política de protección de datos que Mr. DLib. +Will\ write\ metadata\ to\ the\ PDFs\ linked\ from\ selected\ entries.=Escribirá metadatos en los PDFs enlazados desde entradas seleccionadas. +Write\ BibTeXEntry\ as\ metadata\ to\ PDF.=Escribir entrada BibTeX como metadatos XMP en el PDF. +Write\ metadata\ for\ all\ PDFs\ in\ current\ library?=¿Escribir metadatos para todos los PDFs de la biblioteca actual? +Writing\ metadata\ for\ selected\ entries...=Escribiendo metadatos para las entradas seleccionadas... Write\ BibTeXEntry\ as\ XMP\ metadata\ to\ PDF.=Escribe BibTeXEntry como metadatos XMP en los PDF. @@ -1587,6 +1636,8 @@ Issue\ report\ successful=Comunicación de problema satisfactoria Your\ issue\ was\ reported\ in\ your\ browser.=Su problema fue comunicado a través del navegador The\ log\ and\ exception\ information\ was\ copied\ to\ your\ clipboard.=La información de excepción y registro fue copiada a su portapapeles Please\ paste\ this\ information\ (with\ Ctrl+V)\ in\ the\ issue\ description.=Por favor, pegue esta información (con Ctrl+V) en la descripción del problema. +Last\ notification=Última notificación +Check\ the\ event\ log\ to\ see\ all\ notifications=Revisa el registro de eventos para ver todas las notificaciones Host=Host/Servidor Port=Puerto @@ -1732,6 +1783,7 @@ Delete\ '%0'=Eliminar '%0' Delete\ from\ disk=Eliminar del disco duro Remove\ from\ entry=Eliminar de la entrada There\ exists\ already\ a\ group\ with\ the\ same\ name.=Ya existe un grupo con el mismo nombre. +If\ you\ use\ it,\ it\ will\ inherit\ all\ entries\ from\ this\ other\ group.=Si lo utiliza, heredará todas las entradas de este otro grupo. Copy\ linked\ file=Copiar archivo enlazado Copy\ linked\ file\ to\ folder...=Copiar archivo enlazado a la carpeta... @@ -1787,6 +1839,7 @@ Could\ not\ connect\ to\ Vim\ server.\ Make\ sure\ that\ Vim\ is\ running\ with\ Could\ not\ connect\ to\ a\ running\ gnuserv\ process.\ Make\ sure\ that\ Emacs\ or\ XEmacs\ is\ running,\ and\ that\ the\ server\ has\ been\ started\ (by\ running\ the\ command\ 'server-start'/'gnuserv-start').=No se puede conectar con un proceso gnuserv en ejecución. Asegúrese de que Emacs o XEmacs se está ejecutando y de que el servidor ha sido iniciado (ejecutando el comando 'server-start'/'gnuserv-start'). Error\ pushing\ entries=Error al enviar entradas +Preamble=Preámbulo Markings=Marcados Use\ selected\ instance=Usar la instancia seleccionada @@ -1805,6 +1858,7 @@ Blog=Blog Check\ integrity=Verificar integridad Cleanup\ URL\ link=Limpiar un enlace URL Cleanup\ URL\ link\ by\ removing\ special\ symbols\ and\ extracting\ simple\ link=Limpiar un enlace URL eliminando los símbolos especiales y extrayendo un enlace simple +Copy\ DOI=Copiar DOI Copy\ DOI\ url=Copiar la url del DOI Development\ version=Versión de desarrollo Export\ selected\ entries=Exportar registros seleccionados @@ -1814,6 +1868,8 @@ JabRef\ resources=Recursos sobre JabRef Manage\ journal\ abbreviations=Administrar abreviaturas de publicaciones Manage\ protected\ terms=gestionar términos protegidos New\ entry\ from\ plain\ text=Nueva entrada desde texto sin formato +Import\ by\ ID=Importar por ID +Enter\ a\ valid\ ID=Introduzca un ID válido New\ sublibrary\ based\ on\ AUX\ file=Nueva subbiblioteca a partir de un archivo AUX Push\ entries\ to\ external\ application\ (%0)=Agregar registros a aplicación externa (%0) Quit=Salir @@ -1873,6 +1929,7 @@ Keyword\ separator=Separador de palabras clave Remove\ keyword=Eliminar palabra clave Are\ you\ sure\ you\ want\ to\ remove\ keyword\:\ "%0"?=¿Está seguro de que desea eliminar la palabra clave\: "%0"? Reset\ to\ default=Restablecer los valores por defecto +String\ constants=Constantes de cadena Export\ all\ entries=Exportar todas las entradas Generate\ citation\ keys=Generar claves de cita Manage\ field\ names\ &\ content=Gestionar nombres y contenido de los campos @@ -2002,6 +2059,7 @@ Please\ provide\ a\ valid\ aux\ file.=Por favor, proporcione un archivo AUX vál Keyword\ delimiter=Separador de palabras clave Hierarchical\ keyword\ delimiter=Separador de palabras clave jerárquicas Escape\ ampersands=Escape de ampersands +Escape\ dollar\ sign=Símbolo de escape dólar Hint\:\n\nTo\ search\ all\ fields\ for\ Smith,\ enter\:\nsmith\n\nTo\ search\ the\ field\ author\ for\ Smith\ and\ the\ field\ title\ for\ electrical,\ enter\:\nauthor\=Smith\ and\ title\=electrical=Consejo\:\n\nPara buscar Pedro en todos los campos, escriba\:\npedro\n\nPara buscar Pedro en el campo author y eléctrico en el campo title, escriba\:\nauthor\=Pedro and title\=eléctrico @@ -2211,7 +2269,11 @@ Reveal\ in\ file\ explorer=Revelar en el explorador de archivos Autolink\ files=Enlazar archivos automáticamente +Custom\ editor\ tabs=Pestañas de editor personalizadas +Custom\ export\ formats=Formatos de exportación personalizados +Custom\ import\ formats=Formatos de importación personalizados +No\ list\ enabled=No hay lista habilitada Protect\ selection=Proteger selección Customized\ preview\ style=Estilo de previsualización personalizado @@ -2238,7 +2300,17 @@ Regular\ expression=Expresión regular Error\ importing.\ See\ the\ error\ log\ for\ details.=Error al importar. Consulte el registro de errores para obtener detalles. - +Error\ from\ import\:\ %0=Error de importación\: %0 +Error\ reading\ PDF\ content\:\ %0=Error al leer el contenido PDF\: %0 +Importing\ bib\ entry=Importando entrada bibliográfica +Importing\ using\ extracted\ PDF\ data=Importación usando datos extraídos del PDF +No\ BibTeX\ data\ found.\ Creating\ empty\ entry\ with\ file\ link=No se encontraron datos BibTeX. Creando entrada vacía con enlace de archivo +No\ metadata\ found.\ Creating\ empty\ entry\ with\ file\ link=No se encontraron metadatos. Creando entrada vacía con enlace de archivo +Processing\ file\ %0=Procesando archivo %0 +Export\ selected=Exportar seleccionados + +Separate\ merged\ citations=Citas fusionadas separadas +Separate\ citations=Citas separadas Custom\ DOI\ URI=URI personalizado de DOI diff --git a/src/main/resources/l10n/JabRef_fr.properties b/src/main/resources/l10n/JabRef_fr.properties index 46020ee888c..42edf5e63ee 100644 --- a/src/main/resources/l10n/JabRef_fr.properties +++ b/src/main/resources/l10n/JabRef_fr.properties @@ -19,7 +19,9 @@ Unable\ to\ monitor\ file\ changes.\ Please\ close\ files\ and\ processes\ and\ %0/%1\ entries=%0/%1 entrées +Export\ operation\ finished\ successfully.=L'opération d'export s'est terminée avec succès. +Reveal\ in\ File\ Explorer=Montrer dans l'explorateur de fichiers %0\ matches\ the\ regular\ expression\ %1=%0 correspond à l'expression régulière %1 @@ -703,16 +705,23 @@ Remove\ group=Supprimer le groupe Remove\ group\ and\ subgroups=Supprimer le groupe et les sous-groupes +Remove\ groups\ and\ subgroups=Supprimer les groupes et les sous-groupes +Remove\ all\ selected\ groups\ and\ keep\ their\ subgroups?=Supprimer tous les groupes sélectionnés et conserver leurs sous-groupes ? +Remove\ group\ "%0"\ and\ keep\ its\ subgroups?=Supprimer le groupe « %0 » et conserver ses sous-groupes ? +Remove\ groups=Supprimer les groupes +Removed\ all\ selected\ groups.=Tous les groupes sélectionnés ont été supprimés. Remove\ group\ "%0"\ and\ its\ subgroups?=Supprimer le groupe « %0 » et ses sous-groupes ? Removed\ group\ "%0"\ and\ its\ subgroups.=Groupe « %0 » et ses sous-groupes supprimés. +Remove\ all\ selected\ groups\ and\ their\ subgroups?=Supprimer tous les groupes sélectionnés et leurs sous-groupes ? +Removed\ all\ selected\ groups\ and\ their\ subgroups.=Tous les groupes sélectionnés et leurs sous-groupes ont été supprimés. Remove\ link=Supprimer le lien @@ -825,6 +834,12 @@ Size=Taille Skipped\ -\ No\ PDF\ linked=Sauté - Pas de PDF lié Skipped\ -\ PDF\ does\ not\ exist=Omis - Le PDF n'existe pas +JabRef\ skipped\ the\ entry.=JabRef a ignoré l'entrée. +Import\ error=Erreur d'importation +Open\ library\ error=Erreur d'ouverture du fichier +Please\ check\ your\ library\ file\ for\ wrong\ syntax.=Veuillez vérifier la syntaxe de votre fichier bibliographique. +SourceTab\ error=Erreur de SourceTab +User\ input\ via\ entry-editor\ in\ `{}bibtex\ source`\ tab\ led\ to\ failure.=La saisie de l'utilisateur via l'éditeur d'entrée dans l'onglet `{}bibtex source` a conduit à un échec. Sort\ subgroups=Trier les sous-groupes @@ -970,7 +985,7 @@ Line\ %0\:\ Found\ corrupted\ citation\ key\ %1\ (contains\ whitespaces).=Ligne Line\ %0\:\ Found\ corrupted\ citation\ key\ %1\ (comma\ missing).=Ligne %0 \: clef de citation corrompue %1 (virgule manquante). No\ full\ text\ document\ found=Aucun texte intégral trouvé Download\ from\ URL=Télécharger depuis l'URL -Rename\ field=Renommer le champ +Rename\ field=Renommer Cannot\ use\ port\ %0\ for\ remote\ operation;\ another\ application\ may\ be\ using\ it.\ Try\ specifying\ another\ port.=Le port %0 ne peut pas être utilisé pour une opération à distance ; un autre logiciel pourrait être en train de l'utiliser. Essayer de spécifier un autre port. @@ -1244,6 +1259,7 @@ Connection\ failed\!=Échec de la connexion \! Connection\ successful\!=Connexion réussie \! SSL\ Configuration=Configuration SSL +SSL\ configuration\ changed=Configuration SSL modifiée SSL\ certificate\ file=Fichier de certificat SSL Duplicate\ Certificates=Dupliquer les certificats You\ already\ added\ this\ certificate=Vous avez déjà ajouté ce certificat @@ -1283,6 +1299,7 @@ Please\ open\ %0\ manually.=Veuillez ouvrir manuellement %0 . The\ link\ has\ been\ copied\ to\ the\ clipboard.=Le lien a été copié dans le presse-papiers. Open\ %0\ file=Ouvrir le fichier %0 +Could\ not\ detect\ terminal\ automatically.\ Please\ define\ a\ custom\ terminal\ in\ the\ preferences.=Impossible de détecter le terminal automatiquement. Veuillez définir un terminal personnalisé dans les préférences. Cannot\ delete\ file=Le fichier ne peut pas être supprimé File\ permission\ error=Erreur due aux permissions du fichier @@ -1670,6 +1687,8 @@ Issue\ report\ successful=Signalement de l'anomalie réussi Your\ issue\ was\ reported\ in\ your\ browser.=Votre anomalie a été affichée dans votre navigateur. The\ log\ and\ exception\ information\ was\ copied\ to\ your\ clipboard.=Le journal et les informations d'anomalie ont été copiées dans votre presse-papier. Please\ paste\ this\ information\ (with\ Ctrl+V)\ in\ the\ issue\ description.=Veuillez coller ces informations (avec Ctrl+V) dans la description de l'anomalie. +Last\ notification=Dernière notification +Check\ the\ event\ log\ to\ see\ all\ notifications=Consultez le journal des événements pour voir toutes les notifications Host=Hôte Port=Port @@ -2477,4 +2496,25 @@ Success\!\ Finished\ writing\ metadata.=Succès \! Écriture des métadonnées t Custom\ API\ key=Clef d'API personnalisée Check\ %0\ API\ Key\ Setting=Vérifier les paramètres de la clef d'API %0 - +Edit\ content=Modifier le contenu +Copy\ or\ Move\ content=Copier ou déplacer le contenu +Overwrite\ field\ content=Écraser le contenu du champ +Set=Définir +Append=Ajouter +Clear\ field\ content=Effacer le contenu du champ +Set\ or\ append\ content=Définir ou ajouter du contenu +Edit\ field\ content\ for\ selected\ entries=Modifier le contenu d'un champ +Rename=Renommer +New\ field\ name=Nouveau nom de champ +Copy\ content=Copier le contenu +Move\ content=Déplacer le contenu +Swap\ content=Permuter le contenu +Copy\ or\ move\ the\ content\ of\ one\ field\ to\ another=Copier ou déplacer le contenu d'un champ vers un autre +Automatic\ field\ editor=Éditeur automatique de champs +From=De +Keep\ Modifications=Enregistrer +To=Vers + +(Note\:\ If\ original\ entries\ lack\ keywords\ to\ qualify\ for\ the\ new\ group\ configuration,\ confirming\ here\ will\ add\ them)=(Note \: si les entrées originales n'ont pas de mots-clefs correspondant à la nouvelle configuration du groupe, confirmer ici les ajoutera) +Assign=Assigner +Do\ not\ assign=Ne pas assigner diff --git a/src/main/resources/l10n/JabRef_it.properties b/src/main/resources/l10n/JabRef_it.properties index 6bac76a45cb..2e9126ad31c 100644 --- a/src/main/resources/l10n/JabRef_it.properties +++ b/src/main/resources/l10n/JabRef_it.properties @@ -2441,21 +2441,24 @@ Search\ results\ from\ open\ libraries=Risultati della ricerca da librerie apert Custom\ API\ key=Chiave API personalizzata Check\ %0\ API\ Key\ Setting=Controlla le impostazioni della chiave API %0 -Edit\ field\ value=Modifica valore campo -Two\ fields=Due campi -Overwrite\ Non\ empty\ fields=Sovrascrivi campi non vuoti +Edit\ content=Modifica il contenuto +Copy\ or\ Move\ content=Copia o Sposta il contenuto +Overwrite\ field\ content=Sovrascrivi il contenuto del campo Set=Imposta Append=Accoda -Clear\ field=Svuota campo -Field\ value=Valore del campo -Edit\ field\ value\ of\ selected\ entries=Modifica il valore del campo delle voci selezionate +Clear\ field\ content=Cancella il contenuto del campo +Set\ or\ append\ content=Imposta o aggiungi il contenuto +Edit\ field\ content\ for\ selected\ entries=Modifica il contenuto del campo per le voci selezionate Rename=Rinomina New\ field\ name=Nuovo nome del campo -Copy\ value=Copia Valore -Move\ value=Sposta valore -Swap\ values=Scambia i valori -Copy\ or\ move\ the\ value\ of\ one\ field\ to\ another=Copia o sposta il valore di un campo in un altro +Copy\ content=Copia il contenuto +Move\ content=Sposta il contenuto +Swap\ content=Scambia il contenuto +Copy\ or\ move\ the\ content\ of\ one\ field\ to\ another=Copia o sposta il contenuto di un campo in un altro Automatic\ field\ editor=Editor automatico dei campi +From=Da +Keep\ Modifications=Mantieni le modifiche +To=A (Note\:\ If\ original\ entries\ lack\ keywords\ to\ qualify\ for\ the\ new\ group\ configuration,\ confirming\ here\ will\ add\ them)=(Nota\: se le voci originali mancano di parole chiave per qualificarsi per la nuova configurazione di gruppo, confermando qui le aggiungeranno) Assign=Assegna diff --git a/src/main/resources/l10n/JabRef_ru.properties b/src/main/resources/l10n/JabRef_ru.properties index 6980f2d32bb..0999c1ddcb0 100644 --- a/src/main/resources/l10n/JabRef_ru.properties +++ b/src/main/resources/l10n/JabRef_ru.properties @@ -2496,20 +2496,10 @@ Success\!\ Finished\ writing\ metadata.=Успех\! Запись метадан Custom\ API\ key=Пользовательский ключ API Check\ %0\ API\ Key\ Setting=Проверьте настройку ключа API %0 -Edit\ field\ value=Изменить значение поля -Two\ fields=Два поля -Overwrite\ Non\ empty\ fields=Перезаписать непустые поля Set=Задать Append=Присоединить -Clear\ field=Очистить поле -Field\ value=Значение поля -Edit\ field\ value\ of\ selected\ entries=Изменить значение поля выбранных записей Rename=Переименовать New\ field\ name=Новое имя поля -Copy\ value=Копировать значение -Move\ value=Переместить значение -Swap\ values=Обменять значения -Copy\ or\ move\ the\ value\ of\ one\ field\ to\ another=Скопировать или переместить значение одного поля в другое Automatic\ field\ editor=Автоматический редактор поля (Note\:\ If\ original\ entries\ lack\ keywords\ to\ qualify\ for\ the\ new\ group\ configuration,\ confirming\ here\ will\ add\ them)=(Примечание\: Если в исходных записях отсутствуют ключевые слова, подходящие для конфигурации новой группы, то при подтверждении здесь они будут добавлены) diff --git a/src/main/resources/l10n/JabRef_zh_CN.properties b/src/main/resources/l10n/JabRef_zh_CN.properties index 1b467a0de4f..baca5d6da46 100644 --- a/src/main/resources/l10n/JabRef_zh_CN.properties +++ b/src/main/resources/l10n/JabRef_zh_CN.properties @@ -2496,20 +2496,10 @@ Success\!\ Finished\ writing\ metadata.=Success\! Finished writing metadata. Custom\ API\ key=自定义API Check\ %0\ API\ Key\ Setting=Check %0 API Key Setting -Edit\ field\ value=编辑字段内容 -Two\ fields=两个字段 -Overwrite\ Non\ empty\ fields=覆盖非空字段 Set=设定 Append=附加 -Clear\ field=清除字段 -Field\ value=字段内容 -Edit\ field\ value\ of\ selected\ entries=编辑选中条目的字段内容 Rename=重命名 New\ field\ name=新的字段名 -Copy\ value=复制值 -Move\ value=移动值 -Swap\ values=交换值 -Copy\ or\ move\ the\ value\ of\ one\ field\ to\ another=复制或移动一个字段的值到另一个字段中 Automatic\ field\ editor=自动化条目编辑 (Note\:\ If\ original\ entries\ lack\ keywords\ to\ qualify\ for\ the\ new\ group\ configuration,\ confirming\ here\ will\ add\ them)=(Note\: If original entries lack keywords to qualify for the new group configuration, confirming here will add them) diff --git a/src/main/resources/l10n/JabRef_zh_TW.properties b/src/main/resources/l10n/JabRef_zh_TW.properties index 288532048c0..6e74ac8da11 100644 --- a/src/main/resources/l10n/JabRef_zh_TW.properties +++ b/src/main/resources/l10n/JabRef_zh_TW.properties @@ -991,7 +991,7 @@ Set\ rank\ to\ five=設定評分為 5 級 Order=順序 -Affected\ fields\:=影響欄位\: +Affected\ fields\:=影響欄位\: Show\ preview\ as\ a\ tab\ in\ entry\ editor=在條目編輯器中以分頁形式顯示預覽 Font=字型 Visual\ theme=界面主題 diff --git a/src/test/java/org/jabref/gui/edit/CopyOrMoveFieldContentTabViewModelTest.java b/src/test/java/org/jabref/gui/edit/CopyOrMoveFieldContentTabViewModelTest.java new file mode 100644 index 00000000000..75a1a176597 --- /dev/null +++ b/src/test/java/org/jabref/gui/edit/CopyOrMoveFieldContentTabViewModelTest.java @@ -0,0 +1,105 @@ +package org.jabref.gui.edit; + +import java.util.List; +import java.util.Optional; + +import org.jabref.gui.StateManager; +import org.jabref.gui.edit.automaticfiededitor.copyormovecontent.CopyOrMoveFieldContentTabViewModel; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +public class CopyOrMoveFieldContentTabViewModelTest { + CopyOrMoveFieldContentTabViewModel copyOrMoveFieldContentTabViewModel; + BibEntry entryA; + BibEntry entryB; + + BibDatabase bibDatabase; + + StateManager stateManager = mock(StateManager.class); + + @BeforeEach + void setup() { + entryA = new BibEntry(BibEntry.DEFAULT_TYPE) + .withField(StandardField.YEAR, "2015") + .withField(StandardField.DATE, "2014"); + + entryB = new BibEntry(BibEntry.DEFAULT_TYPE) + .withField(StandardField.DATE, "1998"); + bibDatabase = new BibDatabase(); + copyOrMoveFieldContentTabViewModel = newTwoFieldsViewModel(entryA, entryB); + } + + @Test + void copyValueDoesNotCopyBlankValues() { + CopyOrMoveFieldContentTabViewModel copyOrMoveFieldContentTabViewModel = newTwoFieldsViewModel(entryA, entryB); + + copyOrMoveFieldContentTabViewModel.fromFieldProperty().set(StandardField.YEAR); + copyOrMoveFieldContentTabViewModel.toFieldProperty().set(StandardField.DATE); + copyOrMoveFieldContentTabViewModel.overwriteFieldContentProperty().set(true); + copyOrMoveFieldContentTabViewModel.copyValue(); + + assertEquals(Optional.of("2015"), entryA.getField(StandardField.DATE), "YEAR field is not copied correctly to the DATE field"); + assertEquals(Optional.of("2015"), entryA.getField(StandardField.YEAR), "YEAR field should not have changed"); + assertEquals(Optional.of("1998"), entryB.getField(StandardField.DATE), "DATE field should not have changed because the YEAR field is blank e.g it doesn't exist"); + } + + @Test + void swapValuesShouldNotSwapFieldValuesIfOneOfTheValuesIsBlank() { + copyOrMoveFieldContentTabViewModel.fromFieldProperty().set(StandardField.YEAR); + copyOrMoveFieldContentTabViewModel.toFieldProperty().set(StandardField.DATE); + copyOrMoveFieldContentTabViewModel.overwriteFieldContentProperty().set(true); + + copyOrMoveFieldContentTabViewModel.swapValues(); + + assertEquals(Optional.of("1998"), entryB.getField(StandardField.DATE)); + assertEquals(Optional.empty(), entryB.getField(StandardField.YEAR)); + } + + @Test + void swapValuesShouldSwapFieldValuesIfBothValuesAreNotBlank() { + copyOrMoveFieldContentTabViewModel.fromFieldProperty().set(StandardField.YEAR); + copyOrMoveFieldContentTabViewModel.toFieldProperty().set(StandardField.DATE); + copyOrMoveFieldContentTabViewModel.overwriteFieldContentProperty().set(true); + + copyOrMoveFieldContentTabViewModel.swapValues(); + + assertEquals(List.of(Optional.of("2014"), Optional.of("2015")), + List.of(entryA.getField(StandardField.YEAR), entryA.getField(StandardField.DATE)), + "YEAR and DATE values didn't swap"); + } + + @Test + void moveValueShouldNotMoveValueIfToFieldIsNotBlankAndOverwriteIsNotEnabled() { + copyOrMoveFieldContentTabViewModel.fromFieldProperty().set(StandardField.YEAR); + copyOrMoveFieldContentTabViewModel.toFieldProperty().set(StandardField.DATE); + copyOrMoveFieldContentTabViewModel.overwriteFieldContentProperty().set(false); + + copyOrMoveFieldContentTabViewModel.moveValue(); + + assertEquals(Optional.of("2015"), entryA.getField(StandardField.YEAR)); + assertEquals(Optional.of("2014"), entryA.getField(StandardField.DATE)); + } + + @Test + void moveValueShouldMoveValueIfOverwriteIsEnabled() { + copyOrMoveFieldContentTabViewModel.fromFieldProperty().set(StandardField.DATE); + copyOrMoveFieldContentTabViewModel.toFieldProperty().set(StandardField.YEAR); + copyOrMoveFieldContentTabViewModel.overwriteFieldContentProperty().set(true); + + copyOrMoveFieldContentTabViewModel.moveValue(); + + assertEquals(Optional.of("1998"), entryB.getField(StandardField.YEAR)); + assertEquals(Optional.empty(), entryB.getField(StandardField.DATE)); + } + + private CopyOrMoveFieldContentTabViewModel newTwoFieldsViewModel(BibEntry... selectedEntries) { + return new CopyOrMoveFieldContentTabViewModel(List.of(selectedEntries), bibDatabase, stateManager); + } +} diff --git a/src/test/java/org/jabref/gui/edit/EditFieldContentTabViewModelTest.java b/src/test/java/org/jabref/gui/edit/EditFieldContentTabViewModelTest.java new file mode 100644 index 00000000000..1221580762a --- /dev/null +++ b/src/test/java/org/jabref/gui/edit/EditFieldContentTabViewModelTest.java @@ -0,0 +1,118 @@ +package org.jabref.gui.edit; + +import java.util.List; +import java.util.Optional; + +import org.jabref.gui.StateManager; +import org.jabref.gui.edit.automaticfiededitor.editfieldcontent.EditFieldContentViewModel; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.mockito.Mockito.mock; + +public class EditFieldContentTabViewModelTest { + EditFieldContentViewModel editFieldContentViewModel; + BibEntry entryA; + BibEntry entryB; + + BibDatabase bibDatabase; + + StateManager stateManager = mock(StateManager.class); + + @BeforeEach + void setup() { + entryA = new BibEntry(BibEntry.DEFAULT_TYPE) + .withField(StandardField.YEAR, "2015") + .withField(StandardField.DATE, "2014"); + + entryB = new BibEntry(BibEntry.DEFAULT_TYPE) + .withField(StandardField.DATE, "1998") + .withField(StandardField.YEAR, ""); + + bibDatabase = new BibDatabase(); + editFieldContentViewModel = new EditFieldContentViewModel(bibDatabase, List.of(entryA, entryB), stateManager); + } + + @Test + void clearSelectedFieldShouldClearFieldContentEvenWhenOverwriteFieldContentIsNotEnabled() { + editFieldContentViewModel.selectedFieldProperty().set(StandardField.YEAR); + editFieldContentViewModel.overwriteFieldContentProperty().set(false); + editFieldContentViewModel.clearSelectedField(); + + assertEquals(Optional.empty(), entryA.getField(StandardField.YEAR)); + } + + @Test + void clearSelectedFieldShouldDoNothingWhenFieldDoesntExistOrIsEmpty() { + editFieldContentViewModel.selectedFieldProperty().set(StandardField.FILE); + editFieldContentViewModel.clearSelectedField(); + + assertEquals(Optional.empty(), entryA.getField(StandardField.FILE)); + } + + @Test + void setFieldValueShouldNotDoAnythingIfOverwriteFieldContentIsNotEnabled() { + editFieldContentViewModel.overwriteFieldContentProperty().set(false); + editFieldContentViewModel.selectedFieldProperty().set(StandardField.YEAR); + editFieldContentViewModel.fieldValueProperty().set("2001"); + editFieldContentViewModel.setFieldValue(); + + assertEquals(Optional.of("2015"), entryA.getField(StandardField.YEAR)); + } + + @Test + void setFieldValueShouldSetFieldValueIfOverwriteFieldContentIsEnabled() { + editFieldContentViewModel.overwriteFieldContentProperty().set(true); + editFieldContentViewModel.selectedFieldProperty().set(StandardField.YEAR); + editFieldContentViewModel.fieldValueProperty().set("2001"); + editFieldContentViewModel.setFieldValue(); + + assertEquals(Optional.of("2001"), entryA.getField(StandardField.YEAR)); + } + + @Test + void setFieldValueShouldSetFieldValueIfFieldContentIsEmpty() { + editFieldContentViewModel.overwriteFieldContentProperty().set(false); + editFieldContentViewModel.selectedFieldProperty().set(StandardField.YEAR); + editFieldContentViewModel.fieldValueProperty().set("2001"); + editFieldContentViewModel.setFieldValue(); + + assertEquals(Optional.of("2001"), entryB.getField(StandardField.YEAR)); + } + + @Test + void appendToFieldValueShouldDoNothingIfOverwriteFieldContentIsNotEnabled() { + editFieldContentViewModel.overwriteFieldContentProperty().set(false); + editFieldContentViewModel.selectedFieldProperty().set(StandardField.YEAR); + editFieldContentViewModel.fieldValueProperty().set("0"); + editFieldContentViewModel.appendToFieldValue(); + + assertEquals(Optional.of("2015"), entryA.getField(StandardField.YEAR)); + } + + @Test + void appendToFieldValueShouldAppendFieldValueIfOverwriteFieldContentIsEnabled() { + editFieldContentViewModel.overwriteFieldContentProperty().set(true); + editFieldContentViewModel.selectedFieldProperty().set(StandardField.YEAR); + editFieldContentViewModel.fieldValueProperty().set("0"); + editFieldContentViewModel.appendToFieldValue(); + + assertEquals(Optional.of("20150"), entryA.getField(StandardField.YEAR)); + } + + @Test + void getAllFieldsShouldNeverBeEmpty() { + assertNotEquals(0, editFieldContentViewModel.getAllFields().size()); + } + + @Test + void getSelectedFieldShouldHaveADefaultValue() { + assertNotEquals(null, editFieldContentViewModel.getSelectedField()); + } +} diff --git a/src/test/java/org/jabref/gui/edit/RenameFieldViewModelTest.java b/src/test/java/org/jabref/gui/edit/RenameFieldViewModelTest.java new file mode 100644 index 00000000000..bf5a246e97f --- /dev/null +++ b/src/test/java/org/jabref/gui/edit/RenameFieldViewModelTest.java @@ -0,0 +1,111 @@ +package org.jabref.gui.edit; + +import java.util.List; +import java.util.Optional; + +import org.jabref.gui.StateManager; +import org.jabref.gui.edit.automaticfiededitor.renamefield.RenameFieldViewModel; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.FieldFactory; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.field.UnknownField; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +public class RenameFieldViewModelTest { + RenameFieldViewModel renameFieldViewModel; + BibEntry entryA; + BibEntry entryB; + + BibDatabase bibDatabase; + + StateManager stateManager = mock(StateManager.class); + + @BeforeEach + void setup() { + entryA = new BibEntry(BibEntry.DEFAULT_TYPE) + .withField(StandardField.YEAR, "2015") + .withField(StandardField.DATE, "2014") + .withField(StandardField.AUTHOR, "Doe"); + + entryB = new BibEntry(BibEntry.DEFAULT_TYPE) + .withField(StandardField.DATE, "1998") + .withField(StandardField.YEAR, "") + .withField(StandardField.AUTHOR, "Eddie"); + + bibDatabase = new BibDatabase(); + renameFieldViewModel = new RenameFieldViewModel(List.of(entryA, entryB), bibDatabase, stateManager); + } + + @Test + void renameFieldShouldRenameFieldIfItExist() { + renameFieldViewModel.selectField(StandardField.DATE); + renameFieldViewModel.setNewFieldName("ETAD"); + renameFieldViewModel.renameField(); + + assertEquals(Optional.of("2014"), entryA.getField(FieldFactory.parseField("ETAD"))); + assertEquals(Optional.empty(), entryA.getField(StandardField.DATE)); + + assertEquals(Optional.of("1998"), entryB.getField(FieldFactory.parseField("ETAD"))); + assertEquals(Optional.empty(), entryB.getField(StandardField.DATE)); + } + + @Test + void renameFieldShouldDoNothingIfFieldDoNotExist() { + Field toRenameField = new UnknownField("Some_field_that_doesnt_exist"); + renameFieldViewModel.selectField(toRenameField); + renameFieldViewModel.setNewFieldName("new_field_name"); + renameFieldViewModel.renameField(); + + assertEquals(Optional.empty(), entryA.getField(toRenameField)); + assertEquals(Optional.empty(), entryA.getField(new UnknownField("new_field_name"))); + + assertEquals(Optional.empty(), entryB.getField(toRenameField)); + assertEquals(Optional.empty(), entryB.getField(new UnknownField("new_field_name"))); + } + + @Test + void renameFieldShouldNotDoAnythingIfTheNewFieldNameIsEmpty() { + renameFieldViewModel.selectField(StandardField.AUTHOR); + renameFieldViewModel.setNewFieldName(""); + renameFieldViewModel.renameField(); + + assertEquals(Optional.of("Doe"), entryA.getField(StandardField.AUTHOR)); + assertEquals(Optional.empty(), entryA.getField(FieldFactory.parseField(""))); + + assertEquals(Optional.of("Eddie"), entryB.getField(StandardField.AUTHOR)); + assertEquals(Optional.empty(), entryB.getField(FieldFactory.parseField(""))); + } + + @Test + void renameFieldShouldNotDoAnythingIfTheNewFieldNameHasWhitespaceCharacters() { + renameFieldViewModel.selectField(StandardField.AUTHOR); + renameFieldViewModel.setNewFieldName("Hello, World"); + renameFieldViewModel.renameField(); + + assertEquals(Optional.of("Doe"), entryA.getField(StandardField.AUTHOR)); + assertEquals(Optional.empty(), entryA.getField(FieldFactory.parseField("Hello, World"))); + + assertEquals(Optional.of("Eddie"), entryB.getField(StandardField.AUTHOR)); + assertEquals(Optional.empty(), entryB.getField(FieldFactory.parseField("Hello, World"))); + } + + @Test + void renameFieldShouldDoNothingWhenThereIsAlreadyAFieldWithTheSameNameAsNewFieldName() { + renameFieldViewModel.selectField(StandardField.DATE); + renameFieldViewModel.setNewFieldName(StandardField.YEAR.getName()); + renameFieldViewModel.renameField(); + + assertEquals(Optional.of("2014"), entryA.getField(StandardField.DATE)); + assertEquals(Optional.of("2015"), entryA.getField(StandardField.YEAR)); + + assertEquals(Optional.empty(), entryB.getField(StandardField.DATE)); + assertEquals(Optional.of("1998"), entryB.getField(StandardField.YEAR)); + } +} diff --git a/src/test/java/org/jabref/logic/bibtex/comparator/GroupDiffTest.java b/src/test/java/org/jabref/logic/bibtex/comparator/GroupDiffTest.java index 1f1c20fc329..9fded7c7802 100644 --- a/src/test/java/org/jabref/logic/bibtex/comparator/GroupDiffTest.java +++ b/src/test/java/org/jabref/logic/bibtex/comparator/GroupDiffTest.java @@ -56,7 +56,7 @@ void compareWithChangedGroup() { Optional groupDiff = GroupDiff.compare(originalMetaData, newMetaData); - Optional expectedGroupDiff = Optional.of(new GroupDiff(newMetaData.getGroups().get(), originalMetaData.getGroups().get())); + Optional expectedGroupDiff = Optional.of(new GroupDiff(originalMetaData.getGroups().get(), newMetaData.getGroups().get())); assertEquals(expectedGroupDiff.get().getNewGroupRoot(), groupDiff.get().getNewGroupRoot()); assertEquals(expectedGroupDiff.get().getOriginalGroupRoot(), groupDiff.get().getOriginalGroupRoot()); diff --git a/src/test/java/org/jabref/logic/bst/BstFunctionsTest.java b/src/test/java/org/jabref/logic/bst/BstFunctionsTest.java new file mode 100644 index 00000000000..1b63e26cbb3 --- /dev/null +++ b/src/test/java/org/jabref/logic/bst/BstFunctionsTest.java @@ -0,0 +1,666 @@ +package org.jabref.logic.bst; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.jabref.logic.bst.util.BstCaseChangersTest; +import org.jabref.logic.bst.util.BstNameFormatterTest; +import org.jabref.logic.bst.util.BstPurifierTest; +import org.jabref.logic.bst.util.BstTextPrefixerTest; +import org.jabref.logic.bst.util.BstWidthCalculatorTest; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.StandardEntryType; + +import org.antlr.v4.runtime.RecognitionException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * For additional tests see for + * + * purify: {@link BstPurifierTest} + * width: {@link BstWidthCalculatorTest} + * format.name: {@link BstNameFormatterTest} + * change.case: {@link BstCaseChangersTest} + * prefix: {@link BstTextPrefixerTest} + * + */ +class BstFunctionsTest { + @Test + public void testCompareFunctions() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test.compare } { + #5 #5 = % TRUE + #1 #2 = % FALSE + #3 #4 < % TRUE + #4 #3 < % FALSE + #4 #4 < % FALSE + #3 #4 > % FALSE + #4 #3 > % TRUE + #4 #4 > % FALSE + "H" "H" = % TRUE + "H" "Ha" = % FALSE + } + EXECUTE { test.compare } + """); + + vm.render(Collections.emptyList()); + + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testArithmeticFunctions() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { + #1 #1 + % 2 + #5 #2 - % 3 + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals(3, vm.getStack().pop()); + assertEquals(2, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testArithmeticFunctionTypeMismatch() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { + #1 "HELLO" + % Should throw exception + } + EXECUTE { test } + """); + + assertThrows(BstVMException.class, () -> vm.render(Collections.emptyList())); + } + + @Test + public void testStringOperations() throws RecognitionException { + // Test for concat (*) and add.period + BstVM vm = new BstVM(""" + FUNCTION { test } { + "H" "ello" * % Hello + "Johnny" add.period$ % Johnny. + "Johnny." add.period$ % Johnny. + "Johnny!" add.period$ % Johnny! + "Johnny?" add.period$ % Johnny? + "Johnny} }}}" add.period$ % Johnny.} + "Johnny!}" add.period$ % Johnny!} + "Johnny?}" add.period$ % Johnny?} + "Johnny.}" add.period$ % Johnny.} + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals("Johnny.}", vm.getStack().pop()); + assertEquals("Johnny?}", vm.getStack().pop()); + assertEquals("Johnny!}", vm.getStack().pop()); + assertEquals("Johnny.}", vm.getStack().pop()); + assertEquals("Johnny?", vm.getStack().pop()); + assertEquals("Johnny!", vm.getStack().pop()); + assertEquals("Johnny.", vm.getStack().pop()); + assertEquals("Johnny.", vm.getStack().pop()); + assertEquals("Hello", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testMissing() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { title } { } { } + FUNCTION { presort } { cite$ 'sort.key$ := } + ITERATE { presort } + READ + SORT + FUNCTION { test } { title missing$ cite$ } + ITERATE { test } + """); + List testEntries = List.of( + BstVMTest.defaultTestEntry(), + new BibEntry(StandardEntryType.Article) + .withCitationKey("test") + .withField(StandardField.AUTHOR, "No title")); + + vm.render(testEntries); + + assertEquals("test", vm.getStack().pop()); // cite + assertEquals(BstVM.TRUE, vm.getStack().pop()); // missing title + assertEquals("canh05", vm.getStack().pop()); // cite + assertEquals(BstVM.FALSE, vm.getStack().pop()); // missing title + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testNumNames() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { + "Johnny Foo { and } Mary Bar" num.names$ + "Johnny Foo and Mary Bar" num.names$ + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals(2, vm.getStack().pop()); + assertEquals(1, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testSubstring() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { + "123456789" #2 #1 substring$ % 2 + "123456789" #4 global.max$ substring$ % 456789 + "123456789" #1 #9 substring$ % 123456789 + "123456789" #1 #10 substring$ % 123456789 + "123456789" #1 #99 substring$ % 123456789 + "123456789" #-7 #3 substring$ % 123 + "123456789" #-1 #1 substring$ % 9 + "123456789" #-1 #3 substring$ % 789 + "123456789" #-2 #2 substring$ % 78 + } EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals("78", vm.getStack().pop()); + assertEquals("789", vm.getStack().pop()); + assertEquals("9", vm.getStack().pop()); + assertEquals("123", vm.getStack().pop()); + assertEquals("123456789", vm.getStack().pop()); + assertEquals("123456789", vm.getStack().pop()); + assertEquals("123456789", vm.getStack().pop()); + assertEquals("456789", vm.getStack().pop()); + assertEquals("2", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testEmpty() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { title } { } { } + READ + STRINGS { s } + FUNCTION { test } { + s empty$ % TRUE + "" empty$ % TRUE + " " empty$ % TRUE + title empty$ % TRUE + " HALLO " empty$ % FALSE + } + ITERATE { test } + """); + List testEntry = List.of(new BibEntry(StandardEntryType.Article)); + + vm.render(testEntry); + + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testFormatNameStatic() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { format }{ "Charles Louis Xavier Joseph de la Vall{\\'e}e Poussin" #1 "{vv~}{ll}{, jj}{, f}?" format.name$ } + EXECUTE { format } + """); + List v = Collections.emptyList(); + + vm.render(v); + + assertEquals("de~la Vall{\\'e}e~Poussin, C.~L. X.~J?", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testFormatNameInEntries() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { author } { } { } + FUNCTION { presort } { cite$ 'sort.key$ := } + ITERATE { presort } + READ + SORT + FUNCTION { format }{ author #2 "{vv~}{ll}{, jj}{, f}?" format.name$ } + ITERATE { format } + """); + List testEntries = List.of( + BstVMTest.defaultTestEntry(), + new BibEntry(StandardEntryType.Book) + .withCitationKey("test") + .withField(StandardField.AUTHOR, "Jonathan Meyer and Charles Louis Xavier Joseph de la Vall{\\'e}e Poussin")); + + vm.render(testEntries); + + assertEquals("de~la Vall{\\'e}e~Poussin, C.~L. X.~J?", vm.getStack().pop()); + assertEquals("Annabi, H?", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testChangeCase() throws RecognitionException { + BstVM vm = new BstVM(""" + STRINGS { title } + READ + FUNCTION { format.title } { + duplicate$ empty$ + { pop$ "" } + { "t" change.case$ } + if$ + } + FUNCTION { test } { + "hello world" "u" change.case$ format.title + "Hello World" format.title + "" format.title + "{A}{D}/{C}ycle: {I}{B}{M}'s {F}ramework for {A}pplication {D}evelopment and {C}ase" "u" change.case$ format.title + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals("{A}{D}/{C}ycle: {I}{B}{M}'s {F}ramework for {A}pplication {D}evelopment and {C}ase", + vm.getStack().pop()); + assertEquals("", vm.getStack().pop()); + assertEquals("Hello world", vm.getStack().pop()); + assertEquals("Hello world", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testTextLength() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { + "hello world" text.length$ % 11 + "Hello {W}orld" text.length$ % 11 + "" text.length$ % 0 + "{A}{D}/{Cycle}" text.length$ % 8 + "{\\This is one character}" text.length$ % 1 + "{\\This {is} {one} {c{h}}aracter as well}" text.length$ % 1 + "{\\And this too" text.length$ % 1 + "These are {\\11}" text.length$ % 11 + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals(11, vm.getStack().pop()); + assertEquals(1, vm.getStack().pop()); + assertEquals(1, vm.getStack().pop()); + assertEquals(1, vm.getStack().pop()); + assertEquals(8, vm.getStack().pop()); + assertEquals(0, vm.getStack().pop()); + assertEquals(11, vm.getStack().pop()); + assertEquals(11, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testIntToStr() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { #3 int.to.str$ #9999 int.to.str$ } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals("9999", vm.getStack().pop()); + assertEquals("3", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testChrToInt() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { "H" chr.to.int$ } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals(72, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testChrToIntIntToChr() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { "H" chr.to.int$ int.to.chr$ } + EXECUTE {test} + """); + + vm.render(Collections.emptyList()); + + assertEquals("H", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testType() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { } { } { } + FUNCTION { presort } { cite$ 'sort.key$ := } + ITERATE { presort } + SORT + FUNCTION { test } { type$ } + ITERATE { test } + """); + List testEntries = List.of( + new BibEntry(StandardEntryType.Article).withCitationKey("a"), + new BibEntry(StandardEntryType.Book).withCitationKey("b"), + new BibEntry(StandardEntryType.Misc).withCitationKey("c"), + new BibEntry(StandardEntryType.InProceedings).withCitationKey("d")); + + vm.render(testEntries); + + assertEquals("inproceedings", vm.getStack().pop()); + assertEquals("misc", vm.getStack().pop()); + assertEquals("book", vm.getStack().pop()); + assertEquals("article", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testCallType() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { title } { } { } + FUNCTION { presort } { cite$ 'sort.key$ := } + ITERATE { presort } + READ + SORT + FUNCTION { inproceedings }{ "InProceedings called on " title * } + FUNCTION { book }{ "Book called on " title * } + ITERATE { call.type$ } + """); + List testEntries = List.of( + BstVMTest.defaultTestEntry(), + new BibEntry(StandardEntryType.Book) + .withCitationKey("test") + .withField(StandardField.TITLE, "Test")); + + vm.render(testEntries); + + assertEquals("Book called on Test", vm.getStack().pop()); + assertEquals( + "InProceedings called on Effective work practices for floss development: A model and propositions", + vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testSwap() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { a } { #3 "Hallo" swap$ } + EXECUTE { a } + """); + + List v = Collections.emptyList(); + vm.render(v); + + assertEquals(3, vm.getStack().pop()); + assertEquals("Hallo", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + void testAssignFunction() { + BstVM vm = new BstVM(""" + INTEGERS { test.var } + FUNCTION { test.func } { #1 'test.var := } + EXECUTE { test.func } + """); + + vm.render(Collections.emptyList()); + + Map functions = vm.latestContext.functions(); + assertTrue(functions.containsKey("test.func")); + assertNotNull(functions.get("test.func")); + assertEquals(1, vm.latestContext.integers().get("test.var")); + } + + @Test + void testSimpleIf() { + BstVM vm = new BstVM(""" + FUNCTION { path1 } { #1 } + FUNCTION { path0 } { #0 } + FUNCTION { test } { + #1 path1 path0 if$ + #0 path1 path0 if$ + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals(0, vm.getStack().pop()); + assertEquals(1, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + void testSimpleWhile() { + BstVM vm = new BstVM(""" + INTEGERS { i } + FUNCTION { test } { + #3 'i := + { i } + { + i + i #1 - + 'i := + } + while$ + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals(1, vm.getStack().pop()); + assertEquals(2, vm.getStack().pop()); + assertEquals(3, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testNestedControlFunctions() throws RecognitionException { + BstVM vm = new BstVM(""" + STRINGS { t } + FUNCTION { not } { { #0 } { #1 } if$ } + FUNCTION { n.dashify } { + "HELLO-WORLD" 't := + "" + { t empty$ not } % while + { + t #1 #1 substring$ "-" = % if + { + t #1 #2 substring$ "--" = not % if + { + "--" * + t #2 global.max$ substring$ 't := + } + { + { t #1 #1 substring$ "-" = } % while + { + "-" * + t #2 global.max$ substring$ 't := + } + while$ + } + if$ + } + { + t #1 #1 substring$ * + t #2 global.max$ substring$ 't := + } + if$ + } + while$ + } + EXECUTE { n.dashify } + """); + List v = Collections.emptyList(); + + vm.render(v); + + assertEquals(1, vm.getStack().size()); + assertEquals("HELLO--WORLD", vm.getStack().pop()); + } + + @Test + public void testLogic() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { not } { { #0 } { #1 } if$ } + FUNCTION { and } { 'skip$ { pop$ #0 } if$ } + FUNCTION { or } { { pop$ #1 } 'skip$ if$ } + FUNCTION { test } { + #1 #1 and + #0 #1 and + #1 #0 and + #0 #0 and + #0 not + #1 not + #1 #1 or + #0 #1 or + #1 #0 or + #0 #0 or + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + /** + * See also {@link BstWidthCalculatorTest} + */ + + @Test + public void testWidth() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { address author title type } { } { label } + STRINGS { longest.label } + INTEGERS { number.label longest.label.width } + FUNCTION { initialize.longest.label } { + "" 'longest.label := + #1 'number.label := + #0 'longest.label.width := + } + FUNCTION {longest.label.pass} { + number.label int.to.str$ 'label := + number.label #1 + 'number.label := + label width$ longest.label.width > + { + label 'longest.label := + label width$ 'longest.label.width := + } + 'skip$ + if$ + } + EXECUTE { initialize.longest.label } + ITERATE { longest.label.pass } + FUNCTION { begin.bib } { + preamble$ empty$ + 'skip$ + { preamble$ write$ newline$ } + if$ + "\\begin{thebibliography}{" longest.label * "}" * + } + EXECUTE {begin.bib} + """); + + List testEntries = List.of(BstVMTest.defaultTestEntry()); + + vm.render(testEntries); + + assertTrue(vm.latestContext.integers().containsKey("longest.label.width")); + assertEquals("\\begin{thebibliography}{1}", vm.getStack().pop()); + } + + @Test + public void testDuplicateEmptyPopSwapIf() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { emphasize } { + duplicate$ empty$ + { pop$ "" } + { "{\\em " swap$ * "}" * } + if$ + } + FUNCTION { test } { + "" emphasize + "Hello" emphasize + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals("{\\em Hello}", vm.getStack().pop()); + assertEquals("", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testPreambleWriteNewlineQuote() { + BstVM vm = new BstVM(""" + FUNCTION { test } { + preamble$ + write$ + newline$ + "hello" + write$ + quote$ "quoted" * quote$ * + write$ + } + EXECUTE { test } + """); + + BibDatabase testDatabase = new BibDatabase(); + testDatabase.setPreamble("A Preamble"); + + String result = vm.render(Collections.emptyList(), testDatabase); + + assertEquals("A Preamble\nhello\"quoted\"", result); + } +} diff --git a/src/test/java/org/jabref/logic/bst/BstVMTest.java b/src/test/java/org/jabref/logic/bst/BstVMTest.java new file mode 100644 index 00000000000..4a66a8d5836 --- /dev/null +++ b/src/test/java/org/jabref/logic/bst/BstVMTest.java @@ -0,0 +1,220 @@ +package org.jabref.logic.bst; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.StandardEntryType; + +import org.antlr.v4.runtime.RecognitionException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BstVMTest { + + public static BibEntry defaultTestEntry() { + return new BibEntry(StandardEntryType.InProceedings) + .withCitationKey("canh05") + .withField(StandardField.AUTHOR, "Crowston, K. and Annabi, H. and Howison, J. and Masango, C.") + .withField(StandardField.TITLE, "Effective work practices for floss development: A model and propositions") + .withField(StandardField.BOOKTITLE, "Hawaii International Conference On System Sciences (HICSS)") + .withField(StandardField.YEAR, "2005") + .withField(StandardField.OWNER, "oezbek") + .withField(StandardField.TIMESTAMP, "2006.05.29") + .withField(StandardField.URL, "http://james.howison.name/publications.html"); + } + + @Test + public void testAbbrv() throws RecognitionException, IOException { + BstVM vm = new BstVM(Path.of("src/test/resources/org/jabref/logic/bst/abbrv.bst")); + List testEntries = List.of(defaultTestEntry()); + + String expected = "\\begin{thebibliography}{1}\\bibitem{canh05}K.~Crowston, H.~Annabi, J.~Howison, and C.~Masango.\\newblock Effective work practices for floss development: A model and propositions.\\newblock In {\\em Hawaii International Conference On System Sciences (HICSS)}, 2005.\\end{thebibliography}"; + String result = vm.render(testEntries); + + assertEquals( + expected.replaceAll("\\s", ""), + result.replaceAll("\\s", "")); + } + + @Test + public void testSimple() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { address author title type } { } { label } + INTEGERS { output.state before.all mid.sentence after.sentence after.block } + FUNCTION { init.state.consts }{ + #0 'before.all := + #1 'mid.sentence := + #2 'after.sentence := + #3 'after.block := + } + STRINGS { s t } + READ + """); + List testEntries = List.of(defaultTestEntry()); + + vm.render(testEntries); + + assertEquals(2, vm.latestContext.strings().size()); + assertEquals(7, vm.latestContext.integers().size()); + assertEquals(1, vm.latestContext.entries().size()); + assertEquals(5, vm.latestContext.entries().get(0).fields.size()); + assertEquals(38, vm.latestContext.functions().size()); + } + + @Test + public void testLabel() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { title } {} { label } + FUNCTION { test } { + label #0 = + title 'label := + #5 label #6 pop$ } + READ + ITERATE { test } + """); + List
- * Documentation can be found in the original bibtex distribution: - *
- * https://www.ctan.org/pkg/bibtex - */ -public class VM implements Warn { - - public static final Integer FALSE = 0; - - public static final Integer TRUE = 1; - - private static final Pattern ADD_PERIOD_PATTERN = Pattern.compile("([^\\.\\?\\!\\}\\s])(\\}|\\s)*$"); - - private static final Logger LOGGER = LoggerFactory.getLogger(VM.class); - - private List entries; - - private Map strings = new HashMap<>(); - - private Map integers = new HashMap<>(); - - private Map functions = new HashMap<>(); - - private Stack stack = new Stack<>(); - - private final Map buildInFunctions; - - private File file; - - private final CommonTree tree; - - private StringBuilder bbl; - - private String preamble = ""; - - public static class Identifier { - - public final String name; - - public Identifier(String name) { - this.name = name; - } - - public String getName() { - return name; - } - } - - public static class Variable { - - public final String name; - - public Variable(String name) { - this.name = name; - } - - public String getName() { - return name; - } - } - - @FunctionalInterface - public interface BstFunction { - void execute(BstEntry context); - } - - public VM(File f) throws RecognitionException, IOException { - this(new ANTLRFileStream(f.getPath())); - this.file = f; - } - - public VM(String s) throws RecognitionException { - this(new ANTLRStringStream(s)); - } - - private VM(CharStream bst) throws RecognitionException { - this(VM.charStream2CommonTree(bst)); - } - - private VM(CommonTree tree) { - this.tree = tree; - - this.buildInFunctions = new HashMap<>(37); - - /* - * Pops the top two (integer) literals, compares them, and pushes - * the integer 1 if the second is greater than the first, 0 - * otherwise. - */ - buildInFunctions.put(">", context -> { - if (stack.size() < 2) { - throw new VMException("Not enough operands on stack for operation >"); - } - Object o2 = stack.pop(); - Object o1 = stack.pop(); - - if (!((o1 instanceof Integer) && (o2 instanceof Integer))) { - throw new VMException("Can only compare two integers with >"); - } - - stack.push(((Integer) o1).compareTo((Integer) o2) > 0 ? VM.TRUE : VM.FALSE); - }); - - /* Analogous to >. */ - buildInFunctions.put("<", context -> { - if (stack.size() < 2) { - throw new VMException("Not enough operands on stack for operation <"); - } - Object o2 = stack.pop(); - Object o1 = stack.pop(); - - if (!((o1 instanceof Integer) && (o2 instanceof Integer))) { - throw new VMException("Can only compare two integers with <"); - } - - stack.push(((Integer) o1).compareTo((Integer) o2) < 0 ? VM.TRUE : VM.FALSE); - }); - - /* - * Pops the top two (both integer or both string) literals, compares - * them, and pushes the integer 1 if they're equal, 0 otherwise. - */ - buildInFunctions.put("=", context -> { - if (stack.size() < 2) { - throw new VMException("Not enough operands on stack for operation ="); - } - Object o1 = stack.pop(); - Object o2 = stack.pop(); - - if ((o1 == null) ^ (o2 == null)) { - stack.push(VM.FALSE); - return; - } - - if ((o1 == null) && (o2 == null)) { - stack.push(VM.TRUE); - return; - } - - stack.push(o1.equals(o2) ? VM.TRUE : VM.FALSE); - }); - - /* Pops the top two (integer) literals and pushes their sum. */ - buildInFunctions.put("+", context -> { - if (stack.size() < 2) { - throw new VMException("Not enough operands on stack for operation +"); - } - Object o2 = stack.pop(); - Object o1 = stack.pop(); - - if (!((o1 instanceof Integer) && (o2 instanceof Integer))) { - throw new VMException("Can only compare two integers with +"); - } - - stack.push((Integer) o1 + (Integer) o2); - }); - - /* - * Pops the top two (integer) literals and pushes their difference - * (the first subtracted from the second). - */ - buildInFunctions.put("-", context -> { - if (stack.size() < 2) { - throw new VMException("Not enough operands on stack for operation -"); - } - Object o2 = stack.pop(); - Object o1 = stack.pop(); - - if (!((o1 instanceof Integer) && (o2 instanceof Integer))) { - throw new VMException("Can only subtract two integers with -"); - } - - stack.push((Integer) o1 - (Integer) o2); - }); - - /* - * Pops the top two (string) literals, concatenates them (in reverse - * order, that is, the order in which pushed), and pushes the - * resulting string. - */ - buildInFunctions.put("*", context -> { - if (stack.size() < 2) { - throw new VMException("Not enough operands on stack for operation *"); - } - Object o2 = stack.pop(); - Object o1 = stack.pop(); - - if (o1 == null) { - o1 = ""; - } - if (o2 == null) { - o2 = ""; - } - - if (!((o1 instanceof String) && (o2 instanceof String))) { - LOGGER.error("o1: {} ({})", o1, o1.getClass()); - LOGGER.error("o2: {} ({})", o2, o2.getClass()); - throw new VMException("Can only concatenate two String with *"); - } - - stack.push(o1.toString() + o2); - }); - - /* - * Pops the top two literals and assigns to the first (which must be - * a global or entry variable) the value of the second. - */ - buildInFunctions.put(":=", context -> { - if (stack.size() < 2) { - throw new VMException("Invalid call to operation :="); - } - Object o1 = stack.pop(); - Object o2 = stack.pop(); - assign(context, o1, o2); - }); - - /* - * Pops the top (string) literal, adds a `.' to it if the last non - * '}' character isn't a `.', `?', or `!', and pushes this resulting - * string. - */ - buildInFunctions.put("add.period$", context -> addPeriodFunction()); - - /* - * Executes the function whose name is the entry type of an entry. - * For example if an entry is of type book, this function executes - * the book function. When given as an argument to the ITERATE - * command, call.type$ actually produces the output for the entries. - * For an entry with an unknown type, it executes the function - * default.type. Thus you should define (before the READ command) - * one function for each standard entry type as well as a - * default.type function. - */ - buildInFunctions.put("call.type$", context -> { - if (context == null) { - throw new VMException("Call.type$ can only be called from within a context (ITERATE or REVERSE)."); - } - VM.this.execute(context.entry.getType().getName(), context); - }); - - buildInFunctions.put("change.case$", new ChangeCaseFunction(this)); - - /* - * Pops the top (string) literal, makes sure it's a single - * character, converts it to the corresponding ASCII integer, and - * pushes this integer. - */ - buildInFunctions.put("chr.to.int$", context -> { - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation chr.to.int$"); - } - Object o1 = stack.pop(); - - if (!((o1 instanceof String) && (((String) o1).length() == 1))) { - throw new VMException("Can only perform chr.to.int$ on string with length 1"); - } - - String s = (String) o1; - - stack.push((int) s.charAt(0)); - }); - - /* - * Pushes the string that was the \cite-command argument for this - * entry. - */ - buildInFunctions.put("cite$", context -> { - if (context == null) { - throw new VMException("Must have an entry to cite$"); - } - stack.push(context.entry.getCitationKey().orElse(null)); - }); - - /* - * Pops the top literal from the stack and pushes two copies of it. - */ - buildInFunctions.put("duplicate$", context -> { - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation duplicate$"); - } - Object o1 = stack.pop(); - - stack.push(o1); - stack.push(o1); - }); - - /* - * Pops the top literal and pushes the integer 1 if it's a missing - * field or a string having no non-white-space characters, 0 - * otherwise. - */ - buildInFunctions.put("empty$", context -> { - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation empty$"); - } - Object o1 = stack.pop(); - - if (o1 == null) { - stack.push(VM.TRUE); - return; - } - - if (!(o1 instanceof String)) { - throw new VMException("Operand does not match function empty$"); - } - - String s = (String) o1; - - stack.push("".equals(s.trim()) ? VM.TRUE : VM.FALSE); - }); - - buildInFunctions.put("format.name$", new FormatNameFunction(this)); - - /* - * Pops the top three literals (they are two function literals and - * an integer literal, in that order); if the integer is greater - * than 0, it executes the second literal, else it executes the - * first. - */ - buildInFunctions.put("if$", context -> { - if (stack.size() < 3) { - throw new VMException("Not enough operands on stack for operation ="); - } - Object f1 = stack.pop(); - Object f2 = stack.pop(); - Object i = stack.pop(); - - if (!((f1 instanceof Identifier) || (f1 instanceof Tree)) - && ((f2 instanceof Identifier) || (f2 instanceof Tree)) && (i instanceof Integer)) { - throw new VMException("Expecting two functions and an integer for if$."); - } - - if ((Integer) i > 0) { - VM.this.executeInContext(f2, context); - } else { - VM.this.executeInContext(f1, context); - } - }); - - /* - * Pops the top (integer) literal, interpreted as the ASCII integer - * value of a single character, converts it to the corresponding - * single-character string, and pushes this string. - */ - buildInFunctions.put("int.to.chr$", context -> { - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation int.to.chr$"); - } - Object o1 = stack.pop(); - - if (!(o1 instanceof Integer)) { - throw new VMException("Can only perform operation int.to.chr$ on an Integer"); - } - - Integer i = (Integer) o1; - - stack.push(String.valueOf((char) i.intValue())); - }); - - /* - * Pops the top (integer) literal, converts it to its (unique) - * string equivalent, and pushes this string. - */ - buildInFunctions.put("int.to.str$", context -> { - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation int.to.str$"); - } - Object o1 = stack.pop(); - - if (!(o1 instanceof Integer)) { - throw new VMException("Can only transform an integer to an string using int.to.str$"); - } - - stack.push(o1.toString()); - }); - - /* - * Pops the top literal and pushes the integer 1 if it's a missing - * field, 0 otherwise. - */ - buildInFunctions.put("missing$", context -> { - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation missing$"); - } - Object o1 = stack.pop(); - - if (o1 == null) { - stack.push(VM.TRUE); - return; - } - - if (!(o1 instanceof String)) { - warn("Not a string or missing field in operation missing$"); - stack.push(VM.TRUE); - return; - } - - stack.push(VM.FALSE); - }); - - /* - * Writes onto the bbl file what is accumulated in the output buffer. - * It writes a blank line if and only if the output buffer is empty. - * Since write$ does reasonable line breaking, you should use this - * function only when you want a blank line or an explicit line - * break. - */ - buildInFunctions.put("newline$", context -> VM.this.bbl.append('\n')); - - /* - * Pops the top (string) literal and pushes the number of names the - * string represents one plus the number of occurrences of the - * substring "and" (ignoring case differences) surrounded by - * non-null white-space at the top brace level. - */ - buildInFunctions.put("num.names$", context -> { - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation num.names$"); - } - Object o1 = stack.pop(); - - if (!(o1 instanceof String)) { - throw new VMException("Need a string at the top of the stack for num.names$"); - } - String s = (String) o1; - - stack.push(AuthorList.parse(s).getNumberOfAuthors()); - }); - - /* - * Pops the top of the stack but doesn't print it; this gets rid of - * an unwanted stack literal. - */ - buildInFunctions.put("pop$", context -> stack.pop()); - - /* - * The |built_in| function {\.{preamble\$}} pushes onto the stack - * the concatenation of all the \.{preamble} strings read from the - * database files. (or the empty string if there where none) - * - * @PREAMBLE strings read from the database files. - */ - buildInFunctions.put("preamble$", context -> { - stack.push(preamble); - }); - - /* - * Pops the top (string) literal, removes nonalphanumeric characters - * except for white-space characters and hyphens and ties (these all get - * converted to a space), removes certain alphabetic characters - * contained in the control sequences associated with a \special - * character", and pushes the resulting string. - */ - buildInFunctions.put("purify$", new PurifyFunction(this)); - - /* - * Pushes the string consisting of the double-quote character. - */ - buildInFunctions.put("quote$", context -> stack.push("\"")); - - /* - * Is a no-op. - */ - buildInFunctions.put("skip$", context -> { - // Nothing to do! Yeah! - }); - - /* - * Pops and prints the whole stack; it's meant to be used for style - * designers while debugging. - */ - buildInFunctions.put("stack$", context -> { - while (!stack.empty()) { - LOGGER.debug("Stack entry {}", stack.pop()); - } - }); - - /* - * Pops the top three literals (they are the two integers literals - * len and start, and a string literal, in that order). It pushes - * the substring of the (at most) len consecutive characters - * starting at the startth character (assuming 1-based indexing) if - * start is positive, and ending at the start-th character - * (including) from the end if start is negative (where the first - * character from the end is the last character). - */ - buildInFunctions.put("substring$", context -> substringFunction()); - - /* - * Swaps the top two literals on the stack. text.length$ Pops the - * top (string) literal, and pushes the number of text characters - * it contains, where an accented character (more precisely, a - * \special character", defined in Section 4) counts as a single - * text character, even if it's missing its matching right brace, - * and where braces don't count as text characters. - */ - buildInFunctions.put("swap$", context -> { - if (stack.size() < 2) { - throw new VMException("Not enough operands on stack for operation swap$"); - } - Object f1 = stack.pop(); - Object f2 = stack.pop(); - - stack.push(f1); - stack.push(f2); - }); - - /* - * text.length$ Pops the top (string) literal, and pushes the number - * of text characters it contains, where an accented character (more - * precisely, a "special character", defined in Section 4) counts as - * a single text character, even if it's missing its matching right - * brace, and where braces don't count as text characters. - * - * From BibTeXing: For the purposes of counting letters in labels, - * BibTEX considers everything contained inside the braces as a - * single letter. - */ - buildInFunctions.put("text.length$", context -> textLengthFunction()); - - /* - * Pops the top two literals (the integer literal len and a string - * literal, in that order). It pushes the substring of the (at most) len - * consecutive text characters starting from the beginning of the - * string. This function is similar to substring$, but this one - * considers a \special character", even if it's missing its matching - * right brace, to be a single text character (rather than however many - * ASCII characters it actually comprises), and this function doesn't - * consider braces to be text characters; furthermore, this function - * appends any needed matching right braces. - */ - buildInFunctions.put("text.prefix$", new TextPrefixFunction(this)); - - /* - * Pops and prints the top of the stack to the log file. It's useful for debugging. - */ - buildInFunctions.put("top$", context -> LOGGER.debug("Stack entry {}", stack.pop())); - - /* - * Pushes the current entry's type (book, article, etc.), but pushes - * the null string if the type is either unknown or undefined. - */ - buildInFunctions.put("type$", context -> { - if (context == null) { - throw new VMException("type$ need a context."); - } - - stack.push(context.entry.getType().getName()); - }); - - /* - * Pops the top (string) literal and prints it following a warning - * message. This also increments a count of the number of warning - * messages issued. - */ - buildInFunctions.put("warning$", new BstFunction() { - int warning = 1; - - @Override - public void execute(BstEntry context) { - LOGGER.warn("Warning (#" + (warning++) + "): " + stack.pop()); - } - }); - - /* - * Pops the top two (function) literals, and keeps executing the - * second as long as the (integer) literal left on the stack by - * executing the first is greater than 0. - */ - buildInFunctions.put("while$", this::whileFunction); - - buildInFunctions.put("width$", new WidthFunction(this)); - - /* - * Pops the top (string) literal and writes it on the output buffer - * (which will result in stuff being written onto the bbl file when - * the buffer fills up). - */ - buildInFunctions.put("write$", context -> { - String s = (String) stack.pop(); - VM.this.bbl.append(s); - }); - } - - private void textLengthFunction() { - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation text.length$"); - } - Object o1 = stack.pop(); - - if (!(o1 instanceof String)) { - throw new VMException("Can only perform operation on a string text.length$"); - } - - String s = (String) o1; - char[] c = s.toCharArray(); - int result = 0; - - // Comments from bibtex.web: - - // sp_ptr := str_start[pop_lit1]; - int i = 0; - - // sp_end := str_start[pop_lit1+1]; - int n = s.length(); - - // sp_brace_level := 0; - int braceLevel = 0; - - // while (sp_ptr < sp_end) do begin - while (i < n) { - // incr(sp_ptr); - i++; - // if (str_pool[sp_ptr-1] = left_brace) then - // begin - if (c[i - 1] == '{') { - // incr(sp_brace_level); - braceLevel++; - // if ((sp_brace_level = 1) and (sp_ptr < sp_end)) then - if ((braceLevel == 1) && (i < n)) { - // if (str_pool[sp_ptr] = backslash) then - // begin - if (c[i] == '\\') { - // incr(sp_ptr); {skip over the |backslash|} - i++; // skip over backslash - // while ((sp_ptr < sp_end) and (sp_brace_level - // > 0)) do begin - while ((i < n) && (braceLevel > 0)) { - // if (str_pool[sp_ptr] = right_brace) then - if (c[i] == '}') { - // decr(sp_brace_level) - braceLevel--; - } else if (c[i] == '{') { - // incr(sp_brace_level); - braceLevel++; - } - // incr(sp_ptr); - i++; - // end; - } - // incr(num_text_chars); - result++; - // end; - } - // end - } - - // else if (str_pool[sp_ptr-1] = right_brace) then - // begin - } else if (c[i - 1] == '}') { - // if (sp_brace_level > 0) then - if (braceLevel > 0) { - // decr(sp_brace_level); - braceLevel--; - // end - } - } else { // else - // incr(num_text_chars); - result++; - } - } - stack.push(result); - } - - private void whileFunction(BstEntry context) { - if (stack.size() < 2) { - throw new VMException("Not enough operands on stack for operation while$"); - } - Object f2 = stack.pop(); - Object f1 = stack.pop(); - - if (!((f1 instanceof Identifier) || (f1 instanceof Tree)) - && ((f2 instanceof Identifier) || (f2 instanceof Tree))) { - throw new VMException("Expecting two functions for while$."); - } - - do { - VM.this.executeInContext(f1, context); - - Object i = stack.pop(); - if (!(i instanceof Integer)) { - throw new VMException("First parameter to while has to return an integer but was " + i); - } - if ((Integer) i <= 0) { - break; - } - VM.this.executeInContext(f2, context); - } while (true); - } - - private void substringFunction() { - if (stack.size() < 3) { - throw new VMException("Not enough operands on stack for operation substring$"); - } - Object o1 = stack.pop(); - Object o2 = stack.pop(); - Object o3 = stack.pop(); - - if (!((o1 instanceof Integer) && (o2 instanceof Integer) && (o3 instanceof String))) { - throw new VMException("Expecting two integers and a string for substring$"); - } - - Integer len = (Integer) o1; - Integer start = (Integer) o2; - - int lenI = len; - int startI = start; - - if (lenI > (Integer.MAX_VALUE / 2)) { - lenI = Integer.MAX_VALUE / 2; - } - - if (startI > (Integer.MAX_VALUE / 2)) { - startI = Integer.MAX_VALUE / 2; - } - - if (startI < (Integer.MIN_VALUE / 2)) { - startI = -Integer.MIN_VALUE / 2; - } - - String s = (String) o3; - - if (startI < 0) { - startI += s.length() + 1; - startI = Math.max(1, (startI + 1) - lenI); - } - stack.push(s.substring(startI - 1, Math.min((startI - 1) + lenI, s.length()))); - } - - private void addPeriodFunction() { - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation add.period$"); - } - Object o1 = stack.pop(); - - if (!(o1 instanceof String)) { - throw new VMException("Can only add a period to a string for add.period$"); - } - - String s = (String) o1; - Matcher m = ADD_PERIOD_PATTERN.matcher(s); - - if (m.find()) { - StringBuilder sb = new StringBuilder(); - m.appendReplacement(sb, m.group(1)); - sb.append('.'); - String group2 = m.group(2); - if (group2 != null) { - sb.append(m.group(2)); - } - stack.push(sb.toString()); - } else { - stack.push(s); - } - } - - private static CommonTree charStream2CommonTree(CharStream bst) throws RecognitionException { - BstLexer lex = new BstLexer(bst); - CommonTokenStream tokens = new CommonTokenStream(lex); - BstParser parser = new BstParser(tokens); - BstParser.program_return r = parser.program(); - return (CommonTree) r.getTree(); - } - - private boolean assign(BstEntry context, Object o1, Object o2) { - if (!(o1 instanceof Identifier) || !((o2 instanceof String) || (o2 instanceof Integer))) { - throw new VMException("Invalid parameters"); - } - - String name = ((Identifier) o1).getName(); - - if (o2 instanceof String) { - if ((context != null) && context.localStrings.containsKey(name)) { - context.localStrings.put(name, (String) o2); - return true; - } - - if (strings.containsKey(name)) { - strings.put(name, (String) o2); - return true; - } - return false; - } - - if ((context != null) && context.localIntegers.containsKey(name)) { - context.localIntegers.put(name, (Integer) o2); - return true; - } - - if (integers.containsKey(name)) { - integers.put(name, (Integer) o2); - return true; - } - return false; - } - - public String run(BibDatabase db) { - preamble = db.getPreamble().orElse(""); - return run(db.getEntries()); - } - - public String run(Collection bibtex) { - return this.run(bibtex, null); - } - - /** - * Transforms the given list of BibEntries to a rendered list of references using the underlying bst file - * - * @param bibEntries list of entries to convert - * @param bibDatabase (may be null) the bibDatabase used for resolving strings / crossref - * @return list of references in plain text form - */ - public String run(Collection bibEntries, BibDatabase bibDatabase) { - Objects.requireNonNull(bibEntries); - - // Reset - bbl = new StringBuilder(); - - strings = new HashMap<>(); - - integers = new HashMap<>(); - integers.put("entry.max$", Integer.MAX_VALUE); - integers.put("global.max$", Integer.MAX_VALUE); - - functions = new HashMap<>(); - functions.putAll(buildInFunctions); - - stack = new Stack<>(); - - // Create entries - entries = new ArrayList<>(bibEntries.size()); - for (BibEntry entry : bibEntries) { - entries.add(new BstEntry(entry)); - } - - // Go - for (int i = 0; i < tree.getChildCount(); i++) { - Tree child = tree.getChild(i); - switch (child.getType()) { - case BstParser.STRINGS: - strings(child); - break; - case BstParser.INTEGERS: - integers(child); - break; - case BstParser.FUNCTION: - function(child); - break; - case BstParser.EXECUTE: - execute(child); - break; - case BstParser.SORT: - sort(); - break; - case BstParser.ITERATE: - iterate(child); - break; - case BstParser.REVERSE: - reverse(child); - break; - case BstParser.ENTRY: - entry(child); - break; - case BstParser.READ: - read(bibDatabase); - break; - case BstParser.MACRO: - macro(child); - break; - default: - LOGGER.info("Unknown type: {}", child.getType()); - break; - } - } - - return bbl.toString(); - } - - /** - * Dredges up from the database file the field values for each entry in the list. It has no arguments. If a database - * entry doesn't have a value for a field (and probably no database entry will have a value for every field), that - * field variable is marked as missing for the entry. - * - * We use null for the missing entry designator. - */ - private void read(BibDatabase bibDatabase) { - FieldWriter fieldWriter = new FieldWriter(new FieldWriterPreferences(true, List.of(StandardField.MONTH), new FieldContentFormatterPreferences())); - for (BstEntry e : entries) { - for (Map.Entry mEntry : e.fields.entrySet()) { - Field field = FieldFactory.parseField(mEntry.getKey()); - String fieldValue = e.entry.getResolvedFieldOrAlias(field, bibDatabase) - .map(content -> { - try { - String result = fieldWriter.write(field, content); - if (result.startsWith("{")) { - // Strip enclosing {} from the output - return result.substring(1, result.length() - 1); - } - if (field == StandardField.MONTH) { - // We don't have the internal BibTeX strings at hand. - // We nevertheless want to have the full month name. - // Thus, we lookup the full month name here. - return Month.parse(result) - .map(month -> month.getFullName()) - .orElse(result); - } - return result; - } catch (InvalidFieldValueException invalidFieldValueException) { - // in case there is something wrong with the content, just return the content itself - return content; - } - }) - .orElse(null); - mEntry.setValue(fieldValue); - } - } - - for (BstEntry e : entries) { - if (!e.fields.containsKey(StandardField.CROSSREF.getName())) { - e.fields.put(StandardField.CROSSREF.getName(), null); - } - } - } - - /** - * Defines a string macro. It has two arguments; the first is the macro's name, which is treated like any other - * variable or function name, and the second is its definition, which must be double-quote-delimited. You must have - * one for each three-letter month abbreviation; in addition, you should have one for common journal names. The - * user's database may override any definition you define using this command. If you want to define a string the - * user can't touch, use the FUNCTION command, which has a compatible syntax. - */ - private void macro(Tree child) { - String name = child.getChild(0).getText(); - String replacement = child.getChild(1).getText(); - functions.put(name, new MacroFunction(replacement)); - } - - public class MacroFunction implements BstFunction { - - private final String replacement; - - public MacroFunction(String replacement) { - this.replacement = replacement; - } - - @Override - public void execute(BstEntry context) { - VM.this.push(replacement); - } - } - - /** - * Declares the fields and entry variables. It has three arguments, each a (possibly empty) list of variable names. - * The three lists are of: fields, integer entry variables, and string entry variables. There is an additional field - * that BibTEX automatically declares, crossref, used for cross referencing. And there is an additional string entry - * variable automatically declared, sort.key$, used by the SORT command. Each of these variables has a value for - * each entry on the list. - */ - private void entry(Tree child) { - // Fields first - Tree t = child.getChild(0); - - for (int i = 0; i < t.getChildCount(); i++) { - String name = t.getChild(i).getText(); - - for (BstEntry entry : entries) { - entry.fields.put(name, null); - } - } - - // Integers - t = child.getChild(1); - - for (int i = 0; i < t.getChildCount(); i++) { - String name = t.getChild(i).getText(); - - for (BstEntry entry : entries) { - entry.localIntegers.put(name, 0); - } - } - // Strings - t = child.getChild(2); - - for (int i = 0; i < t.getChildCount(); i++) { - String name = t.getChild(i).getText(); - for (BstEntry entry : entries) { - entry.localStrings.put(name, null); - } - } - for (BstEntry entry : entries) { - entry.localStrings.put("sort.key$", null); - } - } - - private void reverse(Tree child) { - BstFunction f = functions.get(child.getChild(0).getText()); - - ListIterator i = entries.listIterator(entries.size()); - while (i.hasPrevious()) { - f.execute(i.previous()); - } - } - - private void iterate(Tree child) { - BstFunction f = functions.get(child.getChild(0).getText()); - - for (BstEntry entry : entries) { - f.execute(entry); - } - } - - /** - * Sorts the entry list using the values of the string entry variable sort.key$. It has no arguments. - */ - private void sort() { - entries.sort(Comparator.comparing(o -> (o.localStrings.get("sort.key$")))); - } - - private void executeInContext(Object o, BstEntry context) { - if (o instanceof Tree) { - Tree t = (Tree) o; - new StackFunction(t).execute(context); - } else if (o instanceof Identifier) { - execute(((Identifier) o).getName(), context); - } - } - - private void execute(Tree child) { - execute(child.getChild(0).getText(), null); - } - - public class StackFunction implements BstFunction { - - private final Tree localTree; - - public StackFunction(Tree stack) { - localTree = stack; - } - - public Tree getTree() { - return localTree; - } - - @Override - public void execute(BstEntry context) { - for (int i = 0; i < localTree.getChildCount(); i++) { - Tree c = localTree.getChild(i); - try { - - switch (c.getType()) { - case BstParser.STRING: - String s = c.getText(); - push(s.substring(1, s.length() - 1)); - break; - case BstParser.INTEGER: - push(Integer.parseInt(c.getText().substring(1))); - break; - case BstParser.QUOTED: - push(new Identifier(c.getText().substring(1))); - break; - case BstParser.STACK: - push(c); - break; - default: - VM.this.execute(c.getText(), context); - break; - } - } catch (VMException e) { - if (file == null) { - LOGGER.error("ERROR " + e.getMessage() + " (" + c.getLine() + ")"); - } else { - LOGGER.error("ERROR " + e.getMessage() + " (" + file.getPath() + ":" - + c.getLine() + ")"); - } - throw e; - } - } - } - } - - private void push(Tree t) { - stack.push(t); - } - - private void execute(String name, BstEntry context) { - if (context != null) { - if (context.fields.containsKey(name)) { - stack.push(context.fields.get(name)); - return; - } - if (context.localStrings.containsKey(name)) { - stack.push(context.localStrings.get(name)); - return; - } - if (context.localIntegers.containsKey(name)) { - stack.push(context.localIntegers.get(name)); - return; - } - } - if (strings.containsKey(name)) { - stack.push(strings.get(name)); - return; - } - if (integers.containsKey(name)) { - stack.push(integers.get(name)); - return; - } - - if (functions.containsKey(name)) { - // OK to have a null context - functions.get(name).execute(context); - return; - } - - throw new VMException("No matching identifier found: " + name); - } - - private void function(Tree child) { - String name = child.getChild(0).getText(); - Tree localStack = child.getChild(1); - functions.put(name, new StackFunction(localStack)); - } - - /** - * Declares global integer variables. It has one argument, a list of variable names. There are two such - * automatically-declared variables, entry.max$ and global.max$, used for limiting the lengths of string vari- - * ables. You may have any number of these commands, but a variable's declaration must precede its use. - */ - private void integers(Tree child) { - Tree t = child.getChild(0); - - for (int i = 0; i < t.getChildCount(); i++) { - String name = t.getChild(i).getText(); - integers.put(name, 0); - } - } - - /** - * Declares global string variables. It has one argument, a list of variable names. You may have any number of these - * commands, but a variable's declaration must precede its use. - * - * @param child - */ - private void strings(Tree child) { - Tree t = child.getChild(0); - - for (int i = 0; i < t.getChildCount(); i++) { - String name = t.getChild(i).getText(); - strings.put(name, null); - } - } - - public static class BstEntry { - - public final BibEntry entry; - - public final Map localStrings = new HashMap<>(); - - // keys filled by org.jabref.logic.bst.VM.entry based on the contents of the bst file - public final Map fields = new HashMap<>(); - - public final Map localIntegers = new HashMap<>(); - - public BstEntry(BibEntry e) { - this.entry = e; - } - } - - private void push(Integer integer) { - stack.push(integer); - } - - private void push(String string) { - stack.push(string); - } - - private void push(Identifier identifier) { - stack.push(identifier); - } - - public Map getStrings() { - return strings; - } - - public Map getIntegers() { - return integers; - } - - public List getEntries() { - return entries; - } - - public Map getFunctions() { - return functions; - } - - public Stack getStack() { - return stack; - } - - @Override - public void warn(String string) { - LOGGER.warn(string); - } -} diff --git a/src/main/java/org/jabref/logic/bst/VMException.java b/src/main/java/org/jabref/logic/bst/VMException.java deleted file mode 100644 index f46c5cd5277..00000000000 --- a/src/main/java/org/jabref/logic/bst/VMException.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.jabref.logic.bst; - -public class VMException extends RuntimeException { - - public VMException(String string) { - super(string); - } -} diff --git a/src/main/java/org/jabref/logic/bst/Warn.java b/src/main/java/org/jabref/logic/bst/Warn.java deleted file mode 100644 index 7a524ad9834..00000000000 --- a/src/main/java/org/jabref/logic/bst/Warn.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.jabref.logic.bst; - -@FunctionalInterface -public interface Warn { - - void warn(String s); -} diff --git a/src/main/java/org/jabref/logic/bst/WidthFunction.java b/src/main/java/org/jabref/logic/bst/WidthFunction.java deleted file mode 100644 index 06784267bf0..00000000000 --- a/src/main/java/org/jabref/logic/bst/WidthFunction.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.jabref.logic.bst; - -import java.util.Stack; - -import org.jabref.logic.bst.VM.BstEntry; -import org.jabref.logic.bst.VM.BstFunction; - -/** - * The |built_in| function {\.{width\$}} pops the top (string) literal and - * pushes the integer that represents its width in units specified by the - * |char_width| array. This function takes the literal literally; that is, it - * assumes each character in the string is to be printed as is, regardless of - * whether the character has a special meaning to \TeX, except that special - * characters (even without their |right_brace|s) are handled specially. If the - * literal isn't a string, it complains and pushes~0. - * - */ -public class WidthFunction implements BstFunction { - - private final VM vm; - - public WidthFunction(VM vm) { - this.vm = vm; - } - - @Override - public void execute(BstEntry context) { - Stack stack = vm.getStack(); - - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation width$"); - } - Object o1 = stack.pop(); - - if (!(o1 instanceof String)) { - vm.warn("A string is needed for change.case$"); - stack.push(0); - return; - } - - stack.push(BibtexWidth.width((String) o1)); - } -} diff --git a/src/main/java/org/jabref/logic/bst/BibtexCaseChanger.java b/src/main/java/org/jabref/logic/bst/util/BstCaseChanger.java similarity index 83% rename from src/main/java/org/jabref/logic/bst/BibtexCaseChanger.java rename to src/main/java/org/jabref/logic/bst/util/BstCaseChanger.java index e05ce998e80..dd3e7c7b05c 100644 --- a/src/main/java/org/jabref/logic/bst/BibtexCaseChanger.java +++ b/src/main/java/org/jabref/logic/bst/util/BstCaseChanger.java @@ -1,4 +1,4 @@ -package org.jabref.logic.bst; +package org.jabref.logic.bst.util; import java.util.Locale; import java.util.Optional; @@ -6,9 +6,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public final class BibtexCaseChanger { +public final class BstCaseChanger { - private static final Logger LOGGER = LoggerFactory.getLogger(BibtexCaseChanger.class); + private static final Logger LOGGER = LoggerFactory.getLogger(BstCaseChanger.class); // stores whether the char before the current char was a colon private boolean prevColon = true; @@ -16,7 +16,7 @@ public final class BibtexCaseChanger { // global variable to store the current brace level private int braceLevel; - public enum FORMAT_MODE { + public enum FormatMode { // First character and character after a ":" as upper case - everything else in lower case. Obey {}. TITLE_LOWERS('t'), @@ -40,7 +40,7 @@ public enum FORMAT_MODE { private final char asChar; - FORMAT_MODE(char asChar) { + FormatMode(char asChar) { this.asChar = asChar; } @@ -53,17 +53,21 @@ public char asChar() { * * @throws IllegalArgumentException if char is not 't', 'l', 'u' */ - public static FORMAT_MODE getFormatModeForBSTFormat(final char bstFormat) { - for (FORMAT_MODE mode : FORMAT_MODE.values()) { + public static FormatMode of(final char bstFormat) { + for (FormatMode mode : FormatMode.values()) { if (mode.asChar == bstFormat) { return mode; } } throw new IllegalArgumentException(); } + + public static FormatMode of(final String bstFormat) { + return of(bstFormat.toLowerCase(Locale.ROOT).charAt(0)); + } } - private BibtexCaseChanger() { + private BstCaseChanger() { } /** @@ -72,11 +76,11 @@ private BibtexCaseChanger() { * @param s the string to handle * @param format the format */ - public static String changeCase(String s, FORMAT_MODE format) { - return (new BibtexCaseChanger()).doChangeCase(s, format); + public static String changeCase(String s, FormatMode format) { + return (new BstCaseChanger()).doChangeCase(s, format); } - private String doChangeCase(String s, FORMAT_MODE format) { + private String doChangeCase(String s, FormatMode format) { char[] c = s.toCharArray(); StringBuilder sb = new StringBuilder(); @@ -93,7 +97,7 @@ private String doChangeCase(String s, FORMAT_MODE format) { i++; continue; } - if ((format == FORMAT_MODE.TITLE_LOWERS) && ((i == 0) || (prevColon && Character.isWhitespace(c[i - 1])))) { + if ((format == FormatMode.TITLE_LOWERS) && ((i == 0) || (prevColon && Character.isWhitespace(c[i - 1])))) { sb.append('{'); i++; prevColon = false; @@ -136,12 +140,9 @@ private String doChangeCase(String s, FORMAT_MODE format) { * is other stuff, too, between braces, but it doesn't try to do anything * special with |colon|s. * - * @param c * @param start the current position. It points to the opening brace - * @param format - * @return */ - private int convertSpecialChar(StringBuilder sb, char[] c, int start, FORMAT_MODE format) { + private int convertSpecialChar(StringBuilder sb, char[] c, int start, FormatMode format) { int i = start; sb.append(c[i]); @@ -152,7 +153,7 @@ private int convertSpecialChar(StringBuilder sb, char[] c, int start, FORMAT_MOD i++; // skip over the |backslash| - Optional s = BibtexCaseChanger.findSpecialChar(c, i); + Optional s = BstCaseChanger.findSpecialChar(c, i); if (s.isPresent()) { i = convertAccented(c, i, s.get(), sb, format); } @@ -174,14 +175,9 @@ private int convertSpecialChar(StringBuilder sb, char[] c, int start, FORMAT_MOD * up) and append the result to the stringBuffer, return the updated * position. * - * @param c - * @param start - * @param s - * @param sb - * @param format * @return the new position */ - private int convertAccented(char[] c, int start, String s, StringBuilder sb, FORMAT_MODE format) { + private int convertAccented(char[] c, int start, String s, StringBuilder sb, FormatMode format) { int pos = start; pos += s.length(); @@ -214,29 +210,27 @@ private int convertAccented(char[] c, int start, String s, StringBuilder sb, FOR return pos; } - private int convertNonControl(char[] c, int start, StringBuilder sb, FORMAT_MODE format) { + private int convertNonControl(char[] c, int start, StringBuilder sb, FormatMode format) { int pos = start; switch (format) { - case TITLE_LOWERS: - case ALL_LOWERS: + case TITLE_LOWERS, ALL_LOWERS -> { sb.append(Character.toLowerCase(c[pos])); pos++; - break; - case ALL_UPPERS: + } + case ALL_UPPERS -> { sb.append(Character.toUpperCase(c[pos])); pos++; - break; - default: - LOGGER.info("convertNonControl - Unknown format: " + format); - break; + } + default -> + LOGGER.info("convertNonControl - Unknown format: " + format); } return pos; } - private int convertCharIfBraceLevelIsZero(char[] c, int start, StringBuilder sb, FORMAT_MODE format) { + private int convertCharIfBraceLevelIsZero(char[] c, int start, StringBuilder sb, FormatMode format) { int i = start; switch (format) { - case TITLE_LOWERS: + case TITLE_LOWERS -> { if ((i == 0) || (prevColon && Character.isWhitespace(c[i - 1]))) { sb.append(c[i]); } else { @@ -247,16 +241,13 @@ private int convertCharIfBraceLevelIsZero(char[] c, int start, StringBuilder sb, } else if (!Character.isWhitespace(c[i])) { prevColon = false; } - break; - case ALL_LOWERS: - sb.append(Character.toLowerCase(c[i])); - break; - case ALL_UPPERS: - sb.append(Character.toUpperCase(c[i])); - break; - default: - LOGGER.info("convertCharIfBraceLevelIsZero - Unknown format: " + format); - break; + } + case ALL_LOWERS -> + sb.append(Character.toLowerCase(c[i])); + case ALL_UPPERS -> + sb.append(Character.toUpperCase(c[i])); + default -> + LOGGER.info("convertCharIfBraceLevelIsZero - Unknown format: " + format); } i++; return i; diff --git a/src/main/java/org/jabref/logic/bst/BibtexNameFormatter.java b/src/main/java/org/jabref/logic/bst/util/BstNameFormatter.java similarity index 76% rename from src/main/java/org/jabref/logic/bst/BibtexNameFormatter.java rename to src/main/java/org/jabref/logic/bst/util/BstNameFormatter.java index aecf571bdf4..c4986dc12dd 100644 --- a/src/main/java/org/jabref/logic/bst/BibtexNameFormatter.java +++ b/src/main/java/org/jabref/logic/bst/util/BstNameFormatter.java @@ -1,13 +1,17 @@ -package org.jabref.logic.bst; +package org.jabref.logic.bst.util; import java.util.Arrays; import java.util.Locale; import java.util.Optional; import java.util.stream.Collectors; +import org.jabref.logic.bst.BstVMException; import org.jabref.model.entry.Author; import org.jabref.model.entry.AuthorList; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * From Bibtex: * @@ -24,9 +28,10 @@ * Sounds easy - is a nightmare... X-( * */ -public class BibtexNameFormatter { +public class BstNameFormatter { + private static final Logger LOGGER = LoggerFactory.getLogger(BstNameFormatter.class); - private BibtexNameFormatter() { + private BstNameFormatter() { } /** @@ -35,23 +40,18 @@ private BibtexNameFormatter() { * @param authorsNameList The string from an author field * @param whichName index of the list, starting with 1 * @param formatString TODO - * @param warn collects the warnings, may-be-null - * @return */ - public static String formatName(String authorsNameList, int whichName, String formatString, Warn warn) { + public static String formatName(String authorsNameList, int whichName, String formatString) { AuthorList al = AuthorList.parse(authorsNameList); if ((whichName < 1) && (whichName > al.getNumberOfAuthors())) { - warn.warn("AuthorList " + authorsNameList + " does not contain an author with number " + whichName); + LOGGER.warn("AuthorList {} does not contain an author with number {}", authorsNameList, whichName); return ""; } - return BibtexNameFormatter.formatName(al.getAuthor(whichName - 1), formatString, warn); + return BstNameFormatter.formatName(al.getAuthor(whichName - 1), formatString); } - /** - * @param warn collects the warnings, may-be-null - */ - public static String formatName(Author author, String format, Warn warn) { + public static String formatName(Author author, String format) { StringBuilder sb = new StringBuilder(); char[] c = format.toCharArray(); @@ -81,11 +81,7 @@ public static String formatName(Author author, String format, Warn warn) { } if ((braceLevel == 1) && Character.isLetter(c[i])) { if ("fvlj".indexOf(c[i]) == -1) { - if (warn != null) { - warn.warn( - "Format string in format.name$ may only contain fvlj on brace level 1 in group " - + group + ": " + format); - } + LOGGER.warn("Format string in format.name$ may only contain fvlj on brace level 1 in group {}: {}", group, format); } else { level1Chars.append(c[i]); } @@ -99,31 +95,26 @@ public static String formatName(Author author, String format, Warn warn) { continue; } - if ((control.length() > 2) && (warn != null)) { - warn.warn("Format string in format.name$ may only be one or two character long on brace level 1 in group " + group + ": " + format); + if ((control.length() > 2)) { + LOGGER.warn("Format string in format.name$ may only be one or two character long on brace level 1 in group {}: {}", group, format); } char type = control.charAt(0); - Optional tokenS; - switch (type) { - case 'f': - tokenS = author.getFirst(); - break; - case 'v': - tokenS = author.getVon(); - break; - case 'l': - tokenS = author.getLast(); - break; - case 'j': - tokenS = author.getJr(); - break; - default: - throw new VMException("Internal error"); - } - - if (!tokenS.isPresent()) { + Optional tokenS = switch (type) { + case 'f' -> + author.getFirst(); + case 'v' -> + author.getVon(); + case 'l' -> + author.getLast(); + case 'j' -> + author.getJr(); + default -> + throw new BstVMException("Internal error"); + }; + + if (tokenS.isEmpty()) { i++; continue; } @@ -135,9 +126,7 @@ public static String formatName(Author author, String format, Warn warn) { if (control.charAt(1) == control.charAt(0)) { abbreviateThatIsSingleLetter = false; } else { - if (warn != null) { - warn.warn("Format string in format.name$ may only contain one type of vlfj on brace level 1 in group " + group + ": " + format); - } + LOGGER.warn("Format string in format.name$ may only contain one type of vlfj on brace level 1 in group {}: {}", group, format); } } @@ -162,7 +151,7 @@ public static String formatName(Author author, String format, Warn warn) { } if (((j + 1) < d.length) && (d[j + 1] == '{')) { StringBuilder interTokenSb = new StringBuilder(); - j = BibtexNameFormatter.consumeToMatchingBrace(interTokenSb, d, j + 1); + j = BstNameFormatter.consumeToMatchingBrace(interTokenSb, d, j + 1); interToken = interTokenSb.substring(1, interTokenSb.length() - 1); } @@ -171,7 +160,7 @@ public static String formatName(Author author, String format, Warn warn) { if (abbreviateThatIsSingleLetter) { String[] dashes = token.split("-"); - token = Arrays.asList(dashes).stream().map(BibtexNameFormatter::getFirstCharOfString) + token = Arrays.stream(dashes).map(BstNameFormatter::getFirstCharOfString) .collect(Collectors.joining(".-")); } @@ -187,7 +176,7 @@ public static String formatName(Author author, String format, Warn warn) { // No clue what this means (What the hell are tokens anyway??? // if (lex_class[name_sep_char[cur_token]] = sep_char) then // append_ex_buf_char_and_check (name_sep_char[cur_token]) - if ((k == (tokens.length - 2)) || (BibtexNameFormatter.numberOfChars(sb.substring(groupStart, sb.length()), 3) < 3)) { + if ((k == (tokens.length - 2)) || (BstNameFormatter.numberOfChars(sb.substring(groupStart, sb.length()), 3) < 3)) { sb.append('~'); } else { sb.append(' '); @@ -212,7 +201,7 @@ public static String formatName(Author author, String format, Warn warn) { if (sb.length() > 0) { boolean noDisTie = false; if ((sb.charAt(sb.length() - 1) == '~') && - ((BibtexNameFormatter.numberOfChars(sb.substring(groupStart, sb.length()), 4) >= 4) || + ((BstNameFormatter.numberOfChars(sb.substring(groupStart, sb.length()), 4) >= 4) || ((sb.length() > 1) && (noDisTie = sb.charAt(sb.length() - 2) == '~')))) { sb.deleteCharAt(sb.length() - 1); if (!noDisTie) { @@ -221,16 +210,14 @@ public static String formatName(Author author, String format, Warn warn) { } } } else if (c[i] == '}') { - if (warn != null) { - warn.warn("Unmatched brace in format string: " + format); - } + LOGGER.warn("Unmatched brace in format string: {}", format); } else { sb.append(c[i]); // verbatim } i++; } - if ((braceLevel != 0) && (warn != null)) { - warn.warn("Unbalanced brace in format string for nameFormat: " + format); + if ((braceLevel != 0)) { + LOGGER.warn("Unbalanced brace in format string for nameFormat: {}", format); } return sb.toString(); @@ -268,7 +255,7 @@ public static String getFirstCharOfString(String s) { } if ((c[i] == '{') && ((i + 1) < c.length) && (c[i + 1] == '\\')) { StringBuilder sb = new StringBuilder(); - BibtexNameFormatter.consumeToMatchingBrace(sb, c, i); + BstNameFormatter.consumeToMatchingBrace(sb, c, i); return sb.toString(); } } diff --git a/src/main/java/org/jabref/logic/bst/BibtexPurify.java b/src/main/java/org/jabref/logic/bst/util/BstPurifier.java similarity index 77% rename from src/main/java/org/jabref/logic/bst/BibtexPurify.java rename to src/main/java/org/jabref/logic/bst/util/BstPurifier.java index 08cb14db207..90818a0749f 100644 --- a/src/main/java/org/jabref/logic/bst/BibtexPurify.java +++ b/src/main/java/org/jabref/logic/bst/util/BstPurifier.java @@ -1,4 +1,7 @@ -package org.jabref.logic.bst; +package org.jabref.logic.bst.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * @@ -10,17 +13,13 @@ * pushes the null string. * */ -public class BibtexPurify { +public class BstPurifier { + private static final Logger LOGGER = LoggerFactory.getLogger(BstPurifier.class); - private BibtexPurify() { + private BstPurifier() { } - /** - * @param toPurify - * @param warn may-be-null - * @return - */ - public static String purify(String toPurify, Warn warn) { + public static String purify(String toPurify) { StringBuilder sb = new StringBuilder(); char[] cs = toPurify.toCharArray(); @@ -41,7 +40,7 @@ public static String purify(String toPurify, Warn warn) { i++; // skip brace while ((i < n) && (braceLevel > 0)) { i++; // skip backslash - BibtexCaseChanger.findSpecialChar(cs, i).ifPresent(sb::append); + BstCaseChanger.findSpecialChar(cs, i).ifPresent(sb::append); while ((i < n) && Character.isLetter(cs[i])) { i++; @@ -63,15 +62,13 @@ public static String purify(String toPurify, Warn warn) { if (braceLevel > 0) { braceLevel--; } else { - if (warn != null) { - warn.warn("Unbalanced brace in string for purify$: " + toPurify); - } + LOGGER.warn("Unbalanced brace in string for purify$: {}", toPurify); } } i++; } - if ((braceLevel != 0) && (warn != null)) { - warn.warn("Unbalanced brace in string for purify$: " + toPurify); + if ((braceLevel != 0)) { + LOGGER.warn("Unbalanced brace in string for purify$: {}", toPurify); } return sb.toString(); diff --git a/src/main/java/org/jabref/logic/bst/BibtexTextPrefix.java b/src/main/java/org/jabref/logic/bst/util/BstTextPrefixer.java similarity index 84% rename from src/main/java/org/jabref/logic/bst/BibtexTextPrefix.java rename to src/main/java/org/jabref/logic/bst/util/BstTextPrefixer.java index 721838e29fe..31cbc71de9b 100644 --- a/src/main/java/org/jabref/logic/bst/BibtexTextPrefix.java +++ b/src/main/java/org/jabref/logic/bst/util/BstTextPrefixer.java @@ -1,4 +1,7 @@ -package org.jabref.logic.bst; +package org.jabref.logic.bst.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The |built_in| function {\.{text.prefix\$}} pops the top two literals (the @@ -14,15 +17,13 @@ * complains and pushes the null string. * */ -public class BibtexTextPrefix { +public class BstTextPrefixer { + private static final Logger LOGGER = LoggerFactory.getLogger(BstTextPrefixer.class); - private BibtexTextPrefix() { + private BstTextPrefixer() { } - /** - * @param warn may-be-null - */ - public static String textPrefix(int inNumOfChars, String toPrefix, Warn warn) { + public static String textPrefix(int inNumOfChars, String toPrefix) { int numOfChars = inNumOfChars; StringBuilder sb = new StringBuilder(); @@ -53,15 +54,13 @@ public static String textPrefix(int inNumOfChars, String toPrefix, Warn warn) { if (braceLevel > 0) { braceLevel--; } else { - if (warn != null) { - warn.warn("Unbalanced brace in string for purify$: " + toPrefix); - } + LOGGER.warn("Unbalanced brace in string for purify$: {}", toPrefix); } } else { numOfChars--; } } - sb.append(toPrefix.substring(0, i)); + sb.append(toPrefix, 0, i); while (braceLevel > 0) { sb.append('}'); braceLevel--; diff --git a/src/main/java/org/jabref/logic/bst/util/BstWidthCalculator.java b/src/main/java/org/jabref/logic/bst/util/BstWidthCalculator.java new file mode 100644 index 00000000000..c286fe1497b --- /dev/null +++ b/src/main/java/org/jabref/logic/bst/util/BstWidthCalculator.java @@ -0,0 +1,241 @@ +package org.jabref.logic.bst.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * The |built_in| function {\.{purify\$}} pops the top (string) literal, removes + * nonalphanumeric characters except for |white_space| and |sep_char| characters + * (these get converted to a |space|) and removes certain alphabetic characters + * contained in the control sequences associated with a special character, and + * pushes the resulting string. If the literal isn't a string, it complains and + * pushes the null string. + * + */ +public class BstWidthCalculator { + + private static final Logger LOGGER = LoggerFactory.getLogger(BstWidthCalculator.class); + + /* + * Quoted from Bibtex: + * + * Now we initialize the system-dependent |char_width| array, for which + * |space| is the only |white_space| character given a nonzero printing + * width. The widths here are taken from Stanford's June~'87 $cmr10$~font + * and represent hundredths of a point (rounded), but since they're used + * only for relative comparisons, the units have no meaning. + */ + + private static int[] widths; + + static { + if (BstWidthCalculator.widths == null) { + BstWidthCalculator.widths = new int[128]; + + for (int i = 0; i < 128; i++) { + BstWidthCalculator.widths[i] = 0; + } + BstWidthCalculator.widths[32] = 278; + BstWidthCalculator.widths[33] = 278; + BstWidthCalculator.widths[34] = 500; + BstWidthCalculator.widths[35] = 833; + BstWidthCalculator.widths[36] = 500; + BstWidthCalculator.widths[37] = 833; + BstWidthCalculator.widths[38] = 778; + BstWidthCalculator.widths[39] = 278; + BstWidthCalculator.widths[40] = 389; + BstWidthCalculator.widths[41] = 389; + BstWidthCalculator.widths[42] = 500; + BstWidthCalculator.widths[43] = 778; + BstWidthCalculator.widths[44] = 278; + BstWidthCalculator.widths[45] = 333; + BstWidthCalculator.widths[46] = 278; + BstWidthCalculator.widths[47] = 500; + BstWidthCalculator.widths[48] = 500; + BstWidthCalculator.widths[49] = 500; + BstWidthCalculator.widths[50] = 500; + BstWidthCalculator.widths[51] = 500; + BstWidthCalculator.widths[52] = 500; + BstWidthCalculator.widths[53] = 500; + BstWidthCalculator.widths[54] = 500; + BstWidthCalculator.widths[55] = 500; + BstWidthCalculator.widths[56] = 500; + BstWidthCalculator.widths[57] = 500; + BstWidthCalculator.widths[58] = 278; + BstWidthCalculator.widths[59] = 278; + BstWidthCalculator.widths[60] = 278; + BstWidthCalculator.widths[61] = 778; + BstWidthCalculator.widths[62] = 472; + BstWidthCalculator.widths[63] = 472; + BstWidthCalculator.widths[64] = 778; + BstWidthCalculator.widths[65] = 750; + BstWidthCalculator.widths[66] = 708; + BstWidthCalculator.widths[67] = 722; + BstWidthCalculator.widths[68] = 764; + BstWidthCalculator.widths[69] = 681; + BstWidthCalculator.widths[70] = 653; + BstWidthCalculator.widths[71] = 785; + BstWidthCalculator.widths[72] = 750; + BstWidthCalculator.widths[73] = 361; + BstWidthCalculator.widths[74] = 514; + BstWidthCalculator.widths[75] = 778; + BstWidthCalculator.widths[76] = 625; + BstWidthCalculator.widths[77] = 917; + BstWidthCalculator.widths[78] = 750; + BstWidthCalculator.widths[79] = 778; + BstWidthCalculator.widths[80] = 681; + BstWidthCalculator.widths[81] = 778; + BstWidthCalculator.widths[82] = 736; + BstWidthCalculator.widths[83] = 556; + BstWidthCalculator.widths[84] = 722; + BstWidthCalculator.widths[85] = 750; + BstWidthCalculator.widths[86] = 750; + BstWidthCalculator.widths[87] = 1028; + BstWidthCalculator.widths[88] = 750; + BstWidthCalculator.widths[89] = 750; + BstWidthCalculator.widths[90] = 611; + BstWidthCalculator.widths[91] = 278; + BstWidthCalculator.widths[92] = 500; + BstWidthCalculator.widths[93] = 278; + BstWidthCalculator.widths[94] = 500; + BstWidthCalculator.widths[95] = 278; + BstWidthCalculator.widths[96] = 278; + BstWidthCalculator.widths[97] = 500; + BstWidthCalculator.widths[98] = 556; + BstWidthCalculator.widths[99] = 444; + BstWidthCalculator.widths[100] = 556; + BstWidthCalculator.widths[101] = 444; + BstWidthCalculator.widths[102] = 306; + BstWidthCalculator.widths[103] = 500; + BstWidthCalculator.widths[104] = 556; + BstWidthCalculator.widths[105] = 278; + BstWidthCalculator.widths[106] = 306; + BstWidthCalculator.widths[107] = 528; + BstWidthCalculator.widths[108] = 278; + BstWidthCalculator.widths[109] = 833; + BstWidthCalculator.widths[110] = 556; + BstWidthCalculator.widths[111] = 500; + BstWidthCalculator.widths[112] = 556; + BstWidthCalculator.widths[113] = 528; + BstWidthCalculator.widths[114] = 392; + BstWidthCalculator.widths[115] = 394; + BstWidthCalculator.widths[116] = 389; + BstWidthCalculator.widths[117] = 556; + BstWidthCalculator.widths[118] = 528; + BstWidthCalculator.widths[119] = 722; + BstWidthCalculator.widths[120] = 528; + BstWidthCalculator.widths[121] = 528; + BstWidthCalculator.widths[122] = 444; + BstWidthCalculator.widths[123] = 500; + BstWidthCalculator.widths[124] = 1000; + BstWidthCalculator.widths[125] = 500; + BstWidthCalculator.widths[126] = 500; + } + } + + private BstWidthCalculator() { + } + + private static int getSpecialCharWidth(char[] c, int pos) { + if ((pos + 1) < c.length) { + if ((c[pos] == 'o') && (c[pos + 1] == 'e')) { + return 778; + } + if ((c[pos] == 'O') && (c[pos + 1] == 'E')) { + return 1014; + } + if ((c[pos] == 'a') && (c[pos + 1] == 'e')) { + return 722; + } + if ((c[pos] == 'A') && (c[pos + 1] == 'E')) { + return 903; + } + if ((c[pos] == 's') && (c[pos + 1] == 's')) { + return 500; + } + } + return BstWidthCalculator.getCharWidth(c[pos]); + } + + public static int getCharWidth(char c) { + if ((c >= 0) && (c < 128)) { + return BstWidthCalculator.widths[c]; + } else { + return 0; + } + } + + public static int width(String toMeasure) { + /* + * From Bibtex: We use the natural width for all but special characters, + * and we complain if the string isn't brace-balanced. + */ + + int i = 0; + int n = toMeasure.length(); + int braceLevel = 0; + char[] c = toMeasure.toCharArray(); + int result = 0; + + /* + * From Bibtex: + * + * We use the natural widths of all characters except that some + * characters have no width: braces, control sequences (except for the + * usual 13 accented and foreign characters, whose widths are given in + * the next module), and |white_space| following control sequences (even + * a null control sequence). + * + */ + while (i < n) { + if (c[i] == '{') { + braceLevel++; + if ((braceLevel == 1) && ((i + 1) < n) && (c[i + 1] == '\\')) { + i++; // skip brace + while ((i < n) && (braceLevel > 0)) { + i++; // skip backslash + + int afterBackslash = i; + while ((i < n) && Character.isLetter(c[i])) { + i++; + } + if ((i < n) && (i == afterBackslash)) { + i++; // Skip non-alpha control seq + } else { + if (BstCaseChanger.findSpecialChar(c, afterBackslash).isPresent()) { + result += BstWidthCalculator.getSpecialCharWidth(c, afterBackslash); + } + } + while ((i < n) && Character.isWhitespace(c[i])) { + i++; + } + while ((i < n) && (braceLevel > 0) && (c[i] != '\\')) { + if (c[i] == '}') { + braceLevel--; + } else if (c[i] == '{') { + braceLevel++; + } else { + result += BstWidthCalculator.getCharWidth(c[i]); + } + i++; + } + } + continue; + } + } else if (c[i] == '}') { + if (braceLevel > 0) { + braceLevel--; + } else { + LOGGER.warn("Too many closing braces in string: " + toMeasure); + } + } + result += BstWidthCalculator.getCharWidth(c[i]); + i++; + } + if (braceLevel > 0) { + LOGGER.warn("No enough closing braces in string: " + toMeasure); + } + return result; + } +} diff --git a/src/main/java/org/jabref/logic/importer/FetcherClientException.java b/src/main/java/org/jabref/logic/importer/FetcherClientException.java new file mode 100644 index 00000000000..8de9d0d6114 --- /dev/null +++ b/src/main/java/org/jabref/logic/importer/FetcherClientException.java @@ -0,0 +1,19 @@ +package org.jabref.logic.importer; + +/** + * Should be thrown when you encounter a http status code error >= 400 and < 500 + */ +public class FetcherClientException extends FetcherException { + + public FetcherClientException(String errorMessage, Throwable cause) { + super(errorMessage, cause); + } + + public FetcherClientException(String errorMessage) { + super(errorMessage); + } + + public FetcherClientException(String errorMessage, String localizedMessage, Throwable cause) { + super(errorMessage, localizedMessage, cause); + } +} diff --git a/src/main/java/org/jabref/logic/importer/FetcherServerException.java b/src/main/java/org/jabref/logic/importer/FetcherServerException.java new file mode 100644 index 00000000000..537d71ff530 --- /dev/null +++ b/src/main/java/org/jabref/logic/importer/FetcherServerException.java @@ -0,0 +1,18 @@ +package org.jabref.logic.importer; +/** + * Should be thrown when you encounter a http status code error >= 500 + */ +public class FetcherServerException extends FetcherException { + + public FetcherServerException(String errorMessage, Throwable cause) { + super(errorMessage, cause); + } + + public FetcherServerException(String errorMessage) { + super(errorMessage); + } + + public FetcherServerException(String errorMessage, String localizedMessage, Throwable cause) { + super(errorMessage, localizedMessage, cause); + } +} diff --git a/src/main/java/org/jabref/logic/importer/IdBasedParserFetcher.java b/src/main/java/org/jabref/logic/importer/IdBasedParserFetcher.java index 6be4a8dddfd..e348b69b8d8 100644 --- a/src/main/java/org/jabref/logic/importer/IdBasedParserFetcher.java +++ b/src/main/java/org/jabref/logic/importer/IdBasedParserFetcher.java @@ -81,7 +81,10 @@ default Optional performSearchById(String identifier) throws FetcherEx } catch (URISyntaxException e) { throw new FetcherException("Search URI is malformed", e); } catch (IOException e) { - // TODO: Catch HTTP Response 401 errors and report that user has no rights to access resource. It might be that there is an UnknownHostException (eutils.ncbi.nlm.nih.gov cannot be resolved). + // check for the case where we already have a FetcherException from UrlDownload + if (e.getCause() instanceof FetcherException fe) { + throw fe; + } throw new FetcherException("A network error occurred", e); } catch (ParseException e) { throw new FetcherException("An internal parser error occurred", e); diff --git a/src/main/java/org/jabref/logic/importer/IdParserFetcher.java b/src/main/java/org/jabref/logic/importer/IdParserFetcher.java index bf4b2bb32b9..b9b5ed92e44 100644 --- a/src/main/java/org/jabref/logic/importer/IdParserFetcher.java +++ b/src/main/java/org/jabref/logic/importer/IdParserFetcher.java @@ -88,8 +88,10 @@ default Optional findIdentifier(BibEntry entry) throws FetcherException { LOGGER.debug("Id not found"); return Optional.empty(); } catch (IOException e) { - // TODO: Catch HTTP Response 401 errors and report that user has no rights to access resource - // TODO catch 503 service unavailable and alert user + // check for the case where we already have a FetcherException from UrlDownload + if (e.getCause() instanceof FetcherException fe) { + throw fe; + } throw new FetcherException("An I/O exception occurred", e); } catch (ParseException e) { throw new FetcherException("An internal parser error occurred", e); diff --git a/src/main/java/org/jabref/logic/importer/ImportFormatReader.java b/src/main/java/org/jabref/logic/importer/ImportFormatReader.java index f3b8826171d..b9b6f4b850c 100644 --- a/src/main/java/org/jabref/logic/importer/ImportFormatReader.java +++ b/src/main/java/org/jabref/logic/importer/ImportFormatReader.java @@ -39,7 +39,6 @@ import org.jabref.model.entry.BibEntry; import org.jabref.model.strings.StringUtil; import org.jabref.model.util.FileUpdateMonitor; -import org.jabref.preferences.GeneralPreferences; public class ImportFormatReader { @@ -51,11 +50,9 @@ public class ImportFormatReader { */ private final List formats = new ArrayList<>(); - private GeneralPreferences generalPreferences; private ImportFormatPreferences importFormatPreferences; - public void resetImportFormats(ImporterPreferences importerPreferences, GeneralPreferences generalPreferences, ImportFormatPreferences newImportFormatPreferences, XmpPreferences xmpPreferences, FileUpdateMonitor fileMonitor) { - this.generalPreferences = generalPreferences; + public void resetImportFormats(ImporterPreferences importerPreferences, ImportFormatPreferences newImportFormatPreferences, XmpPreferences xmpPreferences, FileUpdateMonitor fileMonitor) { this.importFormatPreferences = newImportFormatPreferences; formats.clear(); diff --git a/src/main/java/org/jabref/logic/importer/PagedSearchBasedFetcher.java b/src/main/java/org/jabref/logic/importer/PagedSearchBasedFetcher.java index 1b8d31a064b..bafae176485 100644 --- a/src/main/java/org/jabref/logic/importer/PagedSearchBasedFetcher.java +++ b/src/main/java/org/jabref/logic/importer/PagedSearchBasedFetcher.java @@ -52,6 +52,7 @@ default int getPageSize() { * @param luceneQuery the root node of the lucene query * @return a list of {@link BibEntry}, which are matched by the query (may be empty) */ + @Override default List performSearch(QueryNode luceneQuery) throws FetcherException { return new ArrayList<>(performSearchPaged(luceneQuery, 0).getContent()); } diff --git a/src/main/java/org/jabref/logic/importer/fetcher/DoiFetcher.java b/src/main/java/org/jabref/logic/importer/fetcher/DoiFetcher.java index 41afea25707..4f0927ef468 100644 --- a/src/main/java/org/jabref/logic/importer/fetcher/DoiFetcher.java +++ b/src/main/java/org/jabref/logic/importer/fetcher/DoiFetcher.java @@ -72,7 +72,6 @@ public Optional performSearchById(String identifier) throws FetcherExc return new Medra().performSearchById(identifier); } URL doiURL = new URL(doi.get().getURIAsASCIIString()); - // BibTeX data URLDownload download = getUrlDownload(doiURL); download.addHeader("Accept", MediaTypes.APPLICATION_BIBTEX); @@ -80,8 +79,11 @@ public Optional performSearchById(String identifier) throws FetcherExc try { bibtexString = download.asString(); } catch (IOException e) { - // an IOException will be thrown if download is unable to download from the doiURL - throw new FetcherException(Localization.lang("No DOI data exists"), e); + // an IOException with a nested FetcherException will be thrown when you encounter a 400x or 500x http status code + if (e.getCause() instanceof FetcherException fe) { + throw fe; + } + throw e; } // BibTeX entry diff --git a/src/main/java/org/jabref/logic/importer/fetcher/IsbnFetcher.java b/src/main/java/org/jabref/logic/importer/fetcher/IsbnFetcher.java index c97184bd36b..f4dedae3e7c 100644 --- a/src/main/java/org/jabref/logic/importer/fetcher/IsbnFetcher.java +++ b/src/main/java/org/jabref/logic/importer/fetcher/IsbnFetcher.java @@ -50,13 +50,19 @@ public Optional performSearchById(String identifier) throws FetcherExc identifier = NEWLINE_SPACE_PATTERN.matcher(identifier).replaceAll(""); OpenLibraryFetcher openLibraryFetcher = new OpenLibraryFetcher(importFormatPreferences); - Optional bibEntry = openLibraryFetcher.performSearchById(identifier); - // nothing found at OpenLibrary: try ebook.de - if (!bibEntry.isPresent()) { - LOGGER.debug("No entry found at OpenLibrary; trying ebook.de"); - IsbnViaEbookDeFetcher isbnViaEbookDeFetcher = new IsbnViaEbookDeFetcher(importFormatPreferences); - bibEntry = isbnViaEbookDeFetcher.performSearchById(identifier); + Optional bibEntry = Optional.empty(); + try { + bibEntry = openLibraryFetcher.performSearchById(identifier); + } catch (FetcherException ex) { + LOGGER.debug("Got a fetcher exception for IBSN search", ex); + } finally { + // nothing found at OpenLibrary: try ebook.de + if (!bibEntry.isPresent()) { + LOGGER.debug("No entry found at OpenLibrary; trying ebook.de"); + IsbnViaEbookDeFetcher isbnViaEbookDeFetcher = new IsbnViaEbookDeFetcher(importFormatPreferences); + bibEntry = isbnViaEbookDeFetcher.performSearchById(identifier); + } } return bibEntry; diff --git a/src/main/java/org/jabref/logic/importer/fileformat/BibTeXMLImporter.java b/src/main/java/org/jabref/logic/importer/fileformat/BibTeXMLImporter.java index 5fb3d8ed900..65301e2a7a1 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/BibTeXMLImporter.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/BibTeXMLImporter.java @@ -184,7 +184,7 @@ private void parse(T entryType, Map fields) { } else if (isMethodToIgnore(method.getName())) { continue; } else if (method.getName().startsWith("get")) { - putIfValueNotNull(fields, FieldFactory.parseField(method.getName().replace("get", "")), (String) method.invoke(entryType)); + putIfValueNotNull(fields, FieldFactory.parseField(entryType, method.getName().replace("get", "")), (String) method.invoke(entryType)); } } catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException e) { LOGGER.error("Could not invoke method", e); diff --git a/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java b/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java index f3e6e7462f5..666ae01a7fb 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java @@ -387,8 +387,8 @@ private String purge(String context, String stringToPurge) { } // strip empty lines while ((runningIndex < indexOfAt) && - (context.charAt(runningIndex) == '\r' || - context.charAt(runningIndex) == '\n')) { + ((context.charAt(runningIndex) == '\r') || + (context.charAt(runningIndex) == '\n'))) { runningIndex++; } return context.substring(runningIndex); diff --git a/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java b/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java index 2ff451e6dca..afbe5af657d 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java @@ -14,6 +14,7 @@ import org.jabref.model.entry.Author; import org.jabref.model.entry.AuthorList; import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.BiblatexSoftwareField; import org.jabref.model.entry.field.Field; import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.field.UnknownField; @@ -95,7 +96,7 @@ public ParserResult importDatabase(BufferedReader reader) throws IOException { StandardEntryType entryType = StandardEntryType.Software; // Map CFF fields to JabRef Fields - HashMap fieldMap = getFieldMappings(); + HashMap fieldMap = getFieldMappings(); for (Map.Entry property : citation.values.entrySet()) { if (fieldMap.containsKey(property.getKey())) { entryMap.put(fieldMap.get(property.getKey()), property.getValue()); @@ -120,7 +121,7 @@ public ParserResult importDatabase(BufferedReader reader) throws IOException { entryMap.put(StandardField.AUTHOR, authorStr); // Select DOI to keep - if (entryMap.get(StandardField.DOI) == null && citation.ids != null) { + if ((entryMap.get(StandardField.DOI) == null) && (citation.ids != null)) { List doiIds = citation.ids.stream() .filter(id -> id.type.equals("doi")) .collect(Collectors.toList()); @@ -137,14 +138,14 @@ public ParserResult importDatabase(BufferedReader reader) throws IOException { .collect(Collectors.toList()); if (swhIds.size() == 1) { - entryMap.put(StandardField.SWHID, swhIds.get(0)); + entryMap.put(BiblatexSoftwareField.SWHID, swhIds.get(0)); } else if (swhIds.size() > 1) { List relSwhIds = swhIds.stream() .filter(id -> id.split(":").length > 3) // quick filter for invalid swhids .filter(id -> id.split(":")[2].equals("rel")) .collect(Collectors.toList()); if (relSwhIds.size() == 1) { - entryMap.put(StandardField.SWHID, relSwhIds.get(0)); + entryMap.put(BiblatexSoftwareField.SWHID, relSwhIds.get(0)); } } } @@ -166,19 +167,19 @@ public boolean isRecognizedFormat(BufferedReader reader) throws IOException { try { citation = mapper.readValue(reader, CffFormat.class); - return citation != null && citation.values.get("title") != null; + return (citation != null) && (citation.values.get("title") != null); } catch (IOException e) { return false; } } - private HashMap getFieldMappings() { - HashMap fieldMappings = new HashMap<>(); + private HashMap getFieldMappings() { + HashMap fieldMappings = new HashMap<>(); fieldMappings.put("title", StandardField.TITLE); fieldMappings.put("version", StandardField.VERSION); fieldMappings.put("doi", StandardField.DOI); - fieldMappings.put("license", StandardField.LICENSE); - fieldMappings.put("repository", StandardField.REPOSITORY); + fieldMappings.put("license", BiblatexSoftwareField.LICENSE); + fieldMappings.put("repository", BiblatexSoftwareField.REPOSITORY); fieldMappings.put("url", StandardField.URL); fieldMappings.put("abstract", StandardField.ABSTRACT); fieldMappings.put("message", StandardField.COMMENT); diff --git a/src/main/java/org/jabref/logic/importer/fileformat/CopacImporter.java b/src/main/java/org/jabref/logic/importer/fileformat/CopacImporter.java index ab857becd0a..ab638ae4cf3 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/CopacImporter.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/CopacImporter.java @@ -137,7 +137,7 @@ public ParserResult importDatabase(BufferedReader reader) throws IOException { } else if ("DT- ".equals(code)) { setOrAppend(b, new UnknownField("documenttype"), line.substring(4).trim(), ", "); } else { - setOrAppend(b, FieldFactory.parseField(code.substring(0, 2)), line.substring(4).trim(), ", "); + setOrAppend(b, FieldFactory.parseField(StandardEntryType.Book, line.substring(0, 2)), line.substring(4).trim(), ", "); } } results.add(b); diff --git a/src/main/java/org/jabref/logic/importer/fileformat/IsiImporter.java b/src/main/java/org/jabref/logic/importer/fileformat/IsiImporter.java index b502977f716..2845756b770 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/IsiImporter.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/IsiImporter.java @@ -281,7 +281,7 @@ public ParserResult importDatabase(BufferedReader reader) throws IOException { if ("ER".equals(beg) || "EF".equals(beg) || "VR".equals(beg) || "FN".equals(beg)) { continue; } - hm.put(FieldFactory.parseField(beg), value); + hm.put(FieldFactory.parseField(type, beg), value); } } diff --git a/src/main/java/org/jabref/logic/importer/fileformat/MedlineImporter.java b/src/main/java/org/jabref/logic/importer/fileformat/MedlineImporter.java index 392fad9772b..b9e9eac39d4 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/MedlineImporter.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/MedlineImporter.java @@ -425,7 +425,7 @@ private void addArticleIdList(Map fields, ArticleIdList articleId if ("pubmed".equals(id.getIdType())) { fields.put(StandardField.PMID, id.getContent()); } else { - fields.put(FieldFactory.parseField(id.getIdType()), id.getContent()); + fields.put(FieldFactory.parseField(StandardEntryType.Article, id.getIdType()), id.getContent()); } } } @@ -499,7 +499,7 @@ private void addKeyWords(Map fields, List allKeyword private void addOtherId(Map fields, List otherID) { for (OtherID id : otherID) { if ((id.getSource() != null) && (id.getContent() != null)) { - fields.put(FieldFactory.parseField(id.getSource()), id.getContent()); + fields.put(FieldFactory.parseField(StandardEntryType.Article, id.getSource()), id.getContent()); } } } diff --git a/src/main/java/org/jabref/logic/importer/util/MetaDataParser.java b/src/main/java/org/jabref/logic/importer/util/MetaDataParser.java index eb1e905adca..5f3d5e29c52 100644 --- a/src/main/java/org/jabref/logic/importer/util/MetaDataParser.java +++ b/src/main/java/org/jabref/logic/importer/util/MetaDataParser.java @@ -65,6 +65,7 @@ public MetaData parse(MetaData metaData, Map data, Character key String user = entry.getKey().substring(MetaData.FILE_DIRECTORY.length() + 1); metaData.setUserFileDirectory(user, getSingleItem(value)); } else if (entry.getKey().startsWith(MetaData.SELECTOR_META_PREFIX)) { + // edge case, it might be one special field e.g. article from biblatex-apa, but we can't distinguish this from any other field and rather prefer to handle it as UnknownField metaData.addContentSelector(ContentSelectors.parse(FieldFactory.parseField(entry.getKey().substring(MetaData.SELECTOR_META_PREFIX.length())), StringUtil.unquote(entry.getValue(), MetaData.ESCAPE_CHARACTER))); } else if (entry.getKey().startsWith(MetaData.FILE_DIRECTORY + "Latex-")) { // The user name comes directly after "FILE_DIRECTORYLatex-" diff --git a/src/main/java/org/jabref/logic/layout/format/NameFormatter.java b/src/main/java/org/jabref/logic/layout/format/NameFormatter.java index 919ff4df2d5..e8812235435 100644 --- a/src/main/java/org/jabref/logic/layout/format/NameFormatter.java +++ b/src/main/java/org/jabref/logic/layout/format/NameFormatter.java @@ -5,7 +5,7 @@ import java.util.Map; import java.util.Objects; -import org.jabref.logic.bst.BibtexNameFormatter; +import org.jabref.logic.bst.util.BstNameFormatter; import org.jabref.logic.layout.LayoutFormatter; import org.jabref.model.entry.AuthorList; @@ -86,7 +86,7 @@ private static String format(String toFormat, AuthorList al, String[] formats) { for (int i = 1; i <= al.getNumberOfAuthors(); i++) { for (int j = 1; j < formats.length; j += 2) { if ("*".equals(formats[j])) { - sb.append(BibtexNameFormatter.formatName(toFormat, i, formats[j + 1], null)); + sb.append(BstNameFormatter.formatName(toFormat, i, formats[j + 1])); break; } else { String[] range = formats[j].split("\\.\\."); @@ -112,7 +112,7 @@ private static String format(String toFormat, AuthorList al, String[] formats) { } if ((s <= i) && (i <= e)) { - sb.append(BibtexNameFormatter.formatName(toFormat, i, formats[j + 1], null)); + sb.append(BstNameFormatter.formatName(toFormat, i, formats[j + 1])); break; } } diff --git a/src/main/java/org/jabref/logic/net/URLDownload.java b/src/main/java/org/jabref/logic/net/URLDownload.java index 77ac1ff3093..c499dcf3635 100644 --- a/src/main/java/org/jabref/logic/net/URLDownload.java +++ b/src/main/java/org/jabref/logic/net/URLDownload.java @@ -40,6 +40,8 @@ import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; +import org.jabref.logic.importer.FetcherClientException; +import org.jabref.logic.importer.FetcherServerException; import org.jabref.logic.util.io.FileUtil; import org.jabref.model.util.FileHelper; @@ -350,21 +352,23 @@ private URLConnection openConnection() throws IOException { if (connection instanceof HttpURLConnection) { // normally, 3xx is redirect int status = ((HttpURLConnection) connection).getResponseCode(); - if (status != HttpURLConnection.HTTP_OK) { - if ((status == HttpURLConnection.HTTP_MOVED_TEMP) - || (status == HttpURLConnection.HTTP_MOVED_PERM) - || (status == HttpURLConnection.HTTP_SEE_OTHER)) { - // get redirect url from "location" header field - String newUrl = connection.getHeaderField("location"); - // open the new connnection again - connection = new URLDownload(newUrl).openConnection(); - } + + if ((status == HttpURLConnection.HTTP_MOVED_TEMP) + || (status == HttpURLConnection.HTTP_MOVED_PERM) + || (status == HttpURLConnection.HTTP_SEE_OTHER)) { + // get redirect url from "location" header field + String newUrl = connection.getHeaderField("location"); + // open the new connection again + connection = new URLDownload(newUrl).openConnection(); + } + if ((status >= 400) && (status < 500)) { + throw new IOException(new FetcherClientException("Encountered HTTP Status code " + status)); + } + if (status >= 500) { + throw new IOException(new FetcherServerException("Encountered HTTP Status Code " + status)); } } - // this does network i/o: GET + read returned headers - connection.connect(); - return connection; } diff --git a/src/main/java/org/jabref/logic/shared/prefs/SharedDatabasePreferences.java b/src/main/java/org/jabref/logic/shared/prefs/SharedDatabasePreferences.java index 260ec297c24..4b1a14fe1c9 100644 --- a/src/main/java/org/jabref/logic/shared/prefs/SharedDatabasePreferences.java +++ b/src/main/java/org/jabref/logic/shared/prefs/SharedDatabasePreferences.java @@ -25,6 +25,8 @@ public class SharedDatabasePreferences { private static final String SHARED_DATABASE_NAME = "sharedDatabaseName"; private static final String SHARED_DATABASE_USER = "sharedDatabaseUser"; private static final String SHARED_DATABASE_PASSWORD = "sharedDatabasePassword"; + private static final String SHARED_DATABASE_FOLDER = "sharedDatabaseFolder"; + private static final String SHARED_DATABASE_AUTOSAVE = "sharedDatabaseAutosave"; private static final String SHARED_DATABASE_REMEMBER_PASSWORD = "sharedDatabaseRememberPassword"; private static final String SHARED_DATABASE_USE_SSL = "sharedDatabaseUseSSL"; private static final String SHARED_DATABASE_KEYSTORE_FILE = "sharedDatabaseKeyStoreFile"; @@ -77,6 +79,14 @@ public boolean getRememberPassword() { return internalPrefs.getBoolean(SHARED_DATABASE_REMEMBER_PASSWORD, false); } + public Optional getFolder() { + return getOptionalValue(SHARED_DATABASE_FOLDER); + } + + public boolean getAutosave() { + return internalPrefs.getBoolean(SHARED_DATABASE_AUTOSAVE, false); + } + public boolean isUseSSL() { return internalPrefs.getBoolean(SHARED_DATABASE_USE_SSL, false); } @@ -109,6 +119,14 @@ public void setRememberPassword(boolean rememberPassword) { internalPrefs.putBoolean(SHARED_DATABASE_REMEMBER_PASSWORD, rememberPassword); } + public void setFolder(String folder) { + internalPrefs.put(SHARED_DATABASE_FOLDER, folder); + } + + public void setAutosave(boolean autosave) { + internalPrefs.putBoolean(SHARED_DATABASE_AUTOSAVE, autosave); + } + public void setUseSSL(boolean useSSL) { internalPrefs.putBoolean(SHARED_DATABASE_USE_SSL, useSSL); } diff --git a/src/main/java/org/jabref/model/database/BibDatabase.java b/src/main/java/org/jabref/model/database/BibDatabase.java index 72a559ff663..52e4e841ce9 100644 --- a/src/main/java/org/jabref/model/database/BibDatabase.java +++ b/src/main/java/org/jabref/model/database/BibDatabase.java @@ -20,6 +20,7 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import javafx.collections.ObservableSet; import org.jabref.logic.bibtex.FieldWriter; import org.jabref.model.database.event.EntriesAddedEvent; @@ -29,6 +30,7 @@ import org.jabref.model.entry.Month; import org.jabref.model.entry.event.EntriesEventSource; import org.jabref.model.entry.event.EntryChangedEvent; +import org.jabref.model.entry.event.FieldAddedOrRemovedEvent; import org.jabref.model.entry.event.FieldChangedEvent; import org.jabref.model.entry.field.Field; import org.jabref.model.entry.field.FieldFactory; @@ -52,6 +54,8 @@ public class BibDatabase { * State attributes */ private final ObservableList entries = FXCollections.synchronizedObservableList(FXCollections.observableArrayList(BibEntry::getObservables)); + + private final ObservableSet visibleFields = FXCollections.observableSet(); private Map bibtexStrings = new ConcurrentHashMap<>(); private final EventBus eventBus = new EventBus(); @@ -136,13 +140,8 @@ public ObservableList getEntries() { * * @return set of fieldnames, that are visible */ - public Set getAllVisibleFields() { - Set allFields = new TreeSet<>(Comparator.comparing(Field::getName)); - for (BibEntry e : getEntries()) { - allFields.addAll(e.getFields()); - } - return allFields.stream().filter(field -> !FieldFactory.isInternalField(field)) - .collect(Collectors.toSet()); + public ObservableSet getAllVisibleFields() { + return visibleFields; } /** @@ -214,6 +213,8 @@ public synchronized void insertEntries(List newEntries, EntriesEventSo eventBus.post(new EntriesAddedEvent(newEntries, newEntries.get(0), eventSource)); } entries.addAll(newEntries); + + updateVisibleFields(); } public synchronized void removeEntry(BibEntry bibEntry) { @@ -251,6 +252,7 @@ public synchronized void removeEntries(List toBeDeleted, EntriesEventS boolean anyRemoved = entries.removeIf(entry -> ids.contains(entry.getId())); if (anyRemoved) { eventBus.post(new EntriesRemovedEvent(toBeDeleted, eventSource)); + updateVisibleFields(); } } @@ -584,6 +586,30 @@ private void relayEntryChangeEvent(FieldChangedEvent event) { eventBus.post(event); } + @Subscribe + private void listen(FieldAddedOrRemovedEvent event) { + // When a field is removed from an entry we can't tell if it's + // still present in other entries, and thus we can't remove it + // from the set of visible fields. However, when a new field is added + // to any entry, we can simply add it to the set because we're + // going to add it whether other entries have it or not + boolean isAdded = visibleFields.add(event.getField()); + if (!isAdded) { + updateVisibleFields(); + } + } + + private void updateVisibleFields() { + visibleFields.clear(); + Set allFields = new TreeSet<>(Comparator.comparing(Field::getName)); + for (BibEntry e : getEntries()) { + allFields.addAll(e.getFields()); + } + visibleFields.addAll(allFields.stream().filter(field -> !FieldFactory.isInternalField(field)) + .filter(field -> StringUtil.isNotBlank(field.getName())) + .collect(Collectors.toSet())); + } + public Optional getReferencedEntry(BibEntry entry) { return entry.getField(StandardField.CROSSREF).flatMap(this::getEntryByCitationKey); } diff --git a/src/main/java/org/jabref/model/entry/BibEntryTypesManager.java b/src/main/java/org/jabref/model/entry/BibEntryTypesManager.java index b737596abd6..836cd39dac8 100644 --- a/src/main/java/org/jabref/model/entry/BibEntryTypesManager.java +++ b/src/main/java/org/jabref/model/entry/BibEntryTypesManager.java @@ -11,6 +11,7 @@ import org.jabref.model.database.BibDatabaseMode; import org.jabref.model.entry.field.BibField; import org.jabref.model.entry.field.FieldFactory; +import org.jabref.model.entry.types.BiblatexAPAEntryTypeDefinitions; import org.jabref.model.entry.types.BiblatexEntryTypeDefinitions; import org.jabref.model.entry.types.BiblatexSoftwareEntryTypeDefinitions; import org.jabref.model.entry.types.BibtexEntryTypeDefinitions; @@ -21,7 +22,7 @@ public class BibEntryTypesManager { public static final String ENTRYTYPE_FLAG = "jabref-entrytype: "; private final InternalEntryTypes BIBTEX = new InternalEntryTypes(Stream.concat(BibtexEntryTypeDefinitions.ALL.stream(), IEEETranEntryTypeDefinitions.ALL.stream()).collect(Collectors.toList())); - private final InternalEntryTypes BIBLATEX = new InternalEntryTypes(Stream.concat(BiblatexEntryTypeDefinitions.ALL.stream(), BiblatexSoftwareEntryTypeDefinitions.ALL.stream()).collect(Collectors.toList())); + private final InternalEntryTypes BIBLATEX = new InternalEntryTypes(Stream.concat(BiblatexEntryTypeDefinitions.ALL.stream(), Stream.concat(BiblatexSoftwareEntryTypeDefinitions.ALL.stream(), BiblatexAPAEntryTypeDefinitions.ALL.stream())).collect(Collectors.toList())); public BibEntryTypesManager() { } @@ -99,6 +100,7 @@ public List getAllCustomTypes(BibDatabaseMode mode) { return customizedTypes.stream() .filter(entryType -> BiblatexEntryTypeDefinitions.ALL.stream().noneMatch(biblatexType -> biblatexType.getType().equals(entryType.getType()))) .filter(entryType -> BiblatexSoftwareEntryTypeDefinitions.ALL.stream().noneMatch(biblatexSoftware -> biblatexSoftware.getType().equals(entryType.getType()))) + .filter(entryType -> BiblatexAPAEntryTypeDefinitions.ALL.stream().noneMatch(biblatexAPA -> biblatexAPA.getType().equals(entryType.getType()))) .collect(Collectors.toList()); } } diff --git a/src/main/java/org/jabref/model/entry/field/BiblatexApaField.java b/src/main/java/org/jabref/model/entry/field/BiblatexApaField.java new file mode 100644 index 00000000000..b2938963efe --- /dev/null +++ b/src/main/java/org/jabref/model/entry/field/BiblatexApaField.java @@ -0,0 +1,82 @@ +package org.jabref.model.entry.field; + +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Optional; +import java.util.Set; + +import org.jabref.model.entry.types.BiblatexApaEntryType; + +public enum BiblatexApaField implements Field { + + AMENDMENT("amendment"), + ARTICLE("article"), + CITATION("citation"), + CITATION_CITEORG("citation_citeorg"), + CITATION_CITEDATE("citation_citedate", FieldProperty.DATE), + CITATION_CITEINFO("citation_citeinfo"), + SECTION("section", FieldProperty.NUMERIC), + SOURCE("source"); + + private final String name; + private final String displayName; + private final Set properties; + + BiblatexApaField(String name) { + this.name = name; + this.displayName = null; + this.properties = EnumSet.noneOf(FieldProperty.class); + } + + BiblatexApaField(String name, String displayName) { + this.name = name; + this.displayName = displayName; + this.properties = EnumSet.noneOf(FieldProperty.class); + } + + BiblatexApaField(String name, String displayName, FieldProperty first, FieldProperty... rest) { + this.name = name; + this.displayName = displayName; + this.properties = EnumSet.of(first, rest); + } + + BiblatexApaField(String name, FieldProperty first, FieldProperty... rest) { + this.name = name; + this.displayName = null; + this.properties = EnumSet.of(first, rest); + } + + public static Optional fromName(T type, String name) { + if (!(type instanceof BiblatexApaEntryType)) { + return Optional.empty(); + } + return Arrays.stream(BiblatexApaField.values()) + .filter(field -> field.getName().equalsIgnoreCase(name)) + .findAny(); + } + + @Override + public Set getProperties() { + return Collections.unmodifiableSet(properties); + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean isStandardField() { + return false; + } + + @Override + public String getDisplayName() { + if (displayName == null) { + return Field.super.getDisplayName(); + } else { + return displayName; + } + } +} diff --git a/src/main/java/org/jabref/model/entry/field/BiblatexSoftwareField.java b/src/main/java/org/jabref/model/entry/field/BiblatexSoftwareField.java new file mode 100644 index 00000000000..fc3bb4dcc48 --- /dev/null +++ b/src/main/java/org/jabref/model/entry/field/BiblatexSoftwareField.java @@ -0,0 +1,82 @@ +package org.jabref.model.entry.field; + +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Optional; +import java.util.Set; + +import org.jabref.model.entry.types.BiblatexSoftwareEntryType; + +public enum BiblatexSoftwareField implements Field { + + HALID("hal_id"), + HALVERSION("hal_version"), + INTRODUCEDIN("introducedin"), + LICENSE("license"), + RELATEDTYPE("relatedtype"), + RELATEDSTRING("relatedstring"), + REPOSITORY("repository"), + SWHID("swhid"); + + private final String name; + private final String displayName; + private final Set properties; + + BiblatexSoftwareField(String name) { + this.name = name; + this.displayName = null; + this.properties = EnumSet.noneOf(FieldProperty.class); + } + + BiblatexSoftwareField(String name, String displayName) { + this.name = name; + this.displayName = displayName; + this.properties = EnumSet.noneOf(FieldProperty.class); + } + + BiblatexSoftwareField(String name, String displayName, FieldProperty first, FieldProperty... rest) { + this.name = name; + this.displayName = displayName; + this.properties = EnumSet.of(first, rest); + } + + BiblatexSoftwareField(String name, FieldProperty first, FieldProperty... rest) { + this.name = name; + this.displayName = null; + this.properties = EnumSet.of(first, rest); + } + + public static Optional fromName(T type, String name) { + if (!(type instanceof BiblatexSoftwareEntryType)) { + return Optional.empty(); + } + return Arrays.stream(BiblatexSoftwareField.values()) + .filter(field -> field.getName().equalsIgnoreCase(name)) + .findAny(); + } + + @Override + public Set getProperties() { + return Collections.unmodifiableSet(properties); + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean isStandardField() { + return false; + } + + @Override + public String getDisplayName() { + if (displayName == null) { + return Field.super.getDisplayName(); + } else { + return displayName; + } + } +} diff --git a/src/main/java/org/jabref/model/entry/field/FieldFactory.java b/src/main/java/org/jabref/model/entry/field/FieldFactory.java index b1c2f3b99b9..07a34b039d3 100644 --- a/src/main/java/org/jabref/model/entry/field/FieldFactory.java +++ b/src/main/java/org/jabref/model/entry/field/FieldFactory.java @@ -73,13 +73,23 @@ public static String serializeFieldsList(Collection fields) { .collect(Collectors.joining(DELIMITER)); } + public static Field parseField(T type, String fieldName) { + return OptionalUtil.orElse( + OptionalUtil.orElse( + OptionalUtil.orElse( + OptionalUtil.orElse( + OptionalUtil.orElse( + InternalField.fromName(fieldName), + StandardField.fromName(fieldName)), + SpecialField.fromName(fieldName)), + IEEEField.fromName(fieldName)), + BiblatexSoftwareField.fromName(type, fieldName)), + BiblatexApaField.fromName(type, fieldName)) + .orElse(new UnknownField(fieldName)); + } + public static Field parseField(String fieldName) { - return OptionalUtil.orElse(OptionalUtil.orElse(OptionalUtil.orElse( - InternalField.fromName(fieldName), - StandardField.fromName(fieldName)), - SpecialField.fromName(fieldName)), - IEEEField.fromName(fieldName)) - .orElse(new UnknownField(fieldName)); + return parseField(null, fieldName); } public static Set getKeyFields() { @@ -138,6 +148,8 @@ private static Set getFieldsFiltered(Predicate selector) { private static Set getAllFields() { Set fields = new HashSet<>(); + fields.addAll(EnumSet.allOf(BiblatexApaField.class)); + fields.addAll(EnumSet.allOf(BiblatexSoftwareField.class)); fields.addAll(EnumSet.allOf(IEEEField.class)); fields.addAll(EnumSet.allOf(InternalField.class)); fields.addAll(EnumSet.allOf(SpecialField.class)); diff --git a/src/main/java/org/jabref/model/entry/field/IEEEField.java b/src/main/java/org/jabref/model/entry/field/IEEEField.java index d00bec02eee..f56ce79ea99 100644 --- a/src/main/java/org/jabref/model/entry/field/IEEEField.java +++ b/src/main/java/org/jabref/model/entry/field/IEEEField.java @@ -31,7 +31,7 @@ public enum IEEEField implements Field { this.properties = EnumSet.of(first, rest); } - public static Optional fromName(String name) { + public static Optional fromName(String name) { return Arrays.stream(IEEEField.values()) .filter(field -> field.getName().equalsIgnoreCase(name)) .findAny(); diff --git a/src/main/java/org/jabref/model/entry/field/SpecialFieldValue.java b/src/main/java/org/jabref/model/entry/field/SpecialFieldValue.java index d4c1f8df7d5..6c3337b41f6 100644 --- a/src/main/java/org/jabref/model/entry/field/SpecialFieldValue.java +++ b/src/main/java/org/jabref/model/entry/field/SpecialFieldValue.java @@ -39,6 +39,7 @@ public enum SpecialFieldValue { public static SpecialFieldValue getRating(int ranking) { return switch (ranking) { + case 0 -> CLEAR_RANK; case 1 -> RANK_1; case 2 -> RANK_2; case 3 -> RANK_3; @@ -58,6 +59,7 @@ public Optional getFieldValue() { public int toRating() { return switch (this) { + case CLEAR_RANK -> 0; case RANK_1 -> 1; case RANK_2 -> 2; case RANK_3 -> 3; diff --git a/src/main/java/org/jabref/model/entry/field/StandardField.java b/src/main/java/org/jabref/model/entry/field/StandardField.java index ef3646802e0..bcf9b710b7d 100644 --- a/src/main/java/org/jabref/model/entry/field/StandardField.java +++ b/src/main/java/org/jabref/model/entry/field/StandardField.java @@ -59,14 +59,11 @@ public enum StandardField implements Field { FOREWORD("foreword", FieldProperty.PERSON_NAMES), FOLDER("folder"), GENDER("gender", FieldProperty.GENDER), - HALID("hal_id"), - HALVERSION("hal_version"), HOLDER("holder", FieldProperty.PERSON_NAMES), HOWPUBLISHED("howpublished"), IDS("ids", FieldProperty.MULTIPLE_ENTRY_LINK), INSTITUTION("institution"), INTRODUCTION("introduction", FieldProperty.PERSON_NAMES), - INTRODUCEDIN("introducedin"), ISBN("isbn", "ISBN", FieldProperty.ISBN), ISRN("isrn", "ISRN"), ISSN("issn", "ISSN"), @@ -81,7 +78,6 @@ public enum StandardField implements Field { LANGUAGE("language", FieldProperty.LANGUAGE), LABEL("label"), LIBRARY("library"), - LICENSE("license"), LOCATION("location"), MAINSUBTITLE("mainsubtitle", FieldProperty.BOOK_NAME), MAINTITLE("maintitle", FieldProperty.BOOK_NAME), @@ -106,10 +102,7 @@ public enum StandardField implements Field { PUBSTATE("pubstate", FieldProperty.PUBLICATION_STATE), PRIMARYCLASS("primaryclass"), RELATED("related", FieldProperty.MULTIPLE_ENTRY_LINK), - RELATEDTYPE("relatedtype"), - RELATEDSTRING("relatedstring"), REPORTNO("reportno"), - REPOSITORY("repository"), REVIEW("review"), REVISION("revision"), SCHOOL("school"), @@ -120,7 +113,6 @@ public enum StandardField implements Field { SORTKEY("sortkey"), SORTNAME("sortname", FieldProperty.PERSON_NAMES), SUBTITLE("subtitle"), - SWHID("swhid"), TITLE("title"), TITLEADDON("titleaddon"), TRANSLATOR("translator", FieldProperty.PERSON_NAMES), diff --git a/src/main/java/org/jabref/model/entry/types/BiblatexAPAEntryTypeDefinitions.java b/src/main/java/org/jabref/model/entry/types/BiblatexAPAEntryTypeDefinitions.java new file mode 100644 index 00000000000..f5c0b89b0be --- /dev/null +++ b/src/main/java/org/jabref/model/entry/types/BiblatexAPAEntryTypeDefinitions.java @@ -0,0 +1,43 @@ +package org.jabref.model.entry.types; + +import java.util.Arrays; +import java.util.List; + +import org.jabref.model.entry.BibEntryType; +import org.jabref.model.entry.BibEntryTypeBuilder; +import org.jabref.model.entry.field.BiblatexApaField; +import org.jabref.model.entry.field.StandardField; + +public class BiblatexAPAEntryTypeDefinitions { + + private static final BibEntryType JURISDICTION = new BibEntryTypeBuilder() + .withType(BiblatexApaEntryType.Jurisdiction) + .withImportantFields(StandardField.ORGANIZATION, BiblatexApaField.CITATION_CITEORG, BiblatexApaField.CITATION_CITEDATE, BiblatexApaField.CITATION_CITEDATE, StandardField.ORIGDATE) + .withRequiredFields(StandardField.TITLE, BiblatexApaField.CITATION, BiblatexApaField.CITATION_CITEINFO, StandardField.URL, StandardField.DATE) + .build(); + + private static final BibEntryType LEGISLATION = new BibEntryTypeBuilder() + .withType(BiblatexApaEntryType.Legislation) + .withImportantFields(StandardField.TITLEADDON, StandardField.ORIGDATE) + .withRequiredFields(StandardField.TITLE, StandardField.LOCATION, StandardField.URL, StandardField.DATE) + .build(); + + private static final BibEntryType LEGADMINMATERIAL = new BibEntryTypeBuilder() + .withType(BiblatexApaEntryType.Legadminmaterial) + .withImportantFields(StandardField.NUMBER, StandardField.SHORTTITLE, StandardField.NOTE, StandardField.KEYWORDS) + .withRequiredFields(StandardField.TITLE, BiblatexApaField.CITATION, StandardField.URL, StandardField.DATE) + .build(); + + private static final BibEntryType CONSTITUTION = new BibEntryTypeBuilder() + .withType(BiblatexApaEntryType.Constitution) + .withImportantFields(BiblatexApaField.ARTICLE, BiblatexApaField.AMENDMENT, StandardField.EVENTDATE, StandardField.KEYWORDS, StandardField.PART, BiblatexApaField.SECTION) + .withRequiredFields(BiblatexApaField.SOURCE, StandardField.TYPE) + .build(); + + private static final BibEntryType LEGAL = new BibEntryTypeBuilder() + .withType(BiblatexApaEntryType.Legal) + .withRequiredFields(StandardField.TITLE, StandardField.DATE, StandardField.URI, StandardField.KEYWORDS, StandardField.PART, BiblatexApaField.SECTION) + .build(); + + public static final List ALL = Arrays.asList(JURISDICTION, LEGISLATION, LEGADMINMATERIAL, CONSTITUTION, LEGAL); +} diff --git a/src/main/java/org/jabref/model/entry/types/BiblatexApaEntryType.java b/src/main/java/org/jabref/model/entry/types/BiblatexApaEntryType.java new file mode 100644 index 00000000000..6a1d8d3abe2 --- /dev/null +++ b/src/main/java/org/jabref/model/entry/types/BiblatexApaEntryType.java @@ -0,0 +1,36 @@ +package org.jabref.model.entry.types; + +import java.util.Arrays; +import java.util.Locale; +import java.util.Optional; + +public enum BiblatexApaEntryType implements EntryType { + + Legislation("Legislation"), + Legadminmaterial("Legadminmaterial"), + Jurisdiction("Jurisdiction"), + Constitution("Constitution"), + Legal("Legal"); + + private final String displayName; + + BiblatexApaEntryType(String displayName) { + this.displayName = displayName; + } + + @Override + public String getName() { + return displayName.toLowerCase(Locale.ENGLISH); + } + + @Override + public String getDisplayName() { + return displayName; + } + + public static Optional fromName(String name) { + return Arrays.stream(BiblatexApaEntryType.values()) + .filter(field -> field.getName().equalsIgnoreCase(name)) + .findAny(); + } +} diff --git a/src/main/java/org/jabref/model/entry/types/BiblatexSoftwareEntryType.java b/src/main/java/org/jabref/model/entry/types/BiblatexSoftwareEntryType.java new file mode 100644 index 00000000000..041c48e592a --- /dev/null +++ b/src/main/java/org/jabref/model/entry/types/BiblatexSoftwareEntryType.java @@ -0,0 +1,35 @@ +package org.jabref.model.entry.types; + +import java.util.Arrays; +import java.util.Locale; +import java.util.Optional; + +public enum BiblatexSoftwareEntryType implements EntryType { + + Dataset("Dataset"), + SoftwareVersion("SoftwareVersion"), + SoftwareModule("SoftwareModule"), + CodeFragment("CodeFragment"); + + private final String displayName; + + BiblatexSoftwareEntryType(String displayName) { + this.displayName = displayName; + } + + public static Optional fromName(String name) { + return Arrays.stream(BiblatexSoftwareEntryType.values()) + .filter(field -> field.getName().equalsIgnoreCase(name)) + .findAny(); + } + + @Override + public String getName() { + return displayName.toLowerCase(Locale.ENGLISH); + } + + @Override + public String getDisplayName() { + return displayName; + } +} diff --git a/src/main/java/org/jabref/model/entry/types/BiblatexSoftwareEntryTypeDefinitions.java b/src/main/java/org/jabref/model/entry/types/BiblatexSoftwareEntryTypeDefinitions.java index 0fc48e6d904..6a2e7f0851e 100644 --- a/src/main/java/org/jabref/model/entry/types/BiblatexSoftwareEntryTypeDefinitions.java +++ b/src/main/java/org/jabref/model/entry/types/BiblatexSoftwareEntryTypeDefinitions.java @@ -5,6 +5,7 @@ import org.jabref.model.entry.BibEntryType; import org.jabref.model.entry.BibEntryTypeBuilder; +import org.jabref.model.entry.field.BiblatexSoftwareField; import org.jabref.model.entry.field.OrFields; import org.jabref.model.entry.field.StandardField; @@ -12,40 +13,40 @@ public class BiblatexSoftwareEntryTypeDefinitions { private static final BibEntryType SOFTWARE = new BibEntryTypeBuilder() .withType(StandardEntryType.Software) .withImportantFields(StandardField.DATE, StandardField.DOI, StandardField.EPRINTTYPE, StandardField.EPRINTCLASS, StandardField.EPRINT, - StandardField.EDITOR, StandardField.FILE, StandardField.HALID, StandardField.HALVERSION, StandardField.INSTITUTION, StandardField.INTRODUCEDIN, - StandardField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, StandardField.PUBLISHER, StandardField.RELATED, - StandardField.RELATEDSTRING, StandardField.REPOSITORY, StandardField.SWHID, StandardField.URLDATE, StandardField.VERSION) + StandardField.EDITOR, StandardField.FILE, BiblatexSoftwareField.HALID, BiblatexSoftwareField.HALVERSION, StandardField.INSTITUTION, BiblatexSoftwareField.INTRODUCEDIN, + BiblatexSoftwareField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, StandardField.PUBLISHER, StandardField.RELATED, + BiblatexSoftwareField.RELATEDSTRING, BiblatexSoftwareField.REPOSITORY, BiblatexSoftwareField.SWHID, StandardField.URLDATE, StandardField.VERSION) .withRequiredFields(new OrFields(StandardField.AUTHOR, StandardField.EDITOR), StandardField.TITLE, StandardField.URL, StandardField.VERSION, StandardField.YEAR) .build(); private static final BibEntryType SOFTWAREVERSION = new BibEntryTypeBuilder() - .withType(StandardEntryType.SoftwareVersion) - .withImportantFields(StandardField.DATE, StandardField.EPRINTCLASS, StandardField.EPRINTTYPE, StandardField.HALID, StandardField.HALVERSION, - StandardField.INSTITUTION, StandardField.INTRODUCEDIN, StandardField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, - StandardField.PUBLISHER, StandardField.RELATED, StandardField.RELATEDTYPE, StandardField.RELATEDSTRING, - StandardField.REPOSITORY, StandardField.SWHID, StandardField.SUBTITLE, StandardField.URLDATE) + .withType(BiblatexSoftwareEntryType.SoftwareVersion) + .withImportantFields(StandardField.DATE, StandardField.EPRINTCLASS, StandardField.EPRINTTYPE, BiblatexSoftwareField.HALID, BiblatexSoftwareField.HALVERSION, + StandardField.INSTITUTION, BiblatexSoftwareField.INTRODUCEDIN, BiblatexSoftwareField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, + StandardField.PUBLISHER, StandardField.RELATED, BiblatexSoftwareField.RELATEDTYPE, BiblatexSoftwareField.RELATEDSTRING, + BiblatexSoftwareField.REPOSITORY, BiblatexSoftwareField.SWHID, StandardField.SUBTITLE, StandardField.URLDATE) .withRequiredFields(new OrFields(StandardField.AUTHOR, StandardField.EDITOR), StandardField.TITLE, StandardField.URL, StandardField.YEAR, StandardField.VERSION) - .withDetailFields(StandardField.DATE, StandardField.EPRINTCLASS, StandardField.EPRINTTYPE, StandardField.HALID, StandardField.HALVERSION, - StandardField.INSTITUTION, StandardField.INTRODUCEDIN, StandardField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, - StandardField.PUBLISHER, StandardField.RELATED, StandardField.RELATEDTYPE, StandardField.RELATEDSTRING, - StandardField.REPOSITORY, StandardField.SWHID, StandardField.SUBTITLE, StandardField.URLDATE) + .withDetailFields(StandardField.DATE, StandardField.EPRINTCLASS, StandardField.EPRINTTYPE, BiblatexSoftwareField.HALID, BiblatexSoftwareField.HALVERSION, + StandardField.INSTITUTION, BiblatexSoftwareField.INTRODUCEDIN, BiblatexSoftwareField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, + StandardField.PUBLISHER, StandardField.RELATED, BiblatexSoftwareField.RELATEDTYPE, BiblatexSoftwareField.RELATEDSTRING, + BiblatexSoftwareField.REPOSITORY, BiblatexSoftwareField.SWHID, StandardField.SUBTITLE, StandardField.URLDATE) .withRequiredFields(new OrFields(StandardField.AUTHOR, StandardField.EDITOR), StandardField.TITLE, StandardField.URL, StandardField.YEAR) .build(); private static final BibEntryType SOFTWAREMODULE = new BibEntryTypeBuilder() - .withType(StandardEntryType.SoftwareModule) + .withType(BiblatexSoftwareEntryType.SoftwareModule) .withImportantFields(StandardField.DATE, StandardField.DOI, StandardField.EPRINTTYPE, StandardField.EPRINTCLASS, StandardField.EPRINT, - StandardField.EDITOR, StandardField.FILE, StandardField.HALID, StandardField.HALVERSION, StandardField.INSTITUTION, StandardField.INTRODUCEDIN, - StandardField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, StandardField.PUBLISHER, StandardField.RELATED, - StandardField.RELATEDSTRING, StandardField.REPOSITORY, StandardField.SWHID, StandardField.URLDATE, StandardField.VERSION) + StandardField.EDITOR, StandardField.FILE, BiblatexSoftwareField.HALID, BiblatexSoftwareField.HALVERSION, StandardField.INSTITUTION, BiblatexSoftwareField.INTRODUCEDIN, + BiblatexSoftwareField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, StandardField.PUBLISHER, StandardField.RELATED, + BiblatexSoftwareField.RELATEDSTRING, BiblatexSoftwareField.REPOSITORY, BiblatexSoftwareField.SWHID, StandardField.URLDATE, StandardField.VERSION) .withRequiredFields(StandardField.AUTHOR, StandardField.SUBTITLE, StandardField.URL, StandardField.YEAR) .build(); private static final BibEntryType CODEFRAGMENT = new BibEntryTypeBuilder() - .withType(StandardEntryType.CodeFragment) + .withType(BiblatexSoftwareEntryType.CodeFragment) .withImportantFields(StandardField.DATE, StandardField.DOI, StandardField.EPRINTTYPE, StandardField.EPRINTCLASS, StandardField.EPRINT, - StandardField.EDITOR, StandardField.FILE, StandardField.HALID, StandardField.HALVERSION, StandardField.INSTITUTION, StandardField.INTRODUCEDIN, - StandardField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, StandardField.PUBLISHER, StandardField.RELATED, - StandardField.RELATEDSTRING, StandardField.REPOSITORY, StandardField.SWHID, StandardField.URLDATE, StandardField.VERSION) + StandardField.EDITOR, StandardField.FILE, BiblatexSoftwareField.HALID, BiblatexSoftwareField.HALVERSION, StandardField.INSTITUTION, BiblatexSoftwareField.INTRODUCEDIN, + BiblatexSoftwareField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, StandardField.PUBLISHER, StandardField.RELATED, + BiblatexSoftwareField.RELATEDSTRING, BiblatexSoftwareField.REPOSITORY, BiblatexSoftwareField.SWHID, StandardField.URLDATE, StandardField.VERSION) .withRequiredFields(StandardField.URL) .build(); diff --git a/src/main/java/org/jabref/model/entry/types/EntryTypeFactory.java b/src/main/java/org/jabref/model/entry/types/EntryTypeFactory.java index 1c94f6dd9a0..6b40a56c4f4 100644 --- a/src/main/java/org/jabref/model/entry/types/EntryTypeFactory.java +++ b/src/main/java/org/jabref/model/entry/types/EntryTypeFactory.java @@ -49,6 +49,8 @@ private static boolean isBiblatex(EntryType type) { public static EntryType parse(String typeName) { List types = new ArrayList<>(Arrays.asList(StandardEntryType.values())); types.addAll(Arrays.asList(IEEETranEntryType.values())); + types.addAll(Arrays.asList(BiblatexSoftwareEntryType.values())); + types.addAll(Arrays.asList(BiblatexApaEntryType.values())); types.addAll(Arrays.asList(SystematicLiteratureReviewStudyEntryType.values())); return types.stream().filter(type -> type.getName().equals(typeName.toLowerCase(Locale.ENGLISH))).findFirst().orElse(new UnknownEntryType(typeName)); diff --git a/src/main/java/org/jabref/model/entry/types/StandardEntryType.java b/src/main/java/org/jabref/model/entry/types/StandardEntryType.java index fb2922e61a0..6f6f91ac6a5 100644 --- a/src/main/java/org/jabref/model/entry/types/StandardEntryType.java +++ b/src/main/java/org/jabref/model/entry/types/StandardEntryType.java @@ -36,10 +36,7 @@ public enum StandardEntryType implements EntryType { Thesis("Thesis"), WWW("WWW"), Software("Software"), - Dataset("Dataset"), - SoftwareVersion("SoftwareVersion"), - SoftwareModule("SoftwareModule"), - CodeFragment("CodeFragment"); + Dataset("Dataset"); private final String displayName; diff --git a/src/main/java/org/jabref/model/strings/StringUtil.java b/src/main/java/org/jabref/model/strings/StringUtil.java index 19f0b7a4542..2a443306e4d 100644 --- a/src/main/java/org/jabref/model/strings/StringUtil.java +++ b/src/main/java/org/jabref/model/strings/StringUtil.java @@ -750,4 +750,15 @@ public static String quoteStringIfSpaceIsContained(String string) { return string; } } + + /** + * Checks if the given string contains any whitespace characters. The supported whitespace characters + * are the set of characters matched by {@code \s} in regular expressions, which are {@code [ \t\n\x0B\f\r]}. + * + * @param s The string to check + * @return {@code True} if the given string does contain at least one whitespace character, {@code False} otherwise + * */ + public static boolean containsWhitespace(String s) { + return s.chars().anyMatch(Character::isWhitespace); + } } diff --git a/src/main/resources/journals/journalList.mv b/src/main/resources/journals/journalList.mv index 52541a1a095..b921eef3b19 100644 Binary files a/src/main/resources/journals/journalList.mv and b/src/main/resources/journals/journalList.mv differ diff --git a/src/main/resources/l10n/JabRef_de.properties b/src/main/resources/l10n/JabRef_de.properties index 72df848e1f7..901ac722dc2 100644 --- a/src/main/resources/l10n/JabRef_de.properties +++ b/src/main/resources/l10n/JabRef_de.properties @@ -2496,21 +2496,24 @@ Success\!\ Finished\ writing\ metadata.=Erfolgreich\! Das Schreiben der Metadate Custom\ API\ key=Eigener API-Schlüssel Check\ %0\ API\ Key\ Setting=%0 API-Schlüsseleinstellungen überprüfen -Edit\ field\ value=Feldinhalt bearbeiten -Two\ fields=Zwei Felder -Overwrite\ Non\ empty\ fields=Nicht leere Felder überschreiben +Edit\ content=Inhalt ändern +Copy\ or\ Move\ content=Inhalt kopieren oder verschieben +Overwrite\ field\ content=Inhalt des Feldes überschreiben Set=Festlegen Append=Anfügen -Clear\ field=Feld leeren -Field\ value=Feldinhalt -Edit\ field\ value\ of\ selected\ entries=Feldinhalt der ausgewählten Einträge bearbeiten +Clear\ field\ content=Feldinhalt löschen +Set\ or\ append\ content=Setze oder füge Inhalt an +Edit\ field\ content\ for\ selected\ entries=Feldinhalt der ausgewählten Einträge bearbeiten Rename=Umbenennen New\ field\ name=Neuer Feldname -Copy\ value=Wert kopieren -Move\ value=Inhalt verschieben -Swap\ values=Werte tauschen -Copy\ or\ move\ the\ value\ of\ one\ field\ to\ another=Den Inhalt eines Feldes in ein anderes kopieren oder verschieben +Copy\ content=Inhalt kopieren +Move\ content=Inhalt verschieben +Swap\ content=Inhalte vertauschen +Copy\ or\ move\ the\ content\ of\ one\ field\ to\ another=Inhalt eines Feldes kopieren oder in ein anderes Feld verschieben Automatic\ field\ editor=Automatischer Feldeditor +From=Von +Keep\ Modifications=Änderungen akzeptieren +To=Nach (Note\:\ If\ original\ entries\ lack\ keywords\ to\ qualify\ for\ the\ new\ group\ configuration,\ confirming\ here\ will\ add\ them)=(Hinweis\: Wenn es den ursprünglichen Einträgen an Schlüsselwörtern fehlt, die sich für die neue Gruppenkonfiguration qualifizieren, wird die Bestätigung hier diese hinzufügen) Assign=Zuweisen diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties index 133fd9e7f2b..4f355612618 100644 --- a/src/main/resources/l10n/JabRef_en.properties +++ b/src/main/resources/l10n/JabRef_en.properties @@ -224,8 +224,6 @@ cut\ entries=cut entries cut\ entry\ %0=cut entry %0 -DOI\ not\ found=DOI not found - Library\ encoding=Library encoding Library\ properties=Library properties @@ -578,8 +576,6 @@ No\ journal\ names\ could\ be\ abbreviated.=No journal names could be abbreviate No\ journal\ names\ could\ be\ unabbreviated.=No journal names could be unabbreviated. -No\ DOI\ data\ exists=No DOI data exists - not=not not\ found=not found @@ -2489,6 +2485,17 @@ Version=Version Error\ downloading=Error downloading +No\ data\ was\ found\ for\ the\ identifier=No data was found for the identifier +Server\ not\ available=Server not available +Fetching\ information\ using\ %0=Fetching information using %0 +Look\ up\ identifier=Look up identifier + +Bibliographic\ data\ not\ found.\ Cause\ is\ likely\ the\ client\ side.\ Please\ check\ connection\ and\ identifier\ for\ correctness.=Bibliographic data not found. Cause is likely the client side. Please check connection and identifier for correctness. +Bibliographic\ data\ not\ found.\ Cause\ is\ likely\ the\ server\ side.\ Please\ try\ agan\ later.=Bibliographic data not found. Cause is likely the server side. Please try agan later. +Error\ message\ %0=Error message %0 +Identifier\ not\ found=Identifier not found + + Error\ while\ writing\ metadata.\ See\ the\ error\ log\ for\ details.=Error while writing metadata. See the error log for details. Failed\ to\ write\ metadata,\ file\ %1\ not\ found.=Failed to write metadata, file %1 not found. Success\!\ Finished\ writing\ metadata.=Success! Finished writing metadata. @@ -2496,22 +2503,27 @@ Success\!\ Finished\ writing\ metadata.=Success! Finished writing metadata. Custom\ API\ key=Custom API key Check\ %0\ API\ Key\ Setting=Check %0 API Key Setting -Edit\ field\ value=Edit field value -Two\ fields=Two fields -Overwrite\ Non\ empty\ fields=Overwrite Non empty fields +Edit\ content=Edit content +Copy\ or\ Move\ content=Copy or Move content +Overwrite\ field\ content=Overwrite field content Set=Set Append=Append -Clear\ field=Clear field -Field\ value=Field value -Edit\ field\ value\ of\ selected\ entries=Edit field value of selected entries +Clear\ field\ content=Clear field content +Set\ or\ append\ content=Set or append content +Edit\ field\ content\ for\ selected\ entries=Edit field content for selected entries Rename=Rename New\ field\ name=New field name -Copy\ value=Copy value -Move\ value=Move value -Swap\ values=Swap values -Copy\ or\ move\ the\ value\ of\ one\ field\ to\ another=Copy or move the value of one field to another +Copy\ content=Copy content +Move\ content=Move content +Swap\ content=Swap content +Copy\ or\ move\ the\ content\ of\ one\ field\ to\ another=Copy or move the content of one field to another Automatic\ field\ editor=Automatic field editor +From=From +Keep\ Modifications=Keep Modifications +To=To (Note\:\ If\ original\ entries\ lack\ keywords\ to\ qualify\ for\ the\ new\ group\ configuration,\ confirming\ here\ will\ add\ them)=(Note: If original entries lack keywords to qualify for the new group configuration, confirming here will add them) Assign=Assign Do\ not\ assign=Do not assign + +Error\ occured\ %0=Error occured %0 diff --git a/src/main/resources/l10n/JabRef_es.properties b/src/main/resources/l10n/JabRef_es.properties index 5667fb916b0..60e35d98ad0 100644 --- a/src/main/resources/l10n/JabRef_es.properties +++ b/src/main/resources/l10n/JabRef_es.properties @@ -1,7 +1,12 @@ +Could\ not\ delete\ empty\ entries.=No se pudieron eliminar entradas vacías. +Delete\ empty\ entries=Eliminar entradas vacías +Empty\ entries=Vaciar entradas +Keep\ empty\ entries=Mantener entradas vacías +Library\ '%0'\ has\ empty\ entries.\ Do\ you\ want\ to\ delete\ them?=La biblioteca '%0' tiene entradas vacías. ¿Quieres eliminarlas? Unable\ to\ monitor\ file\ changes.\ Please\ close\ files\ and\ processes\ and\ restart.\ You\ may\ encounter\ errors\ if\ you\ continue\ with\ this\ session.=No es posible supervisar los cambios en los archivos. Cierre los archivos y procesos y reinicie. Puede que se produzcan errores si continúa con esta sesión. %0\ contains\ the\ regular\ expression\ %1=%0 contiene la expresión regular %1 @@ -15,6 +20,7 @@ Unable\ to\ monitor\ file\ changes.\ Please\ close\ files\ and\ processes\ and\ %0/%1\ entries=%0/%1 entradas +Reveal\ in\ File\ Explorer=Mostrar en Explorador de Archivos %0\ matches\ the\ regular\ expression\ %1=%0 coincidencias con la Expresión Regular %1 @@ -49,6 +55,7 @@ The\ path\ need\ not\ be\ on\ the\ classpath\ of\ JabRef.=La ruta no debe estar Add\ a\ regular\ expression\ for\ the\ key\ pattern.=Añadir una expresión regular para el patrón clave. +Add\ entry\ manually=Añadir entrada manualmente Add\ selected\ entries\ to\ this\ group=Añadir entradas seleccionadas a este grupo @@ -166,6 +173,8 @@ Copy=Copiar Copy\ title=Copiar título Copy\ \\cite{citation\ key}=Copiar \\cite{citation key} +Copy\ citation\ (html)=Copiar cita (html) +Copy\ citation\ (text)=Copiar cita (texto) Copy\ citation\ key=Copiar clave de cita Copy\ citation\ key\ and\ link=Copiar clave de cita y enlace Copy\ citation\ key\ and\ title=Copiar clave y título de cita @@ -214,13 +223,17 @@ cut\ entries=Cortar entradas cut\ entry\ %0=corte de entrada %0 +DOI\ not\ found=DOI no encontrado Library\ encoding=Codificación de la biblioteca Library\ properties=Propiedades de la biblioteca +%0\ -\ Library\ properties=%0 - Propiedades de la biblioteca Default=Por defecto +Character\ encoding\ UTF-8\ is\ not\ supported.=La codificación de caracteres UTF-8 no restá soportada. +UTF-8\ could\ not\ be\ used\ to\ encode\ the\ following\ characters\:\ %0=UTF-8 no se pudo utilizar para codificar los siguientes caracteres\: %0 The\ chosen\ encoding\ '%0'\ could\ not\ encode\ the\ following\ characters\:=La codificación de caracteres '%' no puede codificar los siguientes caracteres\: Downloading=Descargando @@ -269,6 +282,7 @@ Downloaded\ website\ as\ an\ HTML\ file.=Se descargó el sitio web como un archi duplicate\ removal=eliminación de duplicados +Duplicate\ fields=Campos duplicados Duplicate\ string\ name=Nombre de cadena duplicado @@ -330,6 +344,7 @@ External\ file\ links=Enlaces a archivos externos External\ programs=Programas externos +Failed\ to\ import\ by\ ID=Error al importar por ID Field=Campo @@ -354,6 +369,7 @@ Filter=Filtro Filter\ groups=Filtros +Finished\ writing\ metadata\ for\ %0\ file\ (%1\ skipped,\ %2\ errors).=Escritura de metadatos para el archivo %0 finalizada (%1 omitidos, %2 errores). First\ select\ the\ entries\ you\ want\ keys\ to\ be\ generated\ for.=En primer lugar, seleccione las entradas para las que desea generar claves @@ -370,6 +386,7 @@ Formatter\ name=Nombre del formateador found\ in\ AUX\ file=encontrado en archivo AUX +Fulltext\ search=Búsqueda de texto completo Fulltext\ for=Texto completo de @@ -448,6 +465,9 @@ Include\ subgroups\:\ When\ selected,\ view\ entries\ contained\ in\ this\ group Independent\ group\:\ When\ selected,\ view\ only\ this\ group's\ entries=Grupo independiente\: ver sólo las entradas de este grupo cuando esté seleccionado. I\ Agree=Acepto +Indexing\ pdf\ files=Indexando archivos pdf +Indexing\ for\ %0=Indexando para %0 +%0\ of\ %1\ linked\ files\ added\ to\ the\ index=%0 de %1 archivos enlazados añadidos al índice Invalid\ citation\ key=La clave de cita no es válida @@ -461,6 +481,8 @@ JabRef\ requests\ recommendations\ from\ Mr.\ DLib,\ which\ is\ an\ external\ se JabRef\ Version\ (Required\ to\ ensure\ backwards\ compatibility\ with\ Mr.\ DLib's\ Web\ Service)=Versión JabRef (necesaria para asegurar la compatibilidad con el servicio web de Mr. DLib) Journal\ abbreviations=Abreviaturas de publicaciones +Journal\ lists\:=Listados de revistas\: +Remove\ journal\ '%0'=Eliminar revista '%0' Keep\ both=Mantener ambos @@ -496,6 +518,7 @@ Main\ file\ directory=Carpeta del archivo principal Manage\ custom\ exports=Administrar exportaciones personalizadas Manage\ custom\ imports=Administrar importaciones personalizadas +External\ file\ types=Tipos de archivos externos Mark\ new\ entries\ with\ owner\ name=Marcar nuevas entradas con nombre de propietario @@ -518,7 +541,9 @@ Moved\ group\ "%0".=Se ha movido el grupo "%0". Mr.\ DLib\ Privacy\ settings=Mr. DLib Configuración de la privacidad +No\ database\ is\ open=No hay ninguna base de datos abierta +We\ need\ a\ database\ to\ export\ from.\ Open\ one.=Necesitamos una base de datos desde la que exportar. Abrir una. No\ recommendations\ received\ from\ Mr.\ DLib\ for\ this\ entry.=No se han recibido recomendaciones del Mr. DLib para esta entrada. @@ -552,6 +577,7 @@ No\ journal\ names\ could\ be\ abbreviated.=No se pudieron abreviar nombres de r No\ journal\ names\ could\ be\ unabbreviated.=No se pudieron expandir nombres de revistas. +No\ DOI\ data\ exists=No existen datos DOI not=no @@ -678,16 +704,23 @@ Remove\ group=Eliminar grupo Remove\ group\ and\ subgroups=Eliminar grupo y subgrupos +Remove\ groups\ and\ subgroups=Eliminar grupos y subgrupos +Remove\ all\ selected\ groups\ and\ keep\ their\ subgroups?=¿Eliminar todos los grupos seleccionados y mantener sus subgrupos? +Remove\ group\ "%0"\ and\ keep\ its\ subgroups?=¿Eliminar grupo "%0" y mantener sus subgrupos? +Remove\ groups=Eliminar grupos +Removed\ all\ selected\ groups.=Eliminados todos los grupos seleccionados. Remove\ group\ "%0"\ and\ its\ subgroups?=¿Eliminar el grupo "%0" y sus subgrupos? Removed\ group\ "%0"\ and\ its\ subgroups.=Se ha eliminado el grupo "%0" y sus subgrupos. +Remove\ all\ selected\ groups\ and\ their\ subgroups?=¿Eliminar todos los grupos seleccionados y sus subgrupos? +Removed\ all\ selected\ groups\ and\ their\ subgroups.=Eliminados todos los grupos seleccionados y sus subgrupos. Remove\ link=Eliminar enlace @@ -719,6 +752,8 @@ Replaces\ Unicode\ ligatures\ with\ their\ expanded\ form=Reemplaza las ligadura Required\ fields=Campos requeridos +Do\ not\ resolve\ BibTeX\ strings=No resolver cadenas BibTeX +Resolve\ BibTeX\ strings\ for\ the\ following\ fields=Resolver las cadenas BibTeX para los siguientes campos resolved=resuelto @@ -726,11 +761,13 @@ Restart=Reiniciar Restart\ required=Reinicio requerido +Return\ to\ dialog=Volver al diálogo Review=Revisar Review\ changes=Revisar cambios Review\ Field\ Migration=Revisar campo de migración +Loading=Cargando Save=Guardar Save\ all\ finished.=Guardar todos los finalizados @@ -744,6 +781,7 @@ Save\ library\ as...=Guardar biblioteca como... Saving=Guardando Saving\ all\ libraries...=Guardando todas las bibliotecas... Saving\ library=Guardando biblioteca +Library\ saved=Biblioteca guardada Saved\ selected\ to\ '%0'.=Selección guardada en '%0'. Search=Buscar @@ -795,6 +833,12 @@ Size=Tamaño Skipped\ -\ No\ PDF\ linked=Omitido - No se enlazó PDF Skipped\ -\ PDF\ does\ not\ exist=Omitido - No existe el PDF +JabRef\ skipped\ the\ entry.=JabRef omitió la entrada. +Import\ error=Error al importar +Open\ library\ error=Error al abrir la biblioteca +Please\ check\ your\ library\ file\ for\ wrong\ syntax.=Por favor, comprueba errores de sintaxis en tu archivo de biblioteca. +SourceTab\ error=Error de SourceTab +User\ input\ via\ entry-editor\ in\ `{}bibtex\ source`\ tab\ led\ to\ failure.=Entrada de usuario a través del editor de entrada en la pestaña `{}bibtex source` condujo al error. Sort\ subgroups=Ordenar subgrupos @@ -883,13 +927,18 @@ Warning=Advertencia Warnings=Advertencias +Warning\:\ You\ added\ field\ "%0"\ twice.\ Only\ one\ will\ be\ kept.=Advertencia\: Añadiste el campo "%0" dos veces. Solo uno será utilizado. web\ link=enlace a web What\ do\ you\ want\ to\ do?=¿Qué desea hacer? Whatever\ option\ you\ choose,\ Mr.\ DLib\ may\ share\ its\ data\ with\ research\ partners\ to\ further\ improve\ recommendation\ quality\ as\ part\ of\ a\ 'living\ lab'.\ Mr.\ DLib\ may\ also\ release\ public\ datasets\ that\ may\ contain\ anonymized\ information\ about\ you\ and\ the\ recommendations\ (sensitive\ information\ such\ as\ metadata\ of\ your\ articles\ will\ be\ anonymised\ through\ e.g.\ hashing).\ Research\ partners\ are\ obliged\ to\ adhere\ to\ the\ same\ strict\ data\ protection\ policy\ as\ Mr.\ DLib.=Sea cual sea la opción que elija, Mr. DLib puede compartir sus datos con socios de investigación para mejorar aún más la calidad de las recomendaciones como parte de un "laboratorio vivo". Mr. DLib también puede publicar conjuntos de datos públicos que pueden contener información anónima sobre usted y las recomendaciones (la información confidencial como los metadatos de sus artículos será anonimizada a través de, por ejemplo, hashing). Los socios de investigación están obligados a adherirse a la misma estricta política de protección de datos que Mr. DLib. +Will\ write\ metadata\ to\ the\ PDFs\ linked\ from\ selected\ entries.=Escribirá metadatos en los PDFs enlazados desde entradas seleccionadas. +Write\ BibTeXEntry\ as\ metadata\ to\ PDF.=Escribir entrada BibTeX como metadatos XMP en el PDF. +Write\ metadata\ for\ all\ PDFs\ in\ current\ library?=¿Escribir metadatos para todos los PDFs de la biblioteca actual? +Writing\ metadata\ for\ selected\ entries...=Escribiendo metadatos para las entradas seleccionadas... Write\ BibTeXEntry\ as\ XMP\ metadata\ to\ PDF.=Escribe BibTeXEntry como metadatos XMP en los PDF. @@ -1587,6 +1636,8 @@ Issue\ report\ successful=Comunicación de problema satisfactoria Your\ issue\ was\ reported\ in\ your\ browser.=Su problema fue comunicado a través del navegador The\ log\ and\ exception\ information\ was\ copied\ to\ your\ clipboard.=La información de excepción y registro fue copiada a su portapapeles Please\ paste\ this\ information\ (with\ Ctrl+V)\ in\ the\ issue\ description.=Por favor, pegue esta información (con Ctrl+V) en la descripción del problema. +Last\ notification=Última notificación +Check\ the\ event\ log\ to\ see\ all\ notifications=Revisa el registro de eventos para ver todas las notificaciones Host=Host/Servidor Port=Puerto @@ -1732,6 +1783,7 @@ Delete\ '%0'=Eliminar '%0' Delete\ from\ disk=Eliminar del disco duro Remove\ from\ entry=Eliminar de la entrada There\ exists\ already\ a\ group\ with\ the\ same\ name.=Ya existe un grupo con el mismo nombre. +If\ you\ use\ it,\ it\ will\ inherit\ all\ entries\ from\ this\ other\ group.=Si lo utiliza, heredará todas las entradas de este otro grupo. Copy\ linked\ file=Copiar archivo enlazado Copy\ linked\ file\ to\ folder...=Copiar archivo enlazado a la carpeta... @@ -1787,6 +1839,7 @@ Could\ not\ connect\ to\ Vim\ server.\ Make\ sure\ that\ Vim\ is\ running\ with\ Could\ not\ connect\ to\ a\ running\ gnuserv\ process.\ Make\ sure\ that\ Emacs\ or\ XEmacs\ is\ running,\ and\ that\ the\ server\ has\ been\ started\ (by\ running\ the\ command\ 'server-start'/'gnuserv-start').=No se puede conectar con un proceso gnuserv en ejecución. Asegúrese de que Emacs o XEmacs se está ejecutando y de que el servidor ha sido iniciado (ejecutando el comando 'server-start'/'gnuserv-start'). Error\ pushing\ entries=Error al enviar entradas +Preamble=Preámbulo Markings=Marcados Use\ selected\ instance=Usar la instancia seleccionada @@ -1805,6 +1858,7 @@ Blog=Blog Check\ integrity=Verificar integridad Cleanup\ URL\ link=Limpiar un enlace URL Cleanup\ URL\ link\ by\ removing\ special\ symbols\ and\ extracting\ simple\ link=Limpiar un enlace URL eliminando los símbolos especiales y extrayendo un enlace simple +Copy\ DOI=Copiar DOI Copy\ DOI\ url=Copiar la url del DOI Development\ version=Versión de desarrollo Export\ selected\ entries=Exportar registros seleccionados @@ -1814,6 +1868,8 @@ JabRef\ resources=Recursos sobre JabRef Manage\ journal\ abbreviations=Administrar abreviaturas de publicaciones Manage\ protected\ terms=gestionar términos protegidos New\ entry\ from\ plain\ text=Nueva entrada desde texto sin formato +Import\ by\ ID=Importar por ID +Enter\ a\ valid\ ID=Introduzca un ID válido New\ sublibrary\ based\ on\ AUX\ file=Nueva subbiblioteca a partir de un archivo AUX Push\ entries\ to\ external\ application\ (%0)=Agregar registros a aplicación externa (%0) Quit=Salir @@ -1873,6 +1929,7 @@ Keyword\ separator=Separador de palabras clave Remove\ keyword=Eliminar palabra clave Are\ you\ sure\ you\ want\ to\ remove\ keyword\:\ "%0"?=¿Está seguro de que desea eliminar la palabra clave\: "%0"? Reset\ to\ default=Restablecer los valores por defecto +String\ constants=Constantes de cadena Export\ all\ entries=Exportar todas las entradas Generate\ citation\ keys=Generar claves de cita Manage\ field\ names\ &\ content=Gestionar nombres y contenido de los campos @@ -2002,6 +2059,7 @@ Please\ provide\ a\ valid\ aux\ file.=Por favor, proporcione un archivo AUX vál Keyword\ delimiter=Separador de palabras clave Hierarchical\ keyword\ delimiter=Separador de palabras clave jerárquicas Escape\ ampersands=Escape de ampersands +Escape\ dollar\ sign=Símbolo de escape dólar Hint\:\n\nTo\ search\ all\ fields\ for\ Smith,\ enter\:\nsmith\n\nTo\ search\ the\ field\ author\ for\ Smith\ and\ the\ field\ title\ for\ electrical,\ enter\:\nauthor\=Smith\ and\ title\=electrical=Consejo\:\n\nPara buscar Pedro en todos los campos, escriba\:\npedro\n\nPara buscar Pedro en el campo author y eléctrico en el campo title, escriba\:\nauthor\=Pedro and title\=eléctrico @@ -2211,7 +2269,11 @@ Reveal\ in\ file\ explorer=Revelar en el explorador de archivos Autolink\ files=Enlazar archivos automáticamente +Custom\ editor\ tabs=Pestañas de editor personalizadas +Custom\ export\ formats=Formatos de exportación personalizados +Custom\ import\ formats=Formatos de importación personalizados +No\ list\ enabled=No hay lista habilitada Protect\ selection=Proteger selección Customized\ preview\ style=Estilo de previsualización personalizado @@ -2238,7 +2300,17 @@ Regular\ expression=Expresión regular Error\ importing.\ See\ the\ error\ log\ for\ details.=Error al importar. Consulte el registro de errores para obtener detalles. - +Error\ from\ import\:\ %0=Error de importación\: %0 +Error\ reading\ PDF\ content\:\ %0=Error al leer el contenido PDF\: %0 +Importing\ bib\ entry=Importando entrada bibliográfica +Importing\ using\ extracted\ PDF\ data=Importación usando datos extraídos del PDF +No\ BibTeX\ data\ found.\ Creating\ empty\ entry\ with\ file\ link=No se encontraron datos BibTeX. Creando entrada vacía con enlace de archivo +No\ metadata\ found.\ Creating\ empty\ entry\ with\ file\ link=No se encontraron metadatos. Creando entrada vacía con enlace de archivo +Processing\ file\ %0=Procesando archivo %0 +Export\ selected=Exportar seleccionados + +Separate\ merged\ citations=Citas fusionadas separadas +Separate\ citations=Citas separadas Custom\ DOI\ URI=URI personalizado de DOI diff --git a/src/main/resources/l10n/JabRef_fr.properties b/src/main/resources/l10n/JabRef_fr.properties index 46020ee888c..42edf5e63ee 100644 --- a/src/main/resources/l10n/JabRef_fr.properties +++ b/src/main/resources/l10n/JabRef_fr.properties @@ -19,7 +19,9 @@ Unable\ to\ monitor\ file\ changes.\ Please\ close\ files\ and\ processes\ and\ %0/%1\ entries=%0/%1 entrées +Export\ operation\ finished\ successfully.=L'opération d'export s'est terminée avec succès. +Reveal\ in\ File\ Explorer=Montrer dans l'explorateur de fichiers %0\ matches\ the\ regular\ expression\ %1=%0 correspond à l'expression régulière %1 @@ -703,16 +705,23 @@ Remove\ group=Supprimer le groupe Remove\ group\ and\ subgroups=Supprimer le groupe et les sous-groupes +Remove\ groups\ and\ subgroups=Supprimer les groupes et les sous-groupes +Remove\ all\ selected\ groups\ and\ keep\ their\ subgroups?=Supprimer tous les groupes sélectionnés et conserver leurs sous-groupes ? +Remove\ group\ "%0"\ and\ keep\ its\ subgroups?=Supprimer le groupe « %0 » et conserver ses sous-groupes ? +Remove\ groups=Supprimer les groupes +Removed\ all\ selected\ groups.=Tous les groupes sélectionnés ont été supprimés. Remove\ group\ "%0"\ and\ its\ subgroups?=Supprimer le groupe « %0 » et ses sous-groupes ? Removed\ group\ "%0"\ and\ its\ subgroups.=Groupe « %0 » et ses sous-groupes supprimés. +Remove\ all\ selected\ groups\ and\ their\ subgroups?=Supprimer tous les groupes sélectionnés et leurs sous-groupes ? +Removed\ all\ selected\ groups\ and\ their\ subgroups.=Tous les groupes sélectionnés et leurs sous-groupes ont été supprimés. Remove\ link=Supprimer le lien @@ -825,6 +834,12 @@ Size=Taille Skipped\ -\ No\ PDF\ linked=Sauté - Pas de PDF lié Skipped\ -\ PDF\ does\ not\ exist=Omis - Le PDF n'existe pas +JabRef\ skipped\ the\ entry.=JabRef a ignoré l'entrée. +Import\ error=Erreur d'importation +Open\ library\ error=Erreur d'ouverture du fichier +Please\ check\ your\ library\ file\ for\ wrong\ syntax.=Veuillez vérifier la syntaxe de votre fichier bibliographique. +SourceTab\ error=Erreur de SourceTab +User\ input\ via\ entry-editor\ in\ `{}bibtex\ source`\ tab\ led\ to\ failure.=La saisie de l'utilisateur via l'éditeur d'entrée dans l'onglet `{}bibtex source` a conduit à un échec. Sort\ subgroups=Trier les sous-groupes @@ -970,7 +985,7 @@ Line\ %0\:\ Found\ corrupted\ citation\ key\ %1\ (contains\ whitespaces).=Ligne Line\ %0\:\ Found\ corrupted\ citation\ key\ %1\ (comma\ missing).=Ligne %0 \: clef de citation corrompue %1 (virgule manquante). No\ full\ text\ document\ found=Aucun texte intégral trouvé Download\ from\ URL=Télécharger depuis l'URL -Rename\ field=Renommer le champ +Rename\ field=Renommer Cannot\ use\ port\ %0\ for\ remote\ operation;\ another\ application\ may\ be\ using\ it.\ Try\ specifying\ another\ port.=Le port %0 ne peut pas être utilisé pour une opération à distance ; un autre logiciel pourrait être en train de l'utiliser. Essayer de spécifier un autre port. @@ -1244,6 +1259,7 @@ Connection\ failed\!=Échec de la connexion \! Connection\ successful\!=Connexion réussie \! SSL\ Configuration=Configuration SSL +SSL\ configuration\ changed=Configuration SSL modifiée SSL\ certificate\ file=Fichier de certificat SSL Duplicate\ Certificates=Dupliquer les certificats You\ already\ added\ this\ certificate=Vous avez déjà ajouté ce certificat @@ -1283,6 +1299,7 @@ Please\ open\ %0\ manually.=Veuillez ouvrir manuellement %0 . The\ link\ has\ been\ copied\ to\ the\ clipboard.=Le lien a été copié dans le presse-papiers. Open\ %0\ file=Ouvrir le fichier %0 +Could\ not\ detect\ terminal\ automatically.\ Please\ define\ a\ custom\ terminal\ in\ the\ preferences.=Impossible de détecter le terminal automatiquement. Veuillez définir un terminal personnalisé dans les préférences. Cannot\ delete\ file=Le fichier ne peut pas être supprimé File\ permission\ error=Erreur due aux permissions du fichier @@ -1670,6 +1687,8 @@ Issue\ report\ successful=Signalement de l'anomalie réussi Your\ issue\ was\ reported\ in\ your\ browser.=Votre anomalie a été affichée dans votre navigateur. The\ log\ and\ exception\ information\ was\ copied\ to\ your\ clipboard.=Le journal et les informations d'anomalie ont été copiées dans votre presse-papier. Please\ paste\ this\ information\ (with\ Ctrl+V)\ in\ the\ issue\ description.=Veuillez coller ces informations (avec Ctrl+V) dans la description de l'anomalie. +Last\ notification=Dernière notification +Check\ the\ event\ log\ to\ see\ all\ notifications=Consultez le journal des événements pour voir toutes les notifications Host=Hôte Port=Port @@ -2477,4 +2496,25 @@ Success\!\ Finished\ writing\ metadata.=Succès \! Écriture des métadonnées t Custom\ API\ key=Clef d'API personnalisée Check\ %0\ API\ Key\ Setting=Vérifier les paramètres de la clef d'API %0 - +Edit\ content=Modifier le contenu +Copy\ or\ Move\ content=Copier ou déplacer le contenu +Overwrite\ field\ content=Écraser le contenu du champ +Set=Définir +Append=Ajouter +Clear\ field\ content=Effacer le contenu du champ +Set\ or\ append\ content=Définir ou ajouter du contenu +Edit\ field\ content\ for\ selected\ entries=Modifier le contenu d'un champ +Rename=Renommer +New\ field\ name=Nouveau nom de champ +Copy\ content=Copier le contenu +Move\ content=Déplacer le contenu +Swap\ content=Permuter le contenu +Copy\ or\ move\ the\ content\ of\ one\ field\ to\ another=Copier ou déplacer le contenu d'un champ vers un autre +Automatic\ field\ editor=Éditeur automatique de champs +From=De +Keep\ Modifications=Enregistrer +To=Vers + +(Note\:\ If\ original\ entries\ lack\ keywords\ to\ qualify\ for\ the\ new\ group\ configuration,\ confirming\ here\ will\ add\ them)=(Note \: si les entrées originales n'ont pas de mots-clefs correspondant à la nouvelle configuration du groupe, confirmer ici les ajoutera) +Assign=Assigner +Do\ not\ assign=Ne pas assigner diff --git a/src/main/resources/l10n/JabRef_it.properties b/src/main/resources/l10n/JabRef_it.properties index 6bac76a45cb..2e9126ad31c 100644 --- a/src/main/resources/l10n/JabRef_it.properties +++ b/src/main/resources/l10n/JabRef_it.properties @@ -2441,21 +2441,24 @@ Search\ results\ from\ open\ libraries=Risultati della ricerca da librerie apert Custom\ API\ key=Chiave API personalizzata Check\ %0\ API\ Key\ Setting=Controlla le impostazioni della chiave API %0 -Edit\ field\ value=Modifica valore campo -Two\ fields=Due campi -Overwrite\ Non\ empty\ fields=Sovrascrivi campi non vuoti +Edit\ content=Modifica il contenuto +Copy\ or\ Move\ content=Copia o Sposta il contenuto +Overwrite\ field\ content=Sovrascrivi il contenuto del campo Set=Imposta Append=Accoda -Clear\ field=Svuota campo -Field\ value=Valore del campo -Edit\ field\ value\ of\ selected\ entries=Modifica il valore del campo delle voci selezionate +Clear\ field\ content=Cancella il contenuto del campo +Set\ or\ append\ content=Imposta o aggiungi il contenuto +Edit\ field\ content\ for\ selected\ entries=Modifica il contenuto del campo per le voci selezionate Rename=Rinomina New\ field\ name=Nuovo nome del campo -Copy\ value=Copia Valore -Move\ value=Sposta valore -Swap\ values=Scambia i valori -Copy\ or\ move\ the\ value\ of\ one\ field\ to\ another=Copia o sposta il valore di un campo in un altro +Copy\ content=Copia il contenuto +Move\ content=Sposta il contenuto +Swap\ content=Scambia il contenuto +Copy\ or\ move\ the\ content\ of\ one\ field\ to\ another=Copia o sposta il contenuto di un campo in un altro Automatic\ field\ editor=Editor automatico dei campi +From=Da +Keep\ Modifications=Mantieni le modifiche +To=A (Note\:\ If\ original\ entries\ lack\ keywords\ to\ qualify\ for\ the\ new\ group\ configuration,\ confirming\ here\ will\ add\ them)=(Nota\: se le voci originali mancano di parole chiave per qualificarsi per la nuova configurazione di gruppo, confermando qui le aggiungeranno) Assign=Assegna diff --git a/src/main/resources/l10n/JabRef_ru.properties b/src/main/resources/l10n/JabRef_ru.properties index 6980f2d32bb..0999c1ddcb0 100644 --- a/src/main/resources/l10n/JabRef_ru.properties +++ b/src/main/resources/l10n/JabRef_ru.properties @@ -2496,20 +2496,10 @@ Success\!\ Finished\ writing\ metadata.=Успех\! Запись метадан Custom\ API\ key=Пользовательский ключ API Check\ %0\ API\ Key\ Setting=Проверьте настройку ключа API %0 -Edit\ field\ value=Изменить значение поля -Two\ fields=Два поля -Overwrite\ Non\ empty\ fields=Перезаписать непустые поля Set=Задать Append=Присоединить -Clear\ field=Очистить поле -Field\ value=Значение поля -Edit\ field\ value\ of\ selected\ entries=Изменить значение поля выбранных записей Rename=Переименовать New\ field\ name=Новое имя поля -Copy\ value=Копировать значение -Move\ value=Переместить значение -Swap\ values=Обменять значения -Copy\ or\ move\ the\ value\ of\ one\ field\ to\ another=Скопировать или переместить значение одного поля в другое Automatic\ field\ editor=Автоматический редактор поля (Note\:\ If\ original\ entries\ lack\ keywords\ to\ qualify\ for\ the\ new\ group\ configuration,\ confirming\ here\ will\ add\ them)=(Примечание\: Если в исходных записях отсутствуют ключевые слова, подходящие для конфигурации новой группы, то при подтверждении здесь они будут добавлены) diff --git a/src/main/resources/l10n/JabRef_zh_CN.properties b/src/main/resources/l10n/JabRef_zh_CN.properties index 1b467a0de4f..baca5d6da46 100644 --- a/src/main/resources/l10n/JabRef_zh_CN.properties +++ b/src/main/resources/l10n/JabRef_zh_CN.properties @@ -2496,20 +2496,10 @@ Success\!\ Finished\ writing\ metadata.=Success\! Finished writing metadata. Custom\ API\ key=自定义API Check\ %0\ API\ Key\ Setting=Check %0 API Key Setting -Edit\ field\ value=编辑字段内容 -Two\ fields=两个字段 -Overwrite\ Non\ empty\ fields=覆盖非空字段 Set=设定 Append=附加 -Clear\ field=清除字段 -Field\ value=字段内容 -Edit\ field\ value\ of\ selected\ entries=编辑选中条目的字段内容 Rename=重命名 New\ field\ name=新的字段名 -Copy\ value=复制值 -Move\ value=移动值 -Swap\ values=交换值 -Copy\ or\ move\ the\ value\ of\ one\ field\ to\ another=复制或移动一个字段的值到另一个字段中 Automatic\ field\ editor=自动化条目编辑 (Note\:\ If\ original\ entries\ lack\ keywords\ to\ qualify\ for\ the\ new\ group\ configuration,\ confirming\ here\ will\ add\ them)=(Note\: If original entries lack keywords to qualify for the new group configuration, confirming here will add them) diff --git a/src/main/resources/l10n/JabRef_zh_TW.properties b/src/main/resources/l10n/JabRef_zh_TW.properties index 288532048c0..6e74ac8da11 100644 --- a/src/main/resources/l10n/JabRef_zh_TW.properties +++ b/src/main/resources/l10n/JabRef_zh_TW.properties @@ -991,7 +991,7 @@ Set\ rank\ to\ five=設定評分為 5 級 Order=順序 -Affected\ fields\:=影響欄位\: +Affected\ fields\:=影響欄位\: Show\ preview\ as\ a\ tab\ in\ entry\ editor=在條目編輯器中以分頁形式顯示預覽 Font=字型 Visual\ theme=界面主題 diff --git a/src/test/java/org/jabref/gui/edit/CopyOrMoveFieldContentTabViewModelTest.java b/src/test/java/org/jabref/gui/edit/CopyOrMoveFieldContentTabViewModelTest.java new file mode 100644 index 00000000000..75a1a176597 --- /dev/null +++ b/src/test/java/org/jabref/gui/edit/CopyOrMoveFieldContentTabViewModelTest.java @@ -0,0 +1,105 @@ +package org.jabref.gui.edit; + +import java.util.List; +import java.util.Optional; + +import org.jabref.gui.StateManager; +import org.jabref.gui.edit.automaticfiededitor.copyormovecontent.CopyOrMoveFieldContentTabViewModel; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +public class CopyOrMoveFieldContentTabViewModelTest { + CopyOrMoveFieldContentTabViewModel copyOrMoveFieldContentTabViewModel; + BibEntry entryA; + BibEntry entryB; + + BibDatabase bibDatabase; + + StateManager stateManager = mock(StateManager.class); + + @BeforeEach + void setup() { + entryA = new BibEntry(BibEntry.DEFAULT_TYPE) + .withField(StandardField.YEAR, "2015") + .withField(StandardField.DATE, "2014"); + + entryB = new BibEntry(BibEntry.DEFAULT_TYPE) + .withField(StandardField.DATE, "1998"); + bibDatabase = new BibDatabase(); + copyOrMoveFieldContentTabViewModel = newTwoFieldsViewModel(entryA, entryB); + } + + @Test + void copyValueDoesNotCopyBlankValues() { + CopyOrMoveFieldContentTabViewModel copyOrMoveFieldContentTabViewModel = newTwoFieldsViewModel(entryA, entryB); + + copyOrMoveFieldContentTabViewModel.fromFieldProperty().set(StandardField.YEAR); + copyOrMoveFieldContentTabViewModel.toFieldProperty().set(StandardField.DATE); + copyOrMoveFieldContentTabViewModel.overwriteFieldContentProperty().set(true); + copyOrMoveFieldContentTabViewModel.copyValue(); + + assertEquals(Optional.of("2015"), entryA.getField(StandardField.DATE), "YEAR field is not copied correctly to the DATE field"); + assertEquals(Optional.of("2015"), entryA.getField(StandardField.YEAR), "YEAR field should not have changed"); + assertEquals(Optional.of("1998"), entryB.getField(StandardField.DATE), "DATE field should not have changed because the YEAR field is blank e.g it doesn't exist"); + } + + @Test + void swapValuesShouldNotSwapFieldValuesIfOneOfTheValuesIsBlank() { + copyOrMoveFieldContentTabViewModel.fromFieldProperty().set(StandardField.YEAR); + copyOrMoveFieldContentTabViewModel.toFieldProperty().set(StandardField.DATE); + copyOrMoveFieldContentTabViewModel.overwriteFieldContentProperty().set(true); + + copyOrMoveFieldContentTabViewModel.swapValues(); + + assertEquals(Optional.of("1998"), entryB.getField(StandardField.DATE)); + assertEquals(Optional.empty(), entryB.getField(StandardField.YEAR)); + } + + @Test + void swapValuesShouldSwapFieldValuesIfBothValuesAreNotBlank() { + copyOrMoveFieldContentTabViewModel.fromFieldProperty().set(StandardField.YEAR); + copyOrMoveFieldContentTabViewModel.toFieldProperty().set(StandardField.DATE); + copyOrMoveFieldContentTabViewModel.overwriteFieldContentProperty().set(true); + + copyOrMoveFieldContentTabViewModel.swapValues(); + + assertEquals(List.of(Optional.of("2014"), Optional.of("2015")), + List.of(entryA.getField(StandardField.YEAR), entryA.getField(StandardField.DATE)), + "YEAR and DATE values didn't swap"); + } + + @Test + void moveValueShouldNotMoveValueIfToFieldIsNotBlankAndOverwriteIsNotEnabled() { + copyOrMoveFieldContentTabViewModel.fromFieldProperty().set(StandardField.YEAR); + copyOrMoveFieldContentTabViewModel.toFieldProperty().set(StandardField.DATE); + copyOrMoveFieldContentTabViewModel.overwriteFieldContentProperty().set(false); + + copyOrMoveFieldContentTabViewModel.moveValue(); + + assertEquals(Optional.of("2015"), entryA.getField(StandardField.YEAR)); + assertEquals(Optional.of("2014"), entryA.getField(StandardField.DATE)); + } + + @Test + void moveValueShouldMoveValueIfOverwriteIsEnabled() { + copyOrMoveFieldContentTabViewModel.fromFieldProperty().set(StandardField.DATE); + copyOrMoveFieldContentTabViewModel.toFieldProperty().set(StandardField.YEAR); + copyOrMoveFieldContentTabViewModel.overwriteFieldContentProperty().set(true); + + copyOrMoveFieldContentTabViewModel.moveValue(); + + assertEquals(Optional.of("1998"), entryB.getField(StandardField.YEAR)); + assertEquals(Optional.empty(), entryB.getField(StandardField.DATE)); + } + + private CopyOrMoveFieldContentTabViewModel newTwoFieldsViewModel(BibEntry... selectedEntries) { + return new CopyOrMoveFieldContentTabViewModel(List.of(selectedEntries), bibDatabase, stateManager); + } +} diff --git a/src/test/java/org/jabref/gui/edit/EditFieldContentTabViewModelTest.java b/src/test/java/org/jabref/gui/edit/EditFieldContentTabViewModelTest.java new file mode 100644 index 00000000000..1221580762a --- /dev/null +++ b/src/test/java/org/jabref/gui/edit/EditFieldContentTabViewModelTest.java @@ -0,0 +1,118 @@ +package org.jabref.gui.edit; + +import java.util.List; +import java.util.Optional; + +import org.jabref.gui.StateManager; +import org.jabref.gui.edit.automaticfiededitor.editfieldcontent.EditFieldContentViewModel; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.mockito.Mockito.mock; + +public class EditFieldContentTabViewModelTest { + EditFieldContentViewModel editFieldContentViewModel; + BibEntry entryA; + BibEntry entryB; + + BibDatabase bibDatabase; + + StateManager stateManager = mock(StateManager.class); + + @BeforeEach + void setup() { + entryA = new BibEntry(BibEntry.DEFAULT_TYPE) + .withField(StandardField.YEAR, "2015") + .withField(StandardField.DATE, "2014"); + + entryB = new BibEntry(BibEntry.DEFAULT_TYPE) + .withField(StandardField.DATE, "1998") + .withField(StandardField.YEAR, ""); + + bibDatabase = new BibDatabase(); + editFieldContentViewModel = new EditFieldContentViewModel(bibDatabase, List.of(entryA, entryB), stateManager); + } + + @Test + void clearSelectedFieldShouldClearFieldContentEvenWhenOverwriteFieldContentIsNotEnabled() { + editFieldContentViewModel.selectedFieldProperty().set(StandardField.YEAR); + editFieldContentViewModel.overwriteFieldContentProperty().set(false); + editFieldContentViewModel.clearSelectedField(); + + assertEquals(Optional.empty(), entryA.getField(StandardField.YEAR)); + } + + @Test + void clearSelectedFieldShouldDoNothingWhenFieldDoesntExistOrIsEmpty() { + editFieldContentViewModel.selectedFieldProperty().set(StandardField.FILE); + editFieldContentViewModel.clearSelectedField(); + + assertEquals(Optional.empty(), entryA.getField(StandardField.FILE)); + } + + @Test + void setFieldValueShouldNotDoAnythingIfOverwriteFieldContentIsNotEnabled() { + editFieldContentViewModel.overwriteFieldContentProperty().set(false); + editFieldContentViewModel.selectedFieldProperty().set(StandardField.YEAR); + editFieldContentViewModel.fieldValueProperty().set("2001"); + editFieldContentViewModel.setFieldValue(); + + assertEquals(Optional.of("2015"), entryA.getField(StandardField.YEAR)); + } + + @Test + void setFieldValueShouldSetFieldValueIfOverwriteFieldContentIsEnabled() { + editFieldContentViewModel.overwriteFieldContentProperty().set(true); + editFieldContentViewModel.selectedFieldProperty().set(StandardField.YEAR); + editFieldContentViewModel.fieldValueProperty().set("2001"); + editFieldContentViewModel.setFieldValue(); + + assertEquals(Optional.of("2001"), entryA.getField(StandardField.YEAR)); + } + + @Test + void setFieldValueShouldSetFieldValueIfFieldContentIsEmpty() { + editFieldContentViewModel.overwriteFieldContentProperty().set(false); + editFieldContentViewModel.selectedFieldProperty().set(StandardField.YEAR); + editFieldContentViewModel.fieldValueProperty().set("2001"); + editFieldContentViewModel.setFieldValue(); + + assertEquals(Optional.of("2001"), entryB.getField(StandardField.YEAR)); + } + + @Test + void appendToFieldValueShouldDoNothingIfOverwriteFieldContentIsNotEnabled() { + editFieldContentViewModel.overwriteFieldContentProperty().set(false); + editFieldContentViewModel.selectedFieldProperty().set(StandardField.YEAR); + editFieldContentViewModel.fieldValueProperty().set("0"); + editFieldContentViewModel.appendToFieldValue(); + + assertEquals(Optional.of("2015"), entryA.getField(StandardField.YEAR)); + } + + @Test + void appendToFieldValueShouldAppendFieldValueIfOverwriteFieldContentIsEnabled() { + editFieldContentViewModel.overwriteFieldContentProperty().set(true); + editFieldContentViewModel.selectedFieldProperty().set(StandardField.YEAR); + editFieldContentViewModel.fieldValueProperty().set("0"); + editFieldContentViewModel.appendToFieldValue(); + + assertEquals(Optional.of("20150"), entryA.getField(StandardField.YEAR)); + } + + @Test + void getAllFieldsShouldNeverBeEmpty() { + assertNotEquals(0, editFieldContentViewModel.getAllFields().size()); + } + + @Test + void getSelectedFieldShouldHaveADefaultValue() { + assertNotEquals(null, editFieldContentViewModel.getSelectedField()); + } +} diff --git a/src/test/java/org/jabref/gui/edit/RenameFieldViewModelTest.java b/src/test/java/org/jabref/gui/edit/RenameFieldViewModelTest.java new file mode 100644 index 00000000000..bf5a246e97f --- /dev/null +++ b/src/test/java/org/jabref/gui/edit/RenameFieldViewModelTest.java @@ -0,0 +1,111 @@ +package org.jabref.gui.edit; + +import java.util.List; +import java.util.Optional; + +import org.jabref.gui.StateManager; +import org.jabref.gui.edit.automaticfiededitor.renamefield.RenameFieldViewModel; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.FieldFactory; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.field.UnknownField; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +public class RenameFieldViewModelTest { + RenameFieldViewModel renameFieldViewModel; + BibEntry entryA; + BibEntry entryB; + + BibDatabase bibDatabase; + + StateManager stateManager = mock(StateManager.class); + + @BeforeEach + void setup() { + entryA = new BibEntry(BibEntry.DEFAULT_TYPE) + .withField(StandardField.YEAR, "2015") + .withField(StandardField.DATE, "2014") + .withField(StandardField.AUTHOR, "Doe"); + + entryB = new BibEntry(BibEntry.DEFAULT_TYPE) + .withField(StandardField.DATE, "1998") + .withField(StandardField.YEAR, "") + .withField(StandardField.AUTHOR, "Eddie"); + + bibDatabase = new BibDatabase(); + renameFieldViewModel = new RenameFieldViewModel(List.of(entryA, entryB), bibDatabase, stateManager); + } + + @Test + void renameFieldShouldRenameFieldIfItExist() { + renameFieldViewModel.selectField(StandardField.DATE); + renameFieldViewModel.setNewFieldName("ETAD"); + renameFieldViewModel.renameField(); + + assertEquals(Optional.of("2014"), entryA.getField(FieldFactory.parseField("ETAD"))); + assertEquals(Optional.empty(), entryA.getField(StandardField.DATE)); + + assertEquals(Optional.of("1998"), entryB.getField(FieldFactory.parseField("ETAD"))); + assertEquals(Optional.empty(), entryB.getField(StandardField.DATE)); + } + + @Test + void renameFieldShouldDoNothingIfFieldDoNotExist() { + Field toRenameField = new UnknownField("Some_field_that_doesnt_exist"); + renameFieldViewModel.selectField(toRenameField); + renameFieldViewModel.setNewFieldName("new_field_name"); + renameFieldViewModel.renameField(); + + assertEquals(Optional.empty(), entryA.getField(toRenameField)); + assertEquals(Optional.empty(), entryA.getField(new UnknownField("new_field_name"))); + + assertEquals(Optional.empty(), entryB.getField(toRenameField)); + assertEquals(Optional.empty(), entryB.getField(new UnknownField("new_field_name"))); + } + + @Test + void renameFieldShouldNotDoAnythingIfTheNewFieldNameIsEmpty() { + renameFieldViewModel.selectField(StandardField.AUTHOR); + renameFieldViewModel.setNewFieldName(""); + renameFieldViewModel.renameField(); + + assertEquals(Optional.of("Doe"), entryA.getField(StandardField.AUTHOR)); + assertEquals(Optional.empty(), entryA.getField(FieldFactory.parseField(""))); + + assertEquals(Optional.of("Eddie"), entryB.getField(StandardField.AUTHOR)); + assertEquals(Optional.empty(), entryB.getField(FieldFactory.parseField(""))); + } + + @Test + void renameFieldShouldNotDoAnythingIfTheNewFieldNameHasWhitespaceCharacters() { + renameFieldViewModel.selectField(StandardField.AUTHOR); + renameFieldViewModel.setNewFieldName("Hello, World"); + renameFieldViewModel.renameField(); + + assertEquals(Optional.of("Doe"), entryA.getField(StandardField.AUTHOR)); + assertEquals(Optional.empty(), entryA.getField(FieldFactory.parseField("Hello, World"))); + + assertEquals(Optional.of("Eddie"), entryB.getField(StandardField.AUTHOR)); + assertEquals(Optional.empty(), entryB.getField(FieldFactory.parseField("Hello, World"))); + } + + @Test + void renameFieldShouldDoNothingWhenThereIsAlreadyAFieldWithTheSameNameAsNewFieldName() { + renameFieldViewModel.selectField(StandardField.DATE); + renameFieldViewModel.setNewFieldName(StandardField.YEAR.getName()); + renameFieldViewModel.renameField(); + + assertEquals(Optional.of("2014"), entryA.getField(StandardField.DATE)); + assertEquals(Optional.of("2015"), entryA.getField(StandardField.YEAR)); + + assertEquals(Optional.empty(), entryB.getField(StandardField.DATE)); + assertEquals(Optional.of("1998"), entryB.getField(StandardField.YEAR)); + } +} diff --git a/src/test/java/org/jabref/logic/bibtex/comparator/GroupDiffTest.java b/src/test/java/org/jabref/logic/bibtex/comparator/GroupDiffTest.java index 1f1c20fc329..9fded7c7802 100644 --- a/src/test/java/org/jabref/logic/bibtex/comparator/GroupDiffTest.java +++ b/src/test/java/org/jabref/logic/bibtex/comparator/GroupDiffTest.java @@ -56,7 +56,7 @@ void compareWithChangedGroup() { Optional groupDiff = GroupDiff.compare(originalMetaData, newMetaData); - Optional expectedGroupDiff = Optional.of(new GroupDiff(newMetaData.getGroups().get(), originalMetaData.getGroups().get())); + Optional expectedGroupDiff = Optional.of(new GroupDiff(originalMetaData.getGroups().get(), newMetaData.getGroups().get())); assertEquals(expectedGroupDiff.get().getNewGroupRoot(), groupDiff.get().getNewGroupRoot()); assertEquals(expectedGroupDiff.get().getOriginalGroupRoot(), groupDiff.get().getOriginalGroupRoot()); diff --git a/src/test/java/org/jabref/logic/bst/BstFunctionsTest.java b/src/test/java/org/jabref/logic/bst/BstFunctionsTest.java new file mode 100644 index 00000000000..1b63e26cbb3 --- /dev/null +++ b/src/test/java/org/jabref/logic/bst/BstFunctionsTest.java @@ -0,0 +1,666 @@ +package org.jabref.logic.bst; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.jabref.logic.bst.util.BstCaseChangersTest; +import org.jabref.logic.bst.util.BstNameFormatterTest; +import org.jabref.logic.bst.util.BstPurifierTest; +import org.jabref.logic.bst.util.BstTextPrefixerTest; +import org.jabref.logic.bst.util.BstWidthCalculatorTest; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.StandardEntryType; + +import org.antlr.v4.runtime.RecognitionException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * For additional tests see for + * + * purify: {@link BstPurifierTest} + * width: {@link BstWidthCalculatorTest} + * format.name: {@link BstNameFormatterTest} + * change.case: {@link BstCaseChangersTest} + * prefix: {@link BstTextPrefixerTest} + * + */ +class BstFunctionsTest { + @Test + public void testCompareFunctions() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test.compare } { + #5 #5 = % TRUE + #1 #2 = % FALSE + #3 #4 < % TRUE + #4 #3 < % FALSE + #4 #4 < % FALSE + #3 #4 > % FALSE + #4 #3 > % TRUE + #4 #4 > % FALSE + "H" "H" = % TRUE + "H" "Ha" = % FALSE + } + EXECUTE { test.compare } + """); + + vm.render(Collections.emptyList()); + + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testArithmeticFunctions() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { + #1 #1 + % 2 + #5 #2 - % 3 + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals(3, vm.getStack().pop()); + assertEquals(2, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testArithmeticFunctionTypeMismatch() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { + #1 "HELLO" + % Should throw exception + } + EXECUTE { test } + """); + + assertThrows(BstVMException.class, () -> vm.render(Collections.emptyList())); + } + + @Test + public void testStringOperations() throws RecognitionException { + // Test for concat (*) and add.period + BstVM vm = new BstVM(""" + FUNCTION { test } { + "H" "ello" * % Hello + "Johnny" add.period$ % Johnny. + "Johnny." add.period$ % Johnny. + "Johnny!" add.period$ % Johnny! + "Johnny?" add.period$ % Johnny? + "Johnny} }}}" add.period$ % Johnny.} + "Johnny!}" add.period$ % Johnny!} + "Johnny?}" add.period$ % Johnny?} + "Johnny.}" add.period$ % Johnny.} + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals("Johnny.}", vm.getStack().pop()); + assertEquals("Johnny?}", vm.getStack().pop()); + assertEquals("Johnny!}", vm.getStack().pop()); + assertEquals("Johnny.}", vm.getStack().pop()); + assertEquals("Johnny?", vm.getStack().pop()); + assertEquals("Johnny!", vm.getStack().pop()); + assertEquals("Johnny.", vm.getStack().pop()); + assertEquals("Johnny.", vm.getStack().pop()); + assertEquals("Hello", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testMissing() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { title } { } { } + FUNCTION { presort } { cite$ 'sort.key$ := } + ITERATE { presort } + READ + SORT + FUNCTION { test } { title missing$ cite$ } + ITERATE { test } + """); + List testEntries = List.of( + BstVMTest.defaultTestEntry(), + new BibEntry(StandardEntryType.Article) + .withCitationKey("test") + .withField(StandardField.AUTHOR, "No title")); + + vm.render(testEntries); + + assertEquals("test", vm.getStack().pop()); // cite + assertEquals(BstVM.TRUE, vm.getStack().pop()); // missing title + assertEquals("canh05", vm.getStack().pop()); // cite + assertEquals(BstVM.FALSE, vm.getStack().pop()); // missing title + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testNumNames() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { + "Johnny Foo { and } Mary Bar" num.names$ + "Johnny Foo and Mary Bar" num.names$ + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals(2, vm.getStack().pop()); + assertEquals(1, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testSubstring() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { + "123456789" #2 #1 substring$ % 2 + "123456789" #4 global.max$ substring$ % 456789 + "123456789" #1 #9 substring$ % 123456789 + "123456789" #1 #10 substring$ % 123456789 + "123456789" #1 #99 substring$ % 123456789 + "123456789" #-7 #3 substring$ % 123 + "123456789" #-1 #1 substring$ % 9 + "123456789" #-1 #3 substring$ % 789 + "123456789" #-2 #2 substring$ % 78 + } EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals("78", vm.getStack().pop()); + assertEquals("789", vm.getStack().pop()); + assertEquals("9", vm.getStack().pop()); + assertEquals("123", vm.getStack().pop()); + assertEquals("123456789", vm.getStack().pop()); + assertEquals("123456789", vm.getStack().pop()); + assertEquals("123456789", vm.getStack().pop()); + assertEquals("456789", vm.getStack().pop()); + assertEquals("2", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testEmpty() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { title } { } { } + READ + STRINGS { s } + FUNCTION { test } { + s empty$ % TRUE + "" empty$ % TRUE + " " empty$ % TRUE + title empty$ % TRUE + " HALLO " empty$ % FALSE + } + ITERATE { test } + """); + List testEntry = List.of(new BibEntry(StandardEntryType.Article)); + + vm.render(testEntry); + + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testFormatNameStatic() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { format }{ "Charles Louis Xavier Joseph de la Vall{\\'e}e Poussin" #1 "{vv~}{ll}{, jj}{, f}?" format.name$ } + EXECUTE { format } + """); + List v = Collections.emptyList(); + + vm.render(v); + + assertEquals("de~la Vall{\\'e}e~Poussin, C.~L. X.~J?", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testFormatNameInEntries() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { author } { } { } + FUNCTION { presort } { cite$ 'sort.key$ := } + ITERATE { presort } + READ + SORT + FUNCTION { format }{ author #2 "{vv~}{ll}{, jj}{, f}?" format.name$ } + ITERATE { format } + """); + List testEntries = List.of( + BstVMTest.defaultTestEntry(), + new BibEntry(StandardEntryType.Book) + .withCitationKey("test") + .withField(StandardField.AUTHOR, "Jonathan Meyer and Charles Louis Xavier Joseph de la Vall{\\'e}e Poussin")); + + vm.render(testEntries); + + assertEquals("de~la Vall{\\'e}e~Poussin, C.~L. X.~J?", vm.getStack().pop()); + assertEquals("Annabi, H?", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testChangeCase() throws RecognitionException { + BstVM vm = new BstVM(""" + STRINGS { title } + READ + FUNCTION { format.title } { + duplicate$ empty$ + { pop$ "" } + { "t" change.case$ } + if$ + } + FUNCTION { test } { + "hello world" "u" change.case$ format.title + "Hello World" format.title + "" format.title + "{A}{D}/{C}ycle: {I}{B}{M}'s {F}ramework for {A}pplication {D}evelopment and {C}ase" "u" change.case$ format.title + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals("{A}{D}/{C}ycle: {I}{B}{M}'s {F}ramework for {A}pplication {D}evelopment and {C}ase", + vm.getStack().pop()); + assertEquals("", vm.getStack().pop()); + assertEquals("Hello world", vm.getStack().pop()); + assertEquals("Hello world", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testTextLength() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { + "hello world" text.length$ % 11 + "Hello {W}orld" text.length$ % 11 + "" text.length$ % 0 + "{A}{D}/{Cycle}" text.length$ % 8 + "{\\This is one character}" text.length$ % 1 + "{\\This {is} {one} {c{h}}aracter as well}" text.length$ % 1 + "{\\And this too" text.length$ % 1 + "These are {\\11}" text.length$ % 11 + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals(11, vm.getStack().pop()); + assertEquals(1, vm.getStack().pop()); + assertEquals(1, vm.getStack().pop()); + assertEquals(1, vm.getStack().pop()); + assertEquals(8, vm.getStack().pop()); + assertEquals(0, vm.getStack().pop()); + assertEquals(11, vm.getStack().pop()); + assertEquals(11, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testIntToStr() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { #3 int.to.str$ #9999 int.to.str$ } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals("9999", vm.getStack().pop()); + assertEquals("3", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testChrToInt() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { "H" chr.to.int$ } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals(72, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testChrToIntIntToChr() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { "H" chr.to.int$ int.to.chr$ } + EXECUTE {test} + """); + + vm.render(Collections.emptyList()); + + assertEquals("H", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testType() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { } { } { } + FUNCTION { presort } { cite$ 'sort.key$ := } + ITERATE { presort } + SORT + FUNCTION { test } { type$ } + ITERATE { test } + """); + List testEntries = List.of( + new BibEntry(StandardEntryType.Article).withCitationKey("a"), + new BibEntry(StandardEntryType.Book).withCitationKey("b"), + new BibEntry(StandardEntryType.Misc).withCitationKey("c"), + new BibEntry(StandardEntryType.InProceedings).withCitationKey("d")); + + vm.render(testEntries); + + assertEquals("inproceedings", vm.getStack().pop()); + assertEquals("misc", vm.getStack().pop()); + assertEquals("book", vm.getStack().pop()); + assertEquals("article", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testCallType() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { title } { } { } + FUNCTION { presort } { cite$ 'sort.key$ := } + ITERATE { presort } + READ + SORT + FUNCTION { inproceedings }{ "InProceedings called on " title * } + FUNCTION { book }{ "Book called on " title * } + ITERATE { call.type$ } + """); + List testEntries = List.of( + BstVMTest.defaultTestEntry(), + new BibEntry(StandardEntryType.Book) + .withCitationKey("test") + .withField(StandardField.TITLE, "Test")); + + vm.render(testEntries); + + assertEquals("Book called on Test", vm.getStack().pop()); + assertEquals( + "InProceedings called on Effective work practices for floss development: A model and propositions", + vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testSwap() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { a } { #3 "Hallo" swap$ } + EXECUTE { a } + """); + + List v = Collections.emptyList(); + vm.render(v); + + assertEquals(3, vm.getStack().pop()); + assertEquals("Hallo", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + void testAssignFunction() { + BstVM vm = new BstVM(""" + INTEGERS { test.var } + FUNCTION { test.func } { #1 'test.var := } + EXECUTE { test.func } + """); + + vm.render(Collections.emptyList()); + + Map functions = vm.latestContext.functions(); + assertTrue(functions.containsKey("test.func")); + assertNotNull(functions.get("test.func")); + assertEquals(1, vm.latestContext.integers().get("test.var")); + } + + @Test + void testSimpleIf() { + BstVM vm = new BstVM(""" + FUNCTION { path1 } { #1 } + FUNCTION { path0 } { #0 } + FUNCTION { test } { + #1 path1 path0 if$ + #0 path1 path0 if$ + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals(0, vm.getStack().pop()); + assertEquals(1, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + void testSimpleWhile() { + BstVM vm = new BstVM(""" + INTEGERS { i } + FUNCTION { test } { + #3 'i := + { i } + { + i + i #1 - + 'i := + } + while$ + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals(1, vm.getStack().pop()); + assertEquals(2, vm.getStack().pop()); + assertEquals(3, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testNestedControlFunctions() throws RecognitionException { + BstVM vm = new BstVM(""" + STRINGS { t } + FUNCTION { not } { { #0 } { #1 } if$ } + FUNCTION { n.dashify } { + "HELLO-WORLD" 't := + "" + { t empty$ not } % while + { + t #1 #1 substring$ "-" = % if + { + t #1 #2 substring$ "--" = not % if + { + "--" * + t #2 global.max$ substring$ 't := + } + { + { t #1 #1 substring$ "-" = } % while + { + "-" * + t #2 global.max$ substring$ 't := + } + while$ + } + if$ + } + { + t #1 #1 substring$ * + t #2 global.max$ substring$ 't := + } + if$ + } + while$ + } + EXECUTE { n.dashify } + """); + List v = Collections.emptyList(); + + vm.render(v); + + assertEquals(1, vm.getStack().size()); + assertEquals("HELLO--WORLD", vm.getStack().pop()); + } + + @Test + public void testLogic() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { not } { { #0 } { #1 } if$ } + FUNCTION { and } { 'skip$ { pop$ #0 } if$ } + FUNCTION { or } { { pop$ #1 } 'skip$ if$ } + FUNCTION { test } { + #1 #1 and + #0 #1 and + #1 #0 and + #0 #0 and + #0 not + #1 not + #1 #1 or + #0 #1 or + #1 #0 or + #0 #0 or + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + /** + * See also {@link BstWidthCalculatorTest} + */ + + @Test + public void testWidth() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { address author title type } { } { label } + STRINGS { longest.label } + INTEGERS { number.label longest.label.width } + FUNCTION { initialize.longest.label } { + "" 'longest.label := + #1 'number.label := + #0 'longest.label.width := + } + FUNCTION {longest.label.pass} { + number.label int.to.str$ 'label := + number.label #1 + 'number.label := + label width$ longest.label.width > + { + label 'longest.label := + label width$ 'longest.label.width := + } + 'skip$ + if$ + } + EXECUTE { initialize.longest.label } + ITERATE { longest.label.pass } + FUNCTION { begin.bib } { + preamble$ empty$ + 'skip$ + { preamble$ write$ newline$ } + if$ + "\\begin{thebibliography}{" longest.label * "}" * + } + EXECUTE {begin.bib} + """); + + List testEntries = List.of(BstVMTest.defaultTestEntry()); + + vm.render(testEntries); + + assertTrue(vm.latestContext.integers().containsKey("longest.label.width")); + assertEquals("\\begin{thebibliography}{1}", vm.getStack().pop()); + } + + @Test + public void testDuplicateEmptyPopSwapIf() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { emphasize } { + duplicate$ empty$ + { pop$ "" } + { "{\\em " swap$ * "}" * } + if$ + } + FUNCTION { test } { + "" emphasize + "Hello" emphasize + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals("{\\em Hello}", vm.getStack().pop()); + assertEquals("", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testPreambleWriteNewlineQuote() { + BstVM vm = new BstVM(""" + FUNCTION { test } { + preamble$ + write$ + newline$ + "hello" + write$ + quote$ "quoted" * quote$ * + write$ + } + EXECUTE { test } + """); + + BibDatabase testDatabase = new BibDatabase(); + testDatabase.setPreamble("A Preamble"); + + String result = vm.render(Collections.emptyList(), testDatabase); + + assertEquals("A Preamble\nhello\"quoted\"", result); + } +} diff --git a/src/test/java/org/jabref/logic/bst/BstVMTest.java b/src/test/java/org/jabref/logic/bst/BstVMTest.java new file mode 100644 index 00000000000..4a66a8d5836 --- /dev/null +++ b/src/test/java/org/jabref/logic/bst/BstVMTest.java @@ -0,0 +1,220 @@ +package org.jabref.logic.bst; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.StandardEntryType; + +import org.antlr.v4.runtime.RecognitionException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BstVMTest { + + public static BibEntry defaultTestEntry() { + return new BibEntry(StandardEntryType.InProceedings) + .withCitationKey("canh05") + .withField(StandardField.AUTHOR, "Crowston, K. and Annabi, H. and Howison, J. and Masango, C.") + .withField(StandardField.TITLE, "Effective work practices for floss development: A model and propositions") + .withField(StandardField.BOOKTITLE, "Hawaii International Conference On System Sciences (HICSS)") + .withField(StandardField.YEAR, "2005") + .withField(StandardField.OWNER, "oezbek") + .withField(StandardField.TIMESTAMP, "2006.05.29") + .withField(StandardField.URL, "http://james.howison.name/publications.html"); + } + + @Test + public void testAbbrv() throws RecognitionException, IOException { + BstVM vm = new BstVM(Path.of("src/test/resources/org/jabref/logic/bst/abbrv.bst")); + List testEntries = List.of(defaultTestEntry()); + + String expected = "\\begin{thebibliography}{1}\\bibitem{canh05}K.~Crowston, H.~Annabi, J.~Howison, and C.~Masango.\\newblock Effective work practices for floss development: A model and propositions.\\newblock In {\\em Hawaii International Conference On System Sciences (HICSS)}, 2005.\\end{thebibliography}"; + String result = vm.render(testEntries); + + assertEquals( + expected.replaceAll("\\s", ""), + result.replaceAll("\\s", "")); + } + + @Test + public void testSimple() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { address author title type } { } { label } + INTEGERS { output.state before.all mid.sentence after.sentence after.block } + FUNCTION { init.state.consts }{ + #0 'before.all := + #1 'mid.sentence := + #2 'after.sentence := + #3 'after.block := + } + STRINGS { s t } + READ + """); + List testEntries = List.of(defaultTestEntry()); + + vm.render(testEntries); + + assertEquals(2, vm.latestContext.strings().size()); + assertEquals(7, vm.latestContext.integers().size()); + assertEquals(1, vm.latestContext.entries().size()); + assertEquals(5, vm.latestContext.entries().get(0).fields.size()); + assertEquals(38, vm.latestContext.functions().size()); + } + + @Test + public void testLabel() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { title } {} { label } + FUNCTION { test } { + label #0 = + title 'label := + #5 label #6 pop$ } + READ + ITERATE { test } + """); + List
- * We use null for the missing entry designator. - */ - private void read(BibDatabase bibDatabase) { - FieldWriter fieldWriter = new FieldWriter(new FieldWriterPreferences(true, List.of(StandardField.MONTH), new FieldContentFormatterPreferences())); - for (BstEntry e : entries) { - for (Map.Entry mEntry : e.fields.entrySet()) { - Field field = FieldFactory.parseField(mEntry.getKey()); - String fieldValue = e.entry.getResolvedFieldOrAlias(field, bibDatabase) - .map(content -> { - try { - String result = fieldWriter.write(field, content); - if (result.startsWith("{")) { - // Strip enclosing {} from the output - return result.substring(1, result.length() - 1); - } - if (field == StandardField.MONTH) { - // We don't have the internal BibTeX strings at hand. - // We nevertheless want to have the full month name. - // Thus, we lookup the full month name here. - return Month.parse(result) - .map(month -> month.getFullName()) - .orElse(result); - } - return result; - } catch (InvalidFieldValueException invalidFieldValueException) { - // in case there is something wrong with the content, just return the content itself - return content; - } - }) - .orElse(null); - mEntry.setValue(fieldValue); - } - } - - for (BstEntry e : entries) { - if (!e.fields.containsKey(StandardField.CROSSREF.getName())) { - e.fields.put(StandardField.CROSSREF.getName(), null); - } - } - } - - /** - * Defines a string macro. It has two arguments; the first is the macro's name, which is treated like any other - * variable or function name, and the second is its definition, which must be double-quote-delimited. You must have - * one for each three-letter month abbreviation; in addition, you should have one for common journal names. The - * user's database may override any definition you define using this command. If you want to define a string the - * user can't touch, use the FUNCTION command, which has a compatible syntax. - */ - private void macro(Tree child) { - String name = child.getChild(0).getText(); - String replacement = child.getChild(1).getText(); - functions.put(name, new MacroFunction(replacement)); - } - - public class MacroFunction implements BstFunction { - - private final String replacement; - - public MacroFunction(String replacement) { - this.replacement = replacement; - } - - @Override - public void execute(BstEntry context) { - VM.this.push(replacement); - } - } - - /** - * Declares the fields and entry variables. It has three arguments, each a (possibly empty) list of variable names. - * The three lists are of: fields, integer entry variables, and string entry variables. There is an additional field - * that BibTEX automatically declares, crossref, used for cross referencing. And there is an additional string entry - * variable automatically declared, sort.key$, used by the SORT command. Each of these variables has a value for - * each entry on the list. - */ - private void entry(Tree child) { - // Fields first - Tree t = child.getChild(0); - - for (int i = 0; i < t.getChildCount(); i++) { - String name = t.getChild(i).getText(); - - for (BstEntry entry : entries) { - entry.fields.put(name, null); - } - } - - // Integers - t = child.getChild(1); - - for (int i = 0; i < t.getChildCount(); i++) { - String name = t.getChild(i).getText(); - - for (BstEntry entry : entries) { - entry.localIntegers.put(name, 0); - } - } - // Strings - t = child.getChild(2); - - for (int i = 0; i < t.getChildCount(); i++) { - String name = t.getChild(i).getText(); - for (BstEntry entry : entries) { - entry.localStrings.put(name, null); - } - } - for (BstEntry entry : entries) { - entry.localStrings.put("sort.key$", null); - } - } - - private void reverse(Tree child) { - BstFunction f = functions.get(child.getChild(0).getText()); - - ListIterator i = entries.listIterator(entries.size()); - while (i.hasPrevious()) { - f.execute(i.previous()); - } - } - - private void iterate(Tree child) { - BstFunction f = functions.get(child.getChild(0).getText()); - - for (BstEntry entry : entries) { - f.execute(entry); - } - } - - /** - * Sorts the entry list using the values of the string entry variable sort.key$. It has no arguments. - */ - private void sort() { - entries.sort(Comparator.comparing(o -> (o.localStrings.get("sort.key$")))); - } - - private void executeInContext(Object o, BstEntry context) { - if (o instanceof Tree) { - Tree t = (Tree) o; - new StackFunction(t).execute(context); - } else if (o instanceof Identifier) { - execute(((Identifier) o).getName(), context); - } - } - - private void execute(Tree child) { - execute(child.getChild(0).getText(), null); - } - - public class StackFunction implements BstFunction { - - private final Tree localTree; - - public StackFunction(Tree stack) { - localTree = stack; - } - - public Tree getTree() { - return localTree; - } - - @Override - public void execute(BstEntry context) { - for (int i = 0; i < localTree.getChildCount(); i++) { - Tree c = localTree.getChild(i); - try { - - switch (c.getType()) { - case BstParser.STRING: - String s = c.getText(); - push(s.substring(1, s.length() - 1)); - break; - case BstParser.INTEGER: - push(Integer.parseInt(c.getText().substring(1))); - break; - case BstParser.QUOTED: - push(new Identifier(c.getText().substring(1))); - break; - case BstParser.STACK: - push(c); - break; - default: - VM.this.execute(c.getText(), context); - break; - } - } catch (VMException e) { - if (file == null) { - LOGGER.error("ERROR " + e.getMessage() + " (" + c.getLine() + ")"); - } else { - LOGGER.error("ERROR " + e.getMessage() + " (" + file.getPath() + ":" - + c.getLine() + ")"); - } - throw e; - } - } - } - } - - private void push(Tree t) { - stack.push(t); - } - - private void execute(String name, BstEntry context) { - if (context != null) { - if (context.fields.containsKey(name)) { - stack.push(context.fields.get(name)); - return; - } - if (context.localStrings.containsKey(name)) { - stack.push(context.localStrings.get(name)); - return; - } - if (context.localIntegers.containsKey(name)) { - stack.push(context.localIntegers.get(name)); - return; - } - } - if (strings.containsKey(name)) { - stack.push(strings.get(name)); - return; - } - if (integers.containsKey(name)) { - stack.push(integers.get(name)); - return; - } - - if (functions.containsKey(name)) { - // OK to have a null context - functions.get(name).execute(context); - return; - } - - throw new VMException("No matching identifier found: " + name); - } - - private void function(Tree child) { - String name = child.getChild(0).getText(); - Tree localStack = child.getChild(1); - functions.put(name, new StackFunction(localStack)); - } - - /** - * Declares global integer variables. It has one argument, a list of variable names. There are two such - * automatically-declared variables, entry.max$ and global.max$, used for limiting the lengths of string vari- - * ables. You may have any number of these commands, but a variable's declaration must precede its use. - */ - private void integers(Tree child) { - Tree t = child.getChild(0); - - for (int i = 0; i < t.getChildCount(); i++) { - String name = t.getChild(i).getText(); - integers.put(name, 0); - } - } - - /** - * Declares global string variables. It has one argument, a list of variable names. You may have any number of these - * commands, but a variable's declaration must precede its use. - * - * @param child - */ - private void strings(Tree child) { - Tree t = child.getChild(0); - - for (int i = 0; i < t.getChildCount(); i++) { - String name = t.getChild(i).getText(); - strings.put(name, null); - } - } - - public static class BstEntry { - - public final BibEntry entry; - - public final Map localStrings = new HashMap<>(); - - // keys filled by org.jabref.logic.bst.VM.entry based on the contents of the bst file - public final Map fields = new HashMap<>(); - - public final Map localIntegers = new HashMap<>(); - - public BstEntry(BibEntry e) { - this.entry = e; - } - } - - private void push(Integer integer) { - stack.push(integer); - } - - private void push(String string) { - stack.push(string); - } - - private void push(Identifier identifier) { - stack.push(identifier); - } - - public Map getStrings() { - return strings; - } - - public Map getIntegers() { - return integers; - } - - public List getEntries() { - return entries; - } - - public Map getFunctions() { - return functions; - } - - public Stack getStack() { - return stack; - } - - @Override - public void warn(String string) { - LOGGER.warn(string); - } -} diff --git a/src/main/java/org/jabref/logic/bst/VMException.java b/src/main/java/org/jabref/logic/bst/VMException.java deleted file mode 100644 index f46c5cd5277..00000000000 --- a/src/main/java/org/jabref/logic/bst/VMException.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.jabref.logic.bst; - -public class VMException extends RuntimeException { - - public VMException(String string) { - super(string); - } -} diff --git a/src/main/java/org/jabref/logic/bst/Warn.java b/src/main/java/org/jabref/logic/bst/Warn.java deleted file mode 100644 index 7a524ad9834..00000000000 --- a/src/main/java/org/jabref/logic/bst/Warn.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.jabref.logic.bst; - -@FunctionalInterface -public interface Warn { - - void warn(String s); -} diff --git a/src/main/java/org/jabref/logic/bst/WidthFunction.java b/src/main/java/org/jabref/logic/bst/WidthFunction.java deleted file mode 100644 index 06784267bf0..00000000000 --- a/src/main/java/org/jabref/logic/bst/WidthFunction.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.jabref.logic.bst; - -import java.util.Stack; - -import org.jabref.logic.bst.VM.BstEntry; -import org.jabref.logic.bst.VM.BstFunction; - -/** - * The |built_in| function {\.{width\$}} pops the top (string) literal and - * pushes the integer that represents its width in units specified by the - * |char_width| array. This function takes the literal literally; that is, it - * assumes each character in the string is to be printed as is, regardless of - * whether the character has a special meaning to \TeX, except that special - * characters (even without their |right_brace|s) are handled specially. If the - * literal isn't a string, it complains and pushes~0. - * - */ -public class WidthFunction implements BstFunction { - - private final VM vm; - - public WidthFunction(VM vm) { - this.vm = vm; - } - - @Override - public void execute(BstEntry context) { - Stack stack = vm.getStack(); - - if (stack.isEmpty()) { - throw new VMException("Not enough operands on stack for operation width$"); - } - Object o1 = stack.pop(); - - if (!(o1 instanceof String)) { - vm.warn("A string is needed for change.case$"); - stack.push(0); - return; - } - - stack.push(BibtexWidth.width((String) o1)); - } -} diff --git a/src/main/java/org/jabref/logic/bst/BibtexCaseChanger.java b/src/main/java/org/jabref/logic/bst/util/BstCaseChanger.java similarity index 83% rename from src/main/java/org/jabref/logic/bst/BibtexCaseChanger.java rename to src/main/java/org/jabref/logic/bst/util/BstCaseChanger.java index e05ce998e80..dd3e7c7b05c 100644 --- a/src/main/java/org/jabref/logic/bst/BibtexCaseChanger.java +++ b/src/main/java/org/jabref/logic/bst/util/BstCaseChanger.java @@ -1,4 +1,4 @@ -package org.jabref.logic.bst; +package org.jabref.logic.bst.util; import java.util.Locale; import java.util.Optional; @@ -6,9 +6,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public final class BibtexCaseChanger { +public final class BstCaseChanger { - private static final Logger LOGGER = LoggerFactory.getLogger(BibtexCaseChanger.class); + private static final Logger LOGGER = LoggerFactory.getLogger(BstCaseChanger.class); // stores whether the char before the current char was a colon private boolean prevColon = true; @@ -16,7 +16,7 @@ public final class BibtexCaseChanger { // global variable to store the current brace level private int braceLevel; - public enum FORMAT_MODE { + public enum FormatMode { // First character and character after a ":" as upper case - everything else in lower case. Obey {}. TITLE_LOWERS('t'), @@ -40,7 +40,7 @@ public enum FORMAT_MODE { private final char asChar; - FORMAT_MODE(char asChar) { + FormatMode(char asChar) { this.asChar = asChar; } @@ -53,17 +53,21 @@ public char asChar() { * * @throws IllegalArgumentException if char is not 't', 'l', 'u' */ - public static FORMAT_MODE getFormatModeForBSTFormat(final char bstFormat) { - for (FORMAT_MODE mode : FORMAT_MODE.values()) { + public static FormatMode of(final char bstFormat) { + for (FormatMode mode : FormatMode.values()) { if (mode.asChar == bstFormat) { return mode; } } throw new IllegalArgumentException(); } + + public static FormatMode of(final String bstFormat) { + return of(bstFormat.toLowerCase(Locale.ROOT).charAt(0)); + } } - private BibtexCaseChanger() { + private BstCaseChanger() { } /** @@ -72,11 +76,11 @@ private BibtexCaseChanger() { * @param s the string to handle * @param format the format */ - public static String changeCase(String s, FORMAT_MODE format) { - return (new BibtexCaseChanger()).doChangeCase(s, format); + public static String changeCase(String s, FormatMode format) { + return (new BstCaseChanger()).doChangeCase(s, format); } - private String doChangeCase(String s, FORMAT_MODE format) { + private String doChangeCase(String s, FormatMode format) { char[] c = s.toCharArray(); StringBuilder sb = new StringBuilder(); @@ -93,7 +97,7 @@ private String doChangeCase(String s, FORMAT_MODE format) { i++; continue; } - if ((format == FORMAT_MODE.TITLE_LOWERS) && ((i == 0) || (prevColon && Character.isWhitespace(c[i - 1])))) { + if ((format == FormatMode.TITLE_LOWERS) && ((i == 0) || (prevColon && Character.isWhitespace(c[i - 1])))) { sb.append('{'); i++; prevColon = false; @@ -136,12 +140,9 @@ private String doChangeCase(String s, FORMAT_MODE format) { * is other stuff, too, between braces, but it doesn't try to do anything * special with |colon|s. * - * @param c * @param start the current position. It points to the opening brace - * @param format - * @return */ - private int convertSpecialChar(StringBuilder sb, char[] c, int start, FORMAT_MODE format) { + private int convertSpecialChar(StringBuilder sb, char[] c, int start, FormatMode format) { int i = start; sb.append(c[i]); @@ -152,7 +153,7 @@ private int convertSpecialChar(StringBuilder sb, char[] c, int start, FORMAT_MOD i++; // skip over the |backslash| - Optional s = BibtexCaseChanger.findSpecialChar(c, i); + Optional s = BstCaseChanger.findSpecialChar(c, i); if (s.isPresent()) { i = convertAccented(c, i, s.get(), sb, format); } @@ -174,14 +175,9 @@ private int convertSpecialChar(StringBuilder sb, char[] c, int start, FORMAT_MOD * up) and append the result to the stringBuffer, return the updated * position. * - * @param c - * @param start - * @param s - * @param sb - * @param format * @return the new position */ - private int convertAccented(char[] c, int start, String s, StringBuilder sb, FORMAT_MODE format) { + private int convertAccented(char[] c, int start, String s, StringBuilder sb, FormatMode format) { int pos = start; pos += s.length(); @@ -214,29 +210,27 @@ private int convertAccented(char[] c, int start, String s, StringBuilder sb, FOR return pos; } - private int convertNonControl(char[] c, int start, StringBuilder sb, FORMAT_MODE format) { + private int convertNonControl(char[] c, int start, StringBuilder sb, FormatMode format) { int pos = start; switch (format) { - case TITLE_LOWERS: - case ALL_LOWERS: + case TITLE_LOWERS, ALL_LOWERS -> { sb.append(Character.toLowerCase(c[pos])); pos++; - break; - case ALL_UPPERS: + } + case ALL_UPPERS -> { sb.append(Character.toUpperCase(c[pos])); pos++; - break; - default: - LOGGER.info("convertNonControl - Unknown format: " + format); - break; + } + default -> + LOGGER.info("convertNonControl - Unknown format: " + format); } return pos; } - private int convertCharIfBraceLevelIsZero(char[] c, int start, StringBuilder sb, FORMAT_MODE format) { + private int convertCharIfBraceLevelIsZero(char[] c, int start, StringBuilder sb, FormatMode format) { int i = start; switch (format) { - case TITLE_LOWERS: + case TITLE_LOWERS -> { if ((i == 0) || (prevColon && Character.isWhitespace(c[i - 1]))) { sb.append(c[i]); } else { @@ -247,16 +241,13 @@ private int convertCharIfBraceLevelIsZero(char[] c, int start, StringBuilder sb, } else if (!Character.isWhitespace(c[i])) { prevColon = false; } - break; - case ALL_LOWERS: - sb.append(Character.toLowerCase(c[i])); - break; - case ALL_UPPERS: - sb.append(Character.toUpperCase(c[i])); - break; - default: - LOGGER.info("convertCharIfBraceLevelIsZero - Unknown format: " + format); - break; + } + case ALL_LOWERS -> + sb.append(Character.toLowerCase(c[i])); + case ALL_UPPERS -> + sb.append(Character.toUpperCase(c[i])); + default -> + LOGGER.info("convertCharIfBraceLevelIsZero - Unknown format: " + format); } i++; return i; diff --git a/src/main/java/org/jabref/logic/bst/BibtexNameFormatter.java b/src/main/java/org/jabref/logic/bst/util/BstNameFormatter.java similarity index 76% rename from src/main/java/org/jabref/logic/bst/BibtexNameFormatter.java rename to src/main/java/org/jabref/logic/bst/util/BstNameFormatter.java index aecf571bdf4..c4986dc12dd 100644 --- a/src/main/java/org/jabref/logic/bst/BibtexNameFormatter.java +++ b/src/main/java/org/jabref/logic/bst/util/BstNameFormatter.java @@ -1,13 +1,17 @@ -package org.jabref.logic.bst; +package org.jabref.logic.bst.util; import java.util.Arrays; import java.util.Locale; import java.util.Optional; import java.util.stream.Collectors; +import org.jabref.logic.bst.BstVMException; import org.jabref.model.entry.Author; import org.jabref.model.entry.AuthorList; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * From Bibtex: * @@ -24,9 +28,10 @@ * Sounds easy - is a nightmare... X-( * */ -public class BibtexNameFormatter { +public class BstNameFormatter { + private static final Logger LOGGER = LoggerFactory.getLogger(BstNameFormatter.class); - private BibtexNameFormatter() { + private BstNameFormatter() { } /** @@ -35,23 +40,18 @@ private BibtexNameFormatter() { * @param authorsNameList The string from an author field * @param whichName index of the list, starting with 1 * @param formatString TODO - * @param warn collects the warnings, may-be-null - * @return */ - public static String formatName(String authorsNameList, int whichName, String formatString, Warn warn) { + public static String formatName(String authorsNameList, int whichName, String formatString) { AuthorList al = AuthorList.parse(authorsNameList); if ((whichName < 1) && (whichName > al.getNumberOfAuthors())) { - warn.warn("AuthorList " + authorsNameList + " does not contain an author with number " + whichName); + LOGGER.warn("AuthorList {} does not contain an author with number {}", authorsNameList, whichName); return ""; } - return BibtexNameFormatter.formatName(al.getAuthor(whichName - 1), formatString, warn); + return BstNameFormatter.formatName(al.getAuthor(whichName - 1), formatString); } - /** - * @param warn collects the warnings, may-be-null - */ - public static String formatName(Author author, String format, Warn warn) { + public static String formatName(Author author, String format) { StringBuilder sb = new StringBuilder(); char[] c = format.toCharArray(); @@ -81,11 +81,7 @@ public static String formatName(Author author, String format, Warn warn) { } if ((braceLevel == 1) && Character.isLetter(c[i])) { if ("fvlj".indexOf(c[i]) == -1) { - if (warn != null) { - warn.warn( - "Format string in format.name$ may only contain fvlj on brace level 1 in group " - + group + ": " + format); - } + LOGGER.warn("Format string in format.name$ may only contain fvlj on brace level 1 in group {}: {}", group, format); } else { level1Chars.append(c[i]); } @@ -99,31 +95,26 @@ public static String formatName(Author author, String format, Warn warn) { continue; } - if ((control.length() > 2) && (warn != null)) { - warn.warn("Format string in format.name$ may only be one or two character long on brace level 1 in group " + group + ": " + format); + if ((control.length() > 2)) { + LOGGER.warn("Format string in format.name$ may only be one or two character long on brace level 1 in group {}: {}", group, format); } char type = control.charAt(0); - Optional tokenS; - switch (type) { - case 'f': - tokenS = author.getFirst(); - break; - case 'v': - tokenS = author.getVon(); - break; - case 'l': - tokenS = author.getLast(); - break; - case 'j': - tokenS = author.getJr(); - break; - default: - throw new VMException("Internal error"); - } - - if (!tokenS.isPresent()) { + Optional tokenS = switch (type) { + case 'f' -> + author.getFirst(); + case 'v' -> + author.getVon(); + case 'l' -> + author.getLast(); + case 'j' -> + author.getJr(); + default -> + throw new BstVMException("Internal error"); + }; + + if (tokenS.isEmpty()) { i++; continue; } @@ -135,9 +126,7 @@ public static String formatName(Author author, String format, Warn warn) { if (control.charAt(1) == control.charAt(0)) { abbreviateThatIsSingleLetter = false; } else { - if (warn != null) { - warn.warn("Format string in format.name$ may only contain one type of vlfj on brace level 1 in group " + group + ": " + format); - } + LOGGER.warn("Format string in format.name$ may only contain one type of vlfj on brace level 1 in group {}: {}", group, format); } } @@ -162,7 +151,7 @@ public static String formatName(Author author, String format, Warn warn) { } if (((j + 1) < d.length) && (d[j + 1] == '{')) { StringBuilder interTokenSb = new StringBuilder(); - j = BibtexNameFormatter.consumeToMatchingBrace(interTokenSb, d, j + 1); + j = BstNameFormatter.consumeToMatchingBrace(interTokenSb, d, j + 1); interToken = interTokenSb.substring(1, interTokenSb.length() - 1); } @@ -171,7 +160,7 @@ public static String formatName(Author author, String format, Warn warn) { if (abbreviateThatIsSingleLetter) { String[] dashes = token.split("-"); - token = Arrays.asList(dashes).stream().map(BibtexNameFormatter::getFirstCharOfString) + token = Arrays.stream(dashes).map(BstNameFormatter::getFirstCharOfString) .collect(Collectors.joining(".-")); } @@ -187,7 +176,7 @@ public static String formatName(Author author, String format, Warn warn) { // No clue what this means (What the hell are tokens anyway??? // if (lex_class[name_sep_char[cur_token]] = sep_char) then // append_ex_buf_char_and_check (name_sep_char[cur_token]) - if ((k == (tokens.length - 2)) || (BibtexNameFormatter.numberOfChars(sb.substring(groupStart, sb.length()), 3) < 3)) { + if ((k == (tokens.length - 2)) || (BstNameFormatter.numberOfChars(sb.substring(groupStart, sb.length()), 3) < 3)) { sb.append('~'); } else { sb.append(' '); @@ -212,7 +201,7 @@ public static String formatName(Author author, String format, Warn warn) { if (sb.length() > 0) { boolean noDisTie = false; if ((sb.charAt(sb.length() - 1) == '~') && - ((BibtexNameFormatter.numberOfChars(sb.substring(groupStart, sb.length()), 4) >= 4) || + ((BstNameFormatter.numberOfChars(sb.substring(groupStart, sb.length()), 4) >= 4) || ((sb.length() > 1) && (noDisTie = sb.charAt(sb.length() - 2) == '~')))) { sb.deleteCharAt(sb.length() - 1); if (!noDisTie) { @@ -221,16 +210,14 @@ public static String formatName(Author author, String format, Warn warn) { } } } else if (c[i] == '}') { - if (warn != null) { - warn.warn("Unmatched brace in format string: " + format); - } + LOGGER.warn("Unmatched brace in format string: {}", format); } else { sb.append(c[i]); // verbatim } i++; } - if ((braceLevel != 0) && (warn != null)) { - warn.warn("Unbalanced brace in format string for nameFormat: " + format); + if ((braceLevel != 0)) { + LOGGER.warn("Unbalanced brace in format string for nameFormat: {}", format); } return sb.toString(); @@ -268,7 +255,7 @@ public static String getFirstCharOfString(String s) { } if ((c[i] == '{') && ((i + 1) < c.length) && (c[i + 1] == '\\')) { StringBuilder sb = new StringBuilder(); - BibtexNameFormatter.consumeToMatchingBrace(sb, c, i); + BstNameFormatter.consumeToMatchingBrace(sb, c, i); return sb.toString(); } } diff --git a/src/main/java/org/jabref/logic/bst/BibtexPurify.java b/src/main/java/org/jabref/logic/bst/util/BstPurifier.java similarity index 77% rename from src/main/java/org/jabref/logic/bst/BibtexPurify.java rename to src/main/java/org/jabref/logic/bst/util/BstPurifier.java index 08cb14db207..90818a0749f 100644 --- a/src/main/java/org/jabref/logic/bst/BibtexPurify.java +++ b/src/main/java/org/jabref/logic/bst/util/BstPurifier.java @@ -1,4 +1,7 @@ -package org.jabref.logic.bst; +package org.jabref.logic.bst.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * @@ -10,17 +13,13 @@ * pushes the null string. * */ -public class BibtexPurify { +public class BstPurifier { + private static final Logger LOGGER = LoggerFactory.getLogger(BstPurifier.class); - private BibtexPurify() { + private BstPurifier() { } - /** - * @param toPurify - * @param warn may-be-null - * @return - */ - public static String purify(String toPurify, Warn warn) { + public static String purify(String toPurify) { StringBuilder sb = new StringBuilder(); char[] cs = toPurify.toCharArray(); @@ -41,7 +40,7 @@ public static String purify(String toPurify, Warn warn) { i++; // skip brace while ((i < n) && (braceLevel > 0)) { i++; // skip backslash - BibtexCaseChanger.findSpecialChar(cs, i).ifPresent(sb::append); + BstCaseChanger.findSpecialChar(cs, i).ifPresent(sb::append); while ((i < n) && Character.isLetter(cs[i])) { i++; @@ -63,15 +62,13 @@ public static String purify(String toPurify, Warn warn) { if (braceLevel > 0) { braceLevel--; } else { - if (warn != null) { - warn.warn("Unbalanced brace in string for purify$: " + toPurify); - } + LOGGER.warn("Unbalanced brace in string for purify$: {}", toPurify); } } i++; } - if ((braceLevel != 0) && (warn != null)) { - warn.warn("Unbalanced brace in string for purify$: " + toPurify); + if ((braceLevel != 0)) { + LOGGER.warn("Unbalanced brace in string for purify$: {}", toPurify); } return sb.toString(); diff --git a/src/main/java/org/jabref/logic/bst/BibtexTextPrefix.java b/src/main/java/org/jabref/logic/bst/util/BstTextPrefixer.java similarity index 84% rename from src/main/java/org/jabref/logic/bst/BibtexTextPrefix.java rename to src/main/java/org/jabref/logic/bst/util/BstTextPrefixer.java index 721838e29fe..31cbc71de9b 100644 --- a/src/main/java/org/jabref/logic/bst/BibtexTextPrefix.java +++ b/src/main/java/org/jabref/logic/bst/util/BstTextPrefixer.java @@ -1,4 +1,7 @@ -package org.jabref.logic.bst; +package org.jabref.logic.bst.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The |built_in| function {\.{text.prefix\$}} pops the top two literals (the @@ -14,15 +17,13 @@ * complains and pushes the null string. * */ -public class BibtexTextPrefix { +public class BstTextPrefixer { + private static final Logger LOGGER = LoggerFactory.getLogger(BstTextPrefixer.class); - private BibtexTextPrefix() { + private BstTextPrefixer() { } - /** - * @param warn may-be-null - */ - public static String textPrefix(int inNumOfChars, String toPrefix, Warn warn) { + public static String textPrefix(int inNumOfChars, String toPrefix) { int numOfChars = inNumOfChars; StringBuilder sb = new StringBuilder(); @@ -53,15 +54,13 @@ public static String textPrefix(int inNumOfChars, String toPrefix, Warn warn) { if (braceLevel > 0) { braceLevel--; } else { - if (warn != null) { - warn.warn("Unbalanced brace in string for purify$: " + toPrefix); - } + LOGGER.warn("Unbalanced brace in string for purify$: {}", toPrefix); } } else { numOfChars--; } } - sb.append(toPrefix.substring(0, i)); + sb.append(toPrefix, 0, i); while (braceLevel > 0) { sb.append('}'); braceLevel--; diff --git a/src/main/java/org/jabref/logic/bst/util/BstWidthCalculator.java b/src/main/java/org/jabref/logic/bst/util/BstWidthCalculator.java new file mode 100644 index 00000000000..c286fe1497b --- /dev/null +++ b/src/main/java/org/jabref/logic/bst/util/BstWidthCalculator.java @@ -0,0 +1,241 @@ +package org.jabref.logic.bst.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * The |built_in| function {\.{purify\$}} pops the top (string) literal, removes + * nonalphanumeric characters except for |white_space| and |sep_char| characters + * (these get converted to a |space|) and removes certain alphabetic characters + * contained in the control sequences associated with a special character, and + * pushes the resulting string. If the literal isn't a string, it complains and + * pushes the null string. + * + */ +public class BstWidthCalculator { + + private static final Logger LOGGER = LoggerFactory.getLogger(BstWidthCalculator.class); + + /* + * Quoted from Bibtex: + * + * Now we initialize the system-dependent |char_width| array, for which + * |space| is the only |white_space| character given a nonzero printing + * width. The widths here are taken from Stanford's June~'87 $cmr10$~font + * and represent hundredths of a point (rounded), but since they're used + * only for relative comparisons, the units have no meaning. + */ + + private static int[] widths; + + static { + if (BstWidthCalculator.widths == null) { + BstWidthCalculator.widths = new int[128]; + + for (int i = 0; i < 128; i++) { + BstWidthCalculator.widths[i] = 0; + } + BstWidthCalculator.widths[32] = 278; + BstWidthCalculator.widths[33] = 278; + BstWidthCalculator.widths[34] = 500; + BstWidthCalculator.widths[35] = 833; + BstWidthCalculator.widths[36] = 500; + BstWidthCalculator.widths[37] = 833; + BstWidthCalculator.widths[38] = 778; + BstWidthCalculator.widths[39] = 278; + BstWidthCalculator.widths[40] = 389; + BstWidthCalculator.widths[41] = 389; + BstWidthCalculator.widths[42] = 500; + BstWidthCalculator.widths[43] = 778; + BstWidthCalculator.widths[44] = 278; + BstWidthCalculator.widths[45] = 333; + BstWidthCalculator.widths[46] = 278; + BstWidthCalculator.widths[47] = 500; + BstWidthCalculator.widths[48] = 500; + BstWidthCalculator.widths[49] = 500; + BstWidthCalculator.widths[50] = 500; + BstWidthCalculator.widths[51] = 500; + BstWidthCalculator.widths[52] = 500; + BstWidthCalculator.widths[53] = 500; + BstWidthCalculator.widths[54] = 500; + BstWidthCalculator.widths[55] = 500; + BstWidthCalculator.widths[56] = 500; + BstWidthCalculator.widths[57] = 500; + BstWidthCalculator.widths[58] = 278; + BstWidthCalculator.widths[59] = 278; + BstWidthCalculator.widths[60] = 278; + BstWidthCalculator.widths[61] = 778; + BstWidthCalculator.widths[62] = 472; + BstWidthCalculator.widths[63] = 472; + BstWidthCalculator.widths[64] = 778; + BstWidthCalculator.widths[65] = 750; + BstWidthCalculator.widths[66] = 708; + BstWidthCalculator.widths[67] = 722; + BstWidthCalculator.widths[68] = 764; + BstWidthCalculator.widths[69] = 681; + BstWidthCalculator.widths[70] = 653; + BstWidthCalculator.widths[71] = 785; + BstWidthCalculator.widths[72] = 750; + BstWidthCalculator.widths[73] = 361; + BstWidthCalculator.widths[74] = 514; + BstWidthCalculator.widths[75] = 778; + BstWidthCalculator.widths[76] = 625; + BstWidthCalculator.widths[77] = 917; + BstWidthCalculator.widths[78] = 750; + BstWidthCalculator.widths[79] = 778; + BstWidthCalculator.widths[80] = 681; + BstWidthCalculator.widths[81] = 778; + BstWidthCalculator.widths[82] = 736; + BstWidthCalculator.widths[83] = 556; + BstWidthCalculator.widths[84] = 722; + BstWidthCalculator.widths[85] = 750; + BstWidthCalculator.widths[86] = 750; + BstWidthCalculator.widths[87] = 1028; + BstWidthCalculator.widths[88] = 750; + BstWidthCalculator.widths[89] = 750; + BstWidthCalculator.widths[90] = 611; + BstWidthCalculator.widths[91] = 278; + BstWidthCalculator.widths[92] = 500; + BstWidthCalculator.widths[93] = 278; + BstWidthCalculator.widths[94] = 500; + BstWidthCalculator.widths[95] = 278; + BstWidthCalculator.widths[96] = 278; + BstWidthCalculator.widths[97] = 500; + BstWidthCalculator.widths[98] = 556; + BstWidthCalculator.widths[99] = 444; + BstWidthCalculator.widths[100] = 556; + BstWidthCalculator.widths[101] = 444; + BstWidthCalculator.widths[102] = 306; + BstWidthCalculator.widths[103] = 500; + BstWidthCalculator.widths[104] = 556; + BstWidthCalculator.widths[105] = 278; + BstWidthCalculator.widths[106] = 306; + BstWidthCalculator.widths[107] = 528; + BstWidthCalculator.widths[108] = 278; + BstWidthCalculator.widths[109] = 833; + BstWidthCalculator.widths[110] = 556; + BstWidthCalculator.widths[111] = 500; + BstWidthCalculator.widths[112] = 556; + BstWidthCalculator.widths[113] = 528; + BstWidthCalculator.widths[114] = 392; + BstWidthCalculator.widths[115] = 394; + BstWidthCalculator.widths[116] = 389; + BstWidthCalculator.widths[117] = 556; + BstWidthCalculator.widths[118] = 528; + BstWidthCalculator.widths[119] = 722; + BstWidthCalculator.widths[120] = 528; + BstWidthCalculator.widths[121] = 528; + BstWidthCalculator.widths[122] = 444; + BstWidthCalculator.widths[123] = 500; + BstWidthCalculator.widths[124] = 1000; + BstWidthCalculator.widths[125] = 500; + BstWidthCalculator.widths[126] = 500; + } + } + + private BstWidthCalculator() { + } + + private static int getSpecialCharWidth(char[] c, int pos) { + if ((pos + 1) < c.length) { + if ((c[pos] == 'o') && (c[pos + 1] == 'e')) { + return 778; + } + if ((c[pos] == 'O') && (c[pos + 1] == 'E')) { + return 1014; + } + if ((c[pos] == 'a') && (c[pos + 1] == 'e')) { + return 722; + } + if ((c[pos] == 'A') && (c[pos + 1] == 'E')) { + return 903; + } + if ((c[pos] == 's') && (c[pos + 1] == 's')) { + return 500; + } + } + return BstWidthCalculator.getCharWidth(c[pos]); + } + + public static int getCharWidth(char c) { + if ((c >= 0) && (c < 128)) { + return BstWidthCalculator.widths[c]; + } else { + return 0; + } + } + + public static int width(String toMeasure) { + /* + * From Bibtex: We use the natural width for all but special characters, + * and we complain if the string isn't brace-balanced. + */ + + int i = 0; + int n = toMeasure.length(); + int braceLevel = 0; + char[] c = toMeasure.toCharArray(); + int result = 0; + + /* + * From Bibtex: + * + * We use the natural widths of all characters except that some + * characters have no width: braces, control sequences (except for the + * usual 13 accented and foreign characters, whose widths are given in + * the next module), and |white_space| following control sequences (even + * a null control sequence). + * + */ + while (i < n) { + if (c[i] == '{') { + braceLevel++; + if ((braceLevel == 1) && ((i + 1) < n) && (c[i + 1] == '\\')) { + i++; // skip brace + while ((i < n) && (braceLevel > 0)) { + i++; // skip backslash + + int afterBackslash = i; + while ((i < n) && Character.isLetter(c[i])) { + i++; + } + if ((i < n) && (i == afterBackslash)) { + i++; // Skip non-alpha control seq + } else { + if (BstCaseChanger.findSpecialChar(c, afterBackslash).isPresent()) { + result += BstWidthCalculator.getSpecialCharWidth(c, afterBackslash); + } + } + while ((i < n) && Character.isWhitespace(c[i])) { + i++; + } + while ((i < n) && (braceLevel > 0) && (c[i] != '\\')) { + if (c[i] == '}') { + braceLevel--; + } else if (c[i] == '{') { + braceLevel++; + } else { + result += BstWidthCalculator.getCharWidth(c[i]); + } + i++; + } + } + continue; + } + } else if (c[i] == '}') { + if (braceLevel > 0) { + braceLevel--; + } else { + LOGGER.warn("Too many closing braces in string: " + toMeasure); + } + } + result += BstWidthCalculator.getCharWidth(c[i]); + i++; + } + if (braceLevel > 0) { + LOGGER.warn("No enough closing braces in string: " + toMeasure); + } + return result; + } +} diff --git a/src/main/java/org/jabref/logic/importer/FetcherClientException.java b/src/main/java/org/jabref/logic/importer/FetcherClientException.java new file mode 100644 index 00000000000..8de9d0d6114 --- /dev/null +++ b/src/main/java/org/jabref/logic/importer/FetcherClientException.java @@ -0,0 +1,19 @@ +package org.jabref.logic.importer; + +/** + * Should be thrown when you encounter a http status code error >= 400 and < 500 + */ +public class FetcherClientException extends FetcherException { + + public FetcherClientException(String errorMessage, Throwable cause) { + super(errorMessage, cause); + } + + public FetcherClientException(String errorMessage) { + super(errorMessage); + } + + public FetcherClientException(String errorMessage, String localizedMessage, Throwable cause) { + super(errorMessage, localizedMessage, cause); + } +} diff --git a/src/main/java/org/jabref/logic/importer/FetcherServerException.java b/src/main/java/org/jabref/logic/importer/FetcherServerException.java new file mode 100644 index 00000000000..537d71ff530 --- /dev/null +++ b/src/main/java/org/jabref/logic/importer/FetcherServerException.java @@ -0,0 +1,18 @@ +package org.jabref.logic.importer; +/** + * Should be thrown when you encounter a http status code error >= 500 + */ +public class FetcherServerException extends FetcherException { + + public FetcherServerException(String errorMessage, Throwable cause) { + super(errorMessage, cause); + } + + public FetcherServerException(String errorMessage) { + super(errorMessage); + } + + public FetcherServerException(String errorMessage, String localizedMessage, Throwable cause) { + super(errorMessage, localizedMessage, cause); + } +} diff --git a/src/main/java/org/jabref/logic/importer/IdBasedParserFetcher.java b/src/main/java/org/jabref/logic/importer/IdBasedParserFetcher.java index 6be4a8dddfd..e348b69b8d8 100644 --- a/src/main/java/org/jabref/logic/importer/IdBasedParserFetcher.java +++ b/src/main/java/org/jabref/logic/importer/IdBasedParserFetcher.java @@ -81,7 +81,10 @@ default Optional performSearchById(String identifier) throws FetcherEx } catch (URISyntaxException e) { throw new FetcherException("Search URI is malformed", e); } catch (IOException e) { - // TODO: Catch HTTP Response 401 errors and report that user has no rights to access resource. It might be that there is an UnknownHostException (eutils.ncbi.nlm.nih.gov cannot be resolved). + // check for the case where we already have a FetcherException from UrlDownload + if (e.getCause() instanceof FetcherException fe) { + throw fe; + } throw new FetcherException("A network error occurred", e); } catch (ParseException e) { throw new FetcherException("An internal parser error occurred", e); diff --git a/src/main/java/org/jabref/logic/importer/IdParserFetcher.java b/src/main/java/org/jabref/logic/importer/IdParserFetcher.java index bf4b2bb32b9..b9b5ed92e44 100644 --- a/src/main/java/org/jabref/logic/importer/IdParserFetcher.java +++ b/src/main/java/org/jabref/logic/importer/IdParserFetcher.java @@ -88,8 +88,10 @@ default Optional findIdentifier(BibEntry entry) throws FetcherException { LOGGER.debug("Id not found"); return Optional.empty(); } catch (IOException e) { - // TODO: Catch HTTP Response 401 errors and report that user has no rights to access resource - // TODO catch 503 service unavailable and alert user + // check for the case where we already have a FetcherException from UrlDownload + if (e.getCause() instanceof FetcherException fe) { + throw fe; + } throw new FetcherException("An I/O exception occurred", e); } catch (ParseException e) { throw new FetcherException("An internal parser error occurred", e); diff --git a/src/main/java/org/jabref/logic/importer/ImportFormatReader.java b/src/main/java/org/jabref/logic/importer/ImportFormatReader.java index f3b8826171d..b9b6f4b850c 100644 --- a/src/main/java/org/jabref/logic/importer/ImportFormatReader.java +++ b/src/main/java/org/jabref/logic/importer/ImportFormatReader.java @@ -39,7 +39,6 @@ import org.jabref.model.entry.BibEntry; import org.jabref.model.strings.StringUtil; import org.jabref.model.util.FileUpdateMonitor; -import org.jabref.preferences.GeneralPreferences; public class ImportFormatReader { @@ -51,11 +50,9 @@ public class ImportFormatReader { */ private final List formats = new ArrayList<>(); - private GeneralPreferences generalPreferences; private ImportFormatPreferences importFormatPreferences; - public void resetImportFormats(ImporterPreferences importerPreferences, GeneralPreferences generalPreferences, ImportFormatPreferences newImportFormatPreferences, XmpPreferences xmpPreferences, FileUpdateMonitor fileMonitor) { - this.generalPreferences = generalPreferences; + public void resetImportFormats(ImporterPreferences importerPreferences, ImportFormatPreferences newImportFormatPreferences, XmpPreferences xmpPreferences, FileUpdateMonitor fileMonitor) { this.importFormatPreferences = newImportFormatPreferences; formats.clear(); diff --git a/src/main/java/org/jabref/logic/importer/PagedSearchBasedFetcher.java b/src/main/java/org/jabref/logic/importer/PagedSearchBasedFetcher.java index 1b8d31a064b..bafae176485 100644 --- a/src/main/java/org/jabref/logic/importer/PagedSearchBasedFetcher.java +++ b/src/main/java/org/jabref/logic/importer/PagedSearchBasedFetcher.java @@ -52,6 +52,7 @@ default int getPageSize() { * @param luceneQuery the root node of the lucene query * @return a list of {@link BibEntry}, which are matched by the query (may be empty) */ + @Override default List performSearch(QueryNode luceneQuery) throws FetcherException { return new ArrayList<>(performSearchPaged(luceneQuery, 0).getContent()); } diff --git a/src/main/java/org/jabref/logic/importer/fetcher/DoiFetcher.java b/src/main/java/org/jabref/logic/importer/fetcher/DoiFetcher.java index 41afea25707..4f0927ef468 100644 --- a/src/main/java/org/jabref/logic/importer/fetcher/DoiFetcher.java +++ b/src/main/java/org/jabref/logic/importer/fetcher/DoiFetcher.java @@ -72,7 +72,6 @@ public Optional performSearchById(String identifier) throws FetcherExc return new Medra().performSearchById(identifier); } URL doiURL = new URL(doi.get().getURIAsASCIIString()); - // BibTeX data URLDownload download = getUrlDownload(doiURL); download.addHeader("Accept", MediaTypes.APPLICATION_BIBTEX); @@ -80,8 +79,11 @@ public Optional performSearchById(String identifier) throws FetcherExc try { bibtexString = download.asString(); } catch (IOException e) { - // an IOException will be thrown if download is unable to download from the doiURL - throw new FetcherException(Localization.lang("No DOI data exists"), e); + // an IOException with a nested FetcherException will be thrown when you encounter a 400x or 500x http status code + if (e.getCause() instanceof FetcherException fe) { + throw fe; + } + throw e; } // BibTeX entry diff --git a/src/main/java/org/jabref/logic/importer/fetcher/IsbnFetcher.java b/src/main/java/org/jabref/logic/importer/fetcher/IsbnFetcher.java index c97184bd36b..f4dedae3e7c 100644 --- a/src/main/java/org/jabref/logic/importer/fetcher/IsbnFetcher.java +++ b/src/main/java/org/jabref/logic/importer/fetcher/IsbnFetcher.java @@ -50,13 +50,19 @@ public Optional performSearchById(String identifier) throws FetcherExc identifier = NEWLINE_SPACE_PATTERN.matcher(identifier).replaceAll(""); OpenLibraryFetcher openLibraryFetcher = new OpenLibraryFetcher(importFormatPreferences); - Optional bibEntry = openLibraryFetcher.performSearchById(identifier); - // nothing found at OpenLibrary: try ebook.de - if (!bibEntry.isPresent()) { - LOGGER.debug("No entry found at OpenLibrary; trying ebook.de"); - IsbnViaEbookDeFetcher isbnViaEbookDeFetcher = new IsbnViaEbookDeFetcher(importFormatPreferences); - bibEntry = isbnViaEbookDeFetcher.performSearchById(identifier); + Optional bibEntry = Optional.empty(); + try { + bibEntry = openLibraryFetcher.performSearchById(identifier); + } catch (FetcherException ex) { + LOGGER.debug("Got a fetcher exception for IBSN search", ex); + } finally { + // nothing found at OpenLibrary: try ebook.de + if (!bibEntry.isPresent()) { + LOGGER.debug("No entry found at OpenLibrary; trying ebook.de"); + IsbnViaEbookDeFetcher isbnViaEbookDeFetcher = new IsbnViaEbookDeFetcher(importFormatPreferences); + bibEntry = isbnViaEbookDeFetcher.performSearchById(identifier); + } } return bibEntry; diff --git a/src/main/java/org/jabref/logic/importer/fileformat/BibTeXMLImporter.java b/src/main/java/org/jabref/logic/importer/fileformat/BibTeXMLImporter.java index 5fb3d8ed900..65301e2a7a1 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/BibTeXMLImporter.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/BibTeXMLImporter.java @@ -184,7 +184,7 @@ private void parse(T entryType, Map fields) { } else if (isMethodToIgnore(method.getName())) { continue; } else if (method.getName().startsWith("get")) { - putIfValueNotNull(fields, FieldFactory.parseField(method.getName().replace("get", "")), (String) method.invoke(entryType)); + putIfValueNotNull(fields, FieldFactory.parseField(entryType, method.getName().replace("get", "")), (String) method.invoke(entryType)); } } catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException e) { LOGGER.error("Could not invoke method", e); diff --git a/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java b/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java index f3e6e7462f5..666ae01a7fb 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java @@ -387,8 +387,8 @@ private String purge(String context, String stringToPurge) { } // strip empty lines while ((runningIndex < indexOfAt) && - (context.charAt(runningIndex) == '\r' || - context.charAt(runningIndex) == '\n')) { + ((context.charAt(runningIndex) == '\r') || + (context.charAt(runningIndex) == '\n'))) { runningIndex++; } return context.substring(runningIndex); diff --git a/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java b/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java index 2ff451e6dca..afbe5af657d 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/CffImporter.java @@ -14,6 +14,7 @@ import org.jabref.model.entry.Author; import org.jabref.model.entry.AuthorList; import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.BiblatexSoftwareField; import org.jabref.model.entry.field.Field; import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.field.UnknownField; @@ -95,7 +96,7 @@ public ParserResult importDatabase(BufferedReader reader) throws IOException { StandardEntryType entryType = StandardEntryType.Software; // Map CFF fields to JabRef Fields - HashMap fieldMap = getFieldMappings(); + HashMap fieldMap = getFieldMappings(); for (Map.Entry property : citation.values.entrySet()) { if (fieldMap.containsKey(property.getKey())) { entryMap.put(fieldMap.get(property.getKey()), property.getValue()); @@ -120,7 +121,7 @@ public ParserResult importDatabase(BufferedReader reader) throws IOException { entryMap.put(StandardField.AUTHOR, authorStr); // Select DOI to keep - if (entryMap.get(StandardField.DOI) == null && citation.ids != null) { + if ((entryMap.get(StandardField.DOI) == null) && (citation.ids != null)) { List doiIds = citation.ids.stream() .filter(id -> id.type.equals("doi")) .collect(Collectors.toList()); @@ -137,14 +138,14 @@ public ParserResult importDatabase(BufferedReader reader) throws IOException { .collect(Collectors.toList()); if (swhIds.size() == 1) { - entryMap.put(StandardField.SWHID, swhIds.get(0)); + entryMap.put(BiblatexSoftwareField.SWHID, swhIds.get(0)); } else if (swhIds.size() > 1) { List relSwhIds = swhIds.stream() .filter(id -> id.split(":").length > 3) // quick filter for invalid swhids .filter(id -> id.split(":")[2].equals("rel")) .collect(Collectors.toList()); if (relSwhIds.size() == 1) { - entryMap.put(StandardField.SWHID, relSwhIds.get(0)); + entryMap.put(BiblatexSoftwareField.SWHID, relSwhIds.get(0)); } } } @@ -166,19 +167,19 @@ public boolean isRecognizedFormat(BufferedReader reader) throws IOException { try { citation = mapper.readValue(reader, CffFormat.class); - return citation != null && citation.values.get("title") != null; + return (citation != null) && (citation.values.get("title") != null); } catch (IOException e) { return false; } } - private HashMap getFieldMappings() { - HashMap fieldMappings = new HashMap<>(); + private HashMap getFieldMappings() { + HashMap fieldMappings = new HashMap<>(); fieldMappings.put("title", StandardField.TITLE); fieldMappings.put("version", StandardField.VERSION); fieldMappings.put("doi", StandardField.DOI); - fieldMappings.put("license", StandardField.LICENSE); - fieldMappings.put("repository", StandardField.REPOSITORY); + fieldMappings.put("license", BiblatexSoftwareField.LICENSE); + fieldMappings.put("repository", BiblatexSoftwareField.REPOSITORY); fieldMappings.put("url", StandardField.URL); fieldMappings.put("abstract", StandardField.ABSTRACT); fieldMappings.put("message", StandardField.COMMENT); diff --git a/src/main/java/org/jabref/logic/importer/fileformat/CopacImporter.java b/src/main/java/org/jabref/logic/importer/fileformat/CopacImporter.java index ab857becd0a..ab638ae4cf3 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/CopacImporter.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/CopacImporter.java @@ -137,7 +137,7 @@ public ParserResult importDatabase(BufferedReader reader) throws IOException { } else if ("DT- ".equals(code)) { setOrAppend(b, new UnknownField("documenttype"), line.substring(4).trim(), ", "); } else { - setOrAppend(b, FieldFactory.parseField(code.substring(0, 2)), line.substring(4).trim(), ", "); + setOrAppend(b, FieldFactory.parseField(StandardEntryType.Book, line.substring(0, 2)), line.substring(4).trim(), ", "); } } results.add(b); diff --git a/src/main/java/org/jabref/logic/importer/fileformat/IsiImporter.java b/src/main/java/org/jabref/logic/importer/fileformat/IsiImporter.java index b502977f716..2845756b770 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/IsiImporter.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/IsiImporter.java @@ -281,7 +281,7 @@ public ParserResult importDatabase(BufferedReader reader) throws IOException { if ("ER".equals(beg) || "EF".equals(beg) || "VR".equals(beg) || "FN".equals(beg)) { continue; } - hm.put(FieldFactory.parseField(beg), value); + hm.put(FieldFactory.parseField(type, beg), value); } } diff --git a/src/main/java/org/jabref/logic/importer/fileformat/MedlineImporter.java b/src/main/java/org/jabref/logic/importer/fileformat/MedlineImporter.java index 392fad9772b..b9e9eac39d4 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/MedlineImporter.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/MedlineImporter.java @@ -425,7 +425,7 @@ private void addArticleIdList(Map fields, ArticleIdList articleId if ("pubmed".equals(id.getIdType())) { fields.put(StandardField.PMID, id.getContent()); } else { - fields.put(FieldFactory.parseField(id.getIdType()), id.getContent()); + fields.put(FieldFactory.parseField(StandardEntryType.Article, id.getIdType()), id.getContent()); } } } @@ -499,7 +499,7 @@ private void addKeyWords(Map fields, List allKeyword private void addOtherId(Map fields, List otherID) { for (OtherID id : otherID) { if ((id.getSource() != null) && (id.getContent() != null)) { - fields.put(FieldFactory.parseField(id.getSource()), id.getContent()); + fields.put(FieldFactory.parseField(StandardEntryType.Article, id.getSource()), id.getContent()); } } } diff --git a/src/main/java/org/jabref/logic/importer/util/MetaDataParser.java b/src/main/java/org/jabref/logic/importer/util/MetaDataParser.java index eb1e905adca..5f3d5e29c52 100644 --- a/src/main/java/org/jabref/logic/importer/util/MetaDataParser.java +++ b/src/main/java/org/jabref/logic/importer/util/MetaDataParser.java @@ -65,6 +65,7 @@ public MetaData parse(MetaData metaData, Map data, Character key String user = entry.getKey().substring(MetaData.FILE_DIRECTORY.length() + 1); metaData.setUserFileDirectory(user, getSingleItem(value)); } else if (entry.getKey().startsWith(MetaData.SELECTOR_META_PREFIX)) { + // edge case, it might be one special field e.g. article from biblatex-apa, but we can't distinguish this from any other field and rather prefer to handle it as UnknownField metaData.addContentSelector(ContentSelectors.parse(FieldFactory.parseField(entry.getKey().substring(MetaData.SELECTOR_META_PREFIX.length())), StringUtil.unquote(entry.getValue(), MetaData.ESCAPE_CHARACTER))); } else if (entry.getKey().startsWith(MetaData.FILE_DIRECTORY + "Latex-")) { // The user name comes directly after "FILE_DIRECTORYLatex-" diff --git a/src/main/java/org/jabref/logic/layout/format/NameFormatter.java b/src/main/java/org/jabref/logic/layout/format/NameFormatter.java index 919ff4df2d5..e8812235435 100644 --- a/src/main/java/org/jabref/logic/layout/format/NameFormatter.java +++ b/src/main/java/org/jabref/logic/layout/format/NameFormatter.java @@ -5,7 +5,7 @@ import java.util.Map; import java.util.Objects; -import org.jabref.logic.bst.BibtexNameFormatter; +import org.jabref.logic.bst.util.BstNameFormatter; import org.jabref.logic.layout.LayoutFormatter; import org.jabref.model.entry.AuthorList; @@ -86,7 +86,7 @@ private static String format(String toFormat, AuthorList al, String[] formats) { for (int i = 1; i <= al.getNumberOfAuthors(); i++) { for (int j = 1; j < formats.length; j += 2) { if ("*".equals(formats[j])) { - sb.append(BibtexNameFormatter.formatName(toFormat, i, formats[j + 1], null)); + sb.append(BstNameFormatter.formatName(toFormat, i, formats[j + 1])); break; } else { String[] range = formats[j].split("\\.\\."); @@ -112,7 +112,7 @@ private static String format(String toFormat, AuthorList al, String[] formats) { } if ((s <= i) && (i <= e)) { - sb.append(BibtexNameFormatter.formatName(toFormat, i, formats[j + 1], null)); + sb.append(BstNameFormatter.formatName(toFormat, i, formats[j + 1])); break; } } diff --git a/src/main/java/org/jabref/logic/net/URLDownload.java b/src/main/java/org/jabref/logic/net/URLDownload.java index 77ac1ff3093..c499dcf3635 100644 --- a/src/main/java/org/jabref/logic/net/URLDownload.java +++ b/src/main/java/org/jabref/logic/net/URLDownload.java @@ -40,6 +40,8 @@ import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; +import org.jabref.logic.importer.FetcherClientException; +import org.jabref.logic.importer.FetcherServerException; import org.jabref.logic.util.io.FileUtil; import org.jabref.model.util.FileHelper; @@ -350,21 +352,23 @@ private URLConnection openConnection() throws IOException { if (connection instanceof HttpURLConnection) { // normally, 3xx is redirect int status = ((HttpURLConnection) connection).getResponseCode(); - if (status != HttpURLConnection.HTTP_OK) { - if ((status == HttpURLConnection.HTTP_MOVED_TEMP) - || (status == HttpURLConnection.HTTP_MOVED_PERM) - || (status == HttpURLConnection.HTTP_SEE_OTHER)) { - // get redirect url from "location" header field - String newUrl = connection.getHeaderField("location"); - // open the new connnection again - connection = new URLDownload(newUrl).openConnection(); - } + + if ((status == HttpURLConnection.HTTP_MOVED_TEMP) + || (status == HttpURLConnection.HTTP_MOVED_PERM) + || (status == HttpURLConnection.HTTP_SEE_OTHER)) { + // get redirect url from "location" header field + String newUrl = connection.getHeaderField("location"); + // open the new connection again + connection = new URLDownload(newUrl).openConnection(); + } + if ((status >= 400) && (status < 500)) { + throw new IOException(new FetcherClientException("Encountered HTTP Status code " + status)); + } + if (status >= 500) { + throw new IOException(new FetcherServerException("Encountered HTTP Status Code " + status)); } } - // this does network i/o: GET + read returned headers - connection.connect(); - return connection; } diff --git a/src/main/java/org/jabref/logic/shared/prefs/SharedDatabasePreferences.java b/src/main/java/org/jabref/logic/shared/prefs/SharedDatabasePreferences.java index 260ec297c24..4b1a14fe1c9 100644 --- a/src/main/java/org/jabref/logic/shared/prefs/SharedDatabasePreferences.java +++ b/src/main/java/org/jabref/logic/shared/prefs/SharedDatabasePreferences.java @@ -25,6 +25,8 @@ public class SharedDatabasePreferences { private static final String SHARED_DATABASE_NAME = "sharedDatabaseName"; private static final String SHARED_DATABASE_USER = "sharedDatabaseUser"; private static final String SHARED_DATABASE_PASSWORD = "sharedDatabasePassword"; + private static final String SHARED_DATABASE_FOLDER = "sharedDatabaseFolder"; + private static final String SHARED_DATABASE_AUTOSAVE = "sharedDatabaseAutosave"; private static final String SHARED_DATABASE_REMEMBER_PASSWORD = "sharedDatabaseRememberPassword"; private static final String SHARED_DATABASE_USE_SSL = "sharedDatabaseUseSSL"; private static final String SHARED_DATABASE_KEYSTORE_FILE = "sharedDatabaseKeyStoreFile"; @@ -77,6 +79,14 @@ public boolean getRememberPassword() { return internalPrefs.getBoolean(SHARED_DATABASE_REMEMBER_PASSWORD, false); } + public Optional getFolder() { + return getOptionalValue(SHARED_DATABASE_FOLDER); + } + + public boolean getAutosave() { + return internalPrefs.getBoolean(SHARED_DATABASE_AUTOSAVE, false); + } + public boolean isUseSSL() { return internalPrefs.getBoolean(SHARED_DATABASE_USE_SSL, false); } @@ -109,6 +119,14 @@ public void setRememberPassword(boolean rememberPassword) { internalPrefs.putBoolean(SHARED_DATABASE_REMEMBER_PASSWORD, rememberPassword); } + public void setFolder(String folder) { + internalPrefs.put(SHARED_DATABASE_FOLDER, folder); + } + + public void setAutosave(boolean autosave) { + internalPrefs.putBoolean(SHARED_DATABASE_AUTOSAVE, autosave); + } + public void setUseSSL(boolean useSSL) { internalPrefs.putBoolean(SHARED_DATABASE_USE_SSL, useSSL); } diff --git a/src/main/java/org/jabref/model/database/BibDatabase.java b/src/main/java/org/jabref/model/database/BibDatabase.java index 72a559ff663..52e4e841ce9 100644 --- a/src/main/java/org/jabref/model/database/BibDatabase.java +++ b/src/main/java/org/jabref/model/database/BibDatabase.java @@ -20,6 +20,7 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import javafx.collections.ObservableSet; import org.jabref.logic.bibtex.FieldWriter; import org.jabref.model.database.event.EntriesAddedEvent; @@ -29,6 +30,7 @@ import org.jabref.model.entry.Month; import org.jabref.model.entry.event.EntriesEventSource; import org.jabref.model.entry.event.EntryChangedEvent; +import org.jabref.model.entry.event.FieldAddedOrRemovedEvent; import org.jabref.model.entry.event.FieldChangedEvent; import org.jabref.model.entry.field.Field; import org.jabref.model.entry.field.FieldFactory; @@ -52,6 +54,8 @@ public class BibDatabase { * State attributes */ private final ObservableList entries = FXCollections.synchronizedObservableList(FXCollections.observableArrayList(BibEntry::getObservables)); + + private final ObservableSet visibleFields = FXCollections.observableSet(); private Map bibtexStrings = new ConcurrentHashMap<>(); private final EventBus eventBus = new EventBus(); @@ -136,13 +140,8 @@ public ObservableList getEntries() { * * @return set of fieldnames, that are visible */ - public Set getAllVisibleFields() { - Set allFields = new TreeSet<>(Comparator.comparing(Field::getName)); - for (BibEntry e : getEntries()) { - allFields.addAll(e.getFields()); - } - return allFields.stream().filter(field -> !FieldFactory.isInternalField(field)) - .collect(Collectors.toSet()); + public ObservableSet getAllVisibleFields() { + return visibleFields; } /** @@ -214,6 +213,8 @@ public synchronized void insertEntries(List newEntries, EntriesEventSo eventBus.post(new EntriesAddedEvent(newEntries, newEntries.get(0), eventSource)); } entries.addAll(newEntries); + + updateVisibleFields(); } public synchronized void removeEntry(BibEntry bibEntry) { @@ -251,6 +252,7 @@ public synchronized void removeEntries(List toBeDeleted, EntriesEventS boolean anyRemoved = entries.removeIf(entry -> ids.contains(entry.getId())); if (anyRemoved) { eventBus.post(new EntriesRemovedEvent(toBeDeleted, eventSource)); + updateVisibleFields(); } } @@ -584,6 +586,30 @@ private void relayEntryChangeEvent(FieldChangedEvent event) { eventBus.post(event); } + @Subscribe + private void listen(FieldAddedOrRemovedEvent event) { + // When a field is removed from an entry we can't tell if it's + // still present in other entries, and thus we can't remove it + // from the set of visible fields. However, when a new field is added + // to any entry, we can simply add it to the set because we're + // going to add it whether other entries have it or not + boolean isAdded = visibleFields.add(event.getField()); + if (!isAdded) { + updateVisibleFields(); + } + } + + private void updateVisibleFields() { + visibleFields.clear(); + Set allFields = new TreeSet<>(Comparator.comparing(Field::getName)); + for (BibEntry e : getEntries()) { + allFields.addAll(e.getFields()); + } + visibleFields.addAll(allFields.stream().filter(field -> !FieldFactory.isInternalField(field)) + .filter(field -> StringUtil.isNotBlank(field.getName())) + .collect(Collectors.toSet())); + } + public Optional getReferencedEntry(BibEntry entry) { return entry.getField(StandardField.CROSSREF).flatMap(this::getEntryByCitationKey); } diff --git a/src/main/java/org/jabref/model/entry/BibEntryTypesManager.java b/src/main/java/org/jabref/model/entry/BibEntryTypesManager.java index b737596abd6..836cd39dac8 100644 --- a/src/main/java/org/jabref/model/entry/BibEntryTypesManager.java +++ b/src/main/java/org/jabref/model/entry/BibEntryTypesManager.java @@ -11,6 +11,7 @@ import org.jabref.model.database.BibDatabaseMode; import org.jabref.model.entry.field.BibField; import org.jabref.model.entry.field.FieldFactory; +import org.jabref.model.entry.types.BiblatexAPAEntryTypeDefinitions; import org.jabref.model.entry.types.BiblatexEntryTypeDefinitions; import org.jabref.model.entry.types.BiblatexSoftwareEntryTypeDefinitions; import org.jabref.model.entry.types.BibtexEntryTypeDefinitions; @@ -21,7 +22,7 @@ public class BibEntryTypesManager { public static final String ENTRYTYPE_FLAG = "jabref-entrytype: "; private final InternalEntryTypes BIBTEX = new InternalEntryTypes(Stream.concat(BibtexEntryTypeDefinitions.ALL.stream(), IEEETranEntryTypeDefinitions.ALL.stream()).collect(Collectors.toList())); - private final InternalEntryTypes BIBLATEX = new InternalEntryTypes(Stream.concat(BiblatexEntryTypeDefinitions.ALL.stream(), BiblatexSoftwareEntryTypeDefinitions.ALL.stream()).collect(Collectors.toList())); + private final InternalEntryTypes BIBLATEX = new InternalEntryTypes(Stream.concat(BiblatexEntryTypeDefinitions.ALL.stream(), Stream.concat(BiblatexSoftwareEntryTypeDefinitions.ALL.stream(), BiblatexAPAEntryTypeDefinitions.ALL.stream())).collect(Collectors.toList())); public BibEntryTypesManager() { } @@ -99,6 +100,7 @@ public List getAllCustomTypes(BibDatabaseMode mode) { return customizedTypes.stream() .filter(entryType -> BiblatexEntryTypeDefinitions.ALL.stream().noneMatch(biblatexType -> biblatexType.getType().equals(entryType.getType()))) .filter(entryType -> BiblatexSoftwareEntryTypeDefinitions.ALL.stream().noneMatch(biblatexSoftware -> biblatexSoftware.getType().equals(entryType.getType()))) + .filter(entryType -> BiblatexAPAEntryTypeDefinitions.ALL.stream().noneMatch(biblatexAPA -> biblatexAPA.getType().equals(entryType.getType()))) .collect(Collectors.toList()); } } diff --git a/src/main/java/org/jabref/model/entry/field/BiblatexApaField.java b/src/main/java/org/jabref/model/entry/field/BiblatexApaField.java new file mode 100644 index 00000000000..b2938963efe --- /dev/null +++ b/src/main/java/org/jabref/model/entry/field/BiblatexApaField.java @@ -0,0 +1,82 @@ +package org.jabref.model.entry.field; + +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Optional; +import java.util.Set; + +import org.jabref.model.entry.types.BiblatexApaEntryType; + +public enum BiblatexApaField implements Field { + + AMENDMENT("amendment"), + ARTICLE("article"), + CITATION("citation"), + CITATION_CITEORG("citation_citeorg"), + CITATION_CITEDATE("citation_citedate", FieldProperty.DATE), + CITATION_CITEINFO("citation_citeinfo"), + SECTION("section", FieldProperty.NUMERIC), + SOURCE("source"); + + private final String name; + private final String displayName; + private final Set properties; + + BiblatexApaField(String name) { + this.name = name; + this.displayName = null; + this.properties = EnumSet.noneOf(FieldProperty.class); + } + + BiblatexApaField(String name, String displayName) { + this.name = name; + this.displayName = displayName; + this.properties = EnumSet.noneOf(FieldProperty.class); + } + + BiblatexApaField(String name, String displayName, FieldProperty first, FieldProperty... rest) { + this.name = name; + this.displayName = displayName; + this.properties = EnumSet.of(first, rest); + } + + BiblatexApaField(String name, FieldProperty first, FieldProperty... rest) { + this.name = name; + this.displayName = null; + this.properties = EnumSet.of(first, rest); + } + + public static Optional fromName(T type, String name) { + if (!(type instanceof BiblatexApaEntryType)) { + return Optional.empty(); + } + return Arrays.stream(BiblatexApaField.values()) + .filter(field -> field.getName().equalsIgnoreCase(name)) + .findAny(); + } + + @Override + public Set getProperties() { + return Collections.unmodifiableSet(properties); + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean isStandardField() { + return false; + } + + @Override + public String getDisplayName() { + if (displayName == null) { + return Field.super.getDisplayName(); + } else { + return displayName; + } + } +} diff --git a/src/main/java/org/jabref/model/entry/field/BiblatexSoftwareField.java b/src/main/java/org/jabref/model/entry/field/BiblatexSoftwareField.java new file mode 100644 index 00000000000..fc3bb4dcc48 --- /dev/null +++ b/src/main/java/org/jabref/model/entry/field/BiblatexSoftwareField.java @@ -0,0 +1,82 @@ +package org.jabref.model.entry.field; + +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Optional; +import java.util.Set; + +import org.jabref.model.entry.types.BiblatexSoftwareEntryType; + +public enum BiblatexSoftwareField implements Field { + + HALID("hal_id"), + HALVERSION("hal_version"), + INTRODUCEDIN("introducedin"), + LICENSE("license"), + RELATEDTYPE("relatedtype"), + RELATEDSTRING("relatedstring"), + REPOSITORY("repository"), + SWHID("swhid"); + + private final String name; + private final String displayName; + private final Set properties; + + BiblatexSoftwareField(String name) { + this.name = name; + this.displayName = null; + this.properties = EnumSet.noneOf(FieldProperty.class); + } + + BiblatexSoftwareField(String name, String displayName) { + this.name = name; + this.displayName = displayName; + this.properties = EnumSet.noneOf(FieldProperty.class); + } + + BiblatexSoftwareField(String name, String displayName, FieldProperty first, FieldProperty... rest) { + this.name = name; + this.displayName = displayName; + this.properties = EnumSet.of(first, rest); + } + + BiblatexSoftwareField(String name, FieldProperty first, FieldProperty... rest) { + this.name = name; + this.displayName = null; + this.properties = EnumSet.of(first, rest); + } + + public static Optional fromName(T type, String name) { + if (!(type instanceof BiblatexSoftwareEntryType)) { + return Optional.empty(); + } + return Arrays.stream(BiblatexSoftwareField.values()) + .filter(field -> field.getName().equalsIgnoreCase(name)) + .findAny(); + } + + @Override + public Set getProperties() { + return Collections.unmodifiableSet(properties); + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean isStandardField() { + return false; + } + + @Override + public String getDisplayName() { + if (displayName == null) { + return Field.super.getDisplayName(); + } else { + return displayName; + } + } +} diff --git a/src/main/java/org/jabref/model/entry/field/FieldFactory.java b/src/main/java/org/jabref/model/entry/field/FieldFactory.java index b1c2f3b99b9..07a34b039d3 100644 --- a/src/main/java/org/jabref/model/entry/field/FieldFactory.java +++ b/src/main/java/org/jabref/model/entry/field/FieldFactory.java @@ -73,13 +73,23 @@ public static String serializeFieldsList(Collection fields) { .collect(Collectors.joining(DELIMITER)); } + public static Field parseField(T type, String fieldName) { + return OptionalUtil.orElse( + OptionalUtil.orElse( + OptionalUtil.orElse( + OptionalUtil.orElse( + OptionalUtil.orElse( + InternalField.fromName(fieldName), + StandardField.fromName(fieldName)), + SpecialField.fromName(fieldName)), + IEEEField.fromName(fieldName)), + BiblatexSoftwareField.fromName(type, fieldName)), + BiblatexApaField.fromName(type, fieldName)) + .orElse(new UnknownField(fieldName)); + } + public static Field parseField(String fieldName) { - return OptionalUtil.orElse(OptionalUtil.orElse(OptionalUtil.orElse( - InternalField.fromName(fieldName), - StandardField.fromName(fieldName)), - SpecialField.fromName(fieldName)), - IEEEField.fromName(fieldName)) - .orElse(new UnknownField(fieldName)); + return parseField(null, fieldName); } public static Set getKeyFields() { @@ -138,6 +148,8 @@ private static Set getFieldsFiltered(Predicate selector) { private static Set getAllFields() { Set fields = new HashSet<>(); + fields.addAll(EnumSet.allOf(BiblatexApaField.class)); + fields.addAll(EnumSet.allOf(BiblatexSoftwareField.class)); fields.addAll(EnumSet.allOf(IEEEField.class)); fields.addAll(EnumSet.allOf(InternalField.class)); fields.addAll(EnumSet.allOf(SpecialField.class)); diff --git a/src/main/java/org/jabref/model/entry/field/IEEEField.java b/src/main/java/org/jabref/model/entry/field/IEEEField.java index d00bec02eee..f56ce79ea99 100644 --- a/src/main/java/org/jabref/model/entry/field/IEEEField.java +++ b/src/main/java/org/jabref/model/entry/field/IEEEField.java @@ -31,7 +31,7 @@ public enum IEEEField implements Field { this.properties = EnumSet.of(first, rest); } - public static Optional fromName(String name) { + public static Optional fromName(String name) { return Arrays.stream(IEEEField.values()) .filter(field -> field.getName().equalsIgnoreCase(name)) .findAny(); diff --git a/src/main/java/org/jabref/model/entry/field/SpecialFieldValue.java b/src/main/java/org/jabref/model/entry/field/SpecialFieldValue.java index d4c1f8df7d5..6c3337b41f6 100644 --- a/src/main/java/org/jabref/model/entry/field/SpecialFieldValue.java +++ b/src/main/java/org/jabref/model/entry/field/SpecialFieldValue.java @@ -39,6 +39,7 @@ public enum SpecialFieldValue { public static SpecialFieldValue getRating(int ranking) { return switch (ranking) { + case 0 -> CLEAR_RANK; case 1 -> RANK_1; case 2 -> RANK_2; case 3 -> RANK_3; @@ -58,6 +59,7 @@ public Optional getFieldValue() { public int toRating() { return switch (this) { + case CLEAR_RANK -> 0; case RANK_1 -> 1; case RANK_2 -> 2; case RANK_3 -> 3; diff --git a/src/main/java/org/jabref/model/entry/field/StandardField.java b/src/main/java/org/jabref/model/entry/field/StandardField.java index ef3646802e0..bcf9b710b7d 100644 --- a/src/main/java/org/jabref/model/entry/field/StandardField.java +++ b/src/main/java/org/jabref/model/entry/field/StandardField.java @@ -59,14 +59,11 @@ public enum StandardField implements Field { FOREWORD("foreword", FieldProperty.PERSON_NAMES), FOLDER("folder"), GENDER("gender", FieldProperty.GENDER), - HALID("hal_id"), - HALVERSION("hal_version"), HOLDER("holder", FieldProperty.PERSON_NAMES), HOWPUBLISHED("howpublished"), IDS("ids", FieldProperty.MULTIPLE_ENTRY_LINK), INSTITUTION("institution"), INTRODUCTION("introduction", FieldProperty.PERSON_NAMES), - INTRODUCEDIN("introducedin"), ISBN("isbn", "ISBN", FieldProperty.ISBN), ISRN("isrn", "ISRN"), ISSN("issn", "ISSN"), @@ -81,7 +78,6 @@ public enum StandardField implements Field { LANGUAGE("language", FieldProperty.LANGUAGE), LABEL("label"), LIBRARY("library"), - LICENSE("license"), LOCATION("location"), MAINSUBTITLE("mainsubtitle", FieldProperty.BOOK_NAME), MAINTITLE("maintitle", FieldProperty.BOOK_NAME), @@ -106,10 +102,7 @@ public enum StandardField implements Field { PUBSTATE("pubstate", FieldProperty.PUBLICATION_STATE), PRIMARYCLASS("primaryclass"), RELATED("related", FieldProperty.MULTIPLE_ENTRY_LINK), - RELATEDTYPE("relatedtype"), - RELATEDSTRING("relatedstring"), REPORTNO("reportno"), - REPOSITORY("repository"), REVIEW("review"), REVISION("revision"), SCHOOL("school"), @@ -120,7 +113,6 @@ public enum StandardField implements Field { SORTKEY("sortkey"), SORTNAME("sortname", FieldProperty.PERSON_NAMES), SUBTITLE("subtitle"), - SWHID("swhid"), TITLE("title"), TITLEADDON("titleaddon"), TRANSLATOR("translator", FieldProperty.PERSON_NAMES), diff --git a/src/main/java/org/jabref/model/entry/types/BiblatexAPAEntryTypeDefinitions.java b/src/main/java/org/jabref/model/entry/types/BiblatexAPAEntryTypeDefinitions.java new file mode 100644 index 00000000000..f5c0b89b0be --- /dev/null +++ b/src/main/java/org/jabref/model/entry/types/BiblatexAPAEntryTypeDefinitions.java @@ -0,0 +1,43 @@ +package org.jabref.model.entry.types; + +import java.util.Arrays; +import java.util.List; + +import org.jabref.model.entry.BibEntryType; +import org.jabref.model.entry.BibEntryTypeBuilder; +import org.jabref.model.entry.field.BiblatexApaField; +import org.jabref.model.entry.field.StandardField; + +public class BiblatexAPAEntryTypeDefinitions { + + private static final BibEntryType JURISDICTION = new BibEntryTypeBuilder() + .withType(BiblatexApaEntryType.Jurisdiction) + .withImportantFields(StandardField.ORGANIZATION, BiblatexApaField.CITATION_CITEORG, BiblatexApaField.CITATION_CITEDATE, BiblatexApaField.CITATION_CITEDATE, StandardField.ORIGDATE) + .withRequiredFields(StandardField.TITLE, BiblatexApaField.CITATION, BiblatexApaField.CITATION_CITEINFO, StandardField.URL, StandardField.DATE) + .build(); + + private static final BibEntryType LEGISLATION = new BibEntryTypeBuilder() + .withType(BiblatexApaEntryType.Legislation) + .withImportantFields(StandardField.TITLEADDON, StandardField.ORIGDATE) + .withRequiredFields(StandardField.TITLE, StandardField.LOCATION, StandardField.URL, StandardField.DATE) + .build(); + + private static final BibEntryType LEGADMINMATERIAL = new BibEntryTypeBuilder() + .withType(BiblatexApaEntryType.Legadminmaterial) + .withImportantFields(StandardField.NUMBER, StandardField.SHORTTITLE, StandardField.NOTE, StandardField.KEYWORDS) + .withRequiredFields(StandardField.TITLE, BiblatexApaField.CITATION, StandardField.URL, StandardField.DATE) + .build(); + + private static final BibEntryType CONSTITUTION = new BibEntryTypeBuilder() + .withType(BiblatexApaEntryType.Constitution) + .withImportantFields(BiblatexApaField.ARTICLE, BiblatexApaField.AMENDMENT, StandardField.EVENTDATE, StandardField.KEYWORDS, StandardField.PART, BiblatexApaField.SECTION) + .withRequiredFields(BiblatexApaField.SOURCE, StandardField.TYPE) + .build(); + + private static final BibEntryType LEGAL = new BibEntryTypeBuilder() + .withType(BiblatexApaEntryType.Legal) + .withRequiredFields(StandardField.TITLE, StandardField.DATE, StandardField.URI, StandardField.KEYWORDS, StandardField.PART, BiblatexApaField.SECTION) + .build(); + + public static final List ALL = Arrays.asList(JURISDICTION, LEGISLATION, LEGADMINMATERIAL, CONSTITUTION, LEGAL); +} diff --git a/src/main/java/org/jabref/model/entry/types/BiblatexApaEntryType.java b/src/main/java/org/jabref/model/entry/types/BiblatexApaEntryType.java new file mode 100644 index 00000000000..6a1d8d3abe2 --- /dev/null +++ b/src/main/java/org/jabref/model/entry/types/BiblatexApaEntryType.java @@ -0,0 +1,36 @@ +package org.jabref.model.entry.types; + +import java.util.Arrays; +import java.util.Locale; +import java.util.Optional; + +public enum BiblatexApaEntryType implements EntryType { + + Legislation("Legislation"), + Legadminmaterial("Legadminmaterial"), + Jurisdiction("Jurisdiction"), + Constitution("Constitution"), + Legal("Legal"); + + private final String displayName; + + BiblatexApaEntryType(String displayName) { + this.displayName = displayName; + } + + @Override + public String getName() { + return displayName.toLowerCase(Locale.ENGLISH); + } + + @Override + public String getDisplayName() { + return displayName; + } + + public static Optional fromName(String name) { + return Arrays.stream(BiblatexApaEntryType.values()) + .filter(field -> field.getName().equalsIgnoreCase(name)) + .findAny(); + } +} diff --git a/src/main/java/org/jabref/model/entry/types/BiblatexSoftwareEntryType.java b/src/main/java/org/jabref/model/entry/types/BiblatexSoftwareEntryType.java new file mode 100644 index 00000000000..041c48e592a --- /dev/null +++ b/src/main/java/org/jabref/model/entry/types/BiblatexSoftwareEntryType.java @@ -0,0 +1,35 @@ +package org.jabref.model.entry.types; + +import java.util.Arrays; +import java.util.Locale; +import java.util.Optional; + +public enum BiblatexSoftwareEntryType implements EntryType { + + Dataset("Dataset"), + SoftwareVersion("SoftwareVersion"), + SoftwareModule("SoftwareModule"), + CodeFragment("CodeFragment"); + + private final String displayName; + + BiblatexSoftwareEntryType(String displayName) { + this.displayName = displayName; + } + + public static Optional fromName(String name) { + return Arrays.stream(BiblatexSoftwareEntryType.values()) + .filter(field -> field.getName().equalsIgnoreCase(name)) + .findAny(); + } + + @Override + public String getName() { + return displayName.toLowerCase(Locale.ENGLISH); + } + + @Override + public String getDisplayName() { + return displayName; + } +} diff --git a/src/main/java/org/jabref/model/entry/types/BiblatexSoftwareEntryTypeDefinitions.java b/src/main/java/org/jabref/model/entry/types/BiblatexSoftwareEntryTypeDefinitions.java index 0fc48e6d904..6a2e7f0851e 100644 --- a/src/main/java/org/jabref/model/entry/types/BiblatexSoftwareEntryTypeDefinitions.java +++ b/src/main/java/org/jabref/model/entry/types/BiblatexSoftwareEntryTypeDefinitions.java @@ -5,6 +5,7 @@ import org.jabref.model.entry.BibEntryType; import org.jabref.model.entry.BibEntryTypeBuilder; +import org.jabref.model.entry.field.BiblatexSoftwareField; import org.jabref.model.entry.field.OrFields; import org.jabref.model.entry.field.StandardField; @@ -12,40 +13,40 @@ public class BiblatexSoftwareEntryTypeDefinitions { private static final BibEntryType SOFTWARE = new BibEntryTypeBuilder() .withType(StandardEntryType.Software) .withImportantFields(StandardField.DATE, StandardField.DOI, StandardField.EPRINTTYPE, StandardField.EPRINTCLASS, StandardField.EPRINT, - StandardField.EDITOR, StandardField.FILE, StandardField.HALID, StandardField.HALVERSION, StandardField.INSTITUTION, StandardField.INTRODUCEDIN, - StandardField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, StandardField.PUBLISHER, StandardField.RELATED, - StandardField.RELATEDSTRING, StandardField.REPOSITORY, StandardField.SWHID, StandardField.URLDATE, StandardField.VERSION) + StandardField.EDITOR, StandardField.FILE, BiblatexSoftwareField.HALID, BiblatexSoftwareField.HALVERSION, StandardField.INSTITUTION, BiblatexSoftwareField.INTRODUCEDIN, + BiblatexSoftwareField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, StandardField.PUBLISHER, StandardField.RELATED, + BiblatexSoftwareField.RELATEDSTRING, BiblatexSoftwareField.REPOSITORY, BiblatexSoftwareField.SWHID, StandardField.URLDATE, StandardField.VERSION) .withRequiredFields(new OrFields(StandardField.AUTHOR, StandardField.EDITOR), StandardField.TITLE, StandardField.URL, StandardField.VERSION, StandardField.YEAR) .build(); private static final BibEntryType SOFTWAREVERSION = new BibEntryTypeBuilder() - .withType(StandardEntryType.SoftwareVersion) - .withImportantFields(StandardField.DATE, StandardField.EPRINTCLASS, StandardField.EPRINTTYPE, StandardField.HALID, StandardField.HALVERSION, - StandardField.INSTITUTION, StandardField.INTRODUCEDIN, StandardField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, - StandardField.PUBLISHER, StandardField.RELATED, StandardField.RELATEDTYPE, StandardField.RELATEDSTRING, - StandardField.REPOSITORY, StandardField.SWHID, StandardField.SUBTITLE, StandardField.URLDATE) + .withType(BiblatexSoftwareEntryType.SoftwareVersion) + .withImportantFields(StandardField.DATE, StandardField.EPRINTCLASS, StandardField.EPRINTTYPE, BiblatexSoftwareField.HALID, BiblatexSoftwareField.HALVERSION, + StandardField.INSTITUTION, BiblatexSoftwareField.INTRODUCEDIN, BiblatexSoftwareField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, + StandardField.PUBLISHER, StandardField.RELATED, BiblatexSoftwareField.RELATEDTYPE, BiblatexSoftwareField.RELATEDSTRING, + BiblatexSoftwareField.REPOSITORY, BiblatexSoftwareField.SWHID, StandardField.SUBTITLE, StandardField.URLDATE) .withRequiredFields(new OrFields(StandardField.AUTHOR, StandardField.EDITOR), StandardField.TITLE, StandardField.URL, StandardField.YEAR, StandardField.VERSION) - .withDetailFields(StandardField.DATE, StandardField.EPRINTCLASS, StandardField.EPRINTTYPE, StandardField.HALID, StandardField.HALVERSION, - StandardField.INSTITUTION, StandardField.INTRODUCEDIN, StandardField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, - StandardField.PUBLISHER, StandardField.RELATED, StandardField.RELATEDTYPE, StandardField.RELATEDSTRING, - StandardField.REPOSITORY, StandardField.SWHID, StandardField.SUBTITLE, StandardField.URLDATE) + .withDetailFields(StandardField.DATE, StandardField.EPRINTCLASS, StandardField.EPRINTTYPE, BiblatexSoftwareField.HALID, BiblatexSoftwareField.HALVERSION, + StandardField.INSTITUTION, BiblatexSoftwareField.INTRODUCEDIN, BiblatexSoftwareField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, + StandardField.PUBLISHER, StandardField.RELATED, BiblatexSoftwareField.RELATEDTYPE, BiblatexSoftwareField.RELATEDSTRING, + BiblatexSoftwareField.REPOSITORY, BiblatexSoftwareField.SWHID, StandardField.SUBTITLE, StandardField.URLDATE) .withRequiredFields(new OrFields(StandardField.AUTHOR, StandardField.EDITOR), StandardField.TITLE, StandardField.URL, StandardField.YEAR) .build(); private static final BibEntryType SOFTWAREMODULE = new BibEntryTypeBuilder() - .withType(StandardEntryType.SoftwareModule) + .withType(BiblatexSoftwareEntryType.SoftwareModule) .withImportantFields(StandardField.DATE, StandardField.DOI, StandardField.EPRINTTYPE, StandardField.EPRINTCLASS, StandardField.EPRINT, - StandardField.EDITOR, StandardField.FILE, StandardField.HALID, StandardField.HALVERSION, StandardField.INSTITUTION, StandardField.INTRODUCEDIN, - StandardField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, StandardField.PUBLISHER, StandardField.RELATED, - StandardField.RELATEDSTRING, StandardField.REPOSITORY, StandardField.SWHID, StandardField.URLDATE, StandardField.VERSION) + StandardField.EDITOR, StandardField.FILE, BiblatexSoftwareField.HALID, BiblatexSoftwareField.HALVERSION, StandardField.INSTITUTION, BiblatexSoftwareField.INTRODUCEDIN, + BiblatexSoftwareField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, StandardField.PUBLISHER, StandardField.RELATED, + BiblatexSoftwareField.RELATEDSTRING, BiblatexSoftwareField.REPOSITORY, BiblatexSoftwareField.SWHID, StandardField.URLDATE, StandardField.VERSION) .withRequiredFields(StandardField.AUTHOR, StandardField.SUBTITLE, StandardField.URL, StandardField.YEAR) .build(); private static final BibEntryType CODEFRAGMENT = new BibEntryTypeBuilder() - .withType(StandardEntryType.CodeFragment) + .withType(BiblatexSoftwareEntryType.CodeFragment) .withImportantFields(StandardField.DATE, StandardField.DOI, StandardField.EPRINTTYPE, StandardField.EPRINTCLASS, StandardField.EPRINT, - StandardField.EDITOR, StandardField.FILE, StandardField.HALID, StandardField.HALVERSION, StandardField.INSTITUTION, StandardField.INTRODUCEDIN, - StandardField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, StandardField.PUBLISHER, StandardField.RELATED, - StandardField.RELATEDSTRING, StandardField.REPOSITORY, StandardField.SWHID, StandardField.URLDATE, StandardField.VERSION) + StandardField.EDITOR, StandardField.FILE, BiblatexSoftwareField.HALID, BiblatexSoftwareField.HALVERSION, StandardField.INSTITUTION, BiblatexSoftwareField.INTRODUCEDIN, + BiblatexSoftwareField.LICENSE, StandardField.MONTH, StandardField.NOTE, StandardField.ORGANIZATION, StandardField.PUBLISHER, StandardField.RELATED, + BiblatexSoftwareField.RELATEDSTRING, BiblatexSoftwareField.REPOSITORY, BiblatexSoftwareField.SWHID, StandardField.URLDATE, StandardField.VERSION) .withRequiredFields(StandardField.URL) .build(); diff --git a/src/main/java/org/jabref/model/entry/types/EntryTypeFactory.java b/src/main/java/org/jabref/model/entry/types/EntryTypeFactory.java index 1c94f6dd9a0..6b40a56c4f4 100644 --- a/src/main/java/org/jabref/model/entry/types/EntryTypeFactory.java +++ b/src/main/java/org/jabref/model/entry/types/EntryTypeFactory.java @@ -49,6 +49,8 @@ private static boolean isBiblatex(EntryType type) { public static EntryType parse(String typeName) { List types = new ArrayList<>(Arrays.asList(StandardEntryType.values())); types.addAll(Arrays.asList(IEEETranEntryType.values())); + types.addAll(Arrays.asList(BiblatexSoftwareEntryType.values())); + types.addAll(Arrays.asList(BiblatexApaEntryType.values())); types.addAll(Arrays.asList(SystematicLiteratureReviewStudyEntryType.values())); return types.stream().filter(type -> type.getName().equals(typeName.toLowerCase(Locale.ENGLISH))).findFirst().orElse(new UnknownEntryType(typeName)); diff --git a/src/main/java/org/jabref/model/entry/types/StandardEntryType.java b/src/main/java/org/jabref/model/entry/types/StandardEntryType.java index fb2922e61a0..6f6f91ac6a5 100644 --- a/src/main/java/org/jabref/model/entry/types/StandardEntryType.java +++ b/src/main/java/org/jabref/model/entry/types/StandardEntryType.java @@ -36,10 +36,7 @@ public enum StandardEntryType implements EntryType { Thesis("Thesis"), WWW("WWW"), Software("Software"), - Dataset("Dataset"), - SoftwareVersion("SoftwareVersion"), - SoftwareModule("SoftwareModule"), - CodeFragment("CodeFragment"); + Dataset("Dataset"); private final String displayName; diff --git a/src/main/java/org/jabref/model/strings/StringUtil.java b/src/main/java/org/jabref/model/strings/StringUtil.java index 19f0b7a4542..2a443306e4d 100644 --- a/src/main/java/org/jabref/model/strings/StringUtil.java +++ b/src/main/java/org/jabref/model/strings/StringUtil.java @@ -750,4 +750,15 @@ public static String quoteStringIfSpaceIsContained(String string) { return string; } } + + /** + * Checks if the given string contains any whitespace characters. The supported whitespace characters + * are the set of characters matched by {@code \s} in regular expressions, which are {@code [ \t\n\x0B\f\r]}. + * + * @param s The string to check + * @return {@code True} if the given string does contain at least one whitespace character, {@code False} otherwise + * */ + public static boolean containsWhitespace(String s) { + return s.chars().anyMatch(Character::isWhitespace); + } } diff --git a/src/main/resources/journals/journalList.mv b/src/main/resources/journals/journalList.mv index 52541a1a095..b921eef3b19 100644 Binary files a/src/main/resources/journals/journalList.mv and b/src/main/resources/journals/journalList.mv differ diff --git a/src/main/resources/l10n/JabRef_de.properties b/src/main/resources/l10n/JabRef_de.properties index 72df848e1f7..901ac722dc2 100644 --- a/src/main/resources/l10n/JabRef_de.properties +++ b/src/main/resources/l10n/JabRef_de.properties @@ -2496,21 +2496,24 @@ Success\!\ Finished\ writing\ metadata.=Erfolgreich\! Das Schreiben der Metadate Custom\ API\ key=Eigener API-Schlüssel Check\ %0\ API\ Key\ Setting=%0 API-Schlüsseleinstellungen überprüfen -Edit\ field\ value=Feldinhalt bearbeiten -Two\ fields=Zwei Felder -Overwrite\ Non\ empty\ fields=Nicht leere Felder überschreiben +Edit\ content=Inhalt ändern +Copy\ or\ Move\ content=Inhalt kopieren oder verschieben +Overwrite\ field\ content=Inhalt des Feldes überschreiben Set=Festlegen Append=Anfügen -Clear\ field=Feld leeren -Field\ value=Feldinhalt -Edit\ field\ value\ of\ selected\ entries=Feldinhalt der ausgewählten Einträge bearbeiten +Clear\ field\ content=Feldinhalt löschen +Set\ or\ append\ content=Setze oder füge Inhalt an +Edit\ field\ content\ for\ selected\ entries=Feldinhalt der ausgewählten Einträge bearbeiten Rename=Umbenennen New\ field\ name=Neuer Feldname -Copy\ value=Wert kopieren -Move\ value=Inhalt verschieben -Swap\ values=Werte tauschen -Copy\ or\ move\ the\ value\ of\ one\ field\ to\ another=Den Inhalt eines Feldes in ein anderes kopieren oder verschieben +Copy\ content=Inhalt kopieren +Move\ content=Inhalt verschieben +Swap\ content=Inhalte vertauschen +Copy\ or\ move\ the\ content\ of\ one\ field\ to\ another=Inhalt eines Feldes kopieren oder in ein anderes Feld verschieben Automatic\ field\ editor=Automatischer Feldeditor +From=Von +Keep\ Modifications=Änderungen akzeptieren +To=Nach (Note\:\ If\ original\ entries\ lack\ keywords\ to\ qualify\ for\ the\ new\ group\ configuration,\ confirming\ here\ will\ add\ them)=(Hinweis\: Wenn es den ursprünglichen Einträgen an Schlüsselwörtern fehlt, die sich für die neue Gruppenkonfiguration qualifizieren, wird die Bestätigung hier diese hinzufügen) Assign=Zuweisen diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties index 133fd9e7f2b..4f355612618 100644 --- a/src/main/resources/l10n/JabRef_en.properties +++ b/src/main/resources/l10n/JabRef_en.properties @@ -224,8 +224,6 @@ cut\ entries=cut entries cut\ entry\ %0=cut entry %0 -DOI\ not\ found=DOI not found - Library\ encoding=Library encoding Library\ properties=Library properties @@ -578,8 +576,6 @@ No\ journal\ names\ could\ be\ abbreviated.=No journal names could be abbreviate No\ journal\ names\ could\ be\ unabbreviated.=No journal names could be unabbreviated. -No\ DOI\ data\ exists=No DOI data exists - not=not not\ found=not found @@ -2489,6 +2485,17 @@ Version=Version Error\ downloading=Error downloading +No\ data\ was\ found\ for\ the\ identifier=No data was found for the identifier +Server\ not\ available=Server not available +Fetching\ information\ using\ %0=Fetching information using %0 +Look\ up\ identifier=Look up identifier + +Bibliographic\ data\ not\ found.\ Cause\ is\ likely\ the\ client\ side.\ Please\ check\ connection\ and\ identifier\ for\ correctness.=Bibliographic data not found. Cause is likely the client side. Please check connection and identifier for correctness. +Bibliographic\ data\ not\ found.\ Cause\ is\ likely\ the\ server\ side.\ Please\ try\ agan\ later.=Bibliographic data not found. Cause is likely the server side. Please try agan later. +Error\ message\ %0=Error message %0 +Identifier\ not\ found=Identifier not found + + Error\ while\ writing\ metadata.\ See\ the\ error\ log\ for\ details.=Error while writing metadata. See the error log for details. Failed\ to\ write\ metadata,\ file\ %1\ not\ found.=Failed to write metadata, file %1 not found. Success\!\ Finished\ writing\ metadata.=Success! Finished writing metadata. @@ -2496,22 +2503,27 @@ Success\!\ Finished\ writing\ metadata.=Success! Finished writing metadata. Custom\ API\ key=Custom API key Check\ %0\ API\ Key\ Setting=Check %0 API Key Setting -Edit\ field\ value=Edit field value -Two\ fields=Two fields -Overwrite\ Non\ empty\ fields=Overwrite Non empty fields +Edit\ content=Edit content +Copy\ or\ Move\ content=Copy or Move content +Overwrite\ field\ content=Overwrite field content Set=Set Append=Append -Clear\ field=Clear field -Field\ value=Field value -Edit\ field\ value\ of\ selected\ entries=Edit field value of selected entries +Clear\ field\ content=Clear field content +Set\ or\ append\ content=Set or append content +Edit\ field\ content\ for\ selected\ entries=Edit field content for selected entries Rename=Rename New\ field\ name=New field name -Copy\ value=Copy value -Move\ value=Move value -Swap\ values=Swap values -Copy\ or\ move\ the\ value\ of\ one\ field\ to\ another=Copy or move the value of one field to another +Copy\ content=Copy content +Move\ content=Move content +Swap\ content=Swap content +Copy\ or\ move\ the\ content\ of\ one\ field\ to\ another=Copy or move the content of one field to another Automatic\ field\ editor=Automatic field editor +From=From +Keep\ Modifications=Keep Modifications +To=To (Note\:\ If\ original\ entries\ lack\ keywords\ to\ qualify\ for\ the\ new\ group\ configuration,\ confirming\ here\ will\ add\ them)=(Note: If original entries lack keywords to qualify for the new group configuration, confirming here will add them) Assign=Assign Do\ not\ assign=Do not assign + +Error\ occured\ %0=Error occured %0 diff --git a/src/main/resources/l10n/JabRef_es.properties b/src/main/resources/l10n/JabRef_es.properties index 5667fb916b0..60e35d98ad0 100644 --- a/src/main/resources/l10n/JabRef_es.properties +++ b/src/main/resources/l10n/JabRef_es.properties @@ -1,7 +1,12 @@ +Could\ not\ delete\ empty\ entries.=No se pudieron eliminar entradas vacías. +Delete\ empty\ entries=Eliminar entradas vacías +Empty\ entries=Vaciar entradas +Keep\ empty\ entries=Mantener entradas vacías +Library\ '%0'\ has\ empty\ entries.\ Do\ you\ want\ to\ delete\ them?=La biblioteca '%0' tiene entradas vacías. ¿Quieres eliminarlas? Unable\ to\ monitor\ file\ changes.\ Please\ close\ files\ and\ processes\ and\ restart.\ You\ may\ encounter\ errors\ if\ you\ continue\ with\ this\ session.=No es posible supervisar los cambios en los archivos. Cierre los archivos y procesos y reinicie. Puede que se produzcan errores si continúa con esta sesión. %0\ contains\ the\ regular\ expression\ %1=%0 contiene la expresión regular %1 @@ -15,6 +20,7 @@ Unable\ to\ monitor\ file\ changes.\ Please\ close\ files\ and\ processes\ and\ %0/%1\ entries=%0/%1 entradas +Reveal\ in\ File\ Explorer=Mostrar en Explorador de Archivos %0\ matches\ the\ regular\ expression\ %1=%0 coincidencias con la Expresión Regular %1 @@ -49,6 +55,7 @@ The\ path\ need\ not\ be\ on\ the\ classpath\ of\ JabRef.=La ruta no debe estar Add\ a\ regular\ expression\ for\ the\ key\ pattern.=Añadir una expresión regular para el patrón clave. +Add\ entry\ manually=Añadir entrada manualmente Add\ selected\ entries\ to\ this\ group=Añadir entradas seleccionadas a este grupo @@ -166,6 +173,8 @@ Copy=Copiar Copy\ title=Copiar título Copy\ \\cite{citation\ key}=Copiar \\cite{citation key} +Copy\ citation\ (html)=Copiar cita (html) +Copy\ citation\ (text)=Copiar cita (texto) Copy\ citation\ key=Copiar clave de cita Copy\ citation\ key\ and\ link=Copiar clave de cita y enlace Copy\ citation\ key\ and\ title=Copiar clave y título de cita @@ -214,13 +223,17 @@ cut\ entries=Cortar entradas cut\ entry\ %0=corte de entrada %0 +DOI\ not\ found=DOI no encontrado Library\ encoding=Codificación de la biblioteca Library\ properties=Propiedades de la biblioteca +%0\ -\ Library\ properties=%0 - Propiedades de la biblioteca Default=Por defecto +Character\ encoding\ UTF-8\ is\ not\ supported.=La codificación de caracteres UTF-8 no restá soportada. +UTF-8\ could\ not\ be\ used\ to\ encode\ the\ following\ characters\:\ %0=UTF-8 no se pudo utilizar para codificar los siguientes caracteres\: %0 The\ chosen\ encoding\ '%0'\ could\ not\ encode\ the\ following\ characters\:=La codificación de caracteres '%' no puede codificar los siguientes caracteres\: Downloading=Descargando @@ -269,6 +282,7 @@ Downloaded\ website\ as\ an\ HTML\ file.=Se descargó el sitio web como un archi duplicate\ removal=eliminación de duplicados +Duplicate\ fields=Campos duplicados Duplicate\ string\ name=Nombre de cadena duplicado @@ -330,6 +344,7 @@ External\ file\ links=Enlaces a archivos externos External\ programs=Programas externos +Failed\ to\ import\ by\ ID=Error al importar por ID Field=Campo @@ -354,6 +369,7 @@ Filter=Filtro Filter\ groups=Filtros +Finished\ writing\ metadata\ for\ %0\ file\ (%1\ skipped,\ %2\ errors).=Escritura de metadatos para el archivo %0 finalizada (%1 omitidos, %2 errores). First\ select\ the\ entries\ you\ want\ keys\ to\ be\ generated\ for.=En primer lugar, seleccione las entradas para las que desea generar claves @@ -370,6 +386,7 @@ Formatter\ name=Nombre del formateador found\ in\ AUX\ file=encontrado en archivo AUX +Fulltext\ search=Búsqueda de texto completo Fulltext\ for=Texto completo de @@ -448,6 +465,9 @@ Include\ subgroups\:\ When\ selected,\ view\ entries\ contained\ in\ this\ group Independent\ group\:\ When\ selected,\ view\ only\ this\ group's\ entries=Grupo independiente\: ver sólo las entradas de este grupo cuando esté seleccionado. I\ Agree=Acepto +Indexing\ pdf\ files=Indexando archivos pdf +Indexing\ for\ %0=Indexando para %0 +%0\ of\ %1\ linked\ files\ added\ to\ the\ index=%0 de %1 archivos enlazados añadidos al índice Invalid\ citation\ key=La clave de cita no es válida @@ -461,6 +481,8 @@ JabRef\ requests\ recommendations\ from\ Mr.\ DLib,\ which\ is\ an\ external\ se JabRef\ Version\ (Required\ to\ ensure\ backwards\ compatibility\ with\ Mr.\ DLib's\ Web\ Service)=Versión JabRef (necesaria para asegurar la compatibilidad con el servicio web de Mr. DLib) Journal\ abbreviations=Abreviaturas de publicaciones +Journal\ lists\:=Listados de revistas\: +Remove\ journal\ '%0'=Eliminar revista '%0' Keep\ both=Mantener ambos @@ -496,6 +518,7 @@ Main\ file\ directory=Carpeta del archivo principal Manage\ custom\ exports=Administrar exportaciones personalizadas Manage\ custom\ imports=Administrar importaciones personalizadas +External\ file\ types=Tipos de archivos externos Mark\ new\ entries\ with\ owner\ name=Marcar nuevas entradas con nombre de propietario @@ -518,7 +541,9 @@ Moved\ group\ "%0".=Se ha movido el grupo "%0". Mr.\ DLib\ Privacy\ settings=Mr. DLib Configuración de la privacidad +No\ database\ is\ open=No hay ninguna base de datos abierta +We\ need\ a\ database\ to\ export\ from.\ Open\ one.=Necesitamos una base de datos desde la que exportar. Abrir una. No\ recommendations\ received\ from\ Mr.\ DLib\ for\ this\ entry.=No se han recibido recomendaciones del Mr. DLib para esta entrada. @@ -552,6 +577,7 @@ No\ journal\ names\ could\ be\ abbreviated.=No se pudieron abreviar nombres de r No\ journal\ names\ could\ be\ unabbreviated.=No se pudieron expandir nombres de revistas. +No\ DOI\ data\ exists=No existen datos DOI not=no @@ -678,16 +704,23 @@ Remove\ group=Eliminar grupo Remove\ group\ and\ subgroups=Eliminar grupo y subgrupos +Remove\ groups\ and\ subgroups=Eliminar grupos y subgrupos +Remove\ all\ selected\ groups\ and\ keep\ their\ subgroups?=¿Eliminar todos los grupos seleccionados y mantener sus subgrupos? +Remove\ group\ "%0"\ and\ keep\ its\ subgroups?=¿Eliminar grupo "%0" y mantener sus subgrupos? +Remove\ groups=Eliminar grupos +Removed\ all\ selected\ groups.=Eliminados todos los grupos seleccionados. Remove\ group\ "%0"\ and\ its\ subgroups?=¿Eliminar el grupo "%0" y sus subgrupos? Removed\ group\ "%0"\ and\ its\ subgroups.=Se ha eliminado el grupo "%0" y sus subgrupos. +Remove\ all\ selected\ groups\ and\ their\ subgroups?=¿Eliminar todos los grupos seleccionados y sus subgrupos? +Removed\ all\ selected\ groups\ and\ their\ subgroups.=Eliminados todos los grupos seleccionados y sus subgrupos. Remove\ link=Eliminar enlace @@ -719,6 +752,8 @@ Replaces\ Unicode\ ligatures\ with\ their\ expanded\ form=Reemplaza las ligadura Required\ fields=Campos requeridos +Do\ not\ resolve\ BibTeX\ strings=No resolver cadenas BibTeX +Resolve\ BibTeX\ strings\ for\ the\ following\ fields=Resolver las cadenas BibTeX para los siguientes campos resolved=resuelto @@ -726,11 +761,13 @@ Restart=Reiniciar Restart\ required=Reinicio requerido +Return\ to\ dialog=Volver al diálogo Review=Revisar Review\ changes=Revisar cambios Review\ Field\ Migration=Revisar campo de migración +Loading=Cargando Save=Guardar Save\ all\ finished.=Guardar todos los finalizados @@ -744,6 +781,7 @@ Save\ library\ as...=Guardar biblioteca como... Saving=Guardando Saving\ all\ libraries...=Guardando todas las bibliotecas... Saving\ library=Guardando biblioteca +Library\ saved=Biblioteca guardada Saved\ selected\ to\ '%0'.=Selección guardada en '%0'. Search=Buscar @@ -795,6 +833,12 @@ Size=Tamaño Skipped\ -\ No\ PDF\ linked=Omitido - No se enlazó PDF Skipped\ -\ PDF\ does\ not\ exist=Omitido - No existe el PDF +JabRef\ skipped\ the\ entry.=JabRef omitió la entrada. +Import\ error=Error al importar +Open\ library\ error=Error al abrir la biblioteca +Please\ check\ your\ library\ file\ for\ wrong\ syntax.=Por favor, comprueba errores de sintaxis en tu archivo de biblioteca. +SourceTab\ error=Error de SourceTab +User\ input\ via\ entry-editor\ in\ `{}bibtex\ source`\ tab\ led\ to\ failure.=Entrada de usuario a través del editor de entrada en la pestaña `{}bibtex source` condujo al error. Sort\ subgroups=Ordenar subgrupos @@ -883,13 +927,18 @@ Warning=Advertencia Warnings=Advertencias +Warning\:\ You\ added\ field\ "%0"\ twice.\ Only\ one\ will\ be\ kept.=Advertencia\: Añadiste el campo "%0" dos veces. Solo uno será utilizado. web\ link=enlace a web What\ do\ you\ want\ to\ do?=¿Qué desea hacer? Whatever\ option\ you\ choose,\ Mr.\ DLib\ may\ share\ its\ data\ with\ research\ partners\ to\ further\ improve\ recommendation\ quality\ as\ part\ of\ a\ 'living\ lab'.\ Mr.\ DLib\ may\ also\ release\ public\ datasets\ that\ may\ contain\ anonymized\ information\ about\ you\ and\ the\ recommendations\ (sensitive\ information\ such\ as\ metadata\ of\ your\ articles\ will\ be\ anonymised\ through\ e.g.\ hashing).\ Research\ partners\ are\ obliged\ to\ adhere\ to\ the\ same\ strict\ data\ protection\ policy\ as\ Mr.\ DLib.=Sea cual sea la opción que elija, Mr. DLib puede compartir sus datos con socios de investigación para mejorar aún más la calidad de las recomendaciones como parte de un "laboratorio vivo". Mr. DLib también puede publicar conjuntos de datos públicos que pueden contener información anónima sobre usted y las recomendaciones (la información confidencial como los metadatos de sus artículos será anonimizada a través de, por ejemplo, hashing). Los socios de investigación están obligados a adherirse a la misma estricta política de protección de datos que Mr. DLib. +Will\ write\ metadata\ to\ the\ PDFs\ linked\ from\ selected\ entries.=Escribirá metadatos en los PDFs enlazados desde entradas seleccionadas. +Write\ BibTeXEntry\ as\ metadata\ to\ PDF.=Escribir entrada BibTeX como metadatos XMP en el PDF. +Write\ metadata\ for\ all\ PDFs\ in\ current\ library?=¿Escribir metadatos para todos los PDFs de la biblioteca actual? +Writing\ metadata\ for\ selected\ entries...=Escribiendo metadatos para las entradas seleccionadas... Write\ BibTeXEntry\ as\ XMP\ metadata\ to\ PDF.=Escribe BibTeXEntry como metadatos XMP en los PDF. @@ -1587,6 +1636,8 @@ Issue\ report\ successful=Comunicación de problema satisfactoria Your\ issue\ was\ reported\ in\ your\ browser.=Su problema fue comunicado a través del navegador The\ log\ and\ exception\ information\ was\ copied\ to\ your\ clipboard.=La información de excepción y registro fue copiada a su portapapeles Please\ paste\ this\ information\ (with\ Ctrl+V)\ in\ the\ issue\ description.=Por favor, pegue esta información (con Ctrl+V) en la descripción del problema. +Last\ notification=Última notificación +Check\ the\ event\ log\ to\ see\ all\ notifications=Revisa el registro de eventos para ver todas las notificaciones Host=Host/Servidor Port=Puerto @@ -1732,6 +1783,7 @@ Delete\ '%0'=Eliminar '%0' Delete\ from\ disk=Eliminar del disco duro Remove\ from\ entry=Eliminar de la entrada There\ exists\ already\ a\ group\ with\ the\ same\ name.=Ya existe un grupo con el mismo nombre. +If\ you\ use\ it,\ it\ will\ inherit\ all\ entries\ from\ this\ other\ group.=Si lo utiliza, heredará todas las entradas de este otro grupo. Copy\ linked\ file=Copiar archivo enlazado Copy\ linked\ file\ to\ folder...=Copiar archivo enlazado a la carpeta... @@ -1787,6 +1839,7 @@ Could\ not\ connect\ to\ Vim\ server.\ Make\ sure\ that\ Vim\ is\ running\ with\ Could\ not\ connect\ to\ a\ running\ gnuserv\ process.\ Make\ sure\ that\ Emacs\ or\ XEmacs\ is\ running,\ and\ that\ the\ server\ has\ been\ started\ (by\ running\ the\ command\ 'server-start'/'gnuserv-start').=No se puede conectar con un proceso gnuserv en ejecución. Asegúrese de que Emacs o XEmacs se está ejecutando y de que el servidor ha sido iniciado (ejecutando el comando 'server-start'/'gnuserv-start'). Error\ pushing\ entries=Error al enviar entradas +Preamble=Preámbulo Markings=Marcados Use\ selected\ instance=Usar la instancia seleccionada @@ -1805,6 +1858,7 @@ Blog=Blog Check\ integrity=Verificar integridad Cleanup\ URL\ link=Limpiar un enlace URL Cleanup\ URL\ link\ by\ removing\ special\ symbols\ and\ extracting\ simple\ link=Limpiar un enlace URL eliminando los símbolos especiales y extrayendo un enlace simple +Copy\ DOI=Copiar DOI Copy\ DOI\ url=Copiar la url del DOI Development\ version=Versión de desarrollo Export\ selected\ entries=Exportar registros seleccionados @@ -1814,6 +1868,8 @@ JabRef\ resources=Recursos sobre JabRef Manage\ journal\ abbreviations=Administrar abreviaturas de publicaciones Manage\ protected\ terms=gestionar términos protegidos New\ entry\ from\ plain\ text=Nueva entrada desde texto sin formato +Import\ by\ ID=Importar por ID +Enter\ a\ valid\ ID=Introduzca un ID válido New\ sublibrary\ based\ on\ AUX\ file=Nueva subbiblioteca a partir de un archivo AUX Push\ entries\ to\ external\ application\ (%0)=Agregar registros a aplicación externa (%0) Quit=Salir @@ -1873,6 +1929,7 @@ Keyword\ separator=Separador de palabras clave Remove\ keyword=Eliminar palabra clave Are\ you\ sure\ you\ want\ to\ remove\ keyword\:\ "%0"?=¿Está seguro de que desea eliminar la palabra clave\: "%0"? Reset\ to\ default=Restablecer los valores por defecto +String\ constants=Constantes de cadena Export\ all\ entries=Exportar todas las entradas Generate\ citation\ keys=Generar claves de cita Manage\ field\ names\ &\ content=Gestionar nombres y contenido de los campos @@ -2002,6 +2059,7 @@ Please\ provide\ a\ valid\ aux\ file.=Por favor, proporcione un archivo AUX vál Keyword\ delimiter=Separador de palabras clave Hierarchical\ keyword\ delimiter=Separador de palabras clave jerárquicas Escape\ ampersands=Escape de ampersands +Escape\ dollar\ sign=Símbolo de escape dólar Hint\:\n\nTo\ search\ all\ fields\ for\ Smith,\ enter\:\nsmith\n\nTo\ search\ the\ field\ author\ for\ Smith\ and\ the\ field\ title\ for\ electrical,\ enter\:\nauthor\=Smith\ and\ title\=electrical=Consejo\:\n\nPara buscar Pedro en todos los campos, escriba\:\npedro\n\nPara buscar Pedro en el campo author y eléctrico en el campo title, escriba\:\nauthor\=Pedro and title\=eléctrico @@ -2211,7 +2269,11 @@ Reveal\ in\ file\ explorer=Revelar en el explorador de archivos Autolink\ files=Enlazar archivos automáticamente +Custom\ editor\ tabs=Pestañas de editor personalizadas +Custom\ export\ formats=Formatos de exportación personalizados +Custom\ import\ formats=Formatos de importación personalizados +No\ list\ enabled=No hay lista habilitada Protect\ selection=Proteger selección Customized\ preview\ style=Estilo de previsualización personalizado @@ -2238,7 +2300,17 @@ Regular\ expression=Expresión regular Error\ importing.\ See\ the\ error\ log\ for\ details.=Error al importar. Consulte el registro de errores para obtener detalles. - +Error\ from\ import\:\ %0=Error de importación\: %0 +Error\ reading\ PDF\ content\:\ %0=Error al leer el contenido PDF\: %0 +Importing\ bib\ entry=Importando entrada bibliográfica +Importing\ using\ extracted\ PDF\ data=Importación usando datos extraídos del PDF +No\ BibTeX\ data\ found.\ Creating\ empty\ entry\ with\ file\ link=No se encontraron datos BibTeX. Creando entrada vacía con enlace de archivo +No\ metadata\ found.\ Creating\ empty\ entry\ with\ file\ link=No se encontraron metadatos. Creando entrada vacía con enlace de archivo +Processing\ file\ %0=Procesando archivo %0 +Export\ selected=Exportar seleccionados + +Separate\ merged\ citations=Citas fusionadas separadas +Separate\ citations=Citas separadas Custom\ DOI\ URI=URI personalizado de DOI diff --git a/src/main/resources/l10n/JabRef_fr.properties b/src/main/resources/l10n/JabRef_fr.properties index 46020ee888c..42edf5e63ee 100644 --- a/src/main/resources/l10n/JabRef_fr.properties +++ b/src/main/resources/l10n/JabRef_fr.properties @@ -19,7 +19,9 @@ Unable\ to\ monitor\ file\ changes.\ Please\ close\ files\ and\ processes\ and\ %0/%1\ entries=%0/%1 entrées +Export\ operation\ finished\ successfully.=L'opération d'export s'est terminée avec succès. +Reveal\ in\ File\ Explorer=Montrer dans l'explorateur de fichiers %0\ matches\ the\ regular\ expression\ %1=%0 correspond à l'expression régulière %1 @@ -703,16 +705,23 @@ Remove\ group=Supprimer le groupe Remove\ group\ and\ subgroups=Supprimer le groupe et les sous-groupes +Remove\ groups\ and\ subgroups=Supprimer les groupes et les sous-groupes +Remove\ all\ selected\ groups\ and\ keep\ their\ subgroups?=Supprimer tous les groupes sélectionnés et conserver leurs sous-groupes ? +Remove\ group\ "%0"\ and\ keep\ its\ subgroups?=Supprimer le groupe « %0 » et conserver ses sous-groupes ? +Remove\ groups=Supprimer les groupes +Removed\ all\ selected\ groups.=Tous les groupes sélectionnés ont été supprimés. Remove\ group\ "%0"\ and\ its\ subgroups?=Supprimer le groupe « %0 » et ses sous-groupes ? Removed\ group\ "%0"\ and\ its\ subgroups.=Groupe « %0 » et ses sous-groupes supprimés. +Remove\ all\ selected\ groups\ and\ their\ subgroups?=Supprimer tous les groupes sélectionnés et leurs sous-groupes ? +Removed\ all\ selected\ groups\ and\ their\ subgroups.=Tous les groupes sélectionnés et leurs sous-groupes ont été supprimés. Remove\ link=Supprimer le lien @@ -825,6 +834,12 @@ Size=Taille Skipped\ -\ No\ PDF\ linked=Sauté - Pas de PDF lié Skipped\ -\ PDF\ does\ not\ exist=Omis - Le PDF n'existe pas +JabRef\ skipped\ the\ entry.=JabRef a ignoré l'entrée. +Import\ error=Erreur d'importation +Open\ library\ error=Erreur d'ouverture du fichier +Please\ check\ your\ library\ file\ for\ wrong\ syntax.=Veuillez vérifier la syntaxe de votre fichier bibliographique. +SourceTab\ error=Erreur de SourceTab +User\ input\ via\ entry-editor\ in\ `{}bibtex\ source`\ tab\ led\ to\ failure.=La saisie de l'utilisateur via l'éditeur d'entrée dans l'onglet `{}bibtex source` a conduit à un échec. Sort\ subgroups=Trier les sous-groupes @@ -970,7 +985,7 @@ Line\ %0\:\ Found\ corrupted\ citation\ key\ %1\ (contains\ whitespaces).=Ligne Line\ %0\:\ Found\ corrupted\ citation\ key\ %1\ (comma\ missing).=Ligne %0 \: clef de citation corrompue %1 (virgule manquante). No\ full\ text\ document\ found=Aucun texte intégral trouvé Download\ from\ URL=Télécharger depuis l'URL -Rename\ field=Renommer le champ +Rename\ field=Renommer Cannot\ use\ port\ %0\ for\ remote\ operation;\ another\ application\ may\ be\ using\ it.\ Try\ specifying\ another\ port.=Le port %0 ne peut pas être utilisé pour une opération à distance ; un autre logiciel pourrait être en train de l'utiliser. Essayer de spécifier un autre port. @@ -1244,6 +1259,7 @@ Connection\ failed\!=Échec de la connexion \! Connection\ successful\!=Connexion réussie \! SSL\ Configuration=Configuration SSL +SSL\ configuration\ changed=Configuration SSL modifiée SSL\ certificate\ file=Fichier de certificat SSL Duplicate\ Certificates=Dupliquer les certificats You\ already\ added\ this\ certificate=Vous avez déjà ajouté ce certificat @@ -1283,6 +1299,7 @@ Please\ open\ %0\ manually.=Veuillez ouvrir manuellement %0 . The\ link\ has\ been\ copied\ to\ the\ clipboard.=Le lien a été copié dans le presse-papiers. Open\ %0\ file=Ouvrir le fichier %0 +Could\ not\ detect\ terminal\ automatically.\ Please\ define\ a\ custom\ terminal\ in\ the\ preferences.=Impossible de détecter le terminal automatiquement. Veuillez définir un terminal personnalisé dans les préférences. Cannot\ delete\ file=Le fichier ne peut pas être supprimé File\ permission\ error=Erreur due aux permissions du fichier @@ -1670,6 +1687,8 @@ Issue\ report\ successful=Signalement de l'anomalie réussi Your\ issue\ was\ reported\ in\ your\ browser.=Votre anomalie a été affichée dans votre navigateur. The\ log\ and\ exception\ information\ was\ copied\ to\ your\ clipboard.=Le journal et les informations d'anomalie ont été copiées dans votre presse-papier. Please\ paste\ this\ information\ (with\ Ctrl+V)\ in\ the\ issue\ description.=Veuillez coller ces informations (avec Ctrl+V) dans la description de l'anomalie. +Last\ notification=Dernière notification +Check\ the\ event\ log\ to\ see\ all\ notifications=Consultez le journal des événements pour voir toutes les notifications Host=Hôte Port=Port @@ -2477,4 +2496,25 @@ Success\!\ Finished\ writing\ metadata.=Succès \! Écriture des métadonnées t Custom\ API\ key=Clef d'API personnalisée Check\ %0\ API\ Key\ Setting=Vérifier les paramètres de la clef d'API %0 - +Edit\ content=Modifier le contenu +Copy\ or\ Move\ content=Copier ou déplacer le contenu +Overwrite\ field\ content=Écraser le contenu du champ +Set=Définir +Append=Ajouter +Clear\ field\ content=Effacer le contenu du champ +Set\ or\ append\ content=Définir ou ajouter du contenu +Edit\ field\ content\ for\ selected\ entries=Modifier le contenu d'un champ +Rename=Renommer +New\ field\ name=Nouveau nom de champ +Copy\ content=Copier le contenu +Move\ content=Déplacer le contenu +Swap\ content=Permuter le contenu +Copy\ or\ move\ the\ content\ of\ one\ field\ to\ another=Copier ou déplacer le contenu d'un champ vers un autre +Automatic\ field\ editor=Éditeur automatique de champs +From=De +Keep\ Modifications=Enregistrer +To=Vers + +(Note\:\ If\ original\ entries\ lack\ keywords\ to\ qualify\ for\ the\ new\ group\ configuration,\ confirming\ here\ will\ add\ them)=(Note \: si les entrées originales n'ont pas de mots-clefs correspondant à la nouvelle configuration du groupe, confirmer ici les ajoutera) +Assign=Assigner +Do\ not\ assign=Ne pas assigner diff --git a/src/main/resources/l10n/JabRef_it.properties b/src/main/resources/l10n/JabRef_it.properties index 6bac76a45cb..2e9126ad31c 100644 --- a/src/main/resources/l10n/JabRef_it.properties +++ b/src/main/resources/l10n/JabRef_it.properties @@ -2441,21 +2441,24 @@ Search\ results\ from\ open\ libraries=Risultati della ricerca da librerie apert Custom\ API\ key=Chiave API personalizzata Check\ %0\ API\ Key\ Setting=Controlla le impostazioni della chiave API %0 -Edit\ field\ value=Modifica valore campo -Two\ fields=Due campi -Overwrite\ Non\ empty\ fields=Sovrascrivi campi non vuoti +Edit\ content=Modifica il contenuto +Copy\ or\ Move\ content=Copia o Sposta il contenuto +Overwrite\ field\ content=Sovrascrivi il contenuto del campo Set=Imposta Append=Accoda -Clear\ field=Svuota campo -Field\ value=Valore del campo -Edit\ field\ value\ of\ selected\ entries=Modifica il valore del campo delle voci selezionate +Clear\ field\ content=Cancella il contenuto del campo +Set\ or\ append\ content=Imposta o aggiungi il contenuto +Edit\ field\ content\ for\ selected\ entries=Modifica il contenuto del campo per le voci selezionate Rename=Rinomina New\ field\ name=Nuovo nome del campo -Copy\ value=Copia Valore -Move\ value=Sposta valore -Swap\ values=Scambia i valori -Copy\ or\ move\ the\ value\ of\ one\ field\ to\ another=Copia o sposta il valore di un campo in un altro +Copy\ content=Copia il contenuto +Move\ content=Sposta il contenuto +Swap\ content=Scambia il contenuto +Copy\ or\ move\ the\ content\ of\ one\ field\ to\ another=Copia o sposta il contenuto di un campo in un altro Automatic\ field\ editor=Editor automatico dei campi +From=Da +Keep\ Modifications=Mantieni le modifiche +To=A (Note\:\ If\ original\ entries\ lack\ keywords\ to\ qualify\ for\ the\ new\ group\ configuration,\ confirming\ here\ will\ add\ them)=(Nota\: se le voci originali mancano di parole chiave per qualificarsi per la nuova configurazione di gruppo, confermando qui le aggiungeranno) Assign=Assegna diff --git a/src/main/resources/l10n/JabRef_ru.properties b/src/main/resources/l10n/JabRef_ru.properties index 6980f2d32bb..0999c1ddcb0 100644 --- a/src/main/resources/l10n/JabRef_ru.properties +++ b/src/main/resources/l10n/JabRef_ru.properties @@ -2496,20 +2496,10 @@ Success\!\ Finished\ writing\ metadata.=Успех\! Запись метадан Custom\ API\ key=Пользовательский ключ API Check\ %0\ API\ Key\ Setting=Проверьте настройку ключа API %0 -Edit\ field\ value=Изменить значение поля -Two\ fields=Два поля -Overwrite\ Non\ empty\ fields=Перезаписать непустые поля Set=Задать Append=Присоединить -Clear\ field=Очистить поле -Field\ value=Значение поля -Edit\ field\ value\ of\ selected\ entries=Изменить значение поля выбранных записей Rename=Переименовать New\ field\ name=Новое имя поля -Copy\ value=Копировать значение -Move\ value=Переместить значение -Swap\ values=Обменять значения -Copy\ or\ move\ the\ value\ of\ one\ field\ to\ another=Скопировать или переместить значение одного поля в другое Automatic\ field\ editor=Автоматический редактор поля (Note\:\ If\ original\ entries\ lack\ keywords\ to\ qualify\ for\ the\ new\ group\ configuration,\ confirming\ here\ will\ add\ them)=(Примечание\: Если в исходных записях отсутствуют ключевые слова, подходящие для конфигурации новой группы, то при подтверждении здесь они будут добавлены) diff --git a/src/main/resources/l10n/JabRef_zh_CN.properties b/src/main/resources/l10n/JabRef_zh_CN.properties index 1b467a0de4f..baca5d6da46 100644 --- a/src/main/resources/l10n/JabRef_zh_CN.properties +++ b/src/main/resources/l10n/JabRef_zh_CN.properties @@ -2496,20 +2496,10 @@ Success\!\ Finished\ writing\ metadata.=Success\! Finished writing metadata. Custom\ API\ key=自定义API Check\ %0\ API\ Key\ Setting=Check %0 API Key Setting -Edit\ field\ value=编辑字段内容 -Two\ fields=两个字段 -Overwrite\ Non\ empty\ fields=覆盖非空字段 Set=设定 Append=附加 -Clear\ field=清除字段 -Field\ value=字段内容 -Edit\ field\ value\ of\ selected\ entries=编辑选中条目的字段内容 Rename=重命名 New\ field\ name=新的字段名 -Copy\ value=复制值 -Move\ value=移动值 -Swap\ values=交换值 -Copy\ or\ move\ the\ value\ of\ one\ field\ to\ another=复制或移动一个字段的值到另一个字段中 Automatic\ field\ editor=自动化条目编辑 (Note\:\ If\ original\ entries\ lack\ keywords\ to\ qualify\ for\ the\ new\ group\ configuration,\ confirming\ here\ will\ add\ them)=(Note\: If original entries lack keywords to qualify for the new group configuration, confirming here will add them) diff --git a/src/main/resources/l10n/JabRef_zh_TW.properties b/src/main/resources/l10n/JabRef_zh_TW.properties index 288532048c0..6e74ac8da11 100644 --- a/src/main/resources/l10n/JabRef_zh_TW.properties +++ b/src/main/resources/l10n/JabRef_zh_TW.properties @@ -991,7 +991,7 @@ Set\ rank\ to\ five=設定評分為 5 級 Order=順序 -Affected\ fields\:=影響欄位\: +Affected\ fields\:=影響欄位\: Show\ preview\ as\ a\ tab\ in\ entry\ editor=在條目編輯器中以分頁形式顯示預覽 Font=字型 Visual\ theme=界面主題 diff --git a/src/test/java/org/jabref/gui/edit/CopyOrMoveFieldContentTabViewModelTest.java b/src/test/java/org/jabref/gui/edit/CopyOrMoveFieldContentTabViewModelTest.java new file mode 100644 index 00000000000..75a1a176597 --- /dev/null +++ b/src/test/java/org/jabref/gui/edit/CopyOrMoveFieldContentTabViewModelTest.java @@ -0,0 +1,105 @@ +package org.jabref.gui.edit; + +import java.util.List; +import java.util.Optional; + +import org.jabref.gui.StateManager; +import org.jabref.gui.edit.automaticfiededitor.copyormovecontent.CopyOrMoveFieldContentTabViewModel; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +public class CopyOrMoveFieldContentTabViewModelTest { + CopyOrMoveFieldContentTabViewModel copyOrMoveFieldContentTabViewModel; + BibEntry entryA; + BibEntry entryB; + + BibDatabase bibDatabase; + + StateManager stateManager = mock(StateManager.class); + + @BeforeEach + void setup() { + entryA = new BibEntry(BibEntry.DEFAULT_TYPE) + .withField(StandardField.YEAR, "2015") + .withField(StandardField.DATE, "2014"); + + entryB = new BibEntry(BibEntry.DEFAULT_TYPE) + .withField(StandardField.DATE, "1998"); + bibDatabase = new BibDatabase(); + copyOrMoveFieldContentTabViewModel = newTwoFieldsViewModel(entryA, entryB); + } + + @Test + void copyValueDoesNotCopyBlankValues() { + CopyOrMoveFieldContentTabViewModel copyOrMoveFieldContentTabViewModel = newTwoFieldsViewModel(entryA, entryB); + + copyOrMoveFieldContentTabViewModel.fromFieldProperty().set(StandardField.YEAR); + copyOrMoveFieldContentTabViewModel.toFieldProperty().set(StandardField.DATE); + copyOrMoveFieldContentTabViewModel.overwriteFieldContentProperty().set(true); + copyOrMoveFieldContentTabViewModel.copyValue(); + + assertEquals(Optional.of("2015"), entryA.getField(StandardField.DATE), "YEAR field is not copied correctly to the DATE field"); + assertEquals(Optional.of("2015"), entryA.getField(StandardField.YEAR), "YEAR field should not have changed"); + assertEquals(Optional.of("1998"), entryB.getField(StandardField.DATE), "DATE field should not have changed because the YEAR field is blank e.g it doesn't exist"); + } + + @Test + void swapValuesShouldNotSwapFieldValuesIfOneOfTheValuesIsBlank() { + copyOrMoveFieldContentTabViewModel.fromFieldProperty().set(StandardField.YEAR); + copyOrMoveFieldContentTabViewModel.toFieldProperty().set(StandardField.DATE); + copyOrMoveFieldContentTabViewModel.overwriteFieldContentProperty().set(true); + + copyOrMoveFieldContentTabViewModel.swapValues(); + + assertEquals(Optional.of("1998"), entryB.getField(StandardField.DATE)); + assertEquals(Optional.empty(), entryB.getField(StandardField.YEAR)); + } + + @Test + void swapValuesShouldSwapFieldValuesIfBothValuesAreNotBlank() { + copyOrMoveFieldContentTabViewModel.fromFieldProperty().set(StandardField.YEAR); + copyOrMoveFieldContentTabViewModel.toFieldProperty().set(StandardField.DATE); + copyOrMoveFieldContentTabViewModel.overwriteFieldContentProperty().set(true); + + copyOrMoveFieldContentTabViewModel.swapValues(); + + assertEquals(List.of(Optional.of("2014"), Optional.of("2015")), + List.of(entryA.getField(StandardField.YEAR), entryA.getField(StandardField.DATE)), + "YEAR and DATE values didn't swap"); + } + + @Test + void moveValueShouldNotMoveValueIfToFieldIsNotBlankAndOverwriteIsNotEnabled() { + copyOrMoveFieldContentTabViewModel.fromFieldProperty().set(StandardField.YEAR); + copyOrMoveFieldContentTabViewModel.toFieldProperty().set(StandardField.DATE); + copyOrMoveFieldContentTabViewModel.overwriteFieldContentProperty().set(false); + + copyOrMoveFieldContentTabViewModel.moveValue(); + + assertEquals(Optional.of("2015"), entryA.getField(StandardField.YEAR)); + assertEquals(Optional.of("2014"), entryA.getField(StandardField.DATE)); + } + + @Test + void moveValueShouldMoveValueIfOverwriteIsEnabled() { + copyOrMoveFieldContentTabViewModel.fromFieldProperty().set(StandardField.DATE); + copyOrMoveFieldContentTabViewModel.toFieldProperty().set(StandardField.YEAR); + copyOrMoveFieldContentTabViewModel.overwriteFieldContentProperty().set(true); + + copyOrMoveFieldContentTabViewModel.moveValue(); + + assertEquals(Optional.of("1998"), entryB.getField(StandardField.YEAR)); + assertEquals(Optional.empty(), entryB.getField(StandardField.DATE)); + } + + private CopyOrMoveFieldContentTabViewModel newTwoFieldsViewModel(BibEntry... selectedEntries) { + return new CopyOrMoveFieldContentTabViewModel(List.of(selectedEntries), bibDatabase, stateManager); + } +} diff --git a/src/test/java/org/jabref/gui/edit/EditFieldContentTabViewModelTest.java b/src/test/java/org/jabref/gui/edit/EditFieldContentTabViewModelTest.java new file mode 100644 index 00000000000..1221580762a --- /dev/null +++ b/src/test/java/org/jabref/gui/edit/EditFieldContentTabViewModelTest.java @@ -0,0 +1,118 @@ +package org.jabref.gui.edit; + +import java.util.List; +import java.util.Optional; + +import org.jabref.gui.StateManager; +import org.jabref.gui.edit.automaticfiededitor.editfieldcontent.EditFieldContentViewModel; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.mockito.Mockito.mock; + +public class EditFieldContentTabViewModelTest { + EditFieldContentViewModel editFieldContentViewModel; + BibEntry entryA; + BibEntry entryB; + + BibDatabase bibDatabase; + + StateManager stateManager = mock(StateManager.class); + + @BeforeEach + void setup() { + entryA = new BibEntry(BibEntry.DEFAULT_TYPE) + .withField(StandardField.YEAR, "2015") + .withField(StandardField.DATE, "2014"); + + entryB = new BibEntry(BibEntry.DEFAULT_TYPE) + .withField(StandardField.DATE, "1998") + .withField(StandardField.YEAR, ""); + + bibDatabase = new BibDatabase(); + editFieldContentViewModel = new EditFieldContentViewModel(bibDatabase, List.of(entryA, entryB), stateManager); + } + + @Test + void clearSelectedFieldShouldClearFieldContentEvenWhenOverwriteFieldContentIsNotEnabled() { + editFieldContentViewModel.selectedFieldProperty().set(StandardField.YEAR); + editFieldContentViewModel.overwriteFieldContentProperty().set(false); + editFieldContentViewModel.clearSelectedField(); + + assertEquals(Optional.empty(), entryA.getField(StandardField.YEAR)); + } + + @Test + void clearSelectedFieldShouldDoNothingWhenFieldDoesntExistOrIsEmpty() { + editFieldContentViewModel.selectedFieldProperty().set(StandardField.FILE); + editFieldContentViewModel.clearSelectedField(); + + assertEquals(Optional.empty(), entryA.getField(StandardField.FILE)); + } + + @Test + void setFieldValueShouldNotDoAnythingIfOverwriteFieldContentIsNotEnabled() { + editFieldContentViewModel.overwriteFieldContentProperty().set(false); + editFieldContentViewModel.selectedFieldProperty().set(StandardField.YEAR); + editFieldContentViewModel.fieldValueProperty().set("2001"); + editFieldContentViewModel.setFieldValue(); + + assertEquals(Optional.of("2015"), entryA.getField(StandardField.YEAR)); + } + + @Test + void setFieldValueShouldSetFieldValueIfOverwriteFieldContentIsEnabled() { + editFieldContentViewModel.overwriteFieldContentProperty().set(true); + editFieldContentViewModel.selectedFieldProperty().set(StandardField.YEAR); + editFieldContentViewModel.fieldValueProperty().set("2001"); + editFieldContentViewModel.setFieldValue(); + + assertEquals(Optional.of("2001"), entryA.getField(StandardField.YEAR)); + } + + @Test + void setFieldValueShouldSetFieldValueIfFieldContentIsEmpty() { + editFieldContentViewModel.overwriteFieldContentProperty().set(false); + editFieldContentViewModel.selectedFieldProperty().set(StandardField.YEAR); + editFieldContentViewModel.fieldValueProperty().set("2001"); + editFieldContentViewModel.setFieldValue(); + + assertEquals(Optional.of("2001"), entryB.getField(StandardField.YEAR)); + } + + @Test + void appendToFieldValueShouldDoNothingIfOverwriteFieldContentIsNotEnabled() { + editFieldContentViewModel.overwriteFieldContentProperty().set(false); + editFieldContentViewModel.selectedFieldProperty().set(StandardField.YEAR); + editFieldContentViewModel.fieldValueProperty().set("0"); + editFieldContentViewModel.appendToFieldValue(); + + assertEquals(Optional.of("2015"), entryA.getField(StandardField.YEAR)); + } + + @Test + void appendToFieldValueShouldAppendFieldValueIfOverwriteFieldContentIsEnabled() { + editFieldContentViewModel.overwriteFieldContentProperty().set(true); + editFieldContentViewModel.selectedFieldProperty().set(StandardField.YEAR); + editFieldContentViewModel.fieldValueProperty().set("0"); + editFieldContentViewModel.appendToFieldValue(); + + assertEquals(Optional.of("20150"), entryA.getField(StandardField.YEAR)); + } + + @Test + void getAllFieldsShouldNeverBeEmpty() { + assertNotEquals(0, editFieldContentViewModel.getAllFields().size()); + } + + @Test + void getSelectedFieldShouldHaveADefaultValue() { + assertNotEquals(null, editFieldContentViewModel.getSelectedField()); + } +} diff --git a/src/test/java/org/jabref/gui/edit/RenameFieldViewModelTest.java b/src/test/java/org/jabref/gui/edit/RenameFieldViewModelTest.java new file mode 100644 index 00000000000..bf5a246e97f --- /dev/null +++ b/src/test/java/org/jabref/gui/edit/RenameFieldViewModelTest.java @@ -0,0 +1,111 @@ +package org.jabref.gui.edit; + +import java.util.List; +import java.util.Optional; + +import org.jabref.gui.StateManager; +import org.jabref.gui.edit.automaticfiededitor.renamefield.RenameFieldViewModel; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.FieldFactory; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.field.UnknownField; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +public class RenameFieldViewModelTest { + RenameFieldViewModel renameFieldViewModel; + BibEntry entryA; + BibEntry entryB; + + BibDatabase bibDatabase; + + StateManager stateManager = mock(StateManager.class); + + @BeforeEach + void setup() { + entryA = new BibEntry(BibEntry.DEFAULT_TYPE) + .withField(StandardField.YEAR, "2015") + .withField(StandardField.DATE, "2014") + .withField(StandardField.AUTHOR, "Doe"); + + entryB = new BibEntry(BibEntry.DEFAULT_TYPE) + .withField(StandardField.DATE, "1998") + .withField(StandardField.YEAR, "") + .withField(StandardField.AUTHOR, "Eddie"); + + bibDatabase = new BibDatabase(); + renameFieldViewModel = new RenameFieldViewModel(List.of(entryA, entryB), bibDatabase, stateManager); + } + + @Test + void renameFieldShouldRenameFieldIfItExist() { + renameFieldViewModel.selectField(StandardField.DATE); + renameFieldViewModel.setNewFieldName("ETAD"); + renameFieldViewModel.renameField(); + + assertEquals(Optional.of("2014"), entryA.getField(FieldFactory.parseField("ETAD"))); + assertEquals(Optional.empty(), entryA.getField(StandardField.DATE)); + + assertEquals(Optional.of("1998"), entryB.getField(FieldFactory.parseField("ETAD"))); + assertEquals(Optional.empty(), entryB.getField(StandardField.DATE)); + } + + @Test + void renameFieldShouldDoNothingIfFieldDoNotExist() { + Field toRenameField = new UnknownField("Some_field_that_doesnt_exist"); + renameFieldViewModel.selectField(toRenameField); + renameFieldViewModel.setNewFieldName("new_field_name"); + renameFieldViewModel.renameField(); + + assertEquals(Optional.empty(), entryA.getField(toRenameField)); + assertEquals(Optional.empty(), entryA.getField(new UnknownField("new_field_name"))); + + assertEquals(Optional.empty(), entryB.getField(toRenameField)); + assertEquals(Optional.empty(), entryB.getField(new UnknownField("new_field_name"))); + } + + @Test + void renameFieldShouldNotDoAnythingIfTheNewFieldNameIsEmpty() { + renameFieldViewModel.selectField(StandardField.AUTHOR); + renameFieldViewModel.setNewFieldName(""); + renameFieldViewModel.renameField(); + + assertEquals(Optional.of("Doe"), entryA.getField(StandardField.AUTHOR)); + assertEquals(Optional.empty(), entryA.getField(FieldFactory.parseField(""))); + + assertEquals(Optional.of("Eddie"), entryB.getField(StandardField.AUTHOR)); + assertEquals(Optional.empty(), entryB.getField(FieldFactory.parseField(""))); + } + + @Test + void renameFieldShouldNotDoAnythingIfTheNewFieldNameHasWhitespaceCharacters() { + renameFieldViewModel.selectField(StandardField.AUTHOR); + renameFieldViewModel.setNewFieldName("Hello, World"); + renameFieldViewModel.renameField(); + + assertEquals(Optional.of("Doe"), entryA.getField(StandardField.AUTHOR)); + assertEquals(Optional.empty(), entryA.getField(FieldFactory.parseField("Hello, World"))); + + assertEquals(Optional.of("Eddie"), entryB.getField(StandardField.AUTHOR)); + assertEquals(Optional.empty(), entryB.getField(FieldFactory.parseField("Hello, World"))); + } + + @Test + void renameFieldShouldDoNothingWhenThereIsAlreadyAFieldWithTheSameNameAsNewFieldName() { + renameFieldViewModel.selectField(StandardField.DATE); + renameFieldViewModel.setNewFieldName(StandardField.YEAR.getName()); + renameFieldViewModel.renameField(); + + assertEquals(Optional.of("2014"), entryA.getField(StandardField.DATE)); + assertEquals(Optional.of("2015"), entryA.getField(StandardField.YEAR)); + + assertEquals(Optional.empty(), entryB.getField(StandardField.DATE)); + assertEquals(Optional.of("1998"), entryB.getField(StandardField.YEAR)); + } +} diff --git a/src/test/java/org/jabref/logic/bibtex/comparator/GroupDiffTest.java b/src/test/java/org/jabref/logic/bibtex/comparator/GroupDiffTest.java index 1f1c20fc329..9fded7c7802 100644 --- a/src/test/java/org/jabref/logic/bibtex/comparator/GroupDiffTest.java +++ b/src/test/java/org/jabref/logic/bibtex/comparator/GroupDiffTest.java @@ -56,7 +56,7 @@ void compareWithChangedGroup() { Optional groupDiff = GroupDiff.compare(originalMetaData, newMetaData); - Optional expectedGroupDiff = Optional.of(new GroupDiff(newMetaData.getGroups().get(), originalMetaData.getGroups().get())); + Optional expectedGroupDiff = Optional.of(new GroupDiff(originalMetaData.getGroups().get(), newMetaData.getGroups().get())); assertEquals(expectedGroupDiff.get().getNewGroupRoot(), groupDiff.get().getNewGroupRoot()); assertEquals(expectedGroupDiff.get().getOriginalGroupRoot(), groupDiff.get().getOriginalGroupRoot()); diff --git a/src/test/java/org/jabref/logic/bst/BstFunctionsTest.java b/src/test/java/org/jabref/logic/bst/BstFunctionsTest.java new file mode 100644 index 00000000000..1b63e26cbb3 --- /dev/null +++ b/src/test/java/org/jabref/logic/bst/BstFunctionsTest.java @@ -0,0 +1,666 @@ +package org.jabref.logic.bst; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.jabref.logic.bst.util.BstCaseChangersTest; +import org.jabref.logic.bst.util.BstNameFormatterTest; +import org.jabref.logic.bst.util.BstPurifierTest; +import org.jabref.logic.bst.util.BstTextPrefixerTest; +import org.jabref.logic.bst.util.BstWidthCalculatorTest; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.StandardEntryType; + +import org.antlr.v4.runtime.RecognitionException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * For additional tests see for + * + * purify: {@link BstPurifierTest} + * width: {@link BstWidthCalculatorTest} + * format.name: {@link BstNameFormatterTest} + * change.case: {@link BstCaseChangersTest} + * prefix: {@link BstTextPrefixerTest} + * + */ +class BstFunctionsTest { + @Test + public void testCompareFunctions() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test.compare } { + #5 #5 = % TRUE + #1 #2 = % FALSE + #3 #4 < % TRUE + #4 #3 < % FALSE + #4 #4 < % FALSE + #3 #4 > % FALSE + #4 #3 > % TRUE + #4 #4 > % FALSE + "H" "H" = % TRUE + "H" "Ha" = % FALSE + } + EXECUTE { test.compare } + """); + + vm.render(Collections.emptyList()); + + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testArithmeticFunctions() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { + #1 #1 + % 2 + #5 #2 - % 3 + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals(3, vm.getStack().pop()); + assertEquals(2, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testArithmeticFunctionTypeMismatch() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { + #1 "HELLO" + % Should throw exception + } + EXECUTE { test } + """); + + assertThrows(BstVMException.class, () -> vm.render(Collections.emptyList())); + } + + @Test + public void testStringOperations() throws RecognitionException { + // Test for concat (*) and add.period + BstVM vm = new BstVM(""" + FUNCTION { test } { + "H" "ello" * % Hello + "Johnny" add.period$ % Johnny. + "Johnny." add.period$ % Johnny. + "Johnny!" add.period$ % Johnny! + "Johnny?" add.period$ % Johnny? + "Johnny} }}}" add.period$ % Johnny.} + "Johnny!}" add.period$ % Johnny!} + "Johnny?}" add.period$ % Johnny?} + "Johnny.}" add.period$ % Johnny.} + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals("Johnny.}", vm.getStack().pop()); + assertEquals("Johnny?}", vm.getStack().pop()); + assertEquals("Johnny!}", vm.getStack().pop()); + assertEquals("Johnny.}", vm.getStack().pop()); + assertEquals("Johnny?", vm.getStack().pop()); + assertEquals("Johnny!", vm.getStack().pop()); + assertEquals("Johnny.", vm.getStack().pop()); + assertEquals("Johnny.", vm.getStack().pop()); + assertEquals("Hello", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testMissing() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { title } { } { } + FUNCTION { presort } { cite$ 'sort.key$ := } + ITERATE { presort } + READ + SORT + FUNCTION { test } { title missing$ cite$ } + ITERATE { test } + """); + List testEntries = List.of( + BstVMTest.defaultTestEntry(), + new BibEntry(StandardEntryType.Article) + .withCitationKey("test") + .withField(StandardField.AUTHOR, "No title")); + + vm.render(testEntries); + + assertEquals("test", vm.getStack().pop()); // cite + assertEquals(BstVM.TRUE, vm.getStack().pop()); // missing title + assertEquals("canh05", vm.getStack().pop()); // cite + assertEquals(BstVM.FALSE, vm.getStack().pop()); // missing title + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testNumNames() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { + "Johnny Foo { and } Mary Bar" num.names$ + "Johnny Foo and Mary Bar" num.names$ + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals(2, vm.getStack().pop()); + assertEquals(1, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testSubstring() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { + "123456789" #2 #1 substring$ % 2 + "123456789" #4 global.max$ substring$ % 456789 + "123456789" #1 #9 substring$ % 123456789 + "123456789" #1 #10 substring$ % 123456789 + "123456789" #1 #99 substring$ % 123456789 + "123456789" #-7 #3 substring$ % 123 + "123456789" #-1 #1 substring$ % 9 + "123456789" #-1 #3 substring$ % 789 + "123456789" #-2 #2 substring$ % 78 + } EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals("78", vm.getStack().pop()); + assertEquals("789", vm.getStack().pop()); + assertEquals("9", vm.getStack().pop()); + assertEquals("123", vm.getStack().pop()); + assertEquals("123456789", vm.getStack().pop()); + assertEquals("123456789", vm.getStack().pop()); + assertEquals("123456789", vm.getStack().pop()); + assertEquals("456789", vm.getStack().pop()); + assertEquals("2", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testEmpty() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { title } { } { } + READ + STRINGS { s } + FUNCTION { test } { + s empty$ % TRUE + "" empty$ % TRUE + " " empty$ % TRUE + title empty$ % TRUE + " HALLO " empty$ % FALSE + } + ITERATE { test } + """); + List testEntry = List.of(new BibEntry(StandardEntryType.Article)); + + vm.render(testEntry); + + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testFormatNameStatic() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { format }{ "Charles Louis Xavier Joseph de la Vall{\\'e}e Poussin" #1 "{vv~}{ll}{, jj}{, f}?" format.name$ } + EXECUTE { format } + """); + List v = Collections.emptyList(); + + vm.render(v); + + assertEquals("de~la Vall{\\'e}e~Poussin, C.~L. X.~J?", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testFormatNameInEntries() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { author } { } { } + FUNCTION { presort } { cite$ 'sort.key$ := } + ITERATE { presort } + READ + SORT + FUNCTION { format }{ author #2 "{vv~}{ll}{, jj}{, f}?" format.name$ } + ITERATE { format } + """); + List testEntries = List.of( + BstVMTest.defaultTestEntry(), + new BibEntry(StandardEntryType.Book) + .withCitationKey("test") + .withField(StandardField.AUTHOR, "Jonathan Meyer and Charles Louis Xavier Joseph de la Vall{\\'e}e Poussin")); + + vm.render(testEntries); + + assertEquals("de~la Vall{\\'e}e~Poussin, C.~L. X.~J?", vm.getStack().pop()); + assertEquals("Annabi, H?", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testChangeCase() throws RecognitionException { + BstVM vm = new BstVM(""" + STRINGS { title } + READ + FUNCTION { format.title } { + duplicate$ empty$ + { pop$ "" } + { "t" change.case$ } + if$ + } + FUNCTION { test } { + "hello world" "u" change.case$ format.title + "Hello World" format.title + "" format.title + "{A}{D}/{C}ycle: {I}{B}{M}'s {F}ramework for {A}pplication {D}evelopment and {C}ase" "u" change.case$ format.title + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals("{A}{D}/{C}ycle: {I}{B}{M}'s {F}ramework for {A}pplication {D}evelopment and {C}ase", + vm.getStack().pop()); + assertEquals("", vm.getStack().pop()); + assertEquals("Hello world", vm.getStack().pop()); + assertEquals("Hello world", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testTextLength() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { + "hello world" text.length$ % 11 + "Hello {W}orld" text.length$ % 11 + "" text.length$ % 0 + "{A}{D}/{Cycle}" text.length$ % 8 + "{\\This is one character}" text.length$ % 1 + "{\\This {is} {one} {c{h}}aracter as well}" text.length$ % 1 + "{\\And this too" text.length$ % 1 + "These are {\\11}" text.length$ % 11 + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals(11, vm.getStack().pop()); + assertEquals(1, vm.getStack().pop()); + assertEquals(1, vm.getStack().pop()); + assertEquals(1, vm.getStack().pop()); + assertEquals(8, vm.getStack().pop()); + assertEquals(0, vm.getStack().pop()); + assertEquals(11, vm.getStack().pop()); + assertEquals(11, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testIntToStr() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { #3 int.to.str$ #9999 int.to.str$ } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals("9999", vm.getStack().pop()); + assertEquals("3", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testChrToInt() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { "H" chr.to.int$ } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals(72, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testChrToIntIntToChr() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { test } { "H" chr.to.int$ int.to.chr$ } + EXECUTE {test} + """); + + vm.render(Collections.emptyList()); + + assertEquals("H", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testType() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { } { } { } + FUNCTION { presort } { cite$ 'sort.key$ := } + ITERATE { presort } + SORT + FUNCTION { test } { type$ } + ITERATE { test } + """); + List testEntries = List.of( + new BibEntry(StandardEntryType.Article).withCitationKey("a"), + new BibEntry(StandardEntryType.Book).withCitationKey("b"), + new BibEntry(StandardEntryType.Misc).withCitationKey("c"), + new BibEntry(StandardEntryType.InProceedings).withCitationKey("d")); + + vm.render(testEntries); + + assertEquals("inproceedings", vm.getStack().pop()); + assertEquals("misc", vm.getStack().pop()); + assertEquals("book", vm.getStack().pop()); + assertEquals("article", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testCallType() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { title } { } { } + FUNCTION { presort } { cite$ 'sort.key$ := } + ITERATE { presort } + READ + SORT + FUNCTION { inproceedings }{ "InProceedings called on " title * } + FUNCTION { book }{ "Book called on " title * } + ITERATE { call.type$ } + """); + List testEntries = List.of( + BstVMTest.defaultTestEntry(), + new BibEntry(StandardEntryType.Book) + .withCitationKey("test") + .withField(StandardField.TITLE, "Test")); + + vm.render(testEntries); + + assertEquals("Book called on Test", vm.getStack().pop()); + assertEquals( + "InProceedings called on Effective work practices for floss development: A model and propositions", + vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testSwap() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { a } { #3 "Hallo" swap$ } + EXECUTE { a } + """); + + List v = Collections.emptyList(); + vm.render(v); + + assertEquals(3, vm.getStack().pop()); + assertEquals("Hallo", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + void testAssignFunction() { + BstVM vm = new BstVM(""" + INTEGERS { test.var } + FUNCTION { test.func } { #1 'test.var := } + EXECUTE { test.func } + """); + + vm.render(Collections.emptyList()); + + Map functions = vm.latestContext.functions(); + assertTrue(functions.containsKey("test.func")); + assertNotNull(functions.get("test.func")); + assertEquals(1, vm.latestContext.integers().get("test.var")); + } + + @Test + void testSimpleIf() { + BstVM vm = new BstVM(""" + FUNCTION { path1 } { #1 } + FUNCTION { path0 } { #0 } + FUNCTION { test } { + #1 path1 path0 if$ + #0 path1 path0 if$ + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals(0, vm.getStack().pop()); + assertEquals(1, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + void testSimpleWhile() { + BstVM vm = new BstVM(""" + INTEGERS { i } + FUNCTION { test } { + #3 'i := + { i } + { + i + i #1 - + 'i := + } + while$ + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals(1, vm.getStack().pop()); + assertEquals(2, vm.getStack().pop()); + assertEquals(3, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testNestedControlFunctions() throws RecognitionException { + BstVM vm = new BstVM(""" + STRINGS { t } + FUNCTION { not } { { #0 } { #1 } if$ } + FUNCTION { n.dashify } { + "HELLO-WORLD" 't := + "" + { t empty$ not } % while + { + t #1 #1 substring$ "-" = % if + { + t #1 #2 substring$ "--" = not % if + { + "--" * + t #2 global.max$ substring$ 't := + } + { + { t #1 #1 substring$ "-" = } % while + { + "-" * + t #2 global.max$ substring$ 't := + } + while$ + } + if$ + } + { + t #1 #1 substring$ * + t #2 global.max$ substring$ 't := + } + if$ + } + while$ + } + EXECUTE { n.dashify } + """); + List v = Collections.emptyList(); + + vm.render(v); + + assertEquals(1, vm.getStack().size()); + assertEquals("HELLO--WORLD", vm.getStack().pop()); + } + + @Test + public void testLogic() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { not } { { #0 } { #1 } if$ } + FUNCTION { and } { 'skip$ { pop$ #0 } if$ } + FUNCTION { or } { { pop$ #1 } 'skip$ if$ } + FUNCTION { test } { + #1 #1 and + #0 #1 and + #1 #0 and + #0 #0 and + #0 not + #1 not + #1 #1 or + #0 #1 or + #1 #0 or + #0 #0 or + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.FALSE, vm.getStack().pop()); + assertEquals(BstVM.TRUE, vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + /** + * See also {@link BstWidthCalculatorTest} + */ + + @Test + public void testWidth() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { address author title type } { } { label } + STRINGS { longest.label } + INTEGERS { number.label longest.label.width } + FUNCTION { initialize.longest.label } { + "" 'longest.label := + #1 'number.label := + #0 'longest.label.width := + } + FUNCTION {longest.label.pass} { + number.label int.to.str$ 'label := + number.label #1 + 'number.label := + label width$ longest.label.width > + { + label 'longest.label := + label width$ 'longest.label.width := + } + 'skip$ + if$ + } + EXECUTE { initialize.longest.label } + ITERATE { longest.label.pass } + FUNCTION { begin.bib } { + preamble$ empty$ + 'skip$ + { preamble$ write$ newline$ } + if$ + "\\begin{thebibliography}{" longest.label * "}" * + } + EXECUTE {begin.bib} + """); + + List testEntries = List.of(BstVMTest.defaultTestEntry()); + + vm.render(testEntries); + + assertTrue(vm.latestContext.integers().containsKey("longest.label.width")); + assertEquals("\\begin{thebibliography}{1}", vm.getStack().pop()); + } + + @Test + public void testDuplicateEmptyPopSwapIf() throws RecognitionException { + BstVM vm = new BstVM(""" + FUNCTION { emphasize } { + duplicate$ empty$ + { pop$ "" } + { "{\\em " swap$ * "}" * } + if$ + } + FUNCTION { test } { + "" emphasize + "Hello" emphasize + } + EXECUTE { test } + """); + + vm.render(Collections.emptyList()); + + assertEquals("{\\em Hello}", vm.getStack().pop()); + assertEquals("", vm.getStack().pop()); + assertEquals(0, vm.getStack().size()); + } + + @Test + public void testPreambleWriteNewlineQuote() { + BstVM vm = new BstVM(""" + FUNCTION { test } { + preamble$ + write$ + newline$ + "hello" + write$ + quote$ "quoted" * quote$ * + write$ + } + EXECUTE { test } + """); + + BibDatabase testDatabase = new BibDatabase(); + testDatabase.setPreamble("A Preamble"); + + String result = vm.render(Collections.emptyList(), testDatabase); + + assertEquals("A Preamble\nhello\"quoted\"", result); + } +} diff --git a/src/test/java/org/jabref/logic/bst/BstVMTest.java b/src/test/java/org/jabref/logic/bst/BstVMTest.java new file mode 100644 index 00000000000..4a66a8d5836 --- /dev/null +++ b/src/test/java/org/jabref/logic/bst/BstVMTest.java @@ -0,0 +1,220 @@ +package org.jabref.logic.bst; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.StandardEntryType; + +import org.antlr.v4.runtime.RecognitionException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BstVMTest { + + public static BibEntry defaultTestEntry() { + return new BibEntry(StandardEntryType.InProceedings) + .withCitationKey("canh05") + .withField(StandardField.AUTHOR, "Crowston, K. and Annabi, H. and Howison, J. and Masango, C.") + .withField(StandardField.TITLE, "Effective work practices for floss development: A model and propositions") + .withField(StandardField.BOOKTITLE, "Hawaii International Conference On System Sciences (HICSS)") + .withField(StandardField.YEAR, "2005") + .withField(StandardField.OWNER, "oezbek") + .withField(StandardField.TIMESTAMP, "2006.05.29") + .withField(StandardField.URL, "http://james.howison.name/publications.html"); + } + + @Test + public void testAbbrv() throws RecognitionException, IOException { + BstVM vm = new BstVM(Path.of("src/test/resources/org/jabref/logic/bst/abbrv.bst")); + List testEntries = List.of(defaultTestEntry()); + + String expected = "\\begin{thebibliography}{1}\\bibitem{canh05}K.~Crowston, H.~Annabi, J.~Howison, and C.~Masango.\\newblock Effective work practices for floss development: A model and propositions.\\newblock In {\\em Hawaii International Conference On System Sciences (HICSS)}, 2005.\\end{thebibliography}"; + String result = vm.render(testEntries); + + assertEquals( + expected.replaceAll("\\s", ""), + result.replaceAll("\\s", "")); + } + + @Test + public void testSimple() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { address author title type } { } { label } + INTEGERS { output.state before.all mid.sentence after.sentence after.block } + FUNCTION { init.state.consts }{ + #0 'before.all := + #1 'mid.sentence := + #2 'after.sentence := + #3 'after.block := + } + STRINGS { s t } + READ + """); + List testEntries = List.of(defaultTestEntry()); + + vm.render(testEntries); + + assertEquals(2, vm.latestContext.strings().size()); + assertEquals(7, vm.latestContext.integers().size()); + assertEquals(1, vm.latestContext.entries().size()); + assertEquals(5, vm.latestContext.entries().get(0).fields.size()); + assertEquals(38, vm.latestContext.functions().size()); + } + + @Test + public void testLabel() throws RecognitionException { + BstVM vm = new BstVM(""" + ENTRY { title } {} { label } + FUNCTION { test } { + label #0 = + title 'label := + #5 label #6 pop$ } + READ + ITERATE { test } + """); + List