diff --git a/.github/workflows/check-outdated-dependencies.yml b/.github/workflows/check-outdated-dependencies.yml index 4e9a2f21299..6aa2cf35268 100644 --- a/.github/workflows/check-outdated-dependencies.yml +++ b/.github/workflows/check-outdated-dependencies.yml @@ -14,7 +14,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@v2 with: - java-version: 14 + java-version: 16 distribution: 'adopt' - name: Look for outdated dependencies run: ./gradlew -q checkOutdatedDependencies diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 90bcbcf863c..47db68d6228 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -10,6 +10,8 @@ on: - 'docs/**' - 'src/test/**' - 'README.md' + tags: + - '*' pull_request: paths-ignore: - 'docs/**' @@ -51,12 +53,12 @@ jobs: with: fetch-depth: 0 - name: Install GitVersion - uses: gittools/actions/gitversion/setup@v0.9.9 + uses: gittools/actions/gitversion/setup@v0.9.10 with: versionSpec: "5.x" - name: Run GitVersion id: gitversion - uses: gittools/actions/gitversion/execute@v0.9.9 + uses: gittools/actions/gitversion/execute@v0.9.10 - name: Set up JDK uses: actions/setup-java@v2 with: @@ -103,7 +105,7 @@ jobs: jpackage --type pkg --dest build/distribution --name JabRef --mac-package-identifier JabRef --app-version "${{ steps.gitversion.outputs.Major }}.${{ steps.gitversion.outputs.Minor }}" --app-image build/distribution/JabRef.app --verbose --type pkg --vendor JabRef --app-version "${{ steps.gitversion.outputs.Major }}.${{ steps.gitversion.outputs.Minor }}" --file-associations buildres/mac/bibtexAssociations.properties --resource-dir buildres/mac productsign --sign "Developer ID Installer: JabRef e.V. (6792V39SK3)" "build/distribution/JabRef-${{ steps.gitversion.outputs.Major }}.${{ steps.gitversion.outputs.Minor }}.pkg" "build/distribution/JabRef-${{ steps.gitversion.outputs.Major }}.${{ steps.gitversion.outputs.Minor }}-signed.pkg" - name: Notarize dmg and pkg installer - if: matrix.os == 'macos-latest' && github.ref == 'refs/heads/main' + if: matrix.os == 'macos-latest' && startsWith(github.ref, 'refs/tags/') shell: bash run: | REQUEST_UUID_DMG=$(xcrun altool --verbose --notarize-app --primary-bundle-id "org.jabref" --username ${{ secrets.OSX_NOTARIZATION_APP_USERNAME }} --password ${{ secrets.OSX_NOTARIZATION_APP_PWD }} --asc-provider "6792V39SK3" --file "build/distribution/JabRef-${{ steps.gitversion.outputs.Major }}.${{ steps.gitversion.outputs.Minor }}.dmg" | grep RequestUUID | awk '{print $3}') @@ -160,12 +162,12 @@ jobs: - name: Fetch all history for all tags and branches run: git fetch --prune --unshallow - name: Install GitVersion - uses: gittools/actions/gitversion/setup@v0.9.9 + uses: gittools/actions/gitversion/setup@v0.9.10 with: versionSpec: '5.x' - name: Run GitVersion id: gitversion - uses: gittools/actions/gitversion/execute@v0.9.9 + uses: gittools/actions/gitversion/execute@v0.9.10 - name: Get linux binaries uses: actions/download-artifact@master with: diff --git a/.github/workflows/refresh-journal-lists.yml b/.github/workflows/refresh-journal-lists.yml index a144641e950..3dfa0fd798d 100644 --- a/.github/workflows/refresh-journal-lists.yml +++ b/.github/workflows/refresh-journal-lists.yml @@ -18,7 +18,8 @@ jobs: - name: Set up JDK uses: actions/setup-java@v2 with: - java-version: 14 + java-version: 16 + distribution: 'adopt' - uses: actions/cache@v1 name: Restore gradle wrapper with: diff --git a/.github/workflows/tests-fetchers.yml b/.github/workflows/tests-fetchers.yml index 769d1b89fb7..ab390d68d0f 100644 --- a/.github/workflows/tests-fetchers.yml +++ b/.github/workflows/tests-fetchers.yml @@ -36,7 +36,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@v2 with: - java-version: 14 + java-version: 16 distribution: 'adopt' - uses: actions/cache@v2 name: Restore gradle cache diff --git a/CHANGELOG.md b/CHANGELOG.md index 306129f377c..4efbdad5a4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,114 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve ### Added +- We added a fulltext search feature. [#2838](https://github.com/JabRef/jabref/pull/2838) + +### Changed + +### Fixed + +- We fixed an issue when checking for a new version when JabRef is used behind a corporate proxy. [#7884](https://github.com/JabRef/jabref/issues/7884) + +### Removed + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +## [5.3] – 2021-07-05 + +### Added + - We added a progress counter to the title bar in Possible Duplicates dialog window. [#7366](https://github.com/JabRef/jabref/issues/7366) - We added new "Customization" tab to the preferences which includes option to choose a custom address for DOI access. [#7337](https://github.com/JabRef/jabref/issues/7337) - We added zbmath to the public databases from which the bibliographic information of an existing entry can be updated. [#7437](https://github.com/JabRef/jabref/issues/7437) @@ -24,6 +132,7 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve - We added two new fields to track the creation and most recent modification date and time for each entry. [koppor#130](https://github.com/koppor/jabref/issues/130) - We added a feature that allows the user to copy highlighted text in the preview window. [#6962](https://github.com/JabRef/jabref/issues/6962) - We added a feature that allows you to create new BibEntry via paste arxivId [#2292](https://github.com/JabRef/jabref/issues/2292) +- We added support for conducting automated and systematic literature search across libraries and git support for persistence [#369](https://github.com/koppor/jabref/issues/369) - We added a add group functionality at the bottom of the side pane. [#4682](https://github.com/JabRef/jabref/issues/4682) - We added a feature that allows the user to choose whether to trust the target site when unable to find a valid certification path from the file download site. [#7616](https://github.com/JabRef/jabref/issues/7616) - We added a feature that allows the user to open all linked files of multiple selected entries by "Open file" option. [#6966](https://github.com/JabRef/jabref/issues/6966) @@ -116,6 +225,7 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve - We fixed an issue where duplicate files (both file names and contents are the same) is downloaded and add to linked files [#6197](https://github.com/JabRef/jabref/issues/6197) - We fixed an issue where changing the appearance of the preview tab did not trigger a restart warning. [#5464](https://github.com/JabRef/jabref/issues/5464) - We fixed an issue where editing "Custom preview style" triggers exception. [#7526](https://github.com/JabRef/jabref/issues/7526) +- We fixed the [SAO/NASA Astrophysics Data System](https://docs.jabref.org/collect/import-using-online-bibliographic-database#sao-nasa-astrophysics-data-system) fetcher. [#7867](https://github.com/JabRef/jabref/pull/7867) - We fixed an issue where a title with multiple applied formattings in EndNote was not imported correctly [forum#2734](https://discourse.jabref.org/t/importing-endnote-label-field-to-jabref-from-xml-file/2734) - We fixed an issue where a `report` in EndNote was imported as `article` [forum#2734](https://discourse.jabref.org/t/importing-endnote-label-field-to-jabref-from-xml-file/2734) - We fixed an issue where the field `publisher` in EndNote was not imported in JabRef [forum#2734](https://discourse.jabref.org/t/importing-endnote-label-field-to-jabref-from-xml-file/2734) @@ -607,11 +717,12 @@ 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.2...HEAD +[Unreleased]: https://github.com/JabRef/jabref/compare/v5.3...HEAD +[5.3]: https://github.com/JabRef/jabref/compare/v5.2...v5.3 [5.2]: https://github.com/JabRef/jabref/compare/v5.1...v5.2 [5.1]: https://github.com/JabRef/jabref/compare/v5.0...v5.1 [5.0]: https://github.com/JabRef/jabref/compare/v5.0-beta...v5.0 [5.0-beta]: https://github.com/JabRef/jabref/compare/v5.0-alpha...v5.0-beta [5.0-alpha]: https://github.com/JabRef/jabref/compare/v4.3...v5.0-alpha - + diff --git a/build.gradle b/build.gradle index d3323d5b580..92831f62c9a 100644 --- a/build.gradle +++ b/build.gradle @@ -123,7 +123,7 @@ dependencies { implementation group: 'org.apache.commons', name: 'commons-csv', version: '1.8' implementation 'com.h2database:h2-mvstore:1.4.200' - implementation group: 'org.apache.tika', name: 'tika-core', version: '1.26' + implementation group: 'org.apache.tika', name: 'tika-core', version: '1.27' // required for reading write-protected PDFs - see https://github.com/JabRef/jabref/pull/942#issuecomment-209252635 implementation 'org.bouncycastle:bcprov-jdk15on:1.69' @@ -142,18 +142,14 @@ dependencies { antlr4 'org.antlr:antlr4:4.9.2' implementation 'org.antlr:antlr4-runtime:4.9.2' - implementation (group: 'org.apache.lucene', name: 'lucene-queryparser', version: '8.9.0') { - exclude group: 'org.apache.lucene', module: 'lucene-sandbox' - } - implementation group: 'org.eclipse.jgit', name: 'org.eclipse.jgit', version: '5.12.0.202106070339-r' - implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.12.3' - implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.12.3' + implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.12.4' + implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.12.4' implementation group: 'org.mariadb.jdbc', name: 'mariadb-java-client', version: '2.7.3' - implementation 'org.postgresql:postgresql:42.2.22' + implementation 'org.postgresql:postgresql:42.2.23' implementation ('com.oracle.ojdbc:ojdbc10:19.3.0.0') { // causing module issues @@ -173,16 +169,16 @@ dependencies { implementation 'de.saxsys:mvvmfx-validation:1.9.0-SNAPSHOT' implementation 'de.saxsys:mvvmfx:1.8.0' implementation 'com.tobiasdiez:easybind:2.2' - implementation 'org.fxmisc.flowless:flowless:0.6.3' + implementation 'org.fxmisc.flowless:flowless:0.6.4' implementation 'org.fxmisc.richtext:richtextfx:0.10.6' implementation group: 'org.glassfish.hk2.external', name: 'jakarta.inject', version: '2.6.1' implementation 'com.jfoenix:jfoenix:9.0.10' implementation 'org.controlsfx:controlsfx:11.1.0' - implementation 'org.jsoup:jsoup:1.13.1' + implementation 'org.jsoup:jsoup:1.14.1' implementation 'com.konghq:unirest-java:3.11.11' - implementation 'org.slf4j:slf4j-api:2.0.0-alpha1' + implementation 'org.slf4j:slf4j-api:2.0.0-alpha2' implementation group: 'org.apache.logging.log4j', name: 'log4j-jcl', version: '3.0.0-SNAPSHOT' implementation group: 'org.apache.logging.log4j', name: 'log4j-slf4j18-impl', version: '3.0.0-SNAPSHOT' implementation group: 'org.apache.logging.log4j', name: 'log4j-plugins', version: '3.0.0-SNAPSHOT' @@ -190,7 +186,7 @@ dependencies { implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '3.0.0-SNAPSHOT' annotationProcessor group: 'org.apache.logging.log4j', name: 'log4j-core', version: '3.0.0-SNAPSHOT' - implementation 'de.undercouch:citeproc-java:3.0.0-alpha.1' + implementation 'de.undercouch:citeproc-java:3.0.0-alpha.2' implementation group: 'jakarta.activation', name: 'jakarta.activation-api', version: '1.2.1' implementation group: 'jakarta.xml.bind', name: 'jakarta.xml.bind-api', version: '2.3.2' @@ -209,19 +205,21 @@ dependencies { implementation 'com.vladsch.flexmark:flexmark-ext-gfm-strikethrough:0.62.2' implementation 'com.vladsch.flexmark:flexmark-ext-gfm-tasklist:0.62.2' - testImplementation 'io.github.classgraph:classgraph:4.8.108' + implementation group: 'net.harawata', name: 'appdirs', version: '1.2.1' + + testImplementation 'io.github.classgraph:classgraph:4.8.110' testImplementation 'org.junit.jupiter:junit-jupiter:5.7.2' testRuntimeOnly 'org.junit.vintage:junit-vintage-engine:5.7.2' testImplementation 'org.junit.platform:junit-platform-launcher:1.7.2' - testImplementation 'net.bytebuddy:byte-buddy-parent:1.11.5' + testImplementation 'net.bytebuddy:byte-buddy-parent:1.11.6' testRuntimeOnly group: 'org.apache.logging.log4j', name: 'log4j-core', version: '3.0.0-SNAPSHOT' testRuntimeOnly group: 'org.apache.logging.log4j', name: 'log4j-jul', version: '3.0.0-SNAPSHOT' testImplementation 'org.mockito:mockito-core:3.11.2' testImplementation 'org.xmlunit:xmlunit-core:2.8.2' testImplementation 'org.xmlunit:xmlunit-matchers:2.8.2' - testRuntimeOnly 'com.tngtech.archunit:archunit-junit5-engine:0.19.0' - testImplementation 'com.tngtech.archunit:archunit-junit5-api:0.19.0' + testRuntimeOnly 'com.tngtech.archunit:archunit-junit5-engine:0.20.0' + testImplementation 'com.tngtech.archunit:archunit-junit5-api:0.20.0' testImplementation "org.testfx:testfx-core:4.0.17-alpha-SNAPSHOT" testImplementation "org.testfx:testfx-junit5:4.0.17-alpha-SNAPSHOT" testImplementation "org.hamcrest:hamcrest-library:2.2" @@ -649,7 +647,6 @@ jlink { provides 'org.mariadb.jdbc.credential.CredentialPlugin' with 'org.mariadb.jdbc.credential.aws.AwsIamCredentialPlugin', 'org.mariadb.jdbc.credential.env.EnvCredentialPlugin', 'org.mariadb.jdbc.credential.system.PropertiesCredentialPlugin' - provides 'org.apache.commons.logging.LogFactory' with 'org.apache.logging.log4j.jcl.LogFactoryImpl' provides 'org.slf4j.spi.SLF4JServiceProvider' with 'org.apache.logging.slf4j.SLF4JServiceProvider' provides 'org.apache.logging.log4j.spi.Provider' with 'org.apache.logging.log4j.core.impl.Log4jProvider' provides 'java.security.Provider' with 'org.bouncycastle.jce.provider.BouncyCastleProvider', diff --git a/buildSrc/src/copied/java/org/jabref/logic/journals/JournalAbbreviationLoader.java b/buildSrc/src/copied/java/org/jabref/logic/journals/JournalAbbreviationLoader.java index 72f0664b046..cf7d6a177ed 100644 --- a/buildSrc/src/copied/java/org/jabref/logic/journals/JournalAbbreviationLoader.java +++ b/buildSrc/src/copied/java/org/jabref/logic/journals/JournalAbbreviationLoader.java @@ -25,9 +25,12 @@ public static JournalAbbreviationRepository loadRepository(JournalAbbreviationPr JournalAbbreviationRepository repository; // Initialize with built-in list try { - Path tempJournalList = Files.createTempDirectory("journal").resolve("journalList.mv"); + Path tempDir = Files.createTempDirectory("jabref-journal"); + Path tempJournalList = tempDir.resolve("journalList.mv"); Files.copy(JournalAbbreviationRepository.class.getResourceAsStream("/journals/journalList.mv"), tempJournalList); repository = new JournalAbbreviationRepository(tempJournalList); + tempDir.toFile().deleteOnExit(); + tempJournalList.toFile().deleteOnExit(); } catch (IOException e) { LOGGER.error("Error while copying journal list", e); return null; diff --git a/buildSrc/src/main/groovy/org/jabref/build/JournalAbbreviationConverter.groovy b/buildSrc/src/main/groovy/org/jabref/build/JournalAbbreviationConverter.groovy index bfeec72191b..be89bf1e4d5 100644 --- a/buildSrc/src/main/groovy/org/jabref/build/JournalAbbreviationConverter.groovy +++ b/buildSrc/src/main/groovy/org/jabref/build/JournalAbbreviationConverter.groovy @@ -27,7 +27,6 @@ abstract class JournalAbbreviationConverter extends DefaultTask { MVMap abbreviationToFull = store.openMap("AbbreviationToFull") inputDir.getAsFileTree().filter({ File f -> f.name.endsWith(".csv") }).getFiles().each { file -> - duplicatesStrategy = DuplicatesStrategy.EXCLUDE def abbreviations = JournalAbbreviationLoader.readJournalListFromFile(file.toPath()) fullToAbbreviation.putAll( diff --git a/buildres/csl/csl-locales/locales-pl-PL.xml b/buildres/csl/csl-locales/locales-pl-PL.xml index a58aa431c78..9f7a984ecf1 100644 --- a/buildres/csl/csl-locales/locales-pl-PL.xml +++ b/buildres/csl/csl-locales/locales-pl-PL.xml @@ -7,6 +7,9 @@ Michal + + Pendzoncymisio + This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License 2012-07-04T23:31:02+00:00 @@ -22,7 +25,7 @@ - udostępniono + dostęp i i inni anonim diff --git a/buildres/csl/csl-styles/Gemfile.lock b/buildres/csl/csl-styles/Gemfile.lock index 3f573f3c5a5..c53fdc40c47 100644 --- a/buildres/csl/csl-styles/Gemfile.lock +++ b/buildres/csl/csl-styles/Gemfile.lock @@ -20,7 +20,7 @@ GIT GEM remote: https://rubygems.org/ specs: - addressable (2.7.0) + addressable (2.8.0) public_suffix (>= 2.0.2, < 5.0) citeproc (1.0.10) namae (~> 1.0) diff --git a/buildres/csl/csl-styles/acta-anaesthesiologica-scandinavica.csl b/buildres/csl/csl-styles/acta-anaesthesiologica-scandinavica.csl index 6b875acf0f5..7991dd062dc 100644 --- a/buildres/csl/csl-styles/acta-anaesthesiologica-scandinavica.csl +++ b/buildres/csl/csl-styles/acta-anaesthesiologica-scandinavica.csl @@ -155,13 +155,10 @@ -<<<<<<< HEAD -======= ->>>>>>> dbb93e7d9be410a475814d5ae49aef83ce158191 diff --git a/buildres/csl/csl-styles/acta-hydrotechnica.csl b/buildres/csl/csl-styles/acta-hydrotechnica.csl new file mode 100644 index 00000000000..19ebb6c10b5 --- /dev/null +++ b/buildres/csl/csl-styles/acta-hydrotechnica.csl @@ -0,0 +1,222 @@ + + diff --git a/buildres/csl/csl-styles/alternatif-politika.csl b/buildres/csl/csl-styles/alternatif-politika.csl new file mode 100644 index 00000000000..0e7c7ec4798 --- /dev/null +++ b/buildres/csl/csl-styles/alternatif-politika.csl @@ -0,0 +1,304 @@ + + diff --git a/buildres/csl/csl-styles/bakhtiniana-journal-of-discourse-studies.csl b/buildres/csl/csl-styles/bakhtiniana-journal-of-discourse-studies.csl new file mode 100644 index 00000000000..0ff6d46c9c4 --- /dev/null +++ b/buildres/csl/csl-styles/bakhtiniana-journal-of-discourse-studies.csl @@ -0,0 +1,484 @@ + + diff --git a/buildres/csl/csl-styles/brain.csl b/buildres/csl/csl-styles/brain.csl deleted file mode 100644 index b7a5e933c1f..00000000000 --- a/buildres/csl/csl-styles/brain.csl +++ /dev/null @@ -1,188 +0,0 @@ - - diff --git a/buildres/csl/csl-styles/civilta-italiana.csl b/buildres/csl/csl-styles/civilta-italiana.csl index 15ade9b4af7..a1c605f703f 100644 --- a/buildres/csl/csl-styles/civilta-italiana.csl +++ b/buildres/csl/csl-styles/civilta-italiana.csl @@ -16,7 +16,7 @@ AIPI Style used in Civiltà Italiana series by Cesati (Italian), based on Ius Ecclesiae style and University of Bologna style, with added support for manuscripts and archive documents. - 2021-03-22T00:00:00+01:00 + 2021-06-27T17:00:00+01:00 This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License @@ -252,11 +252,11 @@ - + - + diff --git a/buildres/csl/csl-styles/dependent/brain.csl b/buildres/csl/csl-styles/dependent/brain.csl new file mode 100644 index 00000000000..81b207a3815 --- /dev/null +++ b/buildres/csl/csl-styles/dependent/brain.csl @@ -0,0 +1,16 @@ + + diff --git a/buildres/csl/csl-styles/dependent/estuaries-and-coasts.csl b/buildres/csl/csl-styles/dependent/estuaries-and-coasts.csl deleted file mode 100644 index ffd1681ae9f..00000000000 --- a/buildres/csl/csl-styles/dependent/estuaries-and-coasts.csl +++ /dev/null @@ -1,19 +0,0 @@ - - diff --git a/buildres/csl/csl-styles/dependent/orthopaedic-journal-of-sports-medicine.csl b/buildres/csl/csl-styles/dependent/orthopaedic-journal-of-sports-medicine.csl new file mode 100644 index 00000000000..dc78d8285b4 --- /dev/null +++ b/buildres/csl/csl-styles/dependent/orthopaedic-journal-of-sports-medicine.csl @@ -0,0 +1,16 @@ + + diff --git a/buildres/csl/csl-styles/discovery-medicine.csl b/buildres/csl/csl-styles/discovery-medicine.csl index f44b5492e56..ae1d7a0e668 100644 --- a/buildres/csl/csl-styles/discovery-medicine.csl +++ b/buildres/csl/csl-styles/discovery-medicine.csl @@ -5,7 +5,6 @@ DM http://www.zotero.org/styles/discovery-medicine - Patrick O'Brien, PhD diff --git a/buildres/csl/csl-styles/eksploatacja-i-niezawodnosc.csl b/buildres/csl/csl-styles/eksploatacja-i-niezawodnosc.csl index ea6c2f0c31a..c3a2b7258e8 100644 --- a/buildres/csl/csl-styles/eksploatacja-i-niezawodnosc.csl +++ b/buildres/csl/csl-styles/eksploatacja-i-niezawodnosc.csl @@ -1,7 +1,7 @@ diff --git a/buildres/csl/csl-styles/estuaries-and-coasts.csl b/buildres/csl/csl-styles/estuaries-and-coasts.csl new file mode 100644 index 00000000000..f7973ac58c0 --- /dev/null +++ b/buildres/csl/csl-styles/estuaries-and-coasts.csl @@ -0,0 +1,461 @@ + + diff --git a/buildres/csl/csl-styles/ethnographiques-org.csl b/buildres/csl/csl-styles/ethnographiques-org.csl index 77350ee67a7..332be820d74 100644 --- a/buildres/csl/csl-styles/ethnographiques-org.csl +++ b/buildres/csl/csl-styles/ethnographiques-org.csl @@ -12,11 +12,7 @@ 1961-9162 -<<<<<<< HEAD - 2021-05-28T07:12:35+00:00 -======= 2021-06-01T16:10:30+00:00 ->>>>>>> dbb93e7d9be410a475814d5ae49aef83ce158191 This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License @@ -43,22 +39,6 @@ -<<<<<<< HEAD - - - - - - - - - - - - - - -======= @@ -67,7 +47,6 @@ ->>>>>>> dbb93e7d9be410a475814d5ae49aef83ce158191 @@ -178,15 +157,9 @@ -<<<<<<< HEAD - - - -======= ->>>>>>> dbb93e7d9be410a475814d5ae49aef83ce158191 @@ -203,13 +176,8 @@ -<<<<<<< HEAD - - -======= ->>>>>>> dbb93e7d9be410a475814d5ae49aef83ce158191 diff --git a/buildres/csl/csl-styles/experimental-biology-and-medicine.csl b/buildres/csl/csl-styles/experimental-biology-and-medicine.csl index fe1703f8fe3..c59d539cfa3 100644 --- a/buildres/csl/csl-styles/experimental-biology-and-medicine.csl +++ b/buildres/csl/csl-styles/experimental-biology-and-medicine.csl @@ -186,15 +186,7 @@ -<<<<<<< HEAD -<<<<<<< HEAD - -======= ->>>>>>> fcf26b3f77925d7e99e703666dbe3c4b706403ac -======= - ->>>>>>> dbb93e7d9be410a475814d5ae49aef83ce158191 diff --git a/buildres/csl/csl-styles/fachhochschule-sudwestfalen.csl b/buildres/csl/csl-styles/fachhochschule-sudwestfalen.csl new file mode 100644 index 00000000000..302730775bc --- /dev/null +++ b/buildres/csl/csl-styles/fachhochschule-sudwestfalen.csl @@ -0,0 +1,225 @@ + + diff --git a/buildres/csl/csl-styles/freie-hochschule-stuttgart.csl b/buildres/csl/csl-styles/freie-hochschule-stuttgart.csl new file mode 100644 index 00000000000..75c6692390e --- /dev/null +++ b/buildres/csl/csl-styles/freie-hochschule-stuttgart.csl @@ -0,0 +1,305 @@ + + diff --git a/buildres/csl/csl-styles/fundamental-and-applied-limnology.csl b/buildres/csl/csl-styles/fundamental-and-applied-limnology.csl new file mode 100644 index 00000000000..dd0fb721e5f --- /dev/null +++ b/buildres/csl/csl-styles/fundamental-and-applied-limnology.csl @@ -0,0 +1,1645 @@ + + diff --git a/buildres/csl/csl-styles/harvard-stellenbosch-university.csl b/buildres/csl/csl-styles/harvard-stellenbosch-university.csl index 7ac7671b557..a126f889fff 100644 --- a/buildres/csl/csl-styles/harvard-stellenbosch-university.csl +++ b/buildres/csl/csl-styles/harvard-stellenbosch-university.csl @@ -2,22 +2,23 @@ diff --git a/buildres/csl/csl-styles/ios-press-books.csl b/buildres/csl/csl-styles/ios-press-books.csl index a2855d20612..677c8b4935b 100644 --- a/buildres/csl/csl-styles/ios-press-books.csl +++ b/buildres/csl/csl-styles/ios-press-books.csl @@ -82,9 +82,6 @@ - - - diff --git a/buildres/csl/csl-styles/iran-manual-of-style.csl b/buildres/csl/csl-styles/iran-manual-of-style.csl new file mode 100644 index 00000000000..af05846b7b7 --- /dev/null +++ b/buildres/csl/csl-styles/iran-manual-of-style.csl @@ -0,0 +1,658 @@ + + diff --git a/buildres/csl/csl-styles/juristische-schulung.csl b/buildres/csl/csl-styles/juristische-schulung.csl index 7f7b7b81549..d9d903431a5 100644 --- a/buildres/csl/csl-styles/juristische-schulung.csl +++ b/buildres/csl/csl-styles/juristische-schulung.csl @@ -144,7 +144,16 @@ - + + + + + + + + + + + + trad. di + trad. di + + + vol. + voll. + + + volume + volumi + + + p. + pp. + + + pagina + pagine + + + col. + coll. + + + colonna + colonne + + + + c. + cc. + + + § + §§ + + in + cit. + citata + consultato il + ibid. + ibidem + pubblicato in + gen. + feb. + mar. + apr. + mag. + giu. + lug. + ago. + set. + ott. + nov. + dic. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/buildres/csl/csl-styles/social-anthropology.csl b/buildres/csl/csl-styles/social-anthropology.csl new file mode 100644 index 00000000000..69159d4dc48 --- /dev/null +++ b/buildres/csl/csl-styles/social-anthropology.csl @@ -0,0 +1,232 @@ + + diff --git a/buildres/csl/csl-styles/society-of-biblical-literature-fullnote-bibliography.csl b/buildres/csl/csl-styles/society-of-biblical-literature-fullnote-bibliography.csl index 695016e2bb9..c909b194293 100644 --- a/buildres/csl/csl-styles/society-of-biblical-literature-fullnote-bibliography.csl +++ b/buildres/csl/csl-styles/society-of-biblical-literature-fullnote-bibliography.csl @@ -32,15 +32,7 @@ Society of Biblical Literature format with full notes and bibliography -<<<<<<< HEAD -<<<<<<< HEAD - 2021-04-13T15:55:26+00:00 -======= 2021-05-10T14:27:36+00:00 ->>>>>>> fcf26b3f77925d7e99e703666dbe3c4b706403ac -======= - 2021-05-10T14:27:36+00:00 ->>>>>>> dbb93e7d9be410a475814d5ae49aef83ce158191 This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License @@ -692,18 +684,8 @@ -<<<<<<< HEAD -<<<<<<< HEAD - @@ -956,15 +938,6 @@ -<<<<<<< HEAD -<<<<<<< HEAD - - - - -======= -======= ->>>>>>> dbb93e7d9be410a475814d5ae49aef83ce158191 @@ -979,10 +952,6 @@ -<<<<<<< HEAD ->>>>>>> fcf26b3f77925d7e99e703666dbe3c4b706403ac -======= ->>>>>>> dbb93e7d9be410a475814d5ae49aef83ce158191 diff --git a/buildres/csl/csl-styles/szociologiai-szemle.csl b/buildres/csl/csl-styles/szociologiai-szemle.csl new file mode 100644 index 00000000000..1d99c391391 --- /dev/null +++ b/buildres/csl/csl-styles/szociologiai-szemle.csl @@ -0,0 +1,511 @@ + + diff --git a/buildres/csl/csl-styles/technische-universitat-dresden-betriebswirtschaftslehre-marketing.csl b/buildres/csl/csl-styles/technische-universitat-dresden-betriebswirtschaftslehre-marketing.csl index f3957197e24..c9a8f2937bf 100644 --- a/buildres/csl/csl-styles/technische-universitat-dresden-betriebswirtschaftslehre-marketing.csl +++ b/buildres/csl/csl-styles/technische-universitat-dresden-betriebswirtschaftslehre-marketing.csl @@ -6,7 +6,7 @@ http://www.zotero.org/styles/technische-universitat-dresden-betriebswirtschaftslehre-marketing - + Christina Wenzel christina.wenzel@slub-dresden.de @@ -15,21 +15,22 @@ Zitierstil entsprechend den Zitierrichtlinien des Lehrstuhls Betriebswirtschaftslehre, insbesondere Marketing,der Fakultät Wirtschaftswissenschaften, Technische Universität Dresden. - 2019-11-06T10:34:10+00:00 + 2021-05-17T10:08:20+00:00 This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License - o. V. + o. V. Zugriff am gehalten auf et al. Hrsg. + - - + @@ -38,7 +39,7 @@ - + - + @@ -69,7 +70,7 @@ - + @@ -85,9 +86,8 @@ - - - + + @@ -121,23 +121,42 @@ - + + + + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + @@ -149,19 +168,19 @@ - + + - - + - - - + + + @@ -175,16 +194,16 @@ - + - + - + @@ -201,13 +220,23 @@ - + + + + + + + + + + + - diff --git a/buildres/csl/csl-styles/the-chinese-journal-of-international-politics.csl b/buildres/csl/csl-styles/the-chinese-journal-of-international-politics.csl new file mode 100644 index 00000000000..70d547e1fb8 --- /dev/null +++ b/buildres/csl/csl-styles/the-chinese-journal-of-international-politics.csl @@ -0,0 +1,216 @@ + + diff --git a/buildres/csl/csl-styles/united-states-international-trade-commission.csl b/buildres/csl/csl-styles/united-states-international-trade-commission.csl new file mode 100644 index 00000000000..e4a86a09689 --- /dev/null +++ b/buildres/csl/csl-styles/united-states-international-trade-commission.csl @@ -0,0 +1,1505 @@ + + diff --git a/buildres/csl/csl-styles/urad-rs-za-makroekonomske-analize-in-razvoj.csl b/buildres/csl/csl-styles/urad-rs-za-makroekonomske-analize-in-razvoj.csl new file mode 100644 index 00000000000..693cfd2e159 --- /dev/null +++ b/buildres/csl/csl-styles/urad-rs-za-makroekonomske-analize-in-razvoj.csl @@ -0,0 +1,1484 @@ + + diff --git a/buildres/csl/csl-styles/veterinary-pathology.csl b/buildres/csl/csl-styles/veterinary-pathology.csl index 037ea41fddb..4dfbfef3a20 100644 --- a/buildres/csl/csl-styles/veterinary-pathology.csl +++ b/buildres/csl/csl-styles/veterinary-pathology.csl @@ -15,7 +15,7 @@ 0300-9858 1544-2217 Vancouver style with small changes for the journal 'Veterinary Pathology' - 2017-01-23T09:28:21+00:00 + 2021-07-12T09:04:43+00:00 This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License @@ -59,18 +59,7 @@ - - - - - - - - - - - - + @@ -161,10 +150,7 @@ - - - - + @@ -235,7 +221,8 @@ - + + diff --git a/buildres/csl/csl-styles/wirtschaftsuniversitat-wien-institut-fur-transportwirtschaft-und-logistik.csl b/buildres/csl/csl-styles/wirtschaftsuniversitat-wien-institut-fur-transportwirtschaft-und-logistik.csl new file mode 100644 index 00000000000..465cb265f18 --- /dev/null +++ b/buildres/csl/csl-styles/wirtschaftsuniversitat-wien-institut-fur-transportwirtschaft-und-logistik.csl @@ -0,0 +1,314 @@ + + diff --git a/buildres/linux/JabRef.desktop b/buildres/linux/JabRef.desktop index 8bbaff35f2f..9fc72cfcaf4 100644 --- a/buildres/linux/JabRef.desktop +++ b/buildres/linux/JabRef.desktop @@ -9,4 +9,4 @@ Type=Application DESKTOP_MIMES Categories=DEPLOY_BUNDLE_CATEGORY Keywords=bibtex;biblatex;latex;bibliography -StartupWMClass=org.jabref.gui.JabRefMain +StartupWMClass=jabref diff --git a/docs/advanced-reading/fetchers.md b/docs/advanced-reading/fetchers.md index 9651ad9f282..a28445d66b9 100644 --- a/docs/advanced-reading/fetchers.md +++ b/docs/advanced-reading/fetchers.md @@ -4,11 +4,11 @@ Fetchers are the implementation of the [search using online services](https://do | Service | Key Source | Environment Variable | Rate Limit | | :--- | :--- | :--- | :--- | -| [IEEEXplore](https://docs.jabref.org/collect/import-using-online-bibliographic-database/ieeexplore) | [IEEE Xplore API portal](https://developer.ieee.org/) | `IEEEAPIKey` | 200 calls/day | +| [IEEEXplore](https://docs.jabref.org/collect/import-using-online-bibliographic-database#ieeexplore) | [IEEE Xplore API portal](https://developer.ieee.org/) | `IEEEAPIKey` | 200 calls/day | | [MathSciNet](http://www.ams.org/mathscinet) | \(none\) | \(none\) | Depending on the current network | -| [SAO/NASA Astrophysics Data System](https://docs.jabref.org/collect/import-using-online-bibliographic-database/ads) | [ADS UI](https://ui.adsabs.harvard.edu/user/settings/token) | `AstrophysicsDataSystemAPIKey` | 5000 calls/day | +| [SAO/NASA Astrophysics Data System](https://docs.jabref.org/collect/import-using-online-bibliographic-database#sao-nasa-astrophysics-data-system) | [ADS UI](https://ui.adsabs.harvard.edu/user/settings/token) | `AstrophysicsDataSystemAPIKey` | 5000 calls/day | | [ScienceDirect](https://www.sciencedirect.com/) | | `ScienceDirectApiKey` | | -| [Springer Nature](https://docs.jabref.org/collect/import-using-online-bibliographic-database/springer) | [Springer Nature API Portal](https://dev.springernature.com/) | `SpringerNatureAPIKey` | 5000 calls/day | +| [Springer Nature](https://docs.jabref.org/collect/import-using-online-bibliographic-database#springer) | [Springer Nature API Portal](https://dev.springernature.com/) | `SpringerNatureAPIKey` | 5000 calls/day | | [Zentralblatt Math](https://www.zbmath.org/) | \(none\) | \(none\) | Depending on the current network | "Depending on the current network" means that it depends whether your request is routed through a network having paid access. For instance, some universities have subscriptions to MathSciNet. diff --git a/docs/getting-into-the-code/code-howtos.md b/docs/getting-into-the-code/code-howtos.md index 93c779a5457..42f96fc958f 100644 --- a/docs/getting-into-the-code/code-howtos.md +++ b/docs/getting-into-the-code/code-howtos.md @@ -124,7 +124,7 @@ JabRef uses the logging facade [SLF4j](https://www.slf4j.org/). All log messages * Obtaining a logger for a class: ```java - private static final Log LOGGER = LogFactory.getLog(.class); + private static final Logger LOGGER = LoggerFactory.getLogger(.class); ``` * If the logging event is caused by an exception, please add the exception to the log message as: diff --git a/docs/getting-into-the-code/guidelines-for-setting-up-a-local-workspace.md b/docs/getting-into-the-code/guidelines-for-setting-up-a-local-workspace.md index 029b97b2b8f..309324f8af2 100644 --- a/docs/getting-into-the-code/guidelines-for-setting-up-a-local-workspace.md +++ b/docs/getting-into-the-code/guidelines-for-setting-up-a-local-workspace.md @@ -18,7 +18,7 @@ This section list the prerequisites you need to get started to develop JabRef. A ### Java Development Kit 16 -A working Java \(Develoment Kit\) 16 installation with Java FX support is required. In the command line \(terminal in Linux, cmd in Windows\) run `javac -version` and make sure that the reported version is Java 16 \(e.g `javac 16`\). If `javac` is not found or a wrong version is reported, check your `PATH` environment variable, your `JAVA_HOME` environment variable or install the most recent JDK. +A working Java \(Develoment Kit\) 16 installation with Java FX support is required. In the command line \(terminal in Linux, cmd in Windows\) run `javac -version` and make sure that the reported version is Java 16 \(e.g., `javac 16`\). If `javac` is not found or a wrong version is reported, check your `PATH` environment variable, your `JAVA_HOME` environment variable or install the most recent JDK. [JavaFX is not part of the default JDK any more](https://www.reddit.com/r/java/comments/82qm9x/javafx_will_be_removed_from_the_java_jdk_in_jdk_11/), it needs to be installed separately if not using a special JDK. @@ -106,7 +106,7 @@ After that, you can open `jabref/build.gradle` as a project. It is crucial that Ensure you have a Java 16 SDK configured by navigating to **File \| Project Structure \| Platform Settings \| SDKs**. If you don't have one, add a new Java JDK and point it to the location of a JDK 16. ![Project Settings](../.gitbook/assets/intellij-choose-jdk-adoptopenjdk-on-windows-project-settings.png) -Navigate to **File \| Project Structure \| Project** and ensure that the projects' SDK is Java 16 ![Use JDK 15 as project SDK](../.gitbook/assets/intellij-choose-jdk15-project-default.png) +Navigate to **File \| Project Structure \| Project** and ensure that the projects' SDK is Java 16 ![Use JDK 16 as project SDK](../.gitbook/assets/intellij-choose-jdk15-project-default.png) Navigate to **File \| Settings \| Build, Execution, Deployment \| Build Tools \| Gradle** and select the "Project SDK" as the Gradle JVM at the bottom. @@ -201,7 +201,7 @@ Finally, ensure that the checkstyle configuration file is in place: 4. Click "Browse" and choose `config/checkstyle/checkstyle.xml` 5. Click "Next" and "Finish" 6. Activate the CheckStyle configuration file by ticking it in the list -7. Ensure that the [latest CheckStyle version](https://checkstyle.org/releasenotes.html) is selected \(8.36 or higher\). 8.36 is required for Java 15. +7. Ensure that the [latest CheckStyle version](https://checkstyle.org/releasenotes.html) is selected \(8.36 or higher\). 8.43 is required for Java 16. 8. Set the "Scan Scope" to "Only Java sources \(including tests\) 9. Save settings by clicking "OK" 10. Your configuration should now look like this: diff --git a/external-libraries.md b/external-libraries.md index 07380ccdada..7d10d263826 100644 --- a/external-libraries.md +++ b/external-libraries.md @@ -14,7 +14,7 @@ Below, there is a howto to generate the content at "Sorted list of runtime depen We follow the [SPDX license identifiers](https://spdx.org/licenses/). In case you add a library, please use these identifiers. For instance, "BSD" is not exact enough, there are numerous variants out there: BSD-2-Clause, BSD-3-Clause-No-Nuclear-Warranty, ... -Note that the SPDX license identifiers are different from the ones used by debian. See https://wiki.debian.org/Proposals/CopyrightFormat for more information. +Note that the SPDX license identifiers are different from the ones used by debian. See for more information. ## bst files @@ -42,6 +42,13 @@ Note: It is important to include v1.5.54 or later as v1.5.54 is the first ver (Sorted alphabetically by Id) +```yaml +Id: com.fasterxml.jackson +Project: Jackson Project +URL: https://github.com/FasterXML/jackson +License: Apache-2.0 +``` + ```yaml Id: com.github.tomtung Project: latex2unicode @@ -85,12 +92,6 @@ URL: https://github.com/lemire/javaewah License: Apache-2.0 ``` -```yaml -Id: com.ibm.icu:icu4j -Project: International Components for Unicode for Java (ICU4J) -URL: https://wiki.eclipse.org/ICU4J -``` - ```yaml Id: com.jfoenix:jfoenix Project: JavaFX MAterial Design Library @@ -175,20 +176,6 @@ URL: http://commons.apache.org/logging/ License: Apache-2.0 ``` -```yaml -Id: de.jensd:fontawesomefx-commons -Project: FontAwesomeFX Commons -URL: https://bitbucket.org/Jerady/fontawesomefx -License: Apache-2.0 -``` - -```yaml -Id: de.jensd:fontawesomefx-materialdesignfont -Project: FontAwesomeFX Material Design Font -URL: https://bitbucket.org/Jerady/fontawesomefx -License: Apache-2.0 -``` - ```yaml Id: de.saxsys:mvvmfx Project: mvvm(fx) @@ -323,7 +310,7 @@ License: Apache-2.0 ``` ```yaml -Id: org.apache.lucene:lucene-ueryparser +Id: org.apache.lucene:lucene-queryparser Project: Apache Lucene URL: https://lucene.apache.org/ License: Apache-2.0 @@ -413,6 +400,13 @@ URL: https://github.com/jhy/jsoup/ License: MIT ``` +```yaml +Id: org.kordamp.ikonli +Project: Ikonli +URL: https://kordamp.org/ikonli/ +License: Apache-2.0 +``` + ```yaml Id: org.mariadb.jdbc:mariadb-java-client Project: MariaDB Java Client @@ -484,31 +478,36 @@ License: MPL-2.0 AND Apache-2.0 ``` ```yaml -Id: org.ow2.asm:asm -Project: ASM -URL: https://asm.ow2.io/ -License: BSD-3-Clause +Id: org.yaml:snakeyaml +Project: SnakeYAML +URL: https://bitbucket.org/asomov/snakeyaml-engine/src/master/ +License: Apache-2.0 ``` ## Sorted list of runtime dependencies output by gradle -1. `gradlew dependencies > build\reports\project\dependencies.txt` -2. Manually edit depedencies.txt to contain the tree of "compileClasspath" and "implementation" only -3. `sed 's/^.* //' < build/reports/project/dependencies.txt | sort | uniq > build/dependencies-for-external-libraries.txt` +1. `gradlew dependencies > build\dependencies.txt` +2. Manually edit depedencies.txt to contain the tree of "compileClasspath" and "implementation" only. Otherwise, libraries such as "Apache Commons Lang 3" are missed. +3. (on WSL) `sed 's/[^a-z]*//' < build/dependencies.txt | sort | uniq > build/dependencies-for-external-libraries.txt` ```text +com.fasterxml.jackson.core:jackson-annotations:2.12.3 +com.fasterxml.jackson.core:jackson-core:2.12.3 +com.fasterxml.jackson.core:jackson-databind:2.12.3 +com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.12.3 +com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.12.3 +com.fasterxml.jackson:jackson-bom:2.12.3 com.github.tomtung:latex2unicode_2.12:0.2.6 com.google.code.gson:gson:2.8.6 -com.google.errorprone:error_prone_annotations:2.3.4 +com.google.errorprone:error_prone_annotations:2.5.1 com.google.guava:failureaccess:1.0.1 -com.google.guava:guava:30.1-jre +com.google.guava:guava:30.1.1-jre com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava com.google.j2objc:j2objc-annotations:1.3 com.googlecode.javaewah:JavaEWAH:1.1.7 com.h2database:h2-mvstore:1.4.200 -com.ibm.icu:icu4j:62.1 com.jfoenix:jfoenix:9.0.10 -com.konghq:unirest-java:3.11.06 +com.konghq:unirest-java:3.11.11 com.microsoft.azure:applicationinsights-core:2.4.1 com.microsoft.azure:applicationinsights-logging-log4j2:2.4.1 com.oracle.ojdbc:ojdbc10:19.3.0.0 @@ -519,7 +518,7 @@ com.oracle.ojdbc:simplefan:19.3.0.0 com.oracle.ojdbc:ucp:19.3.0.0 com.sun.istack:istack-commons-runtime:3.0.8 com.sun.xml.fastinfoset:FastInfoset:1.2.16 -com.tobiasdiez:easybind:2.1.0 +com.tobiasdiez:easybind:2.2 com.vladsch.flexmark:flexmark-ext-gfm-strikethrough:0.62.2 com.vladsch.flexmark:flexmark-ext-gfm-tasklist:0.62.2 com.vladsch.flexmark:flexmark-util-ast:0.62.2 @@ -538,21 +537,20 @@ com.vladsch.flexmark:flexmark:0.62.2 commons-cli:commons-cli:1.4 commons-codec:commons-codec:1.11 commons-logging:commons-logging:1.2 -de.jensd:fontawesomefx-commons:11.0 -de.jensd:fontawesomefx-materialdesignfont:1.7.22-11 de.saxsys:mvvmfx-validation:1.9.0-SNAPSHOT de.saxsys:mvvmfx:1.8.0 -de.undercouch:citeproc-java:2.1.0-SNAPSHOT +de.undercouch:citeproc-java:3.0.0-alpha.2 eu.lestard:doc-annotations:0.2 info.debatty:java-string-similarity:2.0.0 -io.github.java-diff-utils:java-diff-utils:4.9 +io.github.java-diff-utils:java-diff-utils:4.10 jakarta.activation:jakarta.activation-api:1.2.1 jakarta.annotation:jakarta.annotation-api:1.3.5 jakarta.xml.bind:jakarta.xml.bind-api:2.3.2 net.jcip:jcip-annotations:1.0 net.jodah:typetools:0.6.1 org.antlr:antlr-runtime:3.5.2 -org.antlr:antlr4-runtime:4.9 +org.antlr:antlr4-runtime:4.7.2 -> 4.9.2 +org.antlr:antlr4-runtime:4.9.2 org.apache.commons:commons-csv:1.8 org.apache.commons:commons-lang3:3.9 org.apache.commons:commons-text:1.8 @@ -566,49 +564,50 @@ org.apache.logging.log4j:log4j-core:3.0.0-SNAPSHOT org.apache.logging.log4j:log4j-jcl:3.0.0-SNAPSHOT org.apache.logging.log4j:log4j-plugins:3.0.0-SNAPSHOT org.apache.logging.log4j:log4j-slf4j18-impl:3.0.0-SNAPSHOT -org.apache.lucene:lucene-core:8.7.0 -org.apache.lucene:lucene-queries:8.7.0 -org.apache.lucene:lucene-queryparser:8.7.0 -org.apache.pdfbox:fontbox:2.0.22 -org.apache.pdfbox:pdfbox:2.0.22 -org.apache.pdfbox:xmpbox:2.0.22 -org.apache.tika:tika-core:1.25 -org.bouncycastle:bcprov-jdk15on:1.67 -org.checkerframework:checker-qual:3.5.0 -org.controlsfx:controlsfx:11.0.3 -org.eclipse.jgit:org.eclipse.jgit:5.10.0.202012080955-r -org.fxmisc.flowless:flowless:0.6.2 -org.fxmisc.richtext:richtextfx:0.10.4 +org.apache.lucene:lucene-core:8.9.0 +org.apache.lucene:lucene-queries:8.9.0 +org.apache.lucene:lucene-queryparser:8.9.0 +org.apache.lucene:lucene-analyzers-common:8.9.0 +org.apache.lucene:lucene-backward-codecs:8.9.0 +org.apache.lucene:lucene-highlighter:8.9.0 +org.apache.pdfbox:fontbox:2.0.24 +org.apache.pdfbox:pdfbox:2.0.24 +org.apache.pdfbox:xmpbox:2.0.24 +org.apache.tika:tika-core:1.26 +org.bouncycastle:bcprov-jdk15on:1.69 +org.checkerframework:checker-qual:3.8.0 +org.controlsfx:controlsfx:11.1.0 +org.eclipse.jgit:org.eclipse.jgit:5.12.0.202106070339-r +org.fxmisc.flowless:flowless:0.6.3 +org.fxmisc.richtext:richtextfx:0.10.6 org.fxmisc.undo:undofx:2.1.0 org.fxmisc.wellbehaved:wellbehavedfx:0.3.3 org.glassfish.hk2.external:jakarta.inject:2.6.1 org.glassfish.jaxb:jaxb-runtime:2.3.2 org.glassfish.jaxb:txw2:2.3.2 -org.graalvm.js:js:19.2.1 -org.graalvm.regex:regex:19.2.1 -org.graalvm.sdk:graal-sdk:19.2.1 -org.graalvm.truffle:truffle-api:19.2.1 org.jbibtex:jbibtex:1.0.17 org.jetbrains:annotations:15.0 org.jsoup:jsoup:1.13.1 org.jvnet.staxex:stax-ex:1.8.1 -org.libreoffice:libreoffice:7.0.3 -org.libreoffice:unoloader:7.0.4 -org.mariadb.jdbc:mariadb-java-client:2.7.1 -org.openjfx:javafx-base:15 -org.openjfx:javafx-controls:15 -org.openjfx:javafx-fxml:15 -org.openjfx:javafx-graphics:15 -org.openjfx:javafx-media:15 -org.openjfx:javafx-swing:15 -org.openjfx:javafx-web:15 -org.ow2.asm:asm-analysis:6.2.1 -org.ow2.asm:asm-commons:6.2.1 -org.ow2.asm:asm-tree:6.2.1 -org.ow2.asm:asm-util:6.2.1 -org.ow2.asm:asm:6.2.1 -org.postgresql:postgresql:42.2.18 +org.kordamp.ikonli:ikonli-core:12.2.0 +org.kordamp.ikonli:ikonli-javafx:12.2.0 +org.kordamp.ikonli:ikonli-materialdesign2-pack:12.2.0 +org.libreoffice:libreoffice:7.1.4 +org.libreoffice:unoloader:7.1.4 +org.mariadb.jdbc:mariadb-java-client:2.7.3 +org.openjfx:javafx-base:16 +org.openjfx:javafx-controls:16 +org.openjfx:javafx-fxml:16 +org.openjfx:javafx-graphics:16 +org.openjfx:javafx-media:16 +org.openjfx:javafx-swing:16 +org.openjfx:javafx-web:16 +org.postgresql:postgresql:42.2.22 org.reactfx:reactfx:2.0-M5 org.scala-lang:scala-library:2.12.8 +org.slf4j:slf4j-api:1.7.12 -> 2.0.0-alpha1 +org.slf4j:slf4j-api:1.7.30 -> 2.0.0-alpha1 +org.slf4j:slf4j-api:1.8.0-beta4 -> 2.0.0-alpha1 org.slf4j:slf4j-api:2.0.0-alpha1 +org.yaml:snakeyaml:1.27 ``` diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f6bdcb2c467..2957f437446 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions-snapshots/gradle-7.2-20210629235357+0000-bin.zip +distributionSha256Sum=bf8b869948901d422e9bb7d1fa61da6a6e19411baa7ad6ee929073df85d6365d +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/lib/lucene.jar b/lib/lucene.jar new file mode 100644 index 00000000000..758012cdc90 Binary files /dev/null and b/lib/lucene.jar differ diff --git a/lucene-jar/lib/build.gradle b/lucene-jar/lib/build.gradle new file mode 100644 index 00000000000..b267c5c7e45 --- /dev/null +++ b/lucene-jar/lib/build.gradle @@ -0,0 +1,23 @@ +plugins { + id 'java-library' + id 'com.github.johnrengelman.shadow' version '7.0.0' +} + +repositories { + mavenCentral() +} + +shadowJar { + mergeServiceFiles() +} + +dependencies { + implementation 'org.apache.lucene:lucene-core:8.9.0' + implementation ('org.apache.lucene:lucene-queryparser:8.9.0') { + exclude module: "lucene-sandbox" + } + implementation 'org.apache.lucene:lucene-queries:8.9.0' + implementation 'org.apache.lucene:lucene-analyzers-common:8.9.0' + implementation 'org.apache.lucene:lucene-backward-codecs:8.9.0' + implementation 'org.apache.lucene:lucene-highlighter:8.9.0' +} diff --git a/lucene-jar/settings.gradle b/lucene-jar/settings.gradle new file mode 100644 index 00000000000..d810394f855 --- /dev/null +++ b/lucene-jar/settings.gradle @@ -0,0 +1,11 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user manual at https://docs.gradle.org/7.0.2/userguide/multi_project_builds.html + */ + +rootProject.name = 'lucene-jar' +include('lib') diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 31eeb3aeb76..dbc864c99ba 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -52,7 +52,7 @@ environment: parts: jabref: plugin: dump - source: https://builds.jabref.org/main/JabRef-5.3-portable_linux.tar.gz + source: https://builds.jabref.org/main/JabRef-5.4-portable_linux.tar.gz stage-packages: - x11-utils override-build: | diff --git a/src/jmh/java/org/jabref/benchmarks/Benchmarks.java b/src/jmh/java/org/jabref/benchmarks/Benchmarks.java index 31935438c71..bfc43d706da 100644 --- a/src/jmh/java/org/jabref/benchmarks/Benchmarks.java +++ b/src/jmh/java/org/jabref/benchmarks/Benchmarks.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; +import java.util.EnumSet; import java.util.List; import java.util.Random; import java.util.stream.Collectors; @@ -28,6 +29,7 @@ import org.jabref.model.groups.KeywordGroup; import org.jabref.model.groups.WordKeywordGroup; import org.jabref.model.metadata.MetaData; +import org.jabref.model.search.rules.SearchRules.SearchFlags; import org.jabref.model.util.DummyFileUpdateMonitor; import org.jabref.preferences.JabRefPreferences; @@ -93,14 +95,14 @@ public String write() throws Exception { @Benchmark public List search() { // FIXME: Reuse SearchWorker here - SearchQuery searchQuery = new SearchQuery("Journal Title 500", false, false); + SearchQuery searchQuery = new SearchQuery("Journal Title 500", EnumSet.noneOf(SearchFlags.class)); return database.getEntries().stream().filter(searchQuery::isMatch).collect(Collectors.toList()); } @Benchmark public List parallelSearch() { // FIXME: Reuse SearchWorker here - SearchQuery searchQuery = new SearchQuery("Journal Title 500", false, false); + SearchQuery searchQuery = new SearchQuery("Journal Title 500", EnumSet.noneOf(SearchFlags.class)); return database.getEntries().parallelStream().filter(searchQuery::isMatch).collect(Collectors.toList()); } diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index f8eba5b2f48..95524dc31b2 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -95,10 +95,10 @@ requires flexmark.util.ast; requires flexmark.util.data; requires com.h2database.mvstore; - requires lucene.queryparser; - requires lucene.core; + requires lucene; requires org.eclipse.jgit; requires com.fasterxml.jackson.databind; requires com.fasterxml.jackson.dataformat.yaml; requires com.fasterxml.jackson.datatype.jsr310; + requires net.harawata.appdirs; } diff --git a/src/main/java/org/jabref/cli/ArgumentProcessor.java b/src/main/java/org/jabref/cli/ArgumentProcessor.java index 50832d464ae..f30aa0cafaf 100644 --- a/src/main/java/org/jabref/cli/ArgumentProcessor.java +++ b/src/main/java/org/jabref/cli/ArgumentProcessor.java @@ -343,8 +343,7 @@ private boolean exportMatches(List loaded) { BibDatabase dataBase = pr.getDatabase(); SearchPreferences searchPreferences = Globals.prefs.getSearchPreferences(); - SearchQuery query = new SearchQuery(searchTerm, searchPreferences.isCaseSensitive(), - searchPreferences.isRegularExpression()); + SearchQuery query = new SearchQuery(searchTerm, searchPreferences.getSearchFlags()); List matches = new DatabaseSearcher(query, dataBase).getMatches(); // export matches diff --git a/src/main/java/org/jabref/gui/JabRefFrame.java b/src/main/java/org/jabref/gui/JabRefFrame.java index 2cc4c2f81d9..6361fd9d76e 100644 --- a/src/main/java/org/jabref/gui/JabRefFrame.java +++ b/src/main/java/org/jabref/gui/JabRefFrame.java @@ -106,8 +106,11 @@ import org.jabref.gui.push.PushToApplicationAction; import org.jabref.gui.push.PushToApplicationsManager; import org.jabref.gui.search.GlobalSearchBar; +import org.jabref.gui.search.RebuildFulltextSearchIndexAction; import org.jabref.gui.shared.ConnectToSharedDatabaseCommand; import org.jabref.gui.shared.PullChangesFromSharedAction; +import org.jabref.gui.slr.ExistingStudySearchAction; +import org.jabref.gui.slr.StartNewStudyAction; import org.jabref.gui.specialfields.SpecialFieldMenuItemFactory; import org.jabref.gui.texparser.ParseLatexAction; import org.jabref.gui.undo.CountingUndoManager; @@ -813,13 +816,15 @@ private MenuBar createMenu() { new SeparatorMenuItem(), - factory.createMenuItem(StandardActions.SEND_AS_EMAIL, new SendAsEMailAction(dialogService, prefs, stateManager)), - pushToApplicationMenuItem - // Disabled until PR #7126 can be merged - // new SeparatorMenuItem(), - // factory.createMenuItem(StandardActions.START_SYSTEMATIC_LITERATURE_REVIEW, - // new StartLiteratureReviewAction(this, Globals.getFileUpdateMonitor(), prefs.getWorkingDir(), - // taskExecutor, prefs, prefs.getImportFormatPreferences(), prefs.getSavePreferences())) + factory.createMenuItem(StandardActions.SEND_AS_EMAIL, new SendAsEMailAction(dialogService, this.prefs, stateManager)), + pushToApplicationMenuItem, + new SeparatorMenuItem(), + factory.createMenuItem(StandardActions.START_NEW_STUDY, new StartNewStudyAction(this, Globals.getFileUpdateMonitor(), Globals.TASK_EXECUTOR, prefs)), + factory.createMenuItem(StandardActions.SEARCH_FOR_EXISTING_STUDY, new ExistingStudySearchAction(this, Globals.getFileUpdateMonitor(), Globals.TASK_EXECUTOR, prefs)), + + new SeparatorMenuItem(), + + factory.createMenuItem(StandardActions.REBUILD_FULLTEXT_SEARCH_INDEX, new RebuildFulltextSearchIndexAction(stateManager, this::getCurrentLibraryTab, dialogService, prefs.getFilePreferences())) ); SidePaneComponent webSearch = sidePaneManager.getComponent(SidePaneType.WEB_SEARCH); diff --git a/src/main/java/org/jabref/gui/JabRefMain.java b/src/main/java/org/jabref/gui/JabRefMain.java index ae0a6acbf96..11636af520a 100644 --- a/src/main/java/org/jabref/gui/JabRefMain.java +++ b/src/main/java/org/jabref/gui/JabRefMain.java @@ -1,6 +1,12 @@ package org.jabref.gui; +import java.io.File; +import java.io.IOException; import java.net.Authenticator; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; import javafx.application.Application; import javafx.application.Platform; @@ -20,6 +26,7 @@ import org.jabref.logic.remote.client.RemoteClient; import org.jabref.logic.util.OS; import org.jabref.migrations.PreferencesMigrations; +import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.database.BibDatabaseMode; import org.jabref.preferences.JabRefPreferences; import org.jabref.preferences.PreferencesService; @@ -59,6 +66,8 @@ public void start(Stage mainStage) { applyPreferences(preferences); + clearOldSearchIndices(); + try { // Process arguments ArgumentProcessor argumentProcessor = new ArgumentProcessor(arguments, ArgumentProcessor.Mode.INITIAL_START); @@ -139,4 +148,24 @@ private static void configureProxy(ProxyPreferences proxyPreferences) { Authenticator.setDefault(new ProxyAuthenticator()); } } + + private static void clearOldSearchIndices() { + Path currentIndexPath = BibDatabaseContext.getFulltextIndexBasePath(); + Path appData = currentIndexPath.getParent(); + + try (DirectoryStream stream = Files.newDirectoryStream(appData)) { + for (Path path : stream) { + if (Files.isDirectory(path) && !path.equals(currentIndexPath)) { + LOGGER.info("Deleting out-of-date fulltext search index at {}.", path); + Files.walk(path) + .sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + + } + } + } catch (IOException e) { + LOGGER.error("Could not access app-directory at {}", appData, e); + } + } } diff --git a/src/main/java/org/jabref/gui/LibraryTab.java b/src/main/java/org/jabref/gui/LibraryTab.java index eef077f6680..c6c55f4681b 100644 --- a/src/main/java/org/jabref/gui/LibraryTab.java +++ b/src/main/java/org/jabref/gui/LibraryTab.java @@ -1,6 +1,7 @@ package org.jabref.gui; import java.io.File; +import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; @@ -43,8 +44,11 @@ import org.jabref.logic.autosaveandbackup.BackupManager; import org.jabref.logic.citationstyle.CitationStyleCache; import org.jabref.logic.importer.ParserResult; +import org.jabref.logic.importer.util.FileFieldParser; import org.jabref.logic.l10n.Localization; import org.jabref.logic.pdf.FileAnnotationCache; +import org.jabref.logic.pdf.search.indexing.IndexingTaskManager; +import org.jabref.logic.pdf.search.indexing.PdfIndexer; import org.jabref.logic.search.SearchQuery; import org.jabref.logic.shared.DatabaseLocation; import org.jabref.logic.util.UpdateField; @@ -56,10 +60,13 @@ import org.jabref.model.database.event.EntriesAddedEvent; import org.jabref.model.database.event.EntriesRemovedEvent; import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.LinkedFile; import org.jabref.model.entry.event.EntriesEventSource; import org.jabref.model.entry.event.EntryChangedEvent; +import org.jabref.model.entry.event.FieldChangedEvent; import org.jabref.model.entry.field.Field; import org.jabref.model.entry.field.FieldFactory; +import org.jabref.model.entry.field.StandardField; import org.jabref.preferences.PreferencesService; import com.google.common.eventbus.Subscribe; @@ -101,6 +108,8 @@ public class LibraryTab extends Tab { // initializing it so we prevent NullPointerException private BackgroundTask dataLoadingTask = BackgroundTask.wrap(() -> null); + private IndexingTaskManager indexingTaskManager = new IndexingTaskManager(Globals.TASK_EXECUTOR); + public LibraryTab(JabRefFrame frame, PreferencesService preferencesService, BibDatabaseContext bibDatabaseContext, @@ -125,6 +134,7 @@ public LibraryTab(JabRefFrame frame, setupAutoCompletion(); this.getDatabase().registerListener(new SearchListener()); + this.getDatabase().registerListener(new IndexUpdateListener()); this.getDatabase().registerListener(new EntriesRemovedListener()); // ensure that at each addition of a new entry, the entry is added to the groups interface @@ -332,6 +342,8 @@ public void updateTabTitle(boolean isChanged) { textProperty().setValue(tabTitle.toString()); setTooltip(new Tooltip(toolTipText.toString())); }); + + indexingTaskManager.updateDatabaseName(tabTitle.toString()); } private List collectAllDatabasePaths() { @@ -846,4 +858,63 @@ public void listen(EntriesRemovedEvent removedEntriesEvent) { DefaultTaskExecutor.runInJavaFXThread(() -> frame.getGlobalSearchBar().performSearch()); } } + + private class IndexUpdateListener { + + public IndexUpdateListener() { + try { + indexingTaskManager.addToIndex(PdfIndexer.of(bibDatabaseContext, preferencesService.getFilePreferences()), bibDatabaseContext); + } catch (IOException e) { + LOGGER.error("Cannot access lucene index", e); + } + } + + @Subscribe + public void listen(EntriesAddedEvent addedEntryEvent) { + try { + PdfIndexer pdfIndexer = PdfIndexer.of(bibDatabaseContext, preferencesService.getFilePreferences()); + for (BibEntry addedEntry : addedEntryEvent.getBibEntries()) { + indexingTaskManager.addToIndex(pdfIndexer, addedEntry, bibDatabaseContext); + } + } catch (IOException e) { + LOGGER.error("Cannot access lucene index", e); + } + } + + @Subscribe + public void listen(EntriesRemovedEvent removedEntriesEvent) { + try { + PdfIndexer pdfIndexer = PdfIndexer.of(bibDatabaseContext, preferencesService.getFilePreferences()); + for (BibEntry removedEntry : removedEntriesEvent.getBibEntries()) { + indexingTaskManager.removeFromIndex(pdfIndexer, removedEntry); + } + } catch (IOException e) { + LOGGER.error("Cannot access lucene index", e); + } + } + + @Subscribe + public void listen(FieldChangedEvent fieldChangedEvent) { + if (fieldChangedEvent.getField().equals(StandardField.FILE)) { + List oldFileList = FileFieldParser.parse(fieldChangedEvent.getOldValue()); + List newFileList = FileFieldParser.parse(fieldChangedEvent.getNewValue()); + + List addedFiles = new ArrayList<>(newFileList); + addedFiles.remove(oldFileList); + List removedFiles = new ArrayList<>(oldFileList); + removedFiles.remove(newFileList); + + try { + indexingTaskManager.addToIndex(PdfIndexer.of(bibDatabaseContext, preferencesService.getFilePreferences()), fieldChangedEvent.getBibEntry(), addedFiles, bibDatabaseContext); + indexingTaskManager.removeFromIndex(PdfIndexer.of(bibDatabaseContext, preferencesService.getFilePreferences()), fieldChangedEvent.getBibEntry(), removedFiles); + } catch (IOException e) { + LOGGER.warn("I/O error when writing lucene index", e); + } + } + } + } + + public IndexingTaskManager getIndexingTaskManager() { + return indexingTaskManager; + } } diff --git a/src/main/java/org/jabref/gui/StartLiteratureReviewAction.java b/src/main/java/org/jabref/gui/StartLiteratureReviewAction.java deleted file mode 100644 index ef2cf84a69f..00000000000 --- a/src/main/java/org/jabref/gui/StartLiteratureReviewAction.java +++ /dev/null @@ -1,90 +0,0 @@ -package org.jabref.gui; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Optional; - -import org.jabref.gui.actions.SimpleCommand; -import org.jabref.gui.importer.actions.OpenDatabaseAction; -import org.jabref.gui.util.BackgroundTask; -import org.jabref.gui.util.FileDialogConfiguration; -import org.jabref.gui.util.TaskExecutor; -import org.jabref.logic.crawler.Crawler; -import org.jabref.logic.crawler.git.GitHandler; -import org.jabref.logic.exporter.SavePreferences; -import org.jabref.logic.importer.ImportFormatPreferences; -import org.jabref.logic.importer.ParseException; -import org.jabref.logic.l10n.Localization; -import org.jabref.model.entry.BibEntryTypesManager; -import org.jabref.model.util.FileUpdateMonitor; -import org.jabref.preferences.PreferencesService; - -import org.eclipse.jgit.api.errors.GitAPIException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class StartLiteratureReviewAction extends SimpleCommand { - private static final Logger LOGGER = LoggerFactory.getLogger(StartLiteratureReviewAction.class); - private final JabRefFrame frame; - private final DialogService dialogService; - private final FileUpdateMonitor fileUpdateMonitor; - private final Path workingDirectory; - private final TaskExecutor taskExecutor; - private final PreferencesService preferencesService; - private final ImportFormatPreferences importFormatPreferneces; - private final SavePreferences savePreferences; - - public StartLiteratureReviewAction(JabRefFrame frame, FileUpdateMonitor fileUpdateMonitor, Path standardWorkingDirectory, TaskExecutor taskExecutor, PreferencesService preferencesService, ImportFormatPreferences importFormatPreferences, SavePreferences savePreferences) { - this.frame = frame; - this.dialogService = frame.getDialogService(); - this.fileUpdateMonitor = fileUpdateMonitor; - this.workingDirectory = getInitialDirectory(standardWorkingDirectory); - this.taskExecutor = taskExecutor; - this.preferencesService = preferencesService; - this.importFormatPreferneces = importFormatPreferences; - this.savePreferences = savePreferences; - } - - @Override - public void execute() { - FileDialogConfiguration fileDialogConfiguration = new FileDialogConfiguration.Builder() - .withInitialDirectory(workingDirectory) - .build(); - - Optional studyDefinitionFile = dialogService.showFileOpenDialog(fileDialogConfiguration); - if (studyDefinitionFile.isEmpty()) { - // Do nothing if selection was canceled - return; - } - final Crawler crawler; - try { - crawler = new Crawler(studyDefinitionFile.get(), new GitHandler(studyDefinitionFile.get().getParent()), fileUpdateMonitor, importFormatPreferneces, savePreferences, preferencesService.getTimestampPreferences(), new BibEntryTypesManager()); - } catch (IOException | ParseException | GitAPIException e) { - LOGGER.error("Error during reading of study definition file.", e); - dialogService.showErrorDialogAndWait(Localization.lang("Error during reading of study definition file."), e); - return; - } - BackgroundTask.wrap(() -> { - crawler.performCrawl(); - return 0; // Return any value to make this a callable instead of a runnable. This allows throwing exceptions. - }) - .onFailure(e -> { - LOGGER.error("Error during persistence of crawling results."); - dialogService.showErrorDialogAndWait(Localization.lang("Error during persistence of crawling results."), e); - }) - .onSuccess(unused -> new OpenDatabaseAction(frame, preferencesService, dialogService).openFile(Path.of(studyDefinitionFile.get().getParent().toString(), "studyResult.bib"), true)) - .executeWith(taskExecutor); - } - - /** - * @return Path of current panel database directory or the standard working directory - */ - private Path getInitialDirectory(Path standardWorkingDirectory) { - if (frame.getBasePanelCount() == 0) { - return standardWorkingDirectory; - } else { - Optional databasePath = frame.getCurrentLibraryTab().getBibDatabaseContext().getDatabasePath(); - return databasePath.map(Path::getParent).orElse(standardWorkingDirectory); - } - } -} diff --git a/src/main/java/org/jabref/gui/actions/StandardActions.java b/src/main/java/org/jabref/gui/actions/StandardActions.java index 59d4be73162..4519e5b3f4a 100644 --- a/src/main/java/org/jabref/gui/actions/StandardActions.java +++ b/src/main/java/org/jabref/gui/actions/StandardActions.java @@ -30,6 +30,7 @@ public enum StandardActions implements Action { DELETE(Localization.lang("Delete"), IconTheme.JabRefIcons.DELETE_ENTRY), DELETE_ENTRY(Localization.lang("Delete Entry"), IconTheme.JabRefIcons.DELETE_ENTRY, KeyBinding.DELETE_ENTRY), SEND_AS_EMAIL(Localization.lang("Send as email"), IconTheme.JabRefIcons.EMAIL), + REBUILD_FULLTEXT_SEARCH_INDEX(Localization.lang("Rebuild fulltext search index"), IconTheme.JabRefIcons.FILE), OPEN_EXTERNAL_FILE(Localization.lang("Open file"), IconTheme.JabRefIcons.FILE, KeyBinding.OPEN_FILE), OPEN_URL(Localization.lang("Open URL or DOI"), IconTheme.JabRefIcons.WWW, KeyBinding.OPEN_URL_OR_DOI), SEARCH_SHORTSCIENCE(Localization.lang("Search ShortScience")), @@ -88,7 +89,8 @@ public enum StandardActions implements Action { PARSE_LATEX(Localization.lang("Search for citations in LaTeX files..."), IconTheme.JabRefIcons.LATEX_CITATIONS), NEW_SUB_LIBRARY_FROM_AUX(Localization.lang("New sublibrary based on AUX file") + "...", Localization.lang("New BibTeX sublibrary") + Localization.lang("This feature generates a new library based on which entries are needed in an existing LaTeX document."), IconTheme.JabRefIcons.NEW), WRITE_XMP(Localization.lang("Write XMP metadata to PDFs"), Localization.lang("Will write XMP metadata to the PDFs linked from selected entries."), KeyBinding.WRITE_XMP), - START_SYSTEMATIC_LITERATURE_REVIEW(Localization.lang("Start systematic literature review")), + START_NEW_STUDY(Localization.lang("Start new systematic literature review")), + SEARCH_FOR_EXISTING_STUDY(Localization.lang("Perform search for existing systematic literature review")), OPEN_DATABASE_FOLDER(Localization.lang("Reveal in file explorer")), OPEN_FOLDER(Localization.lang("Open folder"), Localization.lang("Open folder"), IconTheme.JabRefIcons.FOLDER, KeyBinding.OPEN_FOLDER), OPEN_FILE(Localization.lang("Open file"), Localization.lang("Open file"), IconTheme.JabRefIcons.FILE, KeyBinding.OPEN_FILE), diff --git a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java index 7665da90056..84c98978b75 100644 --- a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java +++ b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java @@ -31,6 +31,7 @@ import org.jabref.gui.StateManager; import org.jabref.gui.citationkeypattern.GenerateCitationKeySingleAction; import org.jabref.gui.entryeditor.fileannotationtab.FileAnnotationTab; +import org.jabref.gui.entryeditor.fileannotationtab.FulltextSearchResultsTab; import org.jabref.gui.externalfiles.ExternalFilesEntryLinker; import org.jabref.gui.externalfiletype.ExternalFileTypes; import org.jabref.gui.help.HelpAction; @@ -267,6 +268,8 @@ private List createTabs() { // LaTeX citations tab entryEditorTabs.add(new LatexCitationsTab(databaseContext, preferencesService, taskExecutor, dialogService)); + entryEditorTabs.add(new FulltextSearchResultsTab(stateManager, preferencesService.getTheme(), preferencesService.getFilePreferences())); + return entryEditorTabs; } diff --git a/src/main/java/org/jabref/gui/entryeditor/fileannotationtab/FulltextSearchResultsTab.java b/src/main/java/org/jabref/gui/entryeditor/fileannotationtab/FulltextSearchResultsTab.java new file mode 100644 index 00000000000..dc83f662a8e --- /dev/null +++ b/src/main/java/org/jabref/gui/entryeditor/fileannotationtab/FulltextSearchResultsTab.java @@ -0,0 +1,82 @@ +package org.jabref.gui.entryeditor.fileannotationtab; + +import java.nio.file.Path; + +import javafx.scene.web.WebView; + +import org.jabref.gui.StateManager; +import org.jabref.gui.entryeditor.EntryEditorTab; +import org.jabref.gui.util.OpenHyperlinksInExternalBrowser; +import org.jabref.gui.util.Theme; +import org.jabref.logic.l10n.Localization; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.LinkedFile; +import org.jabref.model.pdf.search.PdfSearchResults; +import org.jabref.model.pdf.search.SearchResult; +import org.jabref.model.search.rules.SearchRules; +import org.jabref.preferences.FilePreferences; + +public class FulltextSearchResultsTab extends EntryEditorTab { + + private final StateManager stateManager; + private final FilePreferences filePreferences; + + private final WebView webView; + + public FulltextSearchResultsTab(StateManager stateManager, Theme theme, FilePreferences filePreferences) { + this.stateManager = stateManager; + this.filePreferences = filePreferences; + webView = new WebView(); + setTheme(theme); + webView.getEngine().loadContent(wrapHTML("

" + Localization.lang("Search results") + "

")); + setContent(webView); + webView.getEngine().getLoadWorker().stateProperty().addListener(new OpenHyperlinksInExternalBrowser(webView)); + setText(Localization.lang("Search results")); + } + + @Override + public boolean shouldShow(BibEntry entry) { + return this.stateManager.activeSearchQueryProperty().isPresent().get() && + this.stateManager.activeSearchQueryProperty().get().isPresent() && + this.stateManager.activeSearchQueryProperty().get().get().getSearchFlags().contains(SearchRules.SearchFlags.FULLTEXT) && + this.stateManager.activeSearchQueryProperty().get().get().getQuery().length() > 0; + } + + @Override + protected void bindToEntry(BibEntry entry) { + if (!shouldShow(entry)) { + return; + } + PdfSearchResults searchResults = stateManager.activeSearchQueryProperty().get().get().getRule().getFulltextResults(stateManager.activeSearchQueryProperty().get().get().getQuery(), entry); + StringBuilder content = new StringBuilder(); + + content.append("

"); + if (searchResults.numSearchResults() == 0) { + content.append(Localization.lang("No search matches.")); + } else { + content.append(Localization.lang("Search results")); + } + content.append("

"); + + for (SearchResult searchResult : searchResults.getSearchResults()) { + content.append("

"); + LinkedFile linkedFile = new LinkedFile("just for link", Path.of(searchResult.getPath()), "pdf"); + Path resolvedPath = linkedFile.findIn(stateManager.getActiveDatabase().get(), filePreferences).orElse(Path.of(searchResult.getPath())); + String link = "" + searchResult.getPath() + ""; + content.append(Localization.lang("Found match in %0", link)); + content.append("

"); + content.append(searchResult.getHtml()); + content.append("

"); + } + + webView.getEngine().loadContent(wrapHTML(content.toString())); + } + + private String wrapHTML(String content) { + return "
" + content + "
"; + } + + public void setTheme(Theme theme) { + theme.getAdditionalStylesheet().ifPresent(location -> webView.getEngine().setUserStyleSheetLocation(location)); + } +} diff --git a/src/main/java/org/jabref/gui/groups/GroupDialogView.java b/src/main/java/org/jabref/gui/groups/GroupDialogView.java index eaa758de519..e1e74ad1b85 100644 --- a/src/main/java/org/jabref/gui/groups/GroupDialogView.java +++ b/src/main/java/org/jabref/gui/groups/GroupDialogView.java @@ -1,6 +1,7 @@ package org.jabref.gui.groups; import java.util.EnumMap; +import java.util.EnumSet; import javafx.application.Platform; import javafx.event.ActionEvent; @@ -21,6 +22,8 @@ import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.groups.AbstractGroup; import org.jabref.model.groups.GroupHierarchyType; +import org.jabref.model.search.rules.SearchRules; +import org.jabref.model.search.rules.SearchRules.SearchFlags; import org.jabref.preferences.PreferencesService; import com.airhacks.afterburner.views.ViewLoader; @@ -124,8 +127,24 @@ public void initialize() { keywordGroupRegex.selectedProperty().bindBidirectional(viewModel.keywordGroupRegexProperty()); searchGroupSearchTerm.textProperty().bindBidirectional(viewModel.searchGroupSearchTermProperty()); - searchGroupCaseSensitive.selectedProperty().bindBidirectional(viewModel.searchGroupCaseSensitiveProperty()); - searchGroupRegex.selectedProperty().bindBidirectional(viewModel.searchGroupRegexProperty()); + searchGroupCaseSensitive.selectedProperty().addListener((observable, oldValue, newValue) -> { + EnumSet searchFlags = viewModel.searchFlagsProperty().get(); + if (newValue) { + searchFlags.add(SearchRules.SearchFlags.CASE_SENSITIVE); + } else { + searchFlags.remove(SearchRules.SearchFlags.CASE_SENSITIVE); + } + viewModel.searchFlagsProperty().set(searchFlags); + }); + searchGroupRegex.selectedProperty().addListener((observable, oldValue, newValue) -> { + EnumSet searchFlags = viewModel.searchFlagsProperty().get(); + if (newValue) { + searchFlags.add(SearchRules.SearchFlags.REGULAR_EXPRESSION); + } else { + searchFlags.remove(SearchRules.SearchFlags.REGULAR_EXPRESSION); + } + viewModel.searchFlagsProperty().set(searchFlags); + }); autoGroupKeywordsOption.selectedProperty().bindBidirectional(viewModel.autoGroupKeywordsOptionProperty()); autoGroupKeywordsField.textProperty().bindBidirectional(viewModel.autoGroupKeywordsFieldProperty()); diff --git a/src/main/java/org/jabref/gui/groups/GroupDialogViewModel.java b/src/main/java/org/jabref/gui/groups/GroupDialogViewModel.java index ebd95f3a8f1..1d5fcc2a79d 100644 --- a/src/main/java/org/jabref/gui/groups/GroupDialogViewModel.java +++ b/src/main/java/org/jabref/gui/groups/GroupDialogViewModel.java @@ -4,6 +4,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.EnumSet; import java.util.List; import java.util.Optional; import java.util.regex.Pattern; @@ -48,6 +49,8 @@ import org.jabref.model.groups.TexGroup; import org.jabref.model.groups.WordKeywordGroup; import org.jabref.model.metadata.MetaData; +import org.jabref.model.search.rules.SearchRules; +import org.jabref.model.search.rules.SearchRules.SearchFlags; import org.jabref.model.strings.StringUtil; import org.jabref.preferences.PreferencesService; @@ -80,8 +83,7 @@ public class GroupDialogViewModel { private final BooleanProperty keywordGroupRegexProperty = new SimpleBooleanProperty(); private final StringProperty searchGroupSearchTermProperty = new SimpleStringProperty(""); - private final BooleanProperty searchGroupCaseSensitiveProperty = new SimpleBooleanProperty(); - private final BooleanProperty searchGroupRegexProperty = new SimpleBooleanProperty(); + private final ObjectProperty> searchFlagsProperty = new SimpleObjectProperty<>(); private final BooleanProperty autoGroupKeywordsOptionProperty = new SimpleBooleanProperty(); private final StringProperty autoGroupKeywordsFieldProperty = new SimpleStringProperty(""); @@ -197,7 +199,7 @@ private void setupValidation() { searchRegexValidator = new FunctionBasedValidator<>( searchGroupSearchTermProperty, input -> { - if (!searchGroupRegexProperty.getValue()) { + if (!searchFlagsProperty.getValue().contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { return true; } @@ -310,8 +312,7 @@ public AbstractGroup resultConverter(ButtonType button) { groupName, groupHierarchySelectedProperty.getValue(), searchGroupSearchTermProperty.getValue().trim(), - searchGroupCaseSensitiveProperty.getValue(), - searchGroupRegexProperty.getValue()); + searchFlagsProperty.getValue()); } else if (typeAutoProperty.getValue()) { if (autoGroupKeywordsOptionProperty.getValue()) { // Set default value for delimiters: ',' for base and '>' for hierarchical @@ -396,8 +397,7 @@ public void setValues() { SearchGroup group = (SearchGroup) editedGroup; searchGroupSearchTermProperty.setValue(group.getSearchExpression()); - searchGroupCaseSensitiveProperty.setValue(group.isCaseSensitive()); - searchGroupRegexProperty.setValue(group.isRegularExpression()); + searchFlagsProperty.setValue(group.getSearchFlags()); } else if (editedGroup.getClass() == ExplicitGroup.class) { typeExplicitProperty.setValue(true); } else if (editedGroup instanceof AutomaticGroup) { @@ -548,12 +548,8 @@ public StringProperty searchGroupSearchTermProperty() { return searchGroupSearchTermProperty; } - public BooleanProperty searchGroupCaseSensitiveProperty() { - return searchGroupCaseSensitiveProperty; - } - - public BooleanProperty searchGroupRegexProperty() { - return searchGroupRegexProperty; + public ObjectProperty> searchFlagsProperty() { + return searchFlagsProperty; } public BooleanProperty autoGroupKeywordsOptionProperty() { diff --git a/src/main/java/org/jabref/gui/icon/IconTheme.java b/src/main/java/org/jabref/gui/icon/IconTheme.java index 7389e233c04..e3e9aab7b23 100644 --- a/src/main/java/org/jabref/gui/icon/IconTheme.java +++ b/src/main/java/org/jabref/gui/icon/IconTheme.java @@ -279,6 +279,7 @@ public enum JabRefIcons implements JabRefIcon { ERROR(MaterialDesignA.ALERT_CIRCLE), CASE_SENSITIVE(MaterialDesignA.ALPHABETICAL), REG_EX(MaterialDesignR.REGEX), + FULLTEXT(MaterialDesignF.FILE_EYE), CONSOLE(MaterialDesignC.CONSOLE), FORUM(MaterialDesignF.FORUM), FACEBOOK(MaterialDesignF.FACEBOOK), diff --git a/src/main/java/org/jabref/gui/importer/ImportCustomEntryTypesDialog.fxml b/src/main/java/org/jabref/gui/importer/ImportCustomEntryTypesDialog.fxml index 295d8d89a74..37a5a1ba3db 100644 --- a/src/main/java/org/jabref/gui/importer/ImportCustomEntryTypesDialog.fxml +++ b/src/main/java/org/jabref/gui/importer/ImportCustomEntryTypesDialog.fxml @@ -5,26 +5,20 @@ - - - + - - - - + + diff --git a/src/main/java/org/jabref/gui/search/GlobalSearchBar.java b/src/main/java/org/jabref/gui/search/GlobalSearchBar.java index 230a6c83f7c..2f19d45bb50 100644 --- a/src/main/java/org/jabref/gui/search/GlobalSearchBar.java +++ b/src/main/java/org/jabref/gui/search/GlobalSearchBar.java @@ -80,6 +80,7 @@ public class GlobalSearchBar extends HBox { private final CustomTextField searchField = SearchTextField.create(); private final ToggleButton caseSensitiveButton; private final ToggleButton regularExpressionButton; + private final ToggleButton fulltextButton; // private final Button searchModeButton; private final Tooltip searchFieldTooltip = new Tooltip(); private final Label currentResults = new Label(""); @@ -123,12 +124,14 @@ public GlobalSearchBar(JabRefFrame frame, StateManager stateManager, Preferences regularExpressionButton = IconTheme.JabRefIcons.REG_EX.asToggleButton(); caseSensitiveButton = IconTheme.JabRefIcons.CASE_SENSITIVE.asToggleButton(); + fulltextButton = IconTheme.JabRefIcons.FULLTEXT.asToggleButton(); // searchModeButton = new Button(); initSearchModifierButtons(); BooleanBinding focusedOrActive = searchField.focusedProperty() .or(regularExpressionButton.focusedProperty()) .or(caseSensitiveButton.focusedProperty()) + .or(fulltextButton.focusedProperty()) .or(searchField.textProperty() .isNotEmpty()); @@ -136,8 +139,10 @@ public GlobalSearchBar(JabRefFrame frame, StateManager stateManager, Preferences regularExpressionButton.visibleProperty().bind(focusedOrActive); caseSensitiveButton.visibleProperty().unbind(); caseSensitiveButton.visibleProperty().bind(focusedOrActive); + fulltextButton.visibleProperty().unbind(); + fulltextButton.visibleProperty().bind(focusedOrActive); - StackPane modifierButtons = new StackPane(new HBox(regularExpressionButton, caseSensitiveButton)); + StackPane modifierButtons = new StackPane(new HBox(regularExpressionButton, caseSensitiveButton, fulltextButton)); modifierButtons.setAlignment(Pos.CENTER); searchField.setRight(new HBox(searchField.getRight(), modifierButtons)); searchField.getStyleClass().add("search-field"); @@ -195,6 +200,15 @@ private void initSearchModifierButtons() { performSearch(); }); + fulltextButton.setSelected(searchPreferences.isFulltext()); + fulltextButton.setTooltip(new Tooltip(Localization.lang("Fulltext search"))); + initSearchModifierButton(fulltextButton); + fulltextButton.setOnAction(event -> { + searchPreferences = searchPreferences.withFulltext(fulltextButton.isSelected()); + preferencesService.storeSearchPreferences(searchPreferences); + performSearch(); + }); + // ToDo: Reimplement searchMode (searchModeButton) /* searchModeButton.setText(searchPreferences.getSearchDisplayMode().getDisplayName()); searchModeButton.setTooltip(new Tooltip(searchPreferences.getSearchDisplayMode().getToolTipText())); @@ -251,7 +265,7 @@ public void performSearch() { return; } - SearchQuery searchQuery = new SearchQuery(this.searchField.getText(), searchPreferences.isCaseSensitive(), searchPreferences.isRegularExpression()); + SearchQuery searchQuery = new SearchQuery(this.searchField.getText(), searchPreferences.getSearchFlags()); if (!searchQuery.isValid()) { informUserAboutInvalidSearchQuery(); return; diff --git a/src/main/java/org/jabref/gui/search/RebuildFulltextSearchIndexAction.java b/src/main/java/org/jabref/gui/search/RebuildFulltextSearchIndexAction.java new file mode 100644 index 00000000000..f103ed07a8d --- /dev/null +++ b/src/main/java/org/jabref/gui/search/RebuildFulltextSearchIndexAction.java @@ -0,0 +1,81 @@ +package org.jabref.gui.search; + +import java.io.IOException; + +import org.jabref.gui.DialogService; +import org.jabref.gui.Globals; +import org.jabref.gui.LibraryTab; +import org.jabref.gui.StateManager; +import org.jabref.gui.actions.SimpleCommand; +import org.jabref.gui.util.BackgroundTask; +import org.jabref.logic.l10n.Localization; +import org.jabref.logic.pdf.search.indexing.PdfIndexer; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.preferences.FilePreferences; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.jabref.gui.actions.ActionHelper.needsDatabase; + +public class RebuildFulltextSearchIndexAction extends SimpleCommand { + + private static final Logger LOGGER = LoggerFactory.getLogger(LibraryTab.class); + + private final StateManager stateManager; + private final GetCurrentLibraryTab currentLibraryTab; + private final DialogService dialogService; + private final FilePreferences filePreferences; + + private BibDatabaseContext databaseContext; + + private boolean shouldContinue = true; + + public RebuildFulltextSearchIndexAction(StateManager stateManager, GetCurrentLibraryTab currentLibraryTab, DialogService dialogService, FilePreferences filePreferences) { + this.stateManager = stateManager; + this.currentLibraryTab = currentLibraryTab; + this.dialogService = dialogService; + this.filePreferences = filePreferences; + + this.executable.bind(needsDatabase(stateManager)); + } + + @Override + public void execute() { + init(); + BackgroundTask.wrap(this::rebuildIndex) + .executeWith(Globals.TASK_EXECUTOR); + } + + public void init() { + if (stateManager.getActiveDatabase().isEmpty()) { + return; + } + + databaseContext = stateManager.getActiveDatabase().get(); + boolean confirm = dialogService.showConfirmationDialogAndWait( + Localization.lang("Rebuild fulltext search index"), + Localization.lang("Rebuild fulltext search index for current library?")); + if (!confirm) { + shouldContinue = false; + return; + } + dialogService.notify(Localization.lang("Rebuilding fulltext search index...")); + } + + private void rebuildIndex() { + if (!shouldContinue || stateManager.getActiveDatabase().isEmpty()) { + return; + } + try { + currentLibraryTab.get().getIndexingTaskManager().createIndex(PdfIndexer.of(databaseContext, filePreferences), databaseContext.getDatabase(), databaseContext); + } catch (IOException e) { + dialogService.notify(Localization.lang("Failed to access fulltext search index")); + LOGGER.error("Failed to access fulltext search index", e); + } + } + + public interface GetCurrentLibraryTab { + LibraryTab get(); + } +} diff --git a/src/main/java/org/jabref/gui/search/rules/describer/ContainsAndRegexBasedSearchRuleDescriber.java b/src/main/java/org/jabref/gui/search/rules/describer/ContainsAndRegexBasedSearchRuleDescriber.java index 4c65d5dd9d4..d411c263c7c 100644 --- a/src/main/java/org/jabref/gui/search/rules/describer/ContainsAndRegexBasedSearchRuleDescriber.java +++ b/src/main/java/org/jabref/gui/search/rules/describer/ContainsAndRegexBasedSearchRuleDescriber.java @@ -1,5 +1,6 @@ package org.jabref.gui.search.rules.describer; +import java.util.EnumSet; import java.util.List; import javafx.scene.text.Text; @@ -7,17 +8,17 @@ import org.jabref.gui.util.TooltipTextUtil; import org.jabref.logic.l10n.Localization; +import org.jabref.model.search.rules.SearchRules; +import org.jabref.model.search.rules.SearchRules.SearchFlags; import org.jabref.model.search.rules.SentenceAnalyzer; public class ContainsAndRegexBasedSearchRuleDescriber implements SearchDescriber { - private final boolean regExp; - private final boolean caseSensitive; + private final EnumSet searchFlags; private final String query; - public ContainsAndRegexBasedSearchRuleDescriber(boolean caseSensitive, boolean regExp, String query) { - this.caseSensitive = caseSensitive; - this.regExp = regExp; + public ContainsAndRegexBasedSearchRuleDescriber(EnumSet searchFlags, String query) { + this.searchFlags = searchFlags; this.query = query; } @@ -26,7 +27,7 @@ public TextFlow getDescription() { List words = new SentenceAnalyzer(query).getWords(); String firstWord = words.isEmpty() ? "" : words.get(0); - String temp = regExp ? Localization.lang( + String temp = searchFlags.contains(SearchRules.SearchFlags.REGULAR_EXPRESSION) ? Localization.lang( "This search contains entries in which any field contains the regular expression %0") : Localization.lang("This search contains entries in which any field contains the term %0"); List textList = TooltipTextUtil.formatToTexts(temp, new TooltipTextUtil.TextReplacement("%0", firstWord, TooltipTextUtil.TextType.BOLD)); @@ -47,7 +48,7 @@ public TextFlow getDescription() { } private Text getCaseSensitiveDescription() { - if (caseSensitive) { + if (searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { return TooltipTextUtil.createText(String.format(" (%s). ", Localization.lang("case sensitive")), TooltipTextUtil.TextType.NORMAL); } else { return TooltipTextUtil.createText(String.format(" (%s). ", Localization.lang("case insensitive")), TooltipTextUtil.TextType.NORMAL); diff --git a/src/main/java/org/jabref/gui/search/rules/describer/GrammarBasedSearchRuleDescriber.java b/src/main/java/org/jabref/gui/search/rules/describer/GrammarBasedSearchRuleDescriber.java index 8b193d12a1b..416aa8dd618 100644 --- a/src/main/java/org/jabref/gui/search/rules/describer/GrammarBasedSearchRuleDescriber.java +++ b/src/main/java/org/jabref/gui/search/rules/describer/GrammarBasedSearchRuleDescriber.java @@ -1,6 +1,7 @@ package org.jabref.gui.search.rules.describer; import java.util.ArrayList; +import java.util.EnumSet; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -12,6 +13,8 @@ import org.jabref.gui.util.TooltipTextUtil; import org.jabref.logic.l10n.Localization; import org.jabref.model.search.rules.GrammarBasedSearchRule; +import org.jabref.model.search.rules.SearchRules; +import org.jabref.model.search.rules.SearchRules.SearchFlags; import org.jabref.model.strings.StringUtil; import org.jabref.search.SearchBaseVisitor; import org.jabref.search.SearchParser; @@ -20,13 +23,11 @@ public class GrammarBasedSearchRuleDescriber implements SearchDescriber { - private final boolean caseSensitive; - private final boolean regExp; + private final EnumSet searchFlags; private final ParseTree parseTree; - public GrammarBasedSearchRuleDescriber(boolean caseSensitive, boolean regExp, ParseTree parseTree) { - this.caseSensitive = caseSensitive; - this.regExp = regExp; + public GrammarBasedSearchRuleDescriber(EnumSet searchFlags, ParseTree parseTree) { + this.searchFlags = searchFlags; this.parseTree = Objects.requireNonNull(parseTree); } @@ -39,7 +40,7 @@ public TextFlow getDescription() { textFlow.getChildren().add(TooltipTextUtil.createText(String.format("%s ", Localization.lang("This search contains entries in which")), TooltipTextUtil.TextType.NORMAL)); textFlow.getChildren().addAll(descriptionSearchBaseVisitor.visit(parseTree)); textFlow.getChildren().add(TooltipTextUtil.createText(". ", TooltipTextUtil.TextType.NORMAL)); - textFlow.getChildren().add(TooltipTextUtil.createText(caseSensitive ? Localization + textFlow.getChildren().add(TooltipTextUtil.createText(searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE) ? Localization .lang("The search is case sensitive.") : Localization.lang("The search is case insensitive."), TooltipTextUtil.TextType.NORMAL)); return textFlow; @@ -84,7 +85,7 @@ public List visitComparison(SearchParser.ComparisonContext context) { final Optional fieldDescriptor = Optional.ofNullable(context.left); final String value = StringUtil.unquote(context.right.getText(), '"'); if (!fieldDescriptor.isPresent()) { - TextFlow description = new ContainsAndRegexBasedSearchRuleDescriber(caseSensitive, regExp, value).getDescription(); + TextFlow description = new ContainsAndRegexBasedSearchRuleDescriber(searchFlags, value).getDescription(); description.getChildren().forEach(it -> textList.add((Text) it)); return textList; } @@ -97,19 +98,19 @@ public List visitComparison(SearchParser.ComparisonContext context) { "any field that matches the regular expression %0") : Localization.lang("the field %0"); if (operator == GrammarBasedSearchRule.ComparisonOperator.CONTAINS) { - if (regExp) { + if (searchFlags.contains(SearchRules.SearchFlags.REGULAR_EXPRESSION)) { temp = Localization.lang("%0 contains the regular expression %1", temp); } else { temp = Localization.lang("%0 contains the term %1", temp); } } else if (operator == GrammarBasedSearchRule.ComparisonOperator.EXACT) { - if (regExp) { + if (searchFlags.contains(SearchRules.SearchFlags.REGULAR_EXPRESSION)) { temp = Localization.lang("%0 matches the regular expression %1", temp); } else { temp = Localization.lang("%0 matches the term %1", temp); } } else if (operator == GrammarBasedSearchRule.ComparisonOperator.DOES_NOT_CONTAIN) { - if (regExp) { + if (searchFlags.contains(SearchRules.SearchFlags.REGULAR_EXPRESSION)) { temp = Localization.lang("%0 doesn't contain the regular expression %1", temp); } else { temp = Localization.lang("%0 doesn't contain the term %1", temp); diff --git a/src/main/java/org/jabref/gui/search/rules/describer/SearchDescribers.java b/src/main/java/org/jabref/gui/search/rules/describer/SearchDescribers.java index 35424d84398..4cc5e2ebd82 100644 --- a/src/main/java/org/jabref/gui/search/rules/describer/SearchDescribers.java +++ b/src/main/java/org/jabref/gui/search/rules/describer/SearchDescribers.java @@ -17,18 +17,12 @@ private SearchDescribers() { * @return the search describer to turn the search into something human understandable */ public static SearchDescriber getSearchDescriberFor(SearchQuery searchQuery) { - if (searchQuery.getRule() instanceof GrammarBasedSearchRule) { - GrammarBasedSearchRule grammarBasedSearchRule = (GrammarBasedSearchRule) searchQuery.getRule(); - - return new GrammarBasedSearchRuleDescriber(grammarBasedSearchRule.isCaseSensitiveSearch(), grammarBasedSearchRule.isRegExpSearch(), grammarBasedSearchRule.getTree()); - } else if (searchQuery.getRule() instanceof ContainBasedSearchRule) { - ContainBasedSearchRule containBasedSearchRule = (ContainBasedSearchRule) searchQuery.getRule(); - - return new ContainsAndRegexBasedSearchRuleDescriber(containBasedSearchRule.isCaseSensitive(), false, searchQuery.getQuery()); - } else if (searchQuery.getRule() instanceof RegexBasedSearchRule) { - RegexBasedSearchRule regexBasedSearchRule = (RegexBasedSearchRule) searchQuery.getRule(); - - return new ContainsAndRegexBasedSearchRuleDescriber(regexBasedSearchRule.isCaseSensitive(), true, searchQuery.getQuery()); + if (searchQuery.getRule() instanceof GrammarBasedSearchRule grammarBasedSearchRule) { + return new GrammarBasedSearchRuleDescriber(grammarBasedSearchRule.getSearchFlags(), grammarBasedSearchRule.getTree()); + } else if (searchQuery.getRule() instanceof ContainBasedSearchRule containBasedSearchRule) { + return new ContainsAndRegexBasedSearchRuleDescriber(containBasedSearchRule.getSearchFlags(), searchQuery.getQuery()); + } else if (searchQuery.getRule() instanceof RegexBasedSearchRule regexBasedSearchRule) { + return new ContainsAndRegexBasedSearchRuleDescriber(regexBasedSearchRule.getSearchFlags(), searchQuery.getQuery()); } else { throw new IllegalStateException("Cannot find a describer for searchRule " + searchQuery.getRule() + " and query " + searchQuery.getQuery()); } diff --git a/src/main/java/org/jabref/gui/slr/ExistingStudySearchAction.java b/src/main/java/org/jabref/gui/slr/ExistingStudySearchAction.java new file mode 100644 index 00000000000..d8d1abc6b26 --- /dev/null +++ b/src/main/java/org/jabref/gui/slr/ExistingStudySearchAction.java @@ -0,0 +1,127 @@ +package org.jabref.gui.slr; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Objects; +import java.util.Optional; + +import org.jabref.gui.DialogService; +import org.jabref.gui.JabRefFrame; +import org.jabref.gui.actions.SimpleCommand; +import org.jabref.gui.importer.actions.OpenDatabaseAction; +import org.jabref.gui.util.BackgroundTask; +import org.jabref.gui.util.DirectoryDialogConfiguration; +import org.jabref.gui.util.TaskExecutor; +import org.jabref.logic.crawler.Crawler; +import org.jabref.logic.exporter.SavePreferences; +import org.jabref.logic.git.SlrGitHandler; +import org.jabref.logic.importer.ImportFormatPreferences; +import org.jabref.logic.importer.ParseException; +import org.jabref.logic.l10n.Localization; +import org.jabref.model.entry.BibEntryTypesManager; +import org.jabref.model.util.FileUpdateMonitor; +import org.jabref.preferences.PreferencesService; + +import org.eclipse.jgit.api.errors.GitAPIException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ExistingStudySearchAction extends SimpleCommand { + private static final Logger LOGGER = LoggerFactory.getLogger(ExistingStudySearchAction.class); + + protected final DialogService dialogService; + protected final Path workingDirectory; + + Path studyDirectory; + + private final JabRefFrame frame; + private final FileUpdateMonitor fileUpdateMonitor; + private final TaskExecutor taskExecutor; + private final PreferencesService preferencesService; + private final ImportFormatPreferences importFormatPreferneces; + private final SavePreferences savePreferences; + // This can be either populated before crawl is called or is populated in the call using the directory dialog. This is helpful if the directory is selected in a previous dialog/UI element + + public ExistingStudySearchAction(JabRefFrame frame, FileUpdateMonitor fileUpdateMonitor, TaskExecutor taskExecutor, PreferencesService preferencesService) { + this.frame = frame; + this.dialogService = frame.getDialogService(); + this.fileUpdateMonitor = fileUpdateMonitor; + this.workingDirectory = getInitialDirectory(preferencesService.getWorkingDir()); + this.taskExecutor = taskExecutor; + this.preferencesService = preferencesService; + this.importFormatPreferneces = preferencesService.getImportFormatPreferences(); + this.savePreferences = preferencesService.getSavePreferences(); + } + + @Override + public void execute() { + // Reset before each execution + studyDirectory = null; + crawl(); + } + + public void crawl() { + if (Objects.isNull(studyDirectory)) { + DirectoryDialogConfiguration directoryDialogConfiguration = new DirectoryDialogConfiguration.Builder() + .withInitialDirectory(workingDirectory) + .build(); + + Optional studyRepositoryRoot = dialogService.showDirectorySelectionDialog(directoryDialogConfiguration); + + if (studyRepositoryRoot.isEmpty()) { + // Do nothing if selection was canceled + return; + } + studyDirectory = studyRepositoryRoot.get(); + } + + try { + setupRepository(studyDirectory); + } catch (IOException | GitAPIException e) { + dialogService.showErrorDialogAndWait(Localization.lang("Study repository could not be created"), e); + return; + } + final Crawler crawler; + try { + crawler = new Crawler(studyDirectory, new SlrGitHandler(studyDirectory), importFormatPreferneces, savePreferences, preferencesService.getTimestampPreferences(), new BibEntryTypesManager(), fileUpdateMonitor); + } catch (IOException | ParseException e) { + LOGGER.error("Error during reading of study definition file.", e); + dialogService.showErrorDialogAndWait(Localization.lang("Error during reading of study definition file."), e); + return; + } + dialogService.notify(Localization.lang("Searching")); + BackgroundTask.wrap(() -> { + crawler.performCrawl(); + return 0; // Return any value to make this a callable instead of a runnable. This allows throwing exceptions. + }) + .onFailure(e -> { + LOGGER.error("Error during persistence of crawling results."); + dialogService.showErrorDialogAndWait(Localization.lang("Error during persistence of crawling results."), e); + }) + .onSuccess(unused -> { + new OpenDatabaseAction(frame, preferencesService, dialogService).openFile(Path.of(studyDirectory.toString(), "studyResult.bib"), true); + // If finished reset command object for next use + studyDirectory = null; + }) + .executeWith(taskExecutor); + } + + /** + * Hook for setting up the repository + */ + protected void setupRepository(Path studyRepositoryRoot) throws IOException, GitAPIException { + // Do nothing as repository is already setup + } + + /** + * @return Path of current panel database directory or the standard working directory + */ + private Path getInitialDirectory(Path standardWorkingDirectory) { + if (frame.getBasePanelCount() == 0) { + return standardWorkingDirectory; + } else { + Optional databasePath = frame.getCurrentLibraryTab().getBibDatabaseContext().getDatabasePath(); + return databasePath.map(Path::getParent).orElse(standardWorkingDirectory); + } + } +} diff --git a/src/main/java/org/jabref/gui/slr/ManageStudyDefinition.css b/src/main/java/org/jabref/gui/slr/ManageStudyDefinition.css new file mode 100644 index 00000000000..e4f5540d22e --- /dev/null +++ b/src/main/java/org/jabref/gui/slr/ManageStudyDefinition.css @@ -0,0 +1,3 @@ +.slr-tab { + -fx-padding: 1em; +} diff --git a/src/main/java/org/jabref/gui/slr/ManageStudyDefinition.fxml b/src/main/java/org/jabref/gui/slr/ManageStudyDefinition.fxml new file mode 100644 index 00000000000..b6ea1d41f7a --- /dev/null +++ b/src/main/java/org/jabref/gui/slr/ManageStudyDefinition.fxml @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
diff --git a/src/main/java/org/jabref/gui/slr/ManageStudyDefinitionView.java b/src/main/java/org/jabref/gui/slr/ManageStudyDefinitionView.java new file mode 100644 index 00000000000..a7b784d06fc --- /dev/null +++ b/src/main/java/org/jabref/gui/slr/ManageStudyDefinitionView.java @@ -0,0 +1,242 @@ +package org.jabref.gui.slr; + +import java.nio.file.Path; +import java.util.Objects; +import java.util.StringJoiner; +import java.util.function.Consumer; + +import javax.inject.Inject; + +import javafx.beans.binding.Bindings; +import javafx.beans.property.SimpleStringProperty; +import javafx.fxml.FXML; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonType; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import javafx.scene.control.cell.CheckBoxTableCell; +import javafx.scene.control.cell.TextFieldTableCell; +import javafx.scene.input.KeyCode; + +import org.jabref.gui.DialogService; +import org.jabref.gui.icon.IconTheme; +import org.jabref.gui.util.BaseDialog; +import org.jabref.gui.util.DirectoryDialogConfiguration; +import org.jabref.gui.util.ValueTableCellFactory; +import org.jabref.gui.util.ViewModelListCellFactory; +import org.jabref.logic.l10n.Localization; +import org.jabref.model.study.Study; +import org.jabref.preferences.PreferencesService; + +import com.airhacks.afterburner.views.ViewLoader; + +/** + * This class controls the user interface of the study definition management dialog. The UI elements and their layout + * are defined in the FXML file. + */ +public class ManageStudyDefinitionView extends BaseDialog { + Path workingDirectory; + + @Inject DialogService dialogService; + @Inject PreferencesService prefs; + + private ManageStudyDefinitionViewModel viewModel; + private final Study study; + + @FXML private TextField studyTitle; + @FXML private TextField addAuthor; + @FXML private TextField addResearchQuestion; + @FXML private TextField addQuery; + @FXML private ComboBox databaseSelector; + @FXML private TextField studyDirectory; + + @FXML private ButtonType saveButtonType; + @FXML private Label helpIcon; + + @FXML private TableView authorTableView; + @FXML private TableColumn authorsColumn; + @FXML private TableColumn authorsActionColumn; + + @FXML private TableView questionTableView; + @FXML private TableColumn questionsColumn; + @FXML private TableColumn questionsActionColumn; + + @FXML private TableView queryTableView; + @FXML private TableColumn queriesColumn; + @FXML private TableColumn queriesActionColumn; + + @FXML private TableView databaseTable; + @FXML private TableColumn databaseEnabledColumn; + @FXML private TableColumn databaseColumn; + @FXML private TableColumn databaseActionColumn; + + /** + * This can be used to either create new study objects or edit existing ones. + * + * @param study null if a new study is created. Otherwise the study object to edit. + * @param studyDirectory the directory where the study to edit is located (null if a new study is created) + */ + public ManageStudyDefinitionView(Study study, Path studyDirectory, Path workingDirectory) { + // If an existing study is edited, open the directory dialog at the directory the study is stored + this.workingDirectory = Objects.isNull(studyDirectory) ? workingDirectory : studyDirectory; + this.setTitle(Objects.isNull(studyDirectory) ? Localization.lang("Define study parameters") : Localization.lang("Manage study definition")); + this.study = study; + + ViewLoader.view(this) + .load() + .setAsDialogPane(this); + + setupSaveButton(); + } + + private void setupSaveButton() { + Button saveButton = ((Button) this.getDialogPane().lookupButton(saveButtonType)); + + saveButton.disableProperty().bind(Bindings.or(Bindings.or( + Bindings.or( + Bindings.or(Bindings.isEmpty(viewModel.getQueries()), Bindings.isEmpty(viewModel.getDatabases())), + Bindings.isEmpty(viewModel.getAuthors())), + viewModel.getTitle().isEmpty()), viewModel.getDirectory().isEmpty())); + + setResultConverter(button -> { + if (button == saveButtonType) { + return viewModel.saveStudy(); + } + // Cancel button will return null + return null; + }); + } + + @FXML + private void initialize() { + viewModel = new ManageStudyDefinitionViewModel(study, workingDirectory, prefs.getImportFormatPreferences()); + + // Listen whether any databases are removed from selection -> Add back to the database selector + studyTitle.textProperty().bindBidirectional(viewModel.titleProperty()); + studyDirectory.textProperty().bindBidirectional(viewModel.getDirectory()); + + initAuthorTab(); + initQuestionsTab(); + initQueriesTab(); + initDatabasesTab(); + } + + private void initAuthorTab() { + setupCommonPropertiesForTables(addAuthor, this::addAuthor, authorsColumn, authorsActionColumn); + setupCellFactories(authorsColumn, authorsActionColumn, viewModel::deleteAuthor); + authorTableView.setItems(viewModel.getAuthors()); + } + + private void initQuestionsTab() { + setupCommonPropertiesForTables(addResearchQuestion, this::addResearchQuestion, questionsColumn, questionsActionColumn); + setupCellFactories(questionsColumn, questionsActionColumn, viewModel::deleteQuestion); + questionTableView.setItems(viewModel.getResearchQuestions()); + } + + private void initQueriesTab() { + setupCommonPropertiesForTables(addQuery, this::addQuery, queriesColumn, queriesActionColumn); + setupCellFactories(queriesColumn, queriesActionColumn, viewModel::deleteQuery); + queryTableView.setItems(viewModel.getQueries()); + + // TODO: Keep until PR #7279 is merged + helpIcon.setTooltip(new Tooltip(new StringJoiner("\n") + .add(Localization.lang("Query terms are separated by spaces.")) + .add(Localization.lang("All query terms are joined using the logical AND, and OR operators") + ".") + .add(Localization.lang("If the sequence of terms is relevant wrap them in double quotes") + "(\").") + .add(Localization.lang("An example:") + " rain AND (clouds OR drops) AND \"precipitation distribution\"") + .toString())); + } + + private void initDatabasesTab() { + new ViewModelListCellFactory().withText(StudyDatabaseItem::getName) + .install(databaseSelector); + databaseSelector.setItems(viewModel.getNonSelectedDatabases()); + + setupCommonPropertiesForTables(databaseSelector, this::addDatabase, databaseColumn, databaseActionColumn); + + databaseEnabledColumn.setResizable(false); + databaseEnabledColumn.setReorderable(false); + databaseEnabledColumn.setCellValueFactory(param -> param.getValue().enabledProperty()); + databaseEnabledColumn.setCellFactory(CheckBoxTableCell.forTableColumn(databaseEnabledColumn)); + + databaseColumn.setCellValueFactory(param -> param.getValue().nameProperty()); + databaseActionColumn.setCellValueFactory(param -> param.getValue().nameProperty()); + new ValueTableCellFactory() + .withGraphic(item -> IconTheme.JabRefIcons.DELETE_ENTRY.getGraphicNode()) + .withTooltip(name -> Localization.lang("Remove")) + .withOnMouseClickedEvent(item -> evt -> + viewModel.removeDatabase(item)) + .install(databaseActionColumn); + + databaseTable.setItems(viewModel.getDatabases()); + } + + private void setupCommonPropertiesForTables(Node addControl, + Runnable addAction, + TableColumn contentColumn, + TableColumn actionColumn) { + addControl.setOnKeyPressed(event -> { + if (event.getCode() == KeyCode.ENTER) { + addAction.run(); + } + }); + + contentColumn.setReorderable(false); + contentColumn.setCellFactory(TextFieldTableCell.forTableColumn()); + actionColumn.setReorderable(false); + actionColumn.setResizable(false); + } + + private void setupCellFactories(TableColumn contentColumn, + TableColumn actionColumn, + Consumer removeAction) { + contentColumn.setCellValueFactory(param -> new SimpleStringProperty(param.getValue())); + actionColumn.setCellValueFactory(param -> new SimpleStringProperty(param.getValue())); + new ValueTableCellFactory() + .withGraphic(item -> IconTheme.JabRefIcons.DELETE_ENTRY.getGraphicNode()) + .withTooltip(name -> Localization.lang("Remove")) + .withOnMouseClickedEvent(item -> evt -> + removeAction.accept(item)) + .install(actionColumn); + } + + @FXML + private void addAuthor() { + viewModel.addAuthor(addAuthor.getText()); + addAuthor.setText(""); + } + + @FXML + private void addResearchQuestion() { + viewModel.addResearchQuestion(addResearchQuestion.getText()); + addResearchQuestion.setText(""); + } + + @FXML + private void addQuery() { + viewModel.addQuery(addQuery.getText()); + addQuery.setText(""); + } + + /** + * Add selected entry from combobox, push onto database pop from nonselecteddatabase (combobox) + */ + @FXML + private void addDatabase() { + viewModel.addDatabase(databaseSelector.getSelectionModel().getSelectedItem()); + } + + @FXML + public void selectStudyDirectory() { + DirectoryDialogConfiguration directoryDialogConfiguration = new DirectoryDialogConfiguration.Builder() + .withInitialDirectory(workingDirectory) + .build(); + + viewModel.setStudyDirectory(dialogService.showDirectorySelectionDialog(directoryDialogConfiguration)); + } +} diff --git a/src/main/java/org/jabref/gui/slr/ManageStudyDefinitionViewModel.java b/src/main/java/org/jabref/gui/slr/ManageStudyDefinitionViewModel.java new file mode 100644 index 00000000000..c7f47a0ca56 --- /dev/null +++ b/src/main/java/org/jabref/gui/slr/ManageStudyDefinitionViewModel.java @@ -0,0 +1,182 @@ +package org.jabref.gui.slr; + +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import javafx.beans.property.Property; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import org.jabref.logic.importer.ImportFormatPreferences; +import org.jabref.logic.importer.SearchBasedFetcher; +import org.jabref.logic.importer.WebFetchers; +import org.jabref.model.study.Study; +import org.jabref.model.study.StudyDatabase; +import org.jabref.model.study.StudyQuery; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class provides a model for managing study definitions. + * To visualize the model one can bind the properties to UI elements. + */ +public class ManageStudyDefinitionViewModel { + private static final Logger LOGGER = LoggerFactory.getLogger(ManageStudyDefinitionViewModel.class); + + private final StringProperty title = new SimpleStringProperty(); + private final ObservableList authors = FXCollections.observableArrayList(); + private final ObservableList researchQuestions = FXCollections.observableArrayList(); + private final ObservableList queries = FXCollections.observableArrayList(); + private final ObservableList databases = FXCollections.observableArrayList(); + // Hold the complement of databases for the selector + private final ObservableList nonSelectedDatabases = FXCollections.observableArrayList(); + private final SimpleStringProperty directory = new SimpleStringProperty(); + private Study study; + + public ManageStudyDefinitionViewModel(Study study, Path studyDirectory, ImportFormatPreferences importFormatPreferences) { + if (Objects.isNull(study)) { + computeNonSelectedDatabases(importFormatPreferences); + return; + } + this.study = study; + title.setValue(study.getTitle()); + authors.addAll(study.getAuthors()); + researchQuestions.addAll(study.getResearchQuestions()); + queries.addAll(study.getQueries().stream().map(StudyQuery::getQuery).collect(Collectors.toList())); + databases.addAll(study.getDatabases() + .stream() + .map(studyDatabase -> new StudyDatabaseItem(studyDatabase.getName(), studyDatabase.isEnabled())) + .collect(Collectors.toList())); + computeNonSelectedDatabases(importFormatPreferences); + if (!Objects.isNull(studyDirectory)) { + this.directory.set(studyDirectory.toString()); + } + } + + private void computeNonSelectedDatabases(ImportFormatPreferences importFormatPreferences) { + nonSelectedDatabases.addAll(WebFetchers.getSearchBasedFetchers(importFormatPreferences) + .stream() + .map(SearchBasedFetcher::getName) + .map(s -> new StudyDatabaseItem(s, true)) + .filter(studyDatabase -> !databases.contains(studyDatabase)) + .collect(Collectors.toList())); + } + + public StringProperty getTitle() { + return title; + } + + public StringProperty getDirectory() { + return directory; + } + + public ObservableList getAuthors() { + return authors; + } + + public ObservableList getResearchQuestions() { + return researchQuestions; + } + + public ObservableList getQueries() { + return queries; + } + + public ObservableList getDatabases() { + return databases; + } + + public ObservableList getNonSelectedDatabases() { + return nonSelectedDatabases; + } + + public void addAuthor(String author) { + if (author.isBlank()) { + return; + } + authors.add(author); + } + + public void addResearchQuestion(String researchQuestion) { + if (researchQuestion.isBlank() || researchQuestions.contains(researchQuestion)) { + return; + } + researchQuestions.add(researchQuestion); + } + + public void addQuery(String query) { + if (query.isBlank()) { + return; + } + queries.add(query); + } + + public void addDatabase(StudyDatabaseItem database) { + if (Objects.isNull(database)) { + return; + } + nonSelectedDatabases.remove(database); + if (!databases.contains(database)) { + databases.add(database); + } + } + + public SlrStudyAndDirectory saveStudy() { + if (Objects.isNull(study)) { + study = new Study(); + } + study.setTitle(title.getValueSafe()); + study.setAuthors(authors); + study.setResearchQuestions(researchQuestions); + study.setQueries(queries.stream().map(StudyQuery::new).collect(Collectors.toList())); + study.setDatabases(databases.stream().map(studyDatabaseItem -> new StudyDatabase(studyDatabaseItem.getName(), studyDatabaseItem.isEnabled())).collect(Collectors.toList())); + Path studyDirectory = null; + try { + studyDirectory = Path.of(directory.getValueSafe()); + } catch (InvalidPathException e) { + LOGGER.error("Invalid path was provided: {}", directory); + } + return new SlrStudyAndDirectory(study, studyDirectory); + } + + public Property titleProperty() { + return title; + } + + public void removeDatabase(String database) { + // If a database is added from the combo box it should be enabled by default + Optional correspondingDatabase = databases.stream().filter(studyDatabaseItem -> studyDatabaseItem.getName().equals(database)).findFirst(); + if (correspondingDatabase.isEmpty()) { + return; + } + StudyDatabaseItem databaseToRemove = correspondingDatabase.get(); + databases.remove(databaseToRemove); + databaseToRemove.setEnabled(true); + nonSelectedDatabases.add(databaseToRemove); + // Resort list + nonSelectedDatabases.sort(Comparator.comparing(StudyDatabaseItem::getName)); + } + + public void setStudyDirectory(Optional studyRepositoryRoot) { + getDirectory().setValue(studyRepositoryRoot.isPresent() ? studyRepositoryRoot.get().toString() : getDirectory().getValueSafe()); + } + + public void deleteAuthor(String item) { + authors.remove(item); + } + + public void deleteQuestion(String item) { + researchQuestions.remove(item); + } + + public void deleteQuery(String item) { + queries.remove(item); + } +} diff --git a/src/main/java/org/jabref/gui/slr/SlrStudyAndDirectory.java b/src/main/java/org/jabref/gui/slr/SlrStudyAndDirectory.java new file mode 100644 index 00000000000..2baf1f38791 --- /dev/null +++ b/src/main/java/org/jabref/gui/slr/SlrStudyAndDirectory.java @@ -0,0 +1,23 @@ +package org.jabref.gui.slr; + +import java.nio.file.Path; + +import org.jabref.model.study.Study; + +public class SlrStudyAndDirectory { + private final Study study; + private final Path studyDirectory; + + public SlrStudyAndDirectory(Study study, Path studyDirectory) { + this.study = study; + this.studyDirectory = studyDirectory; + } + + public Path getStudyDirectory() { + return studyDirectory; + } + + public Study getStudy() { + return study; + } +} diff --git a/src/main/java/org/jabref/gui/slr/StartNewStudyAction.java b/src/main/java/org/jabref/gui/slr/StartNewStudyAction.java new file mode 100644 index 00000000000..463db54daaa --- /dev/null +++ b/src/main/java/org/jabref/gui/slr/StartNewStudyAction.java @@ -0,0 +1,41 @@ +package org.jabref.gui.slr; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; + +import org.jabref.gui.JabRefFrame; +import org.jabref.gui.util.TaskExecutor; +import org.jabref.logic.crawler.StudyYamlParser; +import org.jabref.model.study.Study; +import org.jabref.model.util.FileUpdateMonitor; +import org.jabref.preferences.PreferencesService; + +import org.eclipse.jgit.api.errors.GitAPIException; + +public class StartNewStudyAction extends ExistingStudySearchAction { + Study newStudy; + + public StartNewStudyAction(JabRefFrame frame, FileUpdateMonitor fileUpdateMonitor, TaskExecutor taskExecutor, PreferencesService prefs) { + super(frame, fileUpdateMonitor, taskExecutor, prefs); + } + + @Override + protected void setupRepository(Path studyRepositoryRoot) throws IOException, GitAPIException { + StudyYamlParser studyYAMLParser = new StudyYamlParser(); + studyYAMLParser.writeStudyYamlFile(newStudy, studyRepositoryRoot.resolve("study.yml")); + } + + @Override + public void execute() { + Optional studyAndDirectory = dialogService.showCustomDialogAndWait(new ManageStudyDefinitionView(null, null, workingDirectory)); + if (studyAndDirectory.isEmpty()) { + return; + } + if (!studyAndDirectory.get().getStudyDirectory().toString().isBlank()) { + studyDirectory = studyAndDirectory.get().getStudyDirectory(); + } + newStudy = studyAndDirectory.get().getStudy(); + crawl(); + } +} diff --git a/src/main/java/org/jabref/gui/slr/StudyDatabaseItem.java b/src/main/java/org/jabref/gui/slr/StudyDatabaseItem.java new file mode 100644 index 00000000000..50ceda09143 --- /dev/null +++ b/src/main/java/org/jabref/gui/slr/StudyDatabaseItem.java @@ -0,0 +1,64 @@ +package org.jabref.gui.slr; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +public class StudyDatabaseItem { + private final StringProperty name; + private final BooleanProperty enabled; + + public StudyDatabaseItem(String name, boolean enabled) { + this.name = new SimpleStringProperty(name); + this.enabled = new SimpleBooleanProperty(enabled); + } + + public String getName() { + return name.getValue(); + } + + public void setName(String name) { + this.name.setValue(name); + } + + public StringProperty nameProperty() { + return name; + } + + public boolean isEnabled() { + return enabled.getValue(); + } + + public void setEnabled(boolean enabled) { + this.enabled.setValue(enabled); + } + + public BooleanProperty enabledProperty() { + return enabled; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + StudyDatabaseItem that = (StudyDatabaseItem) o; + + if (isEnabled() != that.isEnabled()) { + return false; + } + return getName() != null ? getName().equals(that.getName()) : that.getName() == null; + } + + @Override + public int hashCode() { + int result = getName() != null ? getName().hashCode() : 0; + result = 31 * result + (isEnabled() ? 1 : 0); + return result; + } +} diff --git a/src/main/java/org/jabref/gui/util/BackgroundTask.java b/src/main/java/org/jabref/gui/util/BackgroundTask.java index 9c9626354d1..280df1bc0ad 100644 --- a/src/main/java/org/jabref/gui/util/BackgroundTask.java +++ b/src/main/java/org/jabref/gui/util/BackgroundTask.java @@ -64,7 +64,7 @@ protected V call() throws Exception { public static BackgroundTask wrap(Runnable runnable) { return new BackgroundTask<>() { @Override - protected Void call() throws Exception { + protected Void call() { runnable.run(); return null; } diff --git a/src/main/java/org/jabref/logic/citationstyle/CSLAdapter.java b/src/main/java/org/jabref/logic/citationstyle/CSLAdapter.java index 5edac0f1f6b..a831e074552 100644 --- a/src/main/java/org/jabref/logic/citationstyle/CSLAdapter.java +++ b/src/main/java/org/jabref/logic/citationstyle/CSLAdapter.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Objects; @@ -124,10 +125,10 @@ public CSLItemData retrieveItem(String id) { } @Override - public String[] getIds() { + public Collection getIds() { return data.stream() .map(entry -> entry.getCitationKey().orElse("")) - .toArray(String[]::new); + .toList(); } } } diff --git a/src/main/java/org/jabref/logic/crawler/Crawler.java b/src/main/java/org/jabref/logic/crawler/Crawler.java index 898af4ffbdb..7137bc7b7ff 100644 --- a/src/main/java/org/jabref/logic/crawler/Crawler.java +++ b/src/main/java/org/jabref/logic/crawler/Crawler.java @@ -4,8 +4,9 @@ import java.nio.file.Path; import java.util.List; -import org.jabref.logic.crawler.git.GitHandler; +import org.jabref.logic.exporter.SaveException; import org.jabref.logic.exporter.SavePreferences; +import org.jabref.logic.git.SlrGitHandler; import org.jabref.logic.importer.ImportFormatPreferences; import org.jabref.logic.importer.ParseException; import org.jabref.logic.preferences.TimestampPreferences; @@ -29,11 +30,9 @@ public class Crawler { /** * Creates a crawler for retrieving studies from E-Libraries * - * @param studyDefinitionFile The path to the study definition file that contains the list of targeted E-Libraries - * and used cross-library queries + * @param studyRepositoryRoot The path to the study repository */ - public Crawler(Path studyDefinitionFile, GitHandler gitHandler, FileUpdateMonitor fileUpdateMonitor, ImportFormatPreferences importFormatPreferences, SavePreferences savePreferences, TimestampPreferences timestampPreferences, BibEntryTypesManager bibEntryTypesManager) throws IllegalArgumentException, IOException, ParseException, GitAPIException { - Path studyRepositoryRoot = studyDefinitionFile.getParent(); + public Crawler(Path studyRepositoryRoot, SlrGitHandler gitHandler, ImportFormatPreferences importFormatPreferences, SavePreferences savePreferences, TimestampPreferences timestampPreferences, BibEntryTypesManager bibEntryTypesManager, FileUpdateMonitor fileUpdateMonitor) throws IllegalArgumentException, IOException, ParseException { studyRepository = new StudyRepository(studyRepositoryRoot, gitHandler, importFormatPreferences, fileUpdateMonitor, savePreferences, timestampPreferences, bibEntryTypesManager); StudyDatabaseToFetcherConverter studyDatabaseToFetcherConverter = new StudyDatabaseToFetcherConverter(studyRepository.getActiveLibraryEntries(), importFormatPreferences); this.studyFetcher = new StudyFetcher(studyDatabaseToFetcherConverter.getActiveFetchers(), studyRepository.getSearchQueryStrings()); @@ -43,9 +42,17 @@ public Crawler(Path studyDefinitionFile, GitHandler gitHandler, FileUpdateMonito * This methods performs the crawling of the active libraries defined in the study definition file. * This method also persists the results in the same folder the study definition file is stored in. * + * The whole process works as follows: + *
    + *
  1. Then the search is executed
  2. + *
  3. The repository changes to the search branch
  4. + *
  5. Afterwards, the results are persisted on the search branch.
  6. + *
  7. Finally, the changes are merged into the work branch
  8. + *
+ * * @throws IOException Thrown if a problem occurred during the persistence of the result. */ - public void performCrawl() throws IOException, GitAPIException { + public void performCrawl() throws IOException, GitAPIException, SaveException { List results = studyFetcher.crawl(); studyRepository.persist(results); } diff --git a/src/main/java/org/jabref/logic/crawler/StudyRepository.java b/src/main/java/org/jabref/logic/crawler/StudyRepository.java index 99aa896cfd5..e36621d0b0d 100644 --- a/src/main/java/org/jabref/logic/crawler/StudyRepository.java +++ b/src/main/java/org/jabref/logic/crawler/StudyRepository.java @@ -3,22 +3,28 @@ import java.io.FileWriter; import java.io.IOException; import java.io.Writer; +import java.nio.charset.UnsupportedCharsetException; import java.nio.file.Files; import java.nio.file.Path; import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; import java.util.List; import java.util.regex.Pattern; import java.util.stream.Collectors; import org.jabref.logic.citationkeypattern.CitationKeyGenerator; -import org.jabref.logic.crawler.git.GitHandler; import org.jabref.logic.database.DatabaseMerger; +import org.jabref.logic.exporter.AtomicFileWriter; import org.jabref.logic.exporter.BibtexDatabaseWriter; +import org.jabref.logic.exporter.SaveException; import org.jabref.logic.exporter.SavePreferences; +import org.jabref.logic.git.SlrGitHandler; import org.jabref.logic.importer.ImportFormatPreferences; import org.jabref.logic.importer.OpenDatabase; import org.jabref.logic.importer.ParseException; import org.jabref.logic.importer.SearchBasedFetcher; +import org.jabref.logic.l10n.Localization; import org.jabref.logic.preferences.TimestampPreferences; import org.jabref.model.database.BibDatabase; import org.jabref.model.database.BibDatabaseContext; @@ -42,15 +48,19 @@ * as well as the sharing, and versioning of results using git. */ class StudyRepository { - // Tests work with study.bib + // Tests work with study.yml private static final String STUDY_DEFINITION_FILE_NAME = "study.yml"; private static final Logger LOGGER = LoggerFactory.getLogger(StudyRepository.class); private static final Pattern MATCHCOLON = Pattern.compile(":"); private static final Pattern MATCHILLEGALCHARACTERS = Pattern.compile("[^A-Za-z0-9_.\\s=-]"); + // Currently we make assumptions about the configuration: the remotes, work and search branch names + private static final String REMOTE = "origin"; + private static final String WORK_BRANCH = "work"; + private static final String SEARCH_BRANCH = "search"; private final Path repositoryPath; private final Path studyDefinitionFile; - private final GitHandler gitHandler; + private final SlrGitHandler gitHandler; private final Study study; private final ImportFormatPreferences importFormatPreferences; private final FileUpdateMonitor fileUpdateMonitor; @@ -70,7 +80,7 @@ class StudyRepository { * @throws ParseException Problem parsing the study definition file. */ public StudyRepository(Path pathToRepository, - GitHandler gitHandler, + SlrGitHandler gitHandler, ImportFormatPreferences importFormatPreferences, FileUpdateMonitor fileUpdateMonitor, SavePreferences savePreferences, @@ -78,11 +88,6 @@ public StudyRepository(Path pathToRepository, BibEntryTypesManager bibEntryTypesManager) throws IOException, ParseException { this.repositoryPath = pathToRepository; this.gitHandler = gitHandler; - try { - gitHandler.updateLocalRepository(); - } catch (GitAPIException e) { - LOGGER.error("Updating repository from remote failed"); - } this.importFormatPreferences = importFormatPreferences; this.fileUpdateMonitor = fileUpdateMonitor; this.studyDefinitionFile = Path.of(repositoryPath.toString(), STUDY_DEFINITION_FILE_NAME); @@ -92,40 +97,75 @@ public StudyRepository(Path pathToRepository, if (Files.notExists(repositoryPath)) { throw new IOException("The given repository does not exists."); - } else if (Files.notExists(studyDefinitionFile)) { + } + try { + gitHandler.createCommitOnCurrentBranch("Save changes before searching.", false); + gitHandler.checkoutBranch(WORK_BRANCH); + updateWorkAndSearchBranch(); + } catch (GitAPIException e) { + LOGGER.error("Could not checkout work branch"); + } + if (Files.notExists(studyDefinitionFile)) { throw new IOException("The study definition file does not exist in the given repository."); } study = parseStudyFile(); - this.setUpRepositoryStructure(); + try { + // Update repository structure on work branch in case of changes + setUpRepositoryStructure(); + gitHandler.createCommitOnCurrentBranch("Setup/Update Repository Structure", false); + gitHandler.checkoutBranch(SEARCH_BRANCH); + // If study definition does not exist on this branch or was changed on work branch, copy it from work + boolean studyDefinitionDoesNotExistOrChanged = !(Files.exists(studyDefinitionFile) && new StudyYamlParser().parseStudyYamlFile(studyDefinitionFile).equalsBesideLastSearchDate(study)); + if (studyDefinitionDoesNotExistOrChanged) { + new StudyYamlParser().writeStudyYamlFile(study, studyDefinitionFile); + } + this.setUpRepositoryStructure(); + gitHandler.createCommitOnCurrentBranch("Setup/Update Repository Structure", false); + } catch (GitAPIException e) { + LOGGER.error("Could not checkout search branch."); + } + try { + gitHandler.checkoutBranch(WORK_BRANCH); + } catch (GitAPIException e) { + LOGGER.error("Could not checkout work branch"); + } } /** * Returns entries stored in the repository for a certain query and fetcher */ public BibDatabaseContext getFetcherResultEntries(String query, String fetcherName) throws IOException { - return OpenDatabase.loadDatabase(getPathToFetcherResultFile(query, fetcherName), importFormatPreferences, timestampPreferences, fileUpdateMonitor).getDatabaseContext(); + if (Files.exists(getPathToFetcherResultFile(query, fetcherName))) { + return OpenDatabase.loadDatabase(getPathToFetcherResultFile(query, fetcherName), importFormatPreferences, timestampPreferences, fileUpdateMonitor).getDatabaseContext(); + } + return new BibDatabaseContext(); } /** * Returns the merged entries stored in the repository for a certain query */ public BibDatabaseContext getQueryResultEntries(String query) throws IOException { - return OpenDatabase.loadDatabase(getPathToQueryResultFile(query), importFormatPreferences, timestampPreferences, fileUpdateMonitor).getDatabaseContext(); + if (Files.exists(getPathToQueryResultFile(query))) { + return OpenDatabase.loadDatabase(getPathToQueryResultFile(query), importFormatPreferences, timestampPreferences, fileUpdateMonitor).getDatabaseContext(); + } + return new BibDatabaseContext(); } /** * Returns the merged entries stored in the repository for all queries */ public BibDatabaseContext getStudyResultEntries() throws IOException { - return OpenDatabase.loadDatabase(getPathToStudyResultFile(), importFormatPreferences, timestampPreferences, fileUpdateMonitor).getDatabaseContext(); + if (Files.exists(getPathToStudyResultFile())) { + return OpenDatabase.loadDatabase(getPathToStudyResultFile(), importFormatPreferences, timestampPreferences, fileUpdateMonitor).getDatabaseContext(); + } + return new BibDatabaseContext(); } /** * The study definition file contains all the definitions of a study. This method extracts this study from the yaml study definition file * * @return Returns the BibEntries parsed from the study definition file. - * @throws IOException Problem opening the input stream. - * @throws ParseException Problem parsing the study definition file. + * @throws IOException Problem opening the input stream. */ private Study parseStudyFile() throws IOException { return new StudyYamlParser().parseStudyYamlFile(studyDefinitionFile); @@ -160,22 +200,70 @@ public Study getStudy() { return study; } - public void persist(List crawlResults) throws IOException, GitAPIException { - try { - gitHandler.updateLocalRepository(); - } catch (GitAPIException e) { - LOGGER.error("Updating repository from remote failed"); - } + /** + * Persists the result locally and remotely by following the steps: + * Precondition: Currently checking out work branch + *
    + *
  1. Update the work and search branch
  2. + *
  3. Persist the results on the search branch
  4. + *
  5. Manually patch the diff of the search branch onto the work branch (as the merging will not work in + * certain cases without a conflict as it is context sensitive. But for this use case we do not need it to be + * context sensitive. So we can just prepend the patch without checking the "context" lines.
  6. + *
  7. Update the remote tracking branches of the work and search branch
  8. + *
+ */ + public void persist(List crawlResults) throws IOException, GitAPIException, SaveException { + updateWorkAndSearchBranch(); + study.setLastSearchDate(LocalDate.now()); + persistStudy(); + gitHandler.createCommitOnCurrentBranch("Update search date", true); + gitHandler.checkoutBranch(SEARCH_BRANCH); persistResults(crawlResults); study.setLastSearchDate(LocalDate.now()); persistStudy(); try { - gitHandler.updateRemoteRepository("Conducted search " + LocalDate.now()); + // First commit changes to search branch branch and update remote + String commitMessage = "Conducted search: " + LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS); + boolean newSearchResults = gitHandler.createCommitOnCurrentBranch(commitMessage, false); + gitHandler.checkoutBranch(WORK_BRANCH); + if (!newSearchResults) { + return; + } + // Patch new results into work branch + gitHandler.appendLatestSearchResultsOntoCurrentBranch(commitMessage + " - Patch", SEARCH_BRANCH); + // Update both remote tracked branches + updateRemoteSearchAndWorkBranch(); } catch (GitAPIException e) { - LOGGER.error("Updating remote repository failed"); + LOGGER.error("Updating remote repository failed", e); } } + /** + * Update the remote tracking branches of the work and search branches + * The currently checked out branch is not changed if the method is executed successfully + */ + private void updateRemoteSearchAndWorkBranch() throws IOException, GitAPIException { + String currentBranch = gitHandler.getCurrentlyCheckedOutBranch(); + gitHandler.checkoutBranch(SEARCH_BRANCH); + gitHandler.pushCommitsToRemoteRepository(); + gitHandler.checkoutBranch(WORK_BRANCH); + gitHandler.pushCommitsToRemoteRepository(); + gitHandler.checkoutBranch(currentBranch); + } + + /** + * Updates the local work and search branches with changes from their tracking remote branches + * The currently checked out branch is not changed if the method is executed successfully + */ + private void updateWorkAndSearchBranch() throws IOException, GitAPIException { + String currentBranch = gitHandler.getCurrentlyCheckedOutBranch(); + gitHandler.checkoutBranch(SEARCH_BRANCH); + gitHandler.pullOnCurrentBranch(); + gitHandler.checkoutBranch(WORK_BRANCH); + gitHandler.pullOnCurrentBranch(); + gitHandler.checkoutBranch(currentBranch); + } + private void persistStudy() throws IOException { new StudyYamlParser().writeStudyYamlFile(study, studyDefinitionFile); } @@ -280,7 +368,7 @@ private String computeIDForQuery(String query) { * * @param crawlResults The results that shall be persisted. */ - private void persistResults(List crawlResults) throws IOException { + private void persistResults(List crawlResults) throws IOException, SaveException { DatabaseMerger merger = new DatabaseMerger(importFormatPreferences.getKeywordSeparator()); BibDatabase newStudyResultEntries = new BibDatabase(); @@ -290,11 +378,12 @@ private void persistResults(List crawlResults) throws IOException { BibDatabase fetcherEntries = fetcherResult.getFetchResult(); BibDatabaseContext existingFetcherResult = getFetcherResultEntries(result.getQuery(), fetcherResult.getFetcherName()); + // Merge new entries into fetcher result file + merger.merge(existingFetcherResult.getDatabase(), fetcherEntries); + // Create citation keys for all entries that do not have one generateCiteKeys(existingFetcherResult, fetcherEntries); - // Merge new entries into fetcher result file - merger.merge(existingFetcherResult.getDatabase(), fetcherEntries); // Aggregate each fetcher result into the query result merger.merge(queryResultEntries, fetcherEntries); @@ -322,11 +411,22 @@ private void generateCiteKeys(BibDatabaseContext existingEntries, BibDatabase ta targetEntries.getEntries().stream().filter(bibEntry -> !bibEntry.hasCitationKey()).forEach(citationKeyGenerator::generateAndSetKey); } - private void writeResultToFile(Path pathToFile, BibDatabase entries) throws IOException { + private void writeResultToFile(Path pathToFile, BibDatabase entries) throws IOException, SaveException { + if (!Files.exists(pathToFile)) { + Files.createFile(pathToFile); + } try (Writer fileWriter = new FileWriter(pathToFile.toFile())) { BibtexDatabaseWriter databaseWriter = new BibtexDatabaseWriter(fileWriter, savePreferences, bibEntryTypesManager); databaseWriter.saveDatabase(new BibDatabaseContext(entries)); } + try (AtomicFileWriter fileWriter = new AtomicFileWriter(pathToFile, savePreferences.getEncoding(), savePreferences.shouldMakeBackup())) { + BibtexDatabaseWriter databaseWriter = new BibtexDatabaseWriter(fileWriter, savePreferences, bibEntryTypesManager); + databaseWriter.saveDatabase(new BibDatabaseContext(entries)); + } catch (UnsupportedCharsetException ex) { + throw new SaveException(Localization.lang("Character encoding '%0' is not supported.", savePreferences.getEncoding().displayName()), ex); + } catch (IOException ex) { + throw new SaveException("Problems saving: " + ex, ex); + } } private Path getPathToFetcherResultFile(String query, String fetcherName) { diff --git a/src/main/java/org/jabref/logic/crawler/git/GitHandler.java b/src/main/java/org/jabref/logic/crawler/git/GitHandler.java deleted file mode 100644 index 439f08dfccd..00000000000 --- a/src/main/java/org/jabref/logic/crawler/git/GitHandler.java +++ /dev/null @@ -1,83 +0,0 @@ -package org.jabref.logic.crawler.git; - -import java.io.IOException; -import java.nio.file.Path; - -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.api.RmCommand; -import org.eclipse.jgit.api.Status; -import org.eclipse.jgit.api.errors.GitAPIException; -import org.eclipse.jgit.transport.CredentialsProvider; -import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * This class handles the updating of the local and remote git repository that is located at the repository path - */ -public class GitHandler { - private static final Logger LOGGER = LoggerFactory.getLogger(GitHandler.class); - private final Path repositoryPath; - private final CredentialsProvider credentialsProvider = new UsernamePasswordCredentialsProvider(System.getenv("GIT_EMAIL"), System.getenv("GIT_PW")); - - /** - * Initialize the handler for the given repository - * - * @param repositoryPath The root of the intialized git repository - */ - public GitHandler(Path repositoryPath) { - this.repositoryPath = repositoryPath; - } - - /** - * Updates the local repository based on the main branch of the original remote repository - */ - public void updateLocalRepository() throws IOException, GitAPIException { - try (Git git = Git.open(this.repositoryPath.toFile())) { - git.pull() - .setRemote("origin") - .setRemoteBranchName("main") - .setCredentialsProvider(credentialsProvider) - .call(); - } - } - - /** - * Adds all the added, changed, and removed files to the index and updates the remote origin repository - * If pushiong to remote fails it fails silently - * - * @param commitMessage The commit message used for the commit to the remote repository - */ - public void updateRemoteRepository(String commitMessage) throws IOException, GitAPIException { - // First get up to date - this.updateLocalRepository(); - try (Git git = Git.open(this.repositoryPath.toFile())) { - Status status = git.status().call(); - if (!status.isClean()) { - // Add new and changed files to index - git.add() - .addFilepattern(".") - .call(); - // Add all removed files to index - if (!status.getMissing().isEmpty()) { - RmCommand removeCommand = git.rm() - .setCached(true); - status.getMissing().forEach(removeCommand::addFilepattern); - removeCommand.call(); - } - git.commit() - .setAllowEmpty(false) - .setMessage(commitMessage) - .call(); - try { - - git.push() - .setCredentialsProvider(credentialsProvider) - .call(); - } catch (GitAPIException e) { - LOGGER.info("Failed to push"); - } - } - } - } -} diff --git a/src/main/java/org/jabref/logic/exporter/GroupSerializer.java b/src/main/java/org/jabref/logic/exporter/GroupSerializer.java index 635532c2f46..4e9c9997dd8 100644 --- a/src/main/java/org/jabref/logic/exporter/GroupSerializer.java +++ b/src/main/java/org/jabref/logic/exporter/GroupSerializer.java @@ -18,6 +18,7 @@ import org.jabref.model.groups.RegexKeywordGroup; import org.jabref.model.groups.SearchGroup; import org.jabref.model.groups.TexGroup; +import org.jabref.model.search.rules.SearchRules; import org.jabref.model.strings.StringUtil; public class GroupSerializer { @@ -69,9 +70,9 @@ private String serializeSearchGroup(SearchGroup group) { sb.append(MetadataSerializationConfiguration.GROUP_UNIT_SEPARATOR); sb.append(StringUtil.quote(group.getSearchExpression(), MetadataSerializationConfiguration.GROUP_UNIT_SEPARATOR, MetadataSerializationConfiguration.GROUP_QUOTE_CHAR)); sb.append(MetadataSerializationConfiguration.GROUP_UNIT_SEPARATOR); - sb.append(StringUtil.booleanToBinaryString(group.isCaseSensitive())); + sb.append(StringUtil.booleanToBinaryString(group.getSearchFlags().contains(SearchRules.SearchFlags.CASE_SENSITIVE))); sb.append(MetadataSerializationConfiguration.GROUP_UNIT_SEPARATOR); - sb.append(StringUtil.booleanToBinaryString(group.isRegularExpression())); + sb.append(StringUtil.booleanToBinaryString(group.getSearchFlags().contains(SearchRules.SearchFlags.REGULAR_EXPRESSION))); sb.append(MetadataSerializationConfiguration.GROUP_UNIT_SEPARATOR); appendGroupDetails(sb, group); diff --git a/src/main/java/org/jabref/logic/formatter/bibtexfields/CleanupUrlFormatter.java b/src/main/java/org/jabref/logic/formatter/bibtexfields/CleanupUrlFormatter.java index 6f480ac40e1..e1299e520e9 100644 --- a/src/main/java/org/jabref/logic/formatter/bibtexfields/CleanupUrlFormatter.java +++ b/src/main/java/org/jabref/logic/formatter/bibtexfields/CleanupUrlFormatter.java @@ -8,15 +8,11 @@ import org.jabref.logic.cleanup.Formatter; import org.jabref.logic.l10n.Localization; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - /** * Cleanup URL link */ public class CleanupUrlFormatter extends Formatter { - private static final Log LOGGER = LogFactory.getLog(CleanupUrlFormatter.class); // This regexp find "url=" or "to=" parameter in full link and get text after them private static final Pattern PATTERN_URL = Pattern.compile("(?:url|to)=([^&]*)"); diff --git a/src/main/java/org/jabref/logic/git/GitHandler.java b/src/main/java/org/jabref/logic/git/GitHandler.java new file mode 100644 index 00000000000..f486ae52878 --- /dev/null +++ b/src/main/java/org/jabref/logic/git/GitHandler.java @@ -0,0 +1,200 @@ +package org.jabref.logic.git; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; + +import org.jabref.logic.util.io.FileUtil; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.RmCommand; +import org.eclipse.jgit.api.Status; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.merge.MergeStrategy; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class handles the updating of the local and remote git repository that is located at the repository path + * This provides an easy to use interface to manage a git repository + */ +public class GitHandler { + static final Logger LOGGER = LoggerFactory.getLogger(GitHandler.class); + final Path repositoryPath; + final File repositoryPathAsFile; + String gitUsername = Optional.ofNullable(System.getenv("GIT_EMAIL")).orElse(""); + String gitPassword = Optional.ofNullable(System.getenv("GIT_PW")).orElse(""); + final CredentialsProvider credentialsProvider = new UsernamePasswordCredentialsProvider(gitUsername, gitPassword); + + /** + * Initialize the handler for the given repository + * + * @param repositoryPath The root of the initialized git repository + */ + public GitHandler(Path repositoryPath) { + this.repositoryPath = repositoryPath; + this.repositoryPathAsFile = this.repositoryPath.toFile(); + if (!isGitRepository()) { + try { + Git.init() + .setDirectory(repositoryPathAsFile) + .call(); + try (Git git = Git.open(repositoryPathAsFile)) { + git.commit() + .setAllowEmpty(true) + .setMessage("Initial commit") + .call(); + } + setupGitIgnore(); + } catch (GitAPIException | IOException e) { + LOGGER.error("Initialization failed"); + } + } + } + + void setupGitIgnore() { + try { + Path gitignore = Path.of(repositoryPath.toString(), ".gitignore"); + if (!Files.exists(gitignore)) { + FileUtil.copyFile(Path.of(this.getClass().getResource("git.gitignore").toURI()), gitignore, false); + } + } catch (URISyntaxException e) { + LOGGER.error("Error occurred during copying of the gitignore file into the git repository."); + } + } + + /** + * Returns true if the given path points to a directory that is a git repository (contains a .git folder) + */ + boolean isGitRepository() { + // For some reason the solution from https://www.eclipse.org/lists/jgit-dev/msg01892.html does not work + // This solution is quite simple but might not work in special cases, for us it should suffice. + return Files.exists(Path.of(repositoryPath.toString(), ".git")); + } + + /** + * Checkout the branch with the specified name, if it does not exist create it + * + * @param branchToCheckout Name of the branch to checkout + */ + public void checkoutBranch(String branchToCheckout) throws IOException, GitAPIException { + try (Git git = Git.open(this.repositoryPathAsFile)) { + Optional branch = getRefForBranch(branchToCheckout); + git.checkout() + // If the branch does not exist, create it + .setCreateBranch(branch.isEmpty()) + .setName(branchToCheckout) + .call(); + } + } + + /** + * Returns the reference of the specified branch + * If it does not exist returns an empty optional + */ + Optional getRefForBranch(String branchName) throws GitAPIException, IOException { + try (Git git = Git.open(this.repositoryPathAsFile)) { + return git.branchList() + .call() + .stream() + .filter(ref -> ref.getName().equals("refs/heads/" + branchName)) + .findAny(); + } + } + + /** + * Creates a commit on the currently checked out branch + * @param amend Whether to amend to the last commit (true), or not (false) + * @return Returns true if a new commit was created. This is the case if the repository was not clean on method invocation + */ + public boolean createCommitOnCurrentBranch(String commitMessage, boolean amend) throws IOException, GitAPIException { + boolean commitCreated = false; + try (Git git = Git.open(this.repositoryPathAsFile)) { + Status status = git.status().call(); + if (!status.isClean()) { + commitCreated = true; + // Add new and changed files to index + git.add() + .addFilepattern(".") + .call(); + // Add all removed files to index + if (!status.getMissing().isEmpty()) { + RmCommand removeCommand = git.rm() + .setCached(true); + status.getMissing().forEach(removeCommand::addFilepattern); + removeCommand.call(); + } + git.commit() + .setAmend(amend) + .setAllowEmpty(false) + .setMessage(commitMessage) + .call(); + } + } + return commitCreated; + } + + /** + * Merges the source branch into the target branch + * + * @param targetBranch the name of the branch that is merged into + * @param sourceBranch the name of the branch that gets merged + */ + public void mergeBranches(String targetBranch, String sourceBranch, MergeStrategy mergeStrategy) throws IOException, GitAPIException { + String currentBranch = this.getCurrentlyCheckedOutBranch(); + try (Git git = Git.open(this.repositoryPathAsFile)) { + Optional sourceBranchRef = getRefForBranch(sourceBranch); + if (sourceBranchRef.isEmpty()) { + // Do nothing + return; + } + this.checkoutBranch(targetBranch); + git.merge() + .include(sourceBranchRef.get()) + .setStrategy(mergeStrategy) + .setMessage("Merge search branch into working branch.") + .call(); + } + this.checkoutBranch(currentBranch); + } + + /** + * Pushes all commits made to the branch that is tracked by the currently checked out branch. + * If pushing to remote fails, it fails silently. + */ + public void pushCommitsToRemoteRepository() throws IOException { + try (Git git = Git.open(this.repositoryPathAsFile)) { + try { + git.push() + .setCredentialsProvider(credentialsProvider) + .call(); + } catch (GitAPIException e) { + LOGGER.info("Failed to push"); + } + } + } + + public void pullOnCurrentBranch() throws IOException { + try (Git git = Git.open(this.repositoryPathAsFile)) { + try { + git.pull() + .setCredentialsProvider(credentialsProvider) + .call(); + } catch (GitAPIException e) { + LOGGER.info("Failed to push"); + } + } + } + + public String getCurrentlyCheckedOutBranch() throws IOException { + try (Git git = Git.open(this.repositoryPathAsFile)) { + return git.getRepository().getBranch(); + } + } +} diff --git a/src/main/java/org/jabref/logic/git/SlrGitHandler.java b/src/main/java/org/jabref/logic/git/SlrGitHandler.java new file mode 100644 index 00000000000..d9bb027d220 --- /dev/null +++ b/src/main/java/org/jabref/logic/git/SlrGitHandler.java @@ -0,0 +1,155 @@ +package org.jabref.logic.git; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.StringJoiner; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.diff.DiffFormatter; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; + +public class SlrGitHandler extends GitHandler { + /** + * Initialize the handler for the given repository + * + * @param repositoryPath The root of the initialized git repository + */ + public SlrGitHandler(Path repositoryPath) { + super(repositoryPath); + } + + public void appendLatestSearchResultsOntoCurrentBranch(String patchMessage, String searchBranchName) throws IOException, GitAPIException { + // Calculate and apply new search results to work branch + String patch = calculatePatchOfNewSearchResults(searchBranchName); + Map result = parsePatchForAddedEntries(patch); + + applyPatch(result); + this.createCommitOnCurrentBranch(patchMessage, false); + } + + /** + * Calculates the diff between the HEAD and the previous commit of the sourceBranch. + * + * @param sourceBranch The name of the branch that is the target of the calculation + * @return Returns the patch (diff) between the head of the sourceBranch and its previous commit HEAD^1 + */ + String calculatePatchOfNewSearchResults(String sourceBranch) throws IOException, GitAPIException { + try (Git git = Git.open(this.repositoryPathAsFile)) { + Optional sourceBranchRef = getRefForBranch(sourceBranch); + if (sourceBranchRef.isEmpty()) { + return ""; + } + Repository repository = git.getRepository(); + ObjectId branchHead = sourceBranchRef.get().getObjectId(); + ObjectId treeIdHead = repository.resolve(branchHead.getName() + "^{tree}"); + ObjectId treeIdHeadParent = repository.resolve(branchHead.getName() + "~1^{tree}"); + + try (ObjectReader reader = repository.newObjectReader()) { + CanonicalTreeParser oldTreeIter = new CanonicalTreeParser(); + oldTreeIter.reset(reader, treeIdHeadParent); + CanonicalTreeParser newTreeIter = new CanonicalTreeParser(); + newTreeIter.reset(reader, treeIdHead); + + ByteArrayOutputStream put = new ByteArrayOutputStream(); + try (DiffFormatter formatter = new DiffFormatter(put)) { + formatter.setRepository(git.getRepository()); + List entries = formatter.scan(oldTreeIter, newTreeIter); + for (DiffEntry entry : entries) { + if (entry.getChangeType().equals(DiffEntry.ChangeType.MODIFY)) { + formatter.format(entry); + } + } + formatter.flush(); + return put.toString(); + } + } + } + } + + /** + * Applies the provided patch on the current branch + * Ignores any changes made to the study definition file. + * The reason for this is that the study definition file cannot be patched the same way as the bib files, as the + * order of fields in the yml file matters. + * + * @param patch the patch (diff) as a string + * @return Returns a map where each file has its path as a key and the string contains the hunk of new results + */ + Map parsePatchForAddedEntries(String patch) throws IOException, GitAPIException { + String[] tokens = patch.split("\n"); + // Tracks for each file the related diff. Represents each file by its relative path + Map diffsPerFile = new HashMap<>(); + boolean content = false; + StringJoiner joiner = null; + String relativePath = null; + for (String currentToken : tokens) { + // Begin of a new diff + if (currentToken.startsWith("diff --git a/")) { + // If the diff is related to a different file, save the diff for the previous file + if (!(Objects.isNull(relativePath) || Objects.isNull(joiner))) { + if (!relativePath.contains("study.yml")) { + diffsPerFile.put(Path.of(repositoryPath.toString(), relativePath), joiner.toString()); + } + } + // Find the relative path of the file that is related with the current diff + relativePath = currentToken.substring(13, currentToken.indexOf(" b/")); + content = false; + joiner = new StringJoiner("\n"); + continue; + } + // From here on content follows + if (currentToken.startsWith("@@ ") && currentToken.endsWith(" @@")) { + content = true; + continue; + } + // Only add "new" lines to diff (no context lines) + if (content && currentToken.startsWith("+")) { + // Do not include + sign + if (joiner != null) { + joiner.add(currentToken.substring(1)); + } + } + } + if (!(Objects.isNull(relativePath) || Objects.isNull(joiner))) { + // For the last file this has to be done at the end + diffsPerFile.put(Path.of(repositoryPath.toString(), relativePath), joiner.toString()); + } + return diffsPerFile; + } + + /** + * Applies for each file (specified as keys), the calculated patch (specified as the value) + * The patch is inserted between the encoding and the contents of the bib files. + */ + void applyPatch(Map patch) { + patch.keySet().forEach(path -> { + try { + String currentContent = Files.readString(path); + String prefix = ""; + if (currentContent.startsWith("% Encoding:")) { + int endOfEncoding = currentContent.indexOf("\n"); + // Include Encoding and the empty line + prefix = currentContent.substring(0, endOfEncoding + 1) + "\n"; + currentContent = currentContent.substring(endOfEncoding + 2); + } + Files.writeString(path, prefix + patch.get(path) + currentContent, StandardCharsets.UTF_8); + } catch (IOException e) { + LOGGER.error("Could not apply patch."); + } + }); + } +} diff --git a/src/main/java/org/jabref/logic/importer/FulltextFetchers.java b/src/main/java/org/jabref/logic/importer/FulltextFetchers.java index a7579573dac..57ad3f6c187 100644 --- a/src/main/java/org/jabref/logic/importer/FulltextFetchers.java +++ b/src/main/java/org/jabref/logic/importer/FulltextFetchers.java @@ -28,6 +28,8 @@ /** * Utility class for trying to resolve URLs to full-text PDF for articles. + * + * Combines multiple {@link FulltextFetcher}s together. Each fetcher is invoked, the "best" result (sorted by the fetcher trust level) is returned. */ public class FulltextFetchers { private static final Logger LOGGER = LoggerFactory.getLogger(FulltextFetchers.class); @@ -59,7 +61,7 @@ public Optional findFullTextPDF(BibEntry entry) { BibEntry clonedEntry = (BibEntry) entry.clone(); Optional doi = clonedEntry.getField(StandardField.DOI).flatMap(DOI::parse); - if (!doi.isPresent()) { + if (doi.isEmpty()) { findDoiForEntry(clonedEntry); } diff --git a/src/main/java/org/jabref/logic/importer/fetcher/AstrophysicsDataSystem.java b/src/main/java/org/jabref/logic/importer/fetcher/AstrophysicsDataSystem.java index 2fff3084f95..c3dc605e7ef 100644 --- a/src/main/java/org/jabref/logic/importer/fetcher/AstrophysicsDataSystem.java +++ b/src/main/java/org/jabref/logic/importer/fetcher/AstrophysicsDataSystem.java @@ -171,7 +171,6 @@ public void doPostCleanup(BibEntry entry) { @Override public List performSearch(BibEntry entry) throws FetcherException { - if (entry.getFieldOrAlias(StandardField.TITLE).isEmpty() && entry.getFieldOrAlias(StandardField.AUTHOR).isEmpty()) { return Collections.emptyList(); } @@ -191,10 +190,8 @@ public List performSearch(BibEntry entry) throws FetcherException { * @return list of bibcodes matching the search request. May be empty */ private List fetchBibcodes(URL url) throws FetcherException { - try { - URLDownload download = new URLDownload(url); - download.addHeader("Authorization", "Bearer " + API_KEY); + URLDownload download = getUrlDownload(url); String content = download.asString(); JSONObject obj = new JSONObject(content); JSONArray codes = obj.getJSONObject("response").getJSONArray("docs"); @@ -275,17 +272,42 @@ private List performSearchByIds(Collection identifiers) throws } } + @Override + public List performSearch(QueryNode luceneQuery) throws FetcherException { + URL urlForQuery; + try { + urlForQuery = getURLForQuery(luceneQuery); + } catch (URISyntaxException e) { + throw new FetcherException("Search URI is malformed", e); + } catch (IOException e) { + throw new FetcherException("A network error occurred", e); + } + List bibCodes = fetchBibcodes(urlForQuery); + List results = performSearchByIds(bibCodes); + return results; + } + @Override public Page performSearchPaged(QueryNode luceneQuery, int pageNumber) throws FetcherException { + URL urlForQuery; try { - // This is currently just interpreting the complex query as a default string query - List bibcodes = fetchBibcodes(getURLForQuery(luceneQuery, pageNumber)); - Collection results = performSearchByIds(bibcodes); - return new Page<>(luceneQuery.toString(), pageNumber, results); + urlForQuery = getURLForQuery(luceneQuery, pageNumber); } catch (URISyntaxException e) { throw new FetcherException("Search URI is malformed", e); } catch (IOException e) { throw new FetcherException("A network error occurred", e); } + // This is currently just interpreting the complex query as a default string query + List bibCodes = fetchBibcodes(urlForQuery); + Collection results = performSearchByIds(bibCodes); + return new Page<>(luceneQuery.toString(), pageNumber, results); } + + @Override + public URLDownload getUrlDownload(URL url) { + URLDownload urlDownload = new URLDownload(url); + urlDownload.addHeader("Authorization", "Bearer " + API_KEY); + return urlDownload; + } + } diff --git a/src/main/java/org/jabref/logic/importer/util/GroupsParser.java b/src/main/java/org/jabref/logic/importer/util/GroupsParser.java index 9ce459a4829..0fc85c6b03a 100644 --- a/src/main/java/org/jabref/logic/importer/util/GroupsParser.java +++ b/src/main/java/org/jabref/logic/importer/util/GroupsParser.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.nio.file.InvalidPathException; import java.nio.file.Path; +import java.util.EnumSet; import java.util.List; import org.jabref.logic.auxparser.DefaultAuxParser; @@ -26,6 +27,8 @@ import org.jabref.model.groups.TexGroup; import org.jabref.model.groups.WordKeywordGroup; import org.jabref.model.metadata.MetaData; +import org.jabref.model.search.rules.SearchRules; +import org.jabref.model.search.rules.SearchRules.SearchFlags; import org.jabref.model.strings.StringUtil; import org.jabref.model.util.FileUpdateMonitor; @@ -274,12 +277,17 @@ private static AbstractGroup searchGroupFromString(String s) { String name = StringUtil.unquote(tok.nextToken(), MetadataSerializationConfiguration.GROUP_QUOTE_CHAR); int context = Integer.parseInt(tok.nextToken()); String expression = StringUtil.unquote(tok.nextToken(), MetadataSerializationConfiguration.GROUP_QUOTE_CHAR); - boolean caseSensitive = Integer.parseInt(tok.nextToken()) == 1; - boolean regExp = Integer.parseInt(tok.nextToken()) == 1; + EnumSet searchFlags = EnumSet.noneOf(SearchFlags.class); + if (Integer.parseInt(tok.nextToken()) == 1) { + searchFlags.add(SearchRules.SearchFlags.CASE_SENSITIVE); + } + if (Integer.parseInt(tok.nextToken()) == 1) { + searchFlags.add(SearchRules.SearchFlags.REGULAR_EXPRESSION); + } // version 0 contained 4 additional booleans to specify search // fields; these are ignored now, all fields are always searched SearchGroup searchGroup = new SearchGroup(name, - GroupHierarchyType.getByNumberOrDefault(context), expression, caseSensitive, regExp + GroupHierarchyType.getByNumberOrDefault(context), expression, searchFlags ); addGroupDetails(tok, searchGroup); return searchGroup; diff --git a/src/main/java/org/jabref/logic/pdf/search/indexing/DocumentReader.java b/src/main/java/org/jabref/logic/pdf/search/indexing/DocumentReader.java new file mode 100644 index 00000000000..00e1ee0ccb3 --- /dev/null +++ b/src/main/java/org/jabref/logic/pdf/search/indexing/DocumentReader.java @@ -0,0 +1,146 @@ +package org.jabref.logic.pdf.search.indexing; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.jabref.gui.LibraryTab; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.LinkedFile; +import org.jabref.model.strings.StringUtil; +import org.jabref.preferences.FilePreferences; + +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.StringField; +import org.apache.lucene.document.TextField; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; +import org.apache.pdfbox.text.PDFTextStripper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.jabref.model.pdf.search.SearchFieldConstants.ANNOTATIONS; +import static org.jabref.model.pdf.search.SearchFieldConstants.CONTENT; +import static org.jabref.model.pdf.search.SearchFieldConstants.MODIFIED; +import static org.jabref.model.pdf.search.SearchFieldConstants.PATH; + +/** + * Utility class for reading the data from LinkedFiles of a BibEntry for Lucene. + */ +public final class DocumentReader { + + private static final Logger LOGGER = LoggerFactory.getLogger(LibraryTab.class); + + private final BibEntry entry; + private final FilePreferences filePreferences; + + /** + * Creates a new DocumentReader using a BibEntry. + * + * @param bibEntry Must not be null and must have at least one LinkedFile. + */ + public DocumentReader(BibEntry bibEntry, FilePreferences filePreferences) { + this.filePreferences = filePreferences; + if (bibEntry.getFiles().isEmpty()) { + throw new IllegalStateException("There are no linked PDF files to this BibEntry!"); + } + + this.entry = bibEntry; + } + + /** + * Reads a LinkedFile of a BibEntry and converts it into a Lucene Document which is then returned. + * + * @return An Optional of a Lucene Document with the (meta)data. Can be empty if there is a problem reading the LinkedFile. + */ + public Optional readLinkedPdf(BibDatabaseContext databaseContext, LinkedFile pdf) { + Optional pdfPath = pdf.findIn(databaseContext, filePreferences); + if (pdfPath.isPresent()) { + try { + return Optional.of(readPdfContents(pdf, pdfPath.get())); + } catch (IOException e) { + LOGGER.error("Could not read pdf file {}!", pdf.getLink(), e); + } + } + return Optional.empty(); + } + + /** + * Reads each LinkedFile of a BibEntry and converts them into Lucene Documents which are then returned. + * + * @return A List of Documents with the (meta)data. Can be empty if there is a problem reading the LinkedFile. + */ + public List readLinkedPdfs(BibDatabaseContext databaseContext) { + return entry.getFiles().stream() + .map((pdf) -> readLinkedPdf(databaseContext, pdf)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + } + + private Document readPdfContents(LinkedFile pdf, Path resolvedPdfPath) throws IOException { + try (PDDocument pdfDocument = PDDocument.load(resolvedPdfPath.toFile())) { + Document newDocument = new Document(); + addIdentifiers(newDocument, pdf.getLink()); + addContentIfNotEmpty(pdfDocument, newDocument); + addMetaData(newDocument, resolvedPdfPath); + return newDocument; + } + } + + private void addMetaData(Document newDocument, Path resolvedPdfPath) { + try { + BasicFileAttributes attributes = Files.readAttributes(resolvedPdfPath, BasicFileAttributes.class); + addStringField(newDocument, MODIFIED, String.valueOf(attributes.lastModifiedTime().to(TimeUnit.SECONDS))); + } catch (IOException e) { + LOGGER.error("Could not read timestamp for {}", resolvedPdfPath, e); + } + } + + private void addStringField(Document newDocument, String field, String value) { + if (!isValidField(value)) { + return; + } + newDocument.add(new StringField(field, value, Field.Store.YES)); + } + + private boolean isValidField(String value) { + return !(StringUtil.isNullOrEmpty(value)); + } + + private void addContentIfNotEmpty(PDDocument pdfDocument, Document newDocument) { + try { + PDFTextStripper pdfTextStripper = new PDFTextStripper(); + pdfTextStripper.setLineSeparator("\n"); + + String pdfContent = pdfTextStripper.getText(pdfDocument); + if (StringUtil.isNotBlank(pdfContent)) { + newDocument.add(new TextField(CONTENT, pdfContent, Field.Store.YES)); + } + for (PDPage page : pdfDocument.getPages()) { + for (PDAnnotation annotation : page.getAnnotations(annotation -> { + if (annotation.getContents() == null) { + return false; + } + return annotation.getSubtype().equals("Text") || annotation.getSubtype().equals("Highlight"); + })) { + newDocument.add(new TextField(ANNOTATIONS, annotation.getContents(), Field.Store.YES)); + } + } + } catch (IOException e) { + LOGGER.info("Could not read contents of PDF document \"{}\"", pdfDocument.toString(), e); + } + } + + private void addIdentifiers(Document newDocument, String path) { + newDocument.add(new StringField(PATH, path, Field.Store.YES)); + } +} diff --git a/src/main/java/org/jabref/logic/pdf/search/indexing/IndexingTaskManager.java b/src/main/java/org/jabref/logic/pdf/search/indexing/IndexingTaskManager.java new file mode 100644 index 00000000000..78c0f1d4328 --- /dev/null +++ b/src/main/java/org/jabref/logic/pdf/search/indexing/IndexingTaskManager.java @@ -0,0 +1,126 @@ +package org.jabref.logic.pdf.search.indexing; + +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +import org.jabref.gui.util.BackgroundTask; +import org.jabref.gui.util.TaskExecutor; +import org.jabref.logic.l10n.Localization; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.LinkedFile; + +/** + * Wrapper around {@link PdfIndexer} to execute all operations in the background. + */ +public class IndexingTaskManager extends BackgroundTask { + + Queue> taskQueue = new LinkedList<>(); + TaskExecutor taskExecutor; + + public IndexingTaskManager(TaskExecutor taskExecutor) { + this.taskExecutor = taskExecutor; + showToUser(true); + // the task itself is a nop, but it's progress property will be updated by the child-tasks it creates that actually interact with the index + this.updateProgress(1, 1); + this.titleProperty().set(Localization.lang("Indexing pdf files")); + this.executeWith(taskExecutor); + } + + @Override + protected Void call() throws Exception { + // update index to make sure it is up to date + this.updateProgress(1, 1); + return null; + } + + private void enqueueTask(BackgroundTask task) { + task.onFinished(() -> { + this.progressProperty().unbind(); + this.updateProgress(1, 1); + taskQueue.poll(); // This is the task that just finished + if (!taskQueue.isEmpty()) { + BackgroundTask nextTask = taskQueue.poll(); + nextTask.executeWith(taskExecutor); + this.progressProperty().bind(nextTask.progressProperty()); + } + }); + taskQueue.add(task); + if (taskQueue.size() == 1) { + task.executeWith(taskExecutor); + this.progressProperty().bind(task.progressProperty()); + } + } + + public void createIndex(PdfIndexer indexer, BibDatabase database, BibDatabaseContext context) { + enqueueTask(new BackgroundTask() { + @Override + protected Void call() throws Exception { + this.updateProgress(-1, 1); + indexer.createIndex(database, context); + return null; + } + }); + } + + public void addToIndex(PdfIndexer indexer, BibDatabaseContext databaseContext) { + enqueueTask(new BackgroundTask() { + @Override + protected Void call() throws Exception { + this.updateProgress(-1, 1); + indexer.addToIndex(databaseContext); + return null; + } + }); + } + + public void addToIndex(PdfIndexer indexer, BibEntry entry, BibDatabaseContext databaseContext) { + enqueueTask(new BackgroundTask() { + @Override + protected Void call() throws Exception { + this.updateProgress(-1, 1); + indexer.addToIndex(entry, databaseContext); + return null; + } + }); + } + + public void addToIndex(PdfIndexer indexer, BibEntry entry, List linkedFiles, BibDatabaseContext databaseContext) { + enqueueTask(new BackgroundTask() { + @Override + protected Void call() throws Exception { + this.updateProgress(-1, 1); + indexer.addToIndex(entry, linkedFiles, databaseContext); + return null; + } + }); + } + + public void removeFromIndex(PdfIndexer indexer, BibEntry entry, List linkedFiles) { + enqueueTask(new BackgroundTask() { + @Override + protected Void call() throws Exception { + this.updateProgress(-1, 1); + indexer.removeFromIndex(entry, linkedFiles); + return null; + } + }); + } + + public void removeFromIndex(PdfIndexer indexer, BibEntry entry) { + enqueueTask(new BackgroundTask() { + @Override + protected Void call() throws Exception { + this.updateProgress(-1, 1); + indexer.removeFromIndex(entry); + return null; + } + }); + } + + public void updateDatabaseName(String name) { + this.updateMessage(name); + } +} diff --git a/src/main/java/org/jabref/logic/pdf/search/indexing/PdfIndexer.java b/src/main/java/org/jabref/logic/pdf/search/indexing/PdfIndexer.java new file mode 100644 index 00000000000..a28784cb73b --- /dev/null +++ b/src/main/java/org/jabref/logic/pdf/search/indexing/PdfIndexer.java @@ -0,0 +1,232 @@ +package org.jabref.logic.pdf.search.indexing; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import javafx.collections.ObservableList; + +import org.jabref.gui.LibraryTab; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.LinkedFile; +import org.jabref.model.pdf.search.EnglishStemAnalyzer; +import org.jabref.model.pdf.search.SearchFieldConstants; +import org.jabref.preferences.FilePreferences; + +import org.apache.lucene.document.Document; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexNotFoundException; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.NIOFSDirectory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Indexes the text of PDF files and adds it into the lucene search index. + */ +public class PdfIndexer { + + private static final Logger LOGGER = LoggerFactory.getLogger(LibraryTab.class); + + private final Directory directoryToIndex; + private BibDatabaseContext databaseContext; + + private final FilePreferences filePreferences; + + public PdfIndexer(Directory indexDirectory, FilePreferences filePreferences) { + this.directoryToIndex = indexDirectory; + this.filePreferences = filePreferences; + } + + public static PdfIndexer of(BibDatabaseContext databaseContext, FilePreferences filePreferences) throws IOException { + return new PdfIndexer(new NIOFSDirectory(databaseContext.getFulltextIndexPath()), filePreferences); + } + + /** + * Adds all PDF files linked to an entry in the database to new Lucene search index. Any previous state of the + * Lucene search index will be deleted! + * + * @param database a bibtex database to link the pdf files to + */ + public void createIndex(BibDatabase database, BibDatabaseContext context) { + this.databaseContext = context; + final ObservableList entries = database.getEntries(); + + // Create new index by creating IndexWriter but not writing anything. + try { + IndexWriter indexWriter = new IndexWriter(directoryToIndex, new IndexWriterConfig(new EnglishStemAnalyzer()).setOpenMode(IndexWriterConfig.OpenMode.CREATE)); + indexWriter.close(); + } catch (IOException e) { + LOGGER.warn("Could not create new Index!", e); + } + // Re-use existing facilities for writing the actual entries + entries.stream().filter(entry -> !entry.getFiles().isEmpty()).forEach(this::writeToIndex); + } + + public void addToIndex(BibDatabaseContext databaseContext) { + for (BibEntry entry : databaseContext.getEntries()) { + addToIndex(entry, databaseContext); + } + } + + /** + * Adds all the pdf files linked to one entry in the database to an existing (or new) Lucene search index + * + * @param entry a bibtex entry to link the pdf files to + * @param databaseContext the associated BibDatabaseContext + */ + public void addToIndex(BibEntry entry, BibDatabaseContext databaseContext) { + addToIndex(entry, entry.getFiles(), databaseContext); + } + + /** + * Adds a list of pdf files linked to one entry in the database to an existing (or new) Lucene search index + * + * @param entry a bibtex entry to link the pdf files to + * @param databaseContext the associated BibDatabaseContext + */ + public void addToIndex(BibEntry entry, List linkedFiles, BibDatabaseContext databaseContext) { + for (LinkedFile linkedFile : linkedFiles) { + addToIndex(entry, linkedFile, databaseContext); + } + } + + /** + * Adds a pdf file linked to one entry in the database to an existing (or new) Lucene search index + * + * @param entry a bibtex entry + * @param linkedFile the link to the pdf files + */ + public void addToIndex(BibEntry entry, LinkedFile linkedFile, BibDatabaseContext databaseContext) { + if (databaseContext != null) { + this.databaseContext = databaseContext; + } + if (!entry.getFiles().isEmpty()) { + writeToIndex(entry, linkedFile); + } + } + + /** + * Removes a pdf file linked to one entry in the database from the index + * @param entry the entry the file is linked to + * @param linkedFile the link to the file to be removed + */ + public void removeFromIndex(BibEntry entry, LinkedFile linkedFile) { + try (IndexWriter indexWriter = new IndexWriter( + directoryToIndex, + new IndexWriterConfig( + new EnglishStemAnalyzer()).setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND)) + ) { + if (!entry.getFiles().isEmpty()) { + indexWriter.deleteDocuments(new Term(SearchFieldConstants.PATH, linkedFile.getLink())); + } + indexWriter.commit(); + } catch (IOException e) { + LOGGER.warn("Could not initialize the IndexWriter!", e); + } + } + + /** + * Removes all files linked to a bib-entry from the index + * @param entry the entry documents are linked to + */ + public void removeFromIndex(BibEntry entry) { + removeFromIndex(entry, entry.getFiles()); + } + + /** + * Removes a list of files linked to a bib-entry from the index + * @param entry the entry documents are linked to + */ + public void removeFromIndex(BibEntry entry, List linkedFiles) { + for (LinkedFile linkedFile : linkedFiles) { + removeFromIndex(entry, linkedFile); + } + } + + /** + * Deletes all entries from the Lucene search index. + */ + public void flushIndex() { + IndexWriterConfig config = new IndexWriterConfig(); + config.setOpenMode(IndexWriterConfig.OpenMode.CREATE); + try (IndexWriter deleter = new IndexWriter(directoryToIndex, config)) { + // Do nothing. Index is deleted. + } catch (IOException e) { + LOGGER.warn("The IndexWriter could not be initialized", e); + } + } + + /** + * Writes all files linked to an entry to the index if the files are not yet in the index or the files on the fs are + * newer than the one in the index. + * @param entry the entry associated with the file + */ + private void writeToIndex(BibEntry entry) { + for (LinkedFile linkedFile : entry.getFiles()) { + writeToIndex(entry, linkedFile); + } + } + + /** + * Writes the file to the index if the file is not yet in the index or the file on the fs is newer than the one in + * the index. + * @param entry the entry associated with the file + * @param linkedFile the file to write to the index + */ + private void writeToIndex(BibEntry entry, LinkedFile linkedFile) { + Optional resolvedPath = linkedFile.findIn(databaseContext, filePreferences); + if (resolvedPath.isEmpty()) { + LOGGER.warn("Could not find {}", linkedFile.getLink()); + return; + } + try { + // Check if a document with this path is already in the index + try { + IndexReader reader = DirectoryReader.open(directoryToIndex); + IndexSearcher searcher = new IndexSearcher(reader); + TermQuery query = new TermQuery(new Term(SearchFieldConstants.PATH, linkedFile.getLink())); + TopDocs topDocs = searcher.search(query, 1); + // If a document was found, check if is less current than the one in the FS + if (topDocs.scoreDocs.length > 0) { + Document doc = reader.document(topDocs.scoreDocs[0].doc); + long indexModificationTime = Long.parseLong(doc.getField(SearchFieldConstants.MODIFIED).stringValue()); + + BasicFileAttributes attributes = Files.readAttributes(resolvedPath.get(), BasicFileAttributes.class); + + if (indexModificationTime >= attributes.lastModifiedTime().to(TimeUnit.SECONDS)) { + return; + } + } + reader.close(); + } catch (IndexNotFoundException e) { + // if there is no index yet, don't need to check anything! + } + // If no document was found, add the new one + Optional document = new DocumentReader(entry, filePreferences).readLinkedPdf(this.databaseContext, linkedFile); + if (document.isPresent()) { + IndexWriter indexWriter = new IndexWriter(directoryToIndex, + new IndexWriterConfig( + new EnglishStemAnalyzer()).setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND)); + indexWriter.addDocument(document.get()); + indexWriter.commit(); + indexWriter.close(); + } + } catch (IOException e) { + LOGGER.warn("Could not add the document to the index!", e); + } + } +} diff --git a/src/main/java/org/jabref/logic/pdf/search/retrieval/PdfSearcher.java b/src/main/java/org/jabref/logic/pdf/search/retrieval/PdfSearcher.java new file mode 100644 index 00000000000..59d320e393f --- /dev/null +++ b/src/main/java/org/jabref/logic/pdf/search/retrieval/PdfSearcher.java @@ -0,0 +1,76 @@ +package org.jabref.logic.pdf.search.retrieval; + +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; + +import org.jabref.gui.LibraryTab; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.pdf.search.EnglishStemAnalyzer; +import org.jabref.model.pdf.search.PdfSearchResults; +import org.jabref.model.pdf.search.SearchResult; +import org.jabref.model.strings.StringUtil; + +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.queryparser.classic.MultiFieldQueryParser; +import org.apache.lucene.queryparser.classic.ParseException; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.NIOFSDirectory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.jabref.model.pdf.search.SearchFieldConstants.PDF_FIELDS; + +public final class PdfSearcher { + + private static final Logger LOGGER = LoggerFactory.getLogger(LibraryTab.class); + + private final Directory indexDirectory; + + private PdfSearcher(Directory indexDirectory) { + this.indexDirectory = indexDirectory; + } + + public static PdfSearcher of(BibDatabaseContext databaseContext) throws IOException { + return new PdfSearcher(new NIOFSDirectory(databaseContext.getFulltextIndexPath())); + } + + /** + * Search for results matching a query in the Lucene search index + * + * @param searchString a pattern to search for matching entries in the index, must not be null + * @param maxHits number of maximum search results, must be positive + * @return a result set of all documents that have matches in any fields + */ + public PdfSearchResults search(final String searchString, final int maxHits) + throws IOException { + if (StringUtil.isBlank(Objects.requireNonNull(searchString, "The search string was null!"))) { + return new PdfSearchResults(); + } + if (maxHits <= 0) { + throw new IllegalArgumentException("Must be called with at least 1 maxHits, was" + maxHits); + } + + try { + List resultDocs = new LinkedList<>(); + + IndexReader reader = DirectoryReader.open(indexDirectory); + IndexSearcher searcher = new IndexSearcher(reader); + Query query = new MultiFieldQueryParser(PDF_FIELDS, new EnglishStemAnalyzer()).parse(searchString); + TopDocs results = searcher.search(query, maxHits); + for (ScoreDoc scoreDoc : results.scoreDocs) { + resultDocs.add(new SearchResult(searcher, query, scoreDoc)); + } + return new PdfSearchResults(resultDocs); + } catch (ParseException e) { + LOGGER.warn("Could not parse query: '" + searchString + "'! \n" + e.getMessage()); + return new PdfSearchResults(); + } + } +} diff --git a/src/main/java/org/jabref/logic/search/SearchQuery.java b/src/main/java/org/jabref/logic/search/SearchQuery.java index 5138edcf178..883830c3eab 100644 --- a/src/main/java/org/jabref/logic/search/SearchQuery.java +++ b/src/main/java/org/jabref/logic/search/SearchQuery.java @@ -1,6 +1,7 @@ package org.jabref.logic.search; import java.util.Collections; +import java.util.EnumSet; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -18,6 +19,7 @@ import org.jabref.model.search.rules.SentenceAnalyzer; public class SearchQuery implements SearchMatcher { + /** * The mode of escaping special characters in regular expressions */ @@ -56,15 +58,13 @@ String format(String regex) { } private final String query; - private final boolean caseSensitive; - private final boolean regularExpression; + private EnumSet searchFlags; private final SearchRule rule; - public SearchQuery(String query, boolean caseSensitive, boolean regularExpression) { + public SearchQuery(String query, EnumSet searchFlags) { this.query = Objects.requireNonNull(query); - this.caseSensitive = caseSensitive; - this.regularExpression = regularExpression; - this.rule = SearchRules.getSearchRuleByQuery(query, caseSensitive, regularExpression); + this.searchFlags = searchFlags; + this.rule = SearchRules.getSearchRuleByQuery(query, searchFlags); } @Override @@ -86,7 +86,7 @@ public boolean isContainsBasedSearch() { } private String getCaseSensitiveDescription() { - if (isCaseSensitive()) { + if (searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { return "case sensitive"; } else { return "case insensitive"; @@ -94,7 +94,7 @@ private String getCaseSensitiveDescription() { } private String getRegularExpressionDescription() { - if (isRegularExpression()) { + if (searchFlags.contains(SearchRules.SearchFlags.REGULAR_EXPRESSION)) { return "regular expression"; } else { return "plain text"; @@ -109,7 +109,7 @@ public String localize() { } private String getLocalizedCaseSensitiveDescription() { - if (isCaseSensitive()) { + if (searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { return Localization.lang("case sensitive"); } else { return Localization.lang("case insensitive"); @@ -117,7 +117,7 @@ private String getLocalizedCaseSensitiveDescription() { } private String getLocalizedRegularExpressionDescription() { - if (isRegularExpression()) { + if (searchFlags.contains(SearchRules.SearchFlags.REGULAR_EXPRESSION)) { return Localization.lang("regular expression"); } else { return Localization.lang("plain text"); @@ -137,19 +137,15 @@ public String getQuery() { return query; } - public boolean isCaseSensitive() { - return caseSensitive; - } - - public boolean isRegularExpression() { - return regularExpression; + public EnumSet getSearchFlags() { + return searchFlags; } /** * Returns a list of words this query searches for. The returned strings can be a regular expression. */ public List getSearchWords() { - if (isRegularExpression()) { + if (searchFlags.contains(SearchRules.SearchFlags.REGULAR_EXPRESSION)) { return Collections.singletonList(getQuery()); } else { // Parses the search query for valid words and returns a list these words. @@ -182,13 +178,13 @@ private Optional joinWordsToPattern(EscapeMode escapeMode) { // compile the words to a regular expression in the form (w1)|(w2)|(w3) Stream joiner = words.stream(); - if (!regularExpression) { + if (!searchFlags.contains(SearchRules.SearchFlags.REGULAR_EXPRESSION)) { // Reformat string when we are looking for a literal match joiner = joiner.map(escapeMode::format); } String searchPattern = joiner.collect(Collectors.joining(")|(", "(", ")")); - if (caseSensitive) { + if (searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { return Optional.of(Pattern.compile(searchPattern)); } else { return Optional.of(Pattern.compile(searchPattern, Pattern.CASE_INSENSITIVE)); diff --git a/src/main/java/org/jabref/logic/util/Version.java b/src/main/java/org/jabref/logic/util/Version.java index 1f6897cf29f..866326068aa 100644 --- a/src/main/java/org/jabref/logic/util/Version.java +++ b/src/main/java/org/jabref/logic/util/Version.java @@ -12,6 +12,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.jabref.logic.net.URLDownload; + import kong.unirest.json.JSONArray; import kong.unirest.json.JSONObject; import org.slf4j.Logger; @@ -94,12 +96,12 @@ public static Version parse(String version) { * Grabs all the available releases from the GitHub repository */ public static List getAllAvailableVersions() throws IOException { + URLDownload.bypassSSLVerification(); HttpURLConnection connection = (HttpURLConnection) new URL(JABREF_GITHUB_RELEASES).openConnection(); connection.setRequestProperty("Accept-Charset", "UTF-8"); try (BufferedReader rd = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { - - List versions = new ArrayList<>(); JSONArray objects = new JSONArray(rd.readLine()); + List versions = new ArrayList<>(objects.length()); for (int i = 0; i < objects.length(); i++) { JSONObject jsonObject = objects.getJSONObject(i); Version version = Version.parse(jsonObject.getString("tag_name").replaceFirst("v", "")); diff --git a/src/main/java/org/jabref/model/database/BibDatabaseContext.java b/src/main/java/org/jabref/model/database/BibDatabaseContext.java index 2b0e713614f..1037b1bc0c6 100644 --- a/src/main/java/org/jabref/model/database/BibDatabaseContext.java +++ b/src/main/java/org/jabref/model/database/BibDatabaseContext.java @@ -9,13 +9,19 @@ import java.util.stream.Collectors; import org.jabref.architecture.AllowedToUseLogic; +import org.jabref.gui.LibraryTab; import org.jabref.logic.shared.DatabaseLocation; import org.jabref.logic.shared.DatabaseSynchronizer; import org.jabref.logic.util.CoarseChangeFilter; import org.jabref.model.entry.BibEntry; import org.jabref.model.metadata.MetaData; +import org.jabref.model.pdf.search.SearchFieldConstants; import org.jabref.preferences.FilePreferences; +import net.harawata.appdirs.AppDirsFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Represents everything related to a BIB file.

The entries are stored in BibDatabase, the other data in MetaData * and the options relevant for this file in Defaults. @@ -23,6 +29,10 @@ @AllowedToUseLogic("because it needs access to shared database features") public class BibDatabaseContext { + public static final String SEARCH_INDEX_BASE_PATH = "JabRef"; + + private static final Logger LOGGER = LoggerFactory.getLogger(LibraryTab.class); + private final BibDatabase database; private MetaData metaData; @@ -191,14 +201,6 @@ public void convertToSharedDatabase(DatabaseSynchronizer dmbsSynchronizer) { this.location = DatabaseLocation.SHARED; } - @Override - public String toString() { - return "BibDatabaseContext{" + - "path=" + path + - ", location=" + location + - '}'; - } - public void convertToLocalDatabase() { if (Objects.nonNull(dbmsListener) && (location == DatabaseLocation.SHARED)) { dbmsListener.unregisterListener(dbmsSynchronizer); @@ -211,4 +213,28 @@ public void convertToLocalDatabase() { public List getEntries() { return database.getEntries(); } + + public static Path getFulltextIndexBasePath() { + return Path.of(AppDirsFactory.getInstance().getUserDataDir(SEARCH_INDEX_BASE_PATH, SearchFieldConstants.VERSION, "org.jabref")); + } + + public Path getFulltextIndexPath() { + Path appData = getFulltextIndexBasePath(); + LOGGER.info("Index path for {} is {}", getDatabasePath().get(), appData.toString()); + if (getDatabasePath().isPresent()) { + return appData.resolve(String.valueOf(this.getDatabasePath().get().hashCode())); + } + return appData.resolve("unsaved"); + } + + @Override + public String toString() { + return "BibDatabaseContext{" + + "metaData=" + metaData + + ", mode=" + getMode() + + ", databasePath=" + getDatabasePath() + + ", biblatexMode=" + isBiblatexMode() + + ", fulltextIndexPath=" + getFulltextIndexPath() + + '}'; + } } diff --git a/src/main/java/org/jabref/model/groups/SearchGroup.java b/src/main/java/org/jabref/model/groups/SearchGroup.java index 2346c23c10a..5e261c438b9 100644 --- a/src/main/java/org/jabref/model/groups/SearchGroup.java +++ b/src/main/java/org/jabref/model/groups/SearchGroup.java @@ -1,9 +1,11 @@ package org.jabref.model.groups; +import java.util.EnumSet; import java.util.Objects; import org.jabref.model.entry.BibEntry; import org.jabref.model.search.GroupSearchQuery; +import org.jabref.model.search.rules.SearchRules.SearchFlags; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,10 +19,9 @@ public class SearchGroup extends AbstractGroup { private static final Logger LOGGER = LoggerFactory.getLogger(SearchGroup.class); private final GroupSearchQuery query; - public SearchGroup(String name, GroupHierarchyType context, String searchExpression, boolean caseSensitive, - boolean isRegEx) { + public SearchGroup(String name, GroupHierarchyType context, String searchExpression, EnumSet searchFlags) { super(name, context); - this.query = new GroupSearchQuery(searchExpression, caseSensitive, isRegEx); + this.query = new GroupSearchQuery(searchExpression, searchFlags); } public String getSearchExpression() { @@ -38,8 +39,7 @@ public boolean equals(Object o) { SearchGroup other = (SearchGroup) o; return getName().equals(other.getName()) && getSearchExpression().equals(other.getSearchExpression()) - && (isCaseSensitive() == other.isCaseSensitive()) - && (isRegularExpression() == other.isRegularExpression()) + && (getSearchFlags().equals(other.getSearchFlags())) && (getHierarchicalContext() == other.getHierarchicalContext()); } @@ -48,11 +48,14 @@ public boolean contains(BibEntry entry) { return query.isMatch(entry); } + public EnumSet getSearchFlags() { + return query.getSearchFlags(); + } + @Override public AbstractGroup deepCopy() { try { - return new SearchGroup(getName(), getHierarchicalContext(), getSearchExpression(), isCaseSensitive(), - isRegularExpression()); + return new SearchGroup(getName(), getHierarchicalContext(), getSearchExpression(), getSearchFlags()); } catch (Throwable t) { // this should never happen, because the constructor obviously // succeeded in creating _this_ instance! @@ -62,14 +65,6 @@ public AbstractGroup deepCopy() { } } - public boolean isCaseSensitive() { - return query.isCaseSensitive(); - } - - public boolean isRegularExpression() { - return query.isRegularExpression(); - } - @Override public boolean isDynamic() { return true; @@ -77,6 +72,6 @@ public boolean isDynamic() { @Override public int hashCode() { - return Objects.hash(getName(), getHierarchicalContext(), getSearchExpression(), isCaseSensitive(), isRegularExpression()); + return Objects.hash(getName(), getHierarchicalContext(), getSearchExpression(), getSearchFlags()); } } diff --git a/src/main/java/org/jabref/model/openoffice/ootext/OOFormat.java b/src/main/java/org/jabref/model/openoffice/ootext/OOFormat.java new file mode 100644 index 00000000000..6b4a96de644 --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/ootext/OOFormat.java @@ -0,0 +1,84 @@ +package org.jabref.model.openoffice.ootext; + +import org.jabref.model.strings.StringUtil; + +/** + * Helper functions to produce some of the markup as understood by OOTextIntoOO.write + * + * These do not cover all tags, only those needed to embed markup + * from Layout and citation marker formatters into citation markers and + * bibliography. + */ +public class OOFormat { + + private OOFormat() { + /* */ + } + + /** + * Mark {@code ootext} as using a character locale known to OO. + * + * @param locale language[-country[-territory]] + * + * https://www.openoffice.org/api/docs/common/ref/com/sun/star/lang/Locale.html + * + * The country part is optional. + * + * The territory part is not only optional, the allowed "codes are vendor and browser-specific", + * so probably best to avoid them if possible. + * + */ + public static OOText setLocale(OOText ootext, String locale) { + return OOText.fromString(String.format("", locale) + ootext.toString() + ""); + } + + /** + * Mark {@code ootext} as using the character locale "zxx", which means "no language", "no + * linguistic content". + * + * Used around citation marks, probably to turn off spellchecking. + * + */ + public static OOText setLocaleNone(OOText ootext) { + return OOFormat.setLocale(ootext, "zxx"); + } + + /** + * Mark {@code ootext} using a character style {@code charStyle} + * + * @param charStyle Name of a character style known to OO. May be empty for "Standard", which in + * turn means do not override any properties. + * + */ + public static OOText setCharStyle(OOText ootext, String charStyle) { + return OOText.fromString(String.format("", charStyle) + + ootext.toString() + + ""); + } + + /** + * Mark {@code ootext} as part of a paragraph with style {@code paraStyle} + */ + public static OOText paragraph(OOText ootext, String paraStyle) { + if (StringUtil.isNullOrEmpty(paraStyle)) { + return paragraph(ootext); + } + String startTag = String.format("

", paraStyle); + return OOText.fromString(startTag + ootext.toString() + "

"); + } + + /** + * Mark {@code ootext} as part of a paragraph. + */ + public static OOText paragraph(OOText ootext) { + return OOText.fromString("

" + ootext.toString() + "

"); + } + + /** + * Format an OO cross-reference showing the target's page number as label to a reference mark. + */ + public static OOText formatReferenceToPageNumberOfReferenceMark(String referenceMarkName) { + String string = String.format("", referenceMarkName); + return OOText.fromString(string); + } + } diff --git a/src/main/java/org/jabref/model/openoffice/ootext/OOText.java b/src/main/java/org/jabref/model/openoffice/ootext/OOText.java new file mode 100644 index 00000000000..5b5fa4caba9 --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/ootext/OOText.java @@ -0,0 +1,61 @@ +package org.jabref.model.openoffice.ootext; + +import java.util.Objects; + +/** + * Text with HTML-like markup as understood by OOTextIntoOO.write + * + * Some of the tags can be added using OOFormat methods. Others come from the layout engine, either + * by interpreting LaTeX markup or from settings in the jstyle file. + */ +public class OOText { + + private final String data; + + private OOText(String data) { + Objects.requireNonNull(data); + this.data = data; + } + + /** @return null for null input, otherwise the argument wrapped into a new OOText */ + public static OOText fromString(String string) { + if (string == null) { + return null; + } + return new OOText(string); + } + + /** @return null for null input, otherwise the string inside the argument */ + public static String toString(OOText ootext) { + if (ootext == null) { + return null; + } + return ootext.data; + } + + @Override + public String toString() { + return data; + } + + @Override + public boolean equals(Object object) { + + if (object == this) { + return true; + } + + if (!(object instanceof OOText)) { + return false; + } + + OOText other = (OOText) object; + + return data.equals(other.data); + } + + @Override + public int hashCode() { + return data.hashCode(); + } +} diff --git a/src/main/java/org/jabref/model/openoffice/ootext/OOTextIntoOO.java b/src/main/java/org/jabref/model/openoffice/ootext/OOTextIntoOO.java new file mode 100644 index 00000000000..6e61faed147 --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/ootext/OOTextIntoOO.java @@ -0,0 +1,810 @@ +package org.jabref.model.openoffice.ootext; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.Stack; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.jabref.architecture.AllowedToUseAwt; +import org.jabref.model.openoffice.uno.CreationException; +import org.jabref.model.openoffice.uno.UnoCast; +import org.jabref.model.openoffice.uno.UnoCrossRef; +import org.jabref.model.openoffice.util.OOPair; +import org.jabref.model.strings.StringUtil; + +import com.sun.star.awt.FontSlant; +import com.sun.star.awt.FontStrikeout; +import com.sun.star.awt.FontUnderline; +import com.sun.star.awt.FontWeight; +import com.sun.star.beans.Property; +import com.sun.star.beans.PropertyAttribute; +import com.sun.star.beans.PropertyState; +import com.sun.star.beans.PropertyVetoException; +import com.sun.star.beans.UnknownPropertyException; +import com.sun.star.beans.XMultiPropertySet; +import com.sun.star.beans.XMultiPropertyStates; +import com.sun.star.beans.XPropertySet; +import com.sun.star.beans.XPropertySetInfo; +import com.sun.star.beans.XPropertyState; +import com.sun.star.lang.Locale; +import com.sun.star.lang.WrappedTargetException; +import com.sun.star.style.CaseMap; +import com.sun.star.text.ControlCharacter; +import com.sun.star.text.XParagraphCursor; +import com.sun.star.text.XText; +import com.sun.star.text.XTextCursor; +import com.sun.star.text.XTextDocument; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Interpret OOText into an OpenOffice or LibreOffice writer document. + */ +@AllowedToUseAwt("Requires AWT for changing document properties") +public class OOTextIntoOO { + + private static final Logger LOGGER = LoggerFactory.getLogger(OOTextIntoOO.class); + + /** + * "ParaStyleName" is an OpenOffice Property name. + */ + private static final String PARA_STYLE_NAME = "ParaStyleName"; + + /* + * Character property names used in multiple locations below. + */ + private static final String CHAR_ESCAPEMENT_HEIGHT = "CharEscapementHeight"; + private static final String CHAR_ESCAPEMENT = "CharEscapement"; + private static final String CHAR_STYLE_NAME = "CharStyleName"; + private static final String CHAR_UNDERLINE = "CharUnderline"; + private static final String CHAR_STRIKEOUT = "CharStrikeout"; + + /* + * SUPERSCRIPT_VALUE and SUPERSCRIPT_HEIGHT are percents of the normal character height + */ + private static final short CHAR_ESCAPEMENT_VALUE_DEFAULT = (short) 0; + private static final short SUPERSCRIPT_VALUE = (short) 33; + private static final short SUBSCRIPT_VALUE = (short) -10; + private static final byte CHAR_ESCAPEMENT_HEIGHT_DEFAULT = (byte) 100; + private static final byte SUPERSCRIPT_HEIGHT = (byte) 58; + private static final byte SUBSCRIPT_HEIGHT = (byte) 58; + + private static final String TAG_NAME_REGEXP = + "(?:b|i|em|tt|smallcaps|sup|sub|u|s|p|span|oo:referenceToPageNumberOfReferenceMark)"; + + private static final String ATTRIBUTE_NAME_REGEXP = + "(?:oo:ParaStyleName|oo:CharStyleName|lang|style|target)"; + + private static final String ATTRIBUTE_VALUE_REGEXP = "\"([^\"]*)\""; + + private static final Pattern HTML_TAG = + Pattern.compile("<(/" + TAG_NAME_REGEXP + ")>" + + "|" + + "<(" + TAG_NAME_REGEXP + ")" + + "((?:\\s+(" + ATTRIBUTE_NAME_REGEXP + ")=" + ATTRIBUTE_VALUE_REGEXP + ")*)" + + ">"); + + private static final Pattern ATTRIBUTE_PATTERN = + Pattern.compile("\\s+(" + ATTRIBUTE_NAME_REGEXP + ")=" + ATTRIBUTE_VALUE_REGEXP); + + private OOTextIntoOO() { + // Hide the public constructor + } + + /** + * Insert a text with formatting indicated by HTML-like tags, into + * a text at the position given by a cursor. + * + * Limitation: understands no entities. It does not receive any either, unless + * the user provides it. + * + * To limit the damage {@code TAG_NAME_REGEXP} and {@code ATTRIBUTE_NAME_REGEXP} + * explicitly lists the names we care about. + * + * Notable changes w.r.t insertOOFormattedTextAtCurrentLocation: + * + * - new tags: + * + * - {@code } + * - earlier was applied from code + * + * - {@code } + * - earlier was applied from code, for "CitationCharacterFormat" + * + * - {@code

} start new paragraph + * - earlier was applied from code + * + * - {@code

} : start new paragraph and apply ParStyleName + * - earlier was applied from code + * + * - {@code } + * - earlier: known, but ignored + * - now: equivalent to {@code } + * - {@code } (self-closing) + * + * - closing tags try to properly restore state (in particular, the "not directly set" state) + * instead of dictating an "off" state. This makes a difference when the value inherited from + * another level (for example the paragraph) is not the "off" state. + * + * An example: a style with + * {@code ReferenceParagraphFormat="JR_bibentry"} + * Assume JR_bibentry in LibreOffice is a paragraph style that prescribes "bold" font. + * LAYOUT only prescribes bold around year. + * Which parts of the bibliography entries should come out as bold? + * + * - The user can format citation marks (it is enough to format their start) and the + * properties not (everywhere) dictated by the style are preserved (where they are not). + * + * @param position The cursor giving the insert location. Not modified. + * @param ootext The marked-up text to insert. + */ + public static void write(XTextDocument doc, XTextCursor position, OOText ootext) + throws + WrappedTargetException, + CreationException { + + Objects.requireNonNull(doc); + Objects.requireNonNull(ootext); + Objects.requireNonNull(position); + + String lText = OOText.toString(ootext); + + LOGGER.debug(lText); + + XText text = position.getText(); + XTextCursor cursor = text.createTextCursorByRange(position); + cursor.collapseToEnd(); + + MyPropertyStack formatStack = new MyPropertyStack(cursor); + Stack expectEnd = new Stack<>(); + + // We need to extract formatting. Use a simple regexp search iteration: + int piv = 0; + Matcher tagMatcher = HTML_TAG.matcher(lText); + while (tagMatcher.find()) { + + String currentSubstring = lText.substring(piv, tagMatcher.start()); + if (!currentSubstring.isEmpty()) { + cursor.setString(currentSubstring); + } + formatStack.apply(cursor); + cursor.collapseToEnd(); + + String endTagName = tagMatcher.group(1); + String startTagName = tagMatcher.group(2); + String attributeListPart = tagMatcher.group(3); + boolean isStartTag = StringUtil.isNullOrEmpty(endTagName); + String tagName = isStartTag ? startTagName : endTagName; + Objects.requireNonNull(tagName); + + // Attibutes parsed into (name,value) pairs. + List> attributes = parseAttributes(attributeListPart); + + // Handle tags: + switch (tagName) { + case "b": + formatStack.pushLayer(setCharWeight(FontWeight.BOLD)); + expectEnd.push("/" + tagName); + break; + case "i": + case "em": + formatStack.pushLayer(setCharPosture(FontSlant.ITALIC)); + expectEnd.push("/" + tagName); + break; + case "smallcaps": + formatStack.pushLayer(setCharCaseMap(CaseMap.SMALLCAPS)); + expectEnd.push("/" + tagName); + break; + case "sup": + formatStack.pushLayer(setSuperScript(formatStack)); + expectEnd.push("/" + tagName); + break; + case "sub": + formatStack.pushLayer(setSubScript(formatStack)); + expectEnd.push("/" + tagName); + break; + case "u": + formatStack.pushLayer(setCharUnderline(FontUnderline.SINGLE)); + expectEnd.push("/" + tagName); + break; + case "s": + formatStack.pushLayer(setCharStrikeout(FontStrikeout.SINGLE)); + expectEnd.push("/" + tagName); + break; + case "/p": + // nop + break; + case "p": + insertParagraphBreak(text, cursor); + cursor.collapseToEnd(); + for (OOPair pair : attributes) { + String key = pair.a; + String value = pair.b; + switch (key) { + case "oo:ParaStyleName": + //

+ if (StringUtil.isNullOrEmpty(value)) { + LOGGER.debug(String.format("oo:ParaStyleName inherited")); + } else { + if (setParagraphStyle(cursor, value)) { + // Presumably tested already: + LOGGER.debug(String.format("oo:ParaStyleName=\"%s\" failed", value)); + } + } + break; + default: + LOGGER.warn(String.format("Unexpected attribute '%s' for <%s>", key, tagName)); + break; + } + } + break; + case "oo:referenceToPageNumberOfReferenceMark": + for (OOPair pair : attributes) { + String key = pair.a; + String value = pair.b; + switch (key) { + case "target": + UnoCrossRef.insertReferenceToPageNumberOfReferenceMark(doc, value, cursor); + break; + default: + LOGGER.warn(String.format("Unexpected attribute '%s' for <%s>", key, tagName)); + break; + } + } + break; + case "tt": + // Note: "Example" names a character style in LibreOffice. + formatStack.pushLayer(setCharStyleName("Example")); + expectEnd.push("/" + tagName); + break; + case "span": + List> settings = new ArrayList<>(); + for (OOPair pair : attributes) { + String key = pair.a; + String value = pair.b; + switch (key) { + case "oo:CharStyleName": + // + settings.addAll(setCharStyleName(value)); + break; + case "lang": + // + // + settings.addAll(setCharLocale(value)); + break; + case "style": + // HTML-style small-caps + if ("font-variant: small-caps".equals(value)) { + settings.addAll(setCharCaseMap(CaseMap.SMALLCAPS)); + break; + } + LOGGER.warn(String.format("Unexpected value %s for attribute '%s' for <%s>", + value, key, tagName)); + break; + default: + LOGGER.warn(String.format("Unexpected attribute '%s' for <%s>", key, tagName)); + break; + } + } + formatStack.pushLayer(settings); + expectEnd.push("/" + tagName); + break; + case "/b": + case "/i": + case "/em": + case "/tt": + case "/smallcaps": + case "/sup": + case "/sub": + case "/u": + case "/s": + case "/span": + formatStack.popLayer(); + String expected = expectEnd.pop(); + if (!tagName.equals(expected)) { + LOGGER.warn(String.format("expected '<%s>', found '<%s>' after '%s'", + expected, + tagName, + currentSubstring)); + } + break; + default: + LOGGER.warn(String.format("ignoring unknown tag '<%s>'", tagName)); + break; + } + + piv = tagMatcher.end(); + } + + if (piv < lText.length()) { + cursor.setString(lText.substring(piv)); + } + formatStack.apply(cursor); + cursor.collapseToEnd(); + + if (!expectEnd.empty()) { + String rest = ""; + for (String s : expectEnd) { + rest = String.format("<%s>", s) + rest; + } + LOGGER.warn(String.format("OOTextIntoOO.write:" + + " expectEnd stack is not empty at the end: %s%n", + rest)); + } + } + + /** + * Purpose: in some cases we do not want to inherit direct + * formatting from the context. + * + * In particular, when filling the bibliography title and body. + */ + public static void removeDirectFormatting(XTextCursor cursor) { + + XMultiPropertyStates mpss = UnoCast.cast(XMultiPropertyStates.class, cursor).get(); + + XPropertySet propertySet = UnoCast.cast(XPropertySet.class, cursor).get(); + XPropertyState xPropertyState = UnoCast.cast(XPropertyState.class, cursor).get(); + + try { + // Special handling + propertySet.setPropertyValue(CHAR_STYLE_NAME, "Standard"); + xPropertyState.setPropertyToDefault("CharCaseMap"); + } catch (UnknownPropertyException | + PropertyVetoException | + WrappedTargetException ex) { + LOGGER.warn("exception caught", ex); + } + + mpss.setAllPropertiesToDefault(); + + /* + * Now that we have called setAllPropertiesToDefault, check which properties are not set to + * default and try to correct what we can and seem necessary. + * + * Note: tested with LibreOffice : 6.4.6.2 + */ + + // Only report those we do not yet know about + final Set knownToFail = Set.of("ListAutoFormat", + "ListId", + "NumberingIsNumber", + "NumberingLevel", + "NumberingRules", + "NumberingStartValue", + "ParaChapterNumberingLevel", + "ParaIsNumberingRestart", + "ParaStyleName"); + + // query again, just in case it matters + propertySet = UnoCast.cast(XPropertySet.class, cursor).get(); + XPropertySetInfo propertySetInfo = propertySet.getPropertySetInfo(); + + // check the result + for (Property p : propertySetInfo.getProperties()) { + if ((p.Attributes & PropertyAttribute.READONLY) != 0) { + continue; + } + try { + if (isPropertyDefault(cursor, p.Name)) { + continue; + } + } catch (UnknownPropertyException ex) { + throw new IllegalStateException("Unexpected UnknownPropertyException", ex); + } + if (knownToFail.contains(p.Name)) { + continue; + } + LOGGER.warn(String.format("OOTextIntoOO.removeDirectFormatting failed on '%s'", p.Name)); + } + } + + static class MyPropertyStack { + + /* + * We only try to control these. Should include all character properties we set, and maybe + * their interdependencies. + * + * For a list of properties see: + * https://www.openoffice.org/api/docs/common/ref/com/sun/star/style/CharacterProperties.html + * + * For interdependencies between properties: + * https://wiki.openoffice.org/wiki/Documentation/DevGuide/Text/Formatting + * (at the end, under "Interdependencies between Properties") + * + */ + static final Set CONTROLLED_PROPERTIES = Set.of( + + /* Used for SuperScript, SubScript. + * + * These three are interdependent: changing one may change others. + */ + "CharEscapement", "CharEscapementHeight", "CharAutoEscapement", + + /* used for Bold */ + "CharWeight", + + /* Used for Italic */ + "CharPosture", + + /* Used for strikeout. These two are interdependent. */ + "CharStrikeout", "CharCrossedOut", + + /* Used for underline. These three are interdependent, but apparently + * we can leave out the last two. + */ + "CharUnderline", // "CharUnderlineColor", "CharUnderlineHasColor", + + /* Used for lang="zxx", to silence spellchecker. */ + "CharLocale", + + /* Used for CitationCharacterFormat. */ + "CharStyleName", + + /* Used for and */ + "CharCaseMap"); + + /** + * The number of properties actually controlled. + */ + final int goodSize; + + /** + * From property name to index in goodNames. + */ + final Map goodNameToIndex; + + /** + * From index to property name. + */ + final String[] goodNames; + + /** + * Maintain a stack of layers, each containing a description of the desired state of + * properties. Each description is an ArrayList of property values, Optional.empty() + * encoding "not directly set". + */ + final Stack>> layers; + + MyPropertyStack(XTextCursor cursor) { + + XPropertySet propertySet = UnoCast.cast(XPropertySet.class, cursor).get(); + XPropertySetInfo propertySetInfo = propertySet.getPropertySetInfo(); + + /* + * On creation, initialize the property name -- index mapping. + */ + this.goodNameToIndex = new HashMap<>(); + int nextIndex = 0; + for (Property p : propertySetInfo.getProperties()) { + if ((p.Attributes & PropertyAttribute.READONLY) != 0) { + continue; + } + if (!CONTROLLED_PROPERTIES.contains(p.Name)) { + continue; + } + this.goodNameToIndex.put(p.Name, nextIndex); + nextIndex++; + } + + this.goodSize = nextIndex; + + this.goodNames = new String[goodSize]; + for (Map.Entry entry : goodNameToIndex.entrySet()) { + goodNames[ entry.getValue() ] = entry.getKey(); + } + + // XMultiPropertySet.setPropertyValues() requires alphabetically sorted property names. + // We adjust here: + Arrays.sort(goodNames); + for (int i = 0; i < goodSize; i++) { + this.goodNameToIndex.put(goodNames[i], i); + } + + /* + * Get the initial state of the properties and add the first layer. + */ + XMultiPropertyStates mpss = UnoCast.cast(XMultiPropertyStates.class, cursor).get(); + PropertyState[] propertyStates; + try { + propertyStates = mpss.getPropertyStates(goodNames); + } catch (UnknownPropertyException ex) { + throw new IllegalStateException("Caught unexpected UnknownPropertyException", ex); + } + + XMultiPropertySet mps = UnoCast.cast(XMultiPropertySet.class, cursor).get(); + Object[] initialValues = mps.getPropertyValues(goodNames); + + ArrayList> initialValuesOpt = new ArrayList<>(goodSize); + + for (int i = 0; i < goodSize; i++) { + if (propertyStates[i] == PropertyState.DIRECT_VALUE) { + initialValuesOpt.add(Optional.of(initialValues[i])); + } else { + initialValuesOpt.add(Optional.empty()); + } + } + + this.layers = new Stack<>(); + this.layers.push(initialValuesOpt); + } + + /** + * Given a list of property name, property value pairs, construct and push a new layer + * describing the intended state after these have been applied. + * + * Opening tags usually call this. + */ + void pushLayer(List> settings) { + ArrayList> oldLayer = layers.peek(); + ArrayList> newLayer = new ArrayList<>(oldLayer); + for (OOPair pair : settings) { + String name = pair.a; + Integer index = goodNameToIndex.get(name); + if (index == null) { + LOGGER.warn(String.format("pushLayer: '%s' is not in goodNameToIndex", name)); + continue; + } + Object newValue = pair.b; + newLayer.set(index, Optional.ofNullable(newValue)); + } + layers.push(newLayer); + } + + /** + * Closing tags just pop a layer. + */ + void popLayer() { + if (layers.size() <= 1) { + LOGGER.warn("popLayer: underflow"); + return; + } + layers.pop(); + } + + /** + * Apply the current desired formatting state to a cursor. + * + * The idea is to minimize the number of calls to OpenOffice. + */ + void apply(XTextCursor cursor) { + XMultiPropertySet mps = UnoCast.cast(XMultiPropertySet.class, cursor).get(); + XMultiPropertyStates mpss = UnoCast.cast(XMultiPropertyStates.class, cursor).get(); + ArrayList> topLayer = layers.peek(); + try { + // select values to be set + ArrayList names = new ArrayList<>(goodSize); + ArrayList values = new ArrayList<>(goodSize); + // and those to be cleared + ArrayList delNames = new ArrayList<>(goodSize); + for (int i = 0; i < goodSize; i++) { + if (topLayer.get(i).isPresent()) { + names.add(goodNames[i]); + values.add(topLayer.get(i).get()); + } else { + delNames.add(goodNames[i]); + } + } + // namesArray must be alphabetically sorted. + String[] namesArray = names.toArray(new String[0]); + String[] delNamesArray = delNames.toArray(new String[0]); + mpss.setPropertiesToDefault(delNamesArray); + mps.setPropertyValues(namesArray, values.toArray()); + } catch (UnknownPropertyException ex) { + LOGGER.warn("UnknownPropertyException in MyPropertyStack.apply", ex); + } catch (PropertyVetoException ex) { + LOGGER.warn("PropertyVetoException in MyPropertyStack.apply"); + } catch (WrappedTargetException ex) { + LOGGER.warn("WrappedTargetException in MyPropertyStack.apply"); + } + } + + // Relative CharEscapement needs to know current values. + Optional getPropertyValue(String name) { + if (goodNameToIndex.containsKey(name)) { + int index = goodNameToIndex.get(name); + ArrayList> topLayer = layers.peek(); + return topLayer.get(index); + } + return Optional.empty(); + } + } + + /** + * Parse HTML-like attributes to a list of (name,value) pairs. + */ + private static List> parseAttributes(String attributes) { + List> res = new ArrayList<>(); + if (attributes == null) { + return res; + } + Matcher attributeMatcher = ATTRIBUTE_PATTERN.matcher(attributes); + while (attributeMatcher.find()) { + String key = attributeMatcher.group(1); + String value = attributeMatcher.group(2); + res.add(new OOPair(key, value)); + } + return res; + } + + /* + * We rely on property values being either DIRECT_VALUE or DEFAULT_VALUE (not + * AMBIGUOUS_VALUE). If the cursor covers a homogeneous region, or is collapsed, then this is + * true. + */ + private static boolean isPropertyDefault(XTextCursor cursor, String propertyName) + throws + UnknownPropertyException { + XPropertyState xPropertyState = UnoCast.cast(XPropertyState.class, cursor).get(); + PropertyState state = xPropertyState.getPropertyState(propertyName); + if (state == PropertyState.AMBIGUOUS_VALUE) { + throw new java.lang.IllegalArgumentException("PropertyState.AMBIGUOUS_VALUE" + + " (expected properties for a homogeneous cursor)"); + } + return state == PropertyState.DEFAULT_VALUE; + } + + /* + * Various property change requests. Their results are passed to MyPropertyStack.pushLayer() + */ + + private static List> setCharWeight(float value) { + List> settings = new ArrayList<>(); + settings.add(new OOPair<>("CharWeight", (Float) value)); + return settings; + } + + private static List> setCharPosture(FontSlant value) { + List> settings = new ArrayList<>(); + settings.add(new OOPair<>("CharPosture", (Object) value)); + return settings; + } + + private static List> setCharCaseMap(short value) { + List> settings = new ArrayList<>(); + settings.add(new OOPair<>("CharCaseMap", (Short) value)); + return settings; + } + + // com.sun.star.awt.FontUnderline + private static List> setCharUnderline(short value) { + List> settings = new ArrayList<>(); + settings.add(new OOPair<>(CHAR_UNDERLINE, (Short) value)); + return settings; + } + + // com.sun.star.awt.FontStrikeout + private static List> setCharStrikeout(short value) { + List> settings = new ArrayList<>(); + settings.add(new OOPair<>(CHAR_STRIKEOUT, (Short) value)); + return settings; + } + + // CharStyleName + private static List> setCharStyleName(String value) { + List> settings = new ArrayList<>(); + if (StringUtil.isNullOrEmpty(value)) { + LOGGER.warn("setCharStyleName: received null or empty value"); + } else { + settings.add(new OOPair<>(CHAR_STYLE_NAME, value)); + } + return settings; + } + + // Locale + private static List> setCharLocale(Locale value) { + List> settings = new ArrayList<>(); + settings.add(new OOPair<>("CharLocale", (Object) value)); + return settings; + } + + /** + * Locale from string encoding: language, language-country or language-country-variant + */ + private static List> setCharLocale(String value) { + if (StringUtil.isNullOrEmpty(value)) { + throw new java.lang.IllegalArgumentException("setCharLocale \"\" or null"); + } + String[] parts = value.split("-"); + String language = (parts.length > 0) ? parts[0] : ""; + String country = (parts.length > 1) ? parts[1] : ""; + String variant = (parts.length > 2) ? parts[2] : ""; + return setCharLocale(new Locale(language, country, variant)); + } + + /* + * SuperScript and SubScript. + * + * @param relative If true, calculate the new values relative to the current values. This allows + * subscript-in-superscript. + */ + private static List> setCharEscapement(Optional value, + Optional height, + boolean relative, + MyPropertyStack formatStack) { + List> settings = new ArrayList<>(); + Optional oldValue = (formatStack + .getPropertyValue(CHAR_ESCAPEMENT) + .map(e -> (short) e)); + + Optional oldHeight = (formatStack + .getPropertyValue(CHAR_ESCAPEMENT_HEIGHT) + .map(e -> (byte) e)); + + if (relative && (value.isPresent() || height.isPresent())) { + double oldHeightFloat = oldHeight.orElse(CHAR_ESCAPEMENT_HEIGHT_DEFAULT) * 0.01; + double oldValueFloat = oldValue.orElse(CHAR_ESCAPEMENT_VALUE_DEFAULT); + double heightFloat = height.orElse(CHAR_ESCAPEMENT_HEIGHT_DEFAULT); + double valueFloat = value.orElse(CHAR_ESCAPEMENT_VALUE_DEFAULT); + byte newHeight = (byte) Math.round(heightFloat * oldHeightFloat); + short newValue = (short) Math.round(valueFloat * oldHeightFloat + oldValueFloat); + if (value.isPresent()) { + settings.add(new OOPair<>(CHAR_ESCAPEMENT, (Short) newValue)); + } + if (height.isPresent()) { + settings.add(new OOPair<>(CHAR_ESCAPEMENT_HEIGHT, (Byte) newHeight)); + } + } else { + if (value.isPresent()) { + settings.add(new OOPair<>(CHAR_ESCAPEMENT, (Short) value.get())); + } + if (height.isPresent()) { + settings.add(new OOPair<>(CHAR_ESCAPEMENT_HEIGHT, (Byte) height.get())); + } + } + return settings; + } + + private static List> setSubScript(MyPropertyStack formatStack) { + return setCharEscapement(Optional.of(SUBSCRIPT_VALUE), + Optional.of(SUBSCRIPT_HEIGHT), + true, + formatStack); + } + + private static List> setSuperScript(MyPropertyStack formatStack) { + return setCharEscapement(Optional.of(SUPERSCRIPT_VALUE), + Optional.of(SUPERSCRIPT_HEIGHT), + true, + formatStack); + } + + /* + * @return true on failure + */ + public static boolean setParagraphStyle(XTextCursor cursor, String paragraphStyle) { + final boolean FAIL = true; + final boolean PASS = false; + + XParagraphCursor paragraphCursor = UnoCast.cast(XParagraphCursor.class, cursor).get(); + XPropertySet propertySet = UnoCast.cast(XPropertySet.class, paragraphCursor).get(); + try { + propertySet.setPropertyValue(PARA_STYLE_NAME, paragraphStyle); + return PASS; + } catch (UnknownPropertyException + | PropertyVetoException + | com.sun.star.lang.IllegalArgumentException + | WrappedTargetException ex) { + return FAIL; + } + } + + private static void insertParagraphBreak(XText text, XTextCursor cursor) { + try { + text.insertControlCharacter(cursor, ControlCharacter.PARAGRAPH_BREAK, true); + } catch (com.sun.star.lang.IllegalArgumentException ex) { + // Assuming it means wrong code for ControlCharacter. + // https://api.libreoffice.org/docs/idl/ref/ does not tell. + // If my assumption is correct, we never get here. + throw new java.lang.IllegalArgumentException("Caught unexpected com.sun.star.lang.IllegalArgumentException", ex); + } + } + +} diff --git a/src/main/java/org/jabref/model/openoffice/rangesort/FunctionalTextViewCursor.java b/src/main/java/org/jabref/model/openoffice/rangesort/FunctionalTextViewCursor.java new file mode 100644 index 00000000000..e08a7f4a2ad --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/rangesort/FunctionalTextViewCursor.java @@ -0,0 +1,143 @@ +package org.jabref.model.openoffice.rangesort; + +import java.util.Arrays; +import java.util.Objects; + +import org.jabref.model.openoffice.uno.UnoCursor; +import org.jabref.model.openoffice.uno.UnoSelection; +import org.jabref.model.openoffice.util.OOResult; + +import com.sun.star.lang.XServiceInfo; +import com.sun.star.text.XTextDocument; +import com.sun.star.text.XTextRange; +import com.sun.star.text.XTextViewCursor; + +/* + * A problem with XTextViewCursor: if it is not in text, then we get a crippled version that does + * not support viewCursor.getStart() or viewCursor.gotoRange(range,false), and will throw an + * exception instead. + * + * Here we manipulate the cursor via XSelectionSupplier.getSelection and XSelectionSupplier.select + * to move it to the text. + * + * Seems to work when the user selected a frame or image. + * In these cases restoring the selection works, too. + * + * When the cursor is in a comment (referred to as "annotation" in OO API) then initialSelection is + * null, and select() fails to get a functional viewCursor. + * + * If FunctionalTextViewCursor.get() reports error, we have to ask the user to move the cursor into + * the text part of the document. + * + * Usage: + * + * OOResult fcursor = FunctionalTextViewCursor.get(doc, msg); + * if (fcursor.isError()) { + * ... + * } else { + * XTextViewCursor viewCursor = fcursor.get().getViewCursor(); + * ... + * fc.restore(); + * } + * + */ +public class FunctionalTextViewCursor { + + /* The initial position of the cursor or null. */ + private XTextRange initialPosition; + + /* The initial selection in the document or null. */ + private XServiceInfo initialSelection; + + /* The view cursor, potentially moved from its original location. */ + private XTextViewCursor viewCursor; + + private FunctionalTextViewCursor(XTextRange initialPosition, + XServiceInfo initialSelection, + XTextViewCursor viewCursor) { + this.initialPosition = initialPosition; + this.initialSelection = initialSelection; + this.viewCursor = viewCursor; + } + + /* + * Get a functional XTextViewCursor or an error message. + * + * The cursor position may differ from the location provided by the user. + * + * On failure the constructor restores the selection. On success, the caller may want to call + * instance.restore() after finished using the cursor. + */ + public static OOResult get(XTextDocument doc) { + + Objects.requireNonNull(doc); + + XTextRange initialPosition = null; + XServiceInfo initialSelection = UnoSelection.getSelectionAsXServiceInfo(doc).orElse(null); + XTextViewCursor viewCursor = UnoCursor.getViewCursor(doc).orElse(null); + if (viewCursor != null) { + try { + initialPosition = UnoCursor.createTextCursorByRange(viewCursor); + viewCursor.getStart(); + return OOResult.ok(new FunctionalTextViewCursor(initialPosition, initialSelection, viewCursor)); + } catch (com.sun.star.uno.RuntimeException ex) { + // bad cursor + viewCursor = null; + initialPosition = null; + } + } + + if (initialSelection == null) { + String errorMessage = ("Selection is not available: cannot provide a functional view cursor"); + return OOResult.error(errorMessage); + } else if (!Arrays.stream(initialSelection.getSupportedServiceNames()) + .anyMatch("com.sun.star.text.TextRanges"::equals)) { + // initialSelection does not support TextRanges. + // We need to change it (and the viewCursor with it). + XTextRange newSelection = doc.getText().getStart(); + UnoSelection.select(doc, newSelection); + viewCursor = UnoCursor.getViewCursor(doc).orElse(null); + } + + if (viewCursor == null) { + restore(doc, initialPosition, initialSelection); + String errorMessage = "Could not get the view cursor"; + return OOResult.error(errorMessage); + } + + try { + viewCursor.getStart(); + } catch (com.sun.star.uno.RuntimeException ex) { + restore(doc, initialPosition, initialSelection); + String errorMessage = "The view cursor failed the functionality test"; + return OOResult.error(errorMessage); + } + + return OOResult.ok(new FunctionalTextViewCursor(initialPosition, initialSelection, viewCursor)); + } + + public XTextViewCursor getViewCursor() { + return viewCursor; + } + + private static void restore(XTextDocument doc, + XTextRange initialPosition, + XServiceInfo initialSelection) { + + if (initialPosition != null) { + XTextViewCursor viewCursor = UnoCursor.getViewCursor(doc).orElse(null); + if (viewCursor != null) { + viewCursor.gotoRange(initialPosition, false); + return; + } + } + if (initialSelection != null) { + UnoSelection.select(doc, initialSelection); + } + } + + /* Restore initial state of viewCursor (possibly by restoring selection) if possible. */ + public void restore(XTextDocument doc) { + FunctionalTextViewCursor.restore(doc, initialPosition, initialSelection); + } +} diff --git a/src/main/java/org/jabref/model/openoffice/rangesort/RangeHolder.java b/src/main/java/org/jabref/model/openoffice/rangesort/RangeHolder.java new file mode 100644 index 00000000000..cae6f99e34e --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/rangesort/RangeHolder.java @@ -0,0 +1,7 @@ +package org.jabref.model.openoffice.rangesort; + +import com.sun.star.text.XTextRange; + +public interface RangeHolder { + XTextRange getRange(); +} diff --git a/src/main/java/org/jabref/model/openoffice/rangesort/RangeOverlap.java b/src/main/java/org/jabref/model/openoffice/rangesort/RangeOverlap.java new file mode 100644 index 00000000000..3edd059d7ff --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/rangesort/RangeOverlap.java @@ -0,0 +1,16 @@ +package org.jabref.model.openoffice.rangesort; + +import java.util.List; + +/** + * Used in reporting range overlaps. + */ +public class RangeOverlap { + public final RangeOverlapKind kind; + public final List valuesForOverlappingRanges; + + public RangeOverlap(RangeOverlapKind kind, List valuesForOverlappingRanges) { + this.kind = kind; + this.valuesForOverlappingRanges = valuesForOverlappingRanges; + } +} diff --git a/src/main/java/org/jabref/model/openoffice/rangesort/RangeOverlapBetween.java b/src/main/java/org/jabref/model/openoffice/rangesort/RangeOverlapBetween.java new file mode 100644 index 00000000000..16b7735fb7a --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/rangesort/RangeOverlapBetween.java @@ -0,0 +1,97 @@ +package org.jabref.model.openoffice.rangesort; + +import java.util.ArrayList; +import java.util.List; + +import org.jabref.model.openoffice.uno.UnoCast; +import org.jabref.model.openoffice.uno.UnoTextRange; +import org.jabref.model.openoffice.util.OOTuple3; + +import com.sun.star.text.XText; +import com.sun.star.text.XTextDocument; +import com.sun.star.text.XTextRange; +import com.sun.star.text.XTextRangeCompare; + +public class RangeOverlapBetween { + + private RangeOverlapBetween() { } + + /** + * Check for any overlap between two sets of XTextRange values. + * + * Assume fewHolders is small (usually a single element, for checking the cursor) + * + * Returns on first problem found. + */ + public static + List> findFirst(XTextDocument doc, + List fewHolders, + List manyHolders, + boolean includeTouching) { + + List> result = new ArrayList<>(); + + if (fewHolders.isEmpty()) { + return result; + } + + /* + * Cache all we need to know about fewHolders. We are trying to minimize the number of calls + * to LO. + */ + List> fewTuples = new ArrayList<>(fewHolders.size()); + + for (V aHolder : fewHolders) { + XText aText = aHolder.getRange().getText(); + fewTuples.add(new OOTuple3<>(aText, + UnoCast.cast(XTextRangeCompare.class, aText).get(), + aHolder)); + } + + /* + * We only go through manyHolders once: fewTuples is in the inner loop. + */ + for (V bHolder : manyHolders) { + XTextRange bRange = bHolder.getRange(); + XText bText = bRange.getText(); + XTextRange bRangeStart = bRange.getStart(); + XTextRange bRangeEnd = bRange.getEnd(); + + for (OOTuple3 tup : fewTuples) { + XText aText = tup.a; + XTextRangeCompare cmp = tup.b; + V aHolder = tup.c; + XTextRange aRange = aHolder.getRange(); + if (aText != bText) { + continue; + } + int abEndToStart = UnoTextRange.compareStartsUnsafe(cmp, aRange.getEnd(), bRangeStart); + if (abEndToStart < 0 || (!includeTouching && (abEndToStart == 0))) { + continue; + } + int baEndToStart = UnoTextRange.compareStartsUnsafe(cmp, bRangeEnd, aRange.getStart()); + if (baEndToStart < 0 || (!includeTouching && (baEndToStart == 0))) { + continue; + } + + boolean equal = UnoTextRange.compareStartsThenEndsUnsafe(cmp, aRange, bRange) == 0; + boolean touching = (abEndToStart == 0 || baEndToStart == 0); + + // In case of two equal collapsed ranges there is an ambiguity : TOUCH or EQUAL_RANGE ? + // + // We return EQUAL_RANGE + RangeOverlapKind kind = (equal ? RangeOverlapKind.EQUAL_RANGE + : (touching ? RangeOverlapKind.TOUCH + : RangeOverlapKind.OVERLAP)); + + List valuesForOverlappingRanges = new ArrayList<>(); + valuesForOverlappingRanges.add(aHolder); + valuesForOverlappingRanges.add(bHolder); + + result.add(new RangeOverlap(kind, valuesForOverlappingRanges)); + return result; + } + } + return result; + } +} diff --git a/src/main/java/org/jabref/model/openoffice/rangesort/RangeOverlapKind.java b/src/main/java/org/jabref/model/openoffice/rangesort/RangeOverlapKind.java new file mode 100644 index 00000000000..2bb7f8f4af7 --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/rangesort/RangeOverlapKind.java @@ -0,0 +1,14 @@ +package org.jabref.model.openoffice.rangesort; + +public enum RangeOverlapKind { + + /** The ranges share a boundary */ + TOUCH, + + /** They share some characters */ + OVERLAP, + + /** They cover the same XTextRange */ + EQUAL_RANGE +} + diff --git a/src/main/java/org/jabref/model/openoffice/rangesort/RangeOverlapWithin.java b/src/main/java/org/jabref/model/openoffice/rangesort/RangeOverlapWithin.java new file mode 100644 index 00000000000..a8534b630ae --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/rangesort/RangeOverlapWithin.java @@ -0,0 +1,121 @@ +package org.jabref.model.openoffice.rangesort; + +import java.util.ArrayList; +import java.util.List; + +import org.jabref.model.openoffice.uno.UnoCast; +import org.jabref.model.openoffice.uno.UnoTextRange; + +import com.sun.star.text.XTextDocument; +import com.sun.star.text.XTextRange; +import com.sun.star.text.XTextRangeCompare; + +public class RangeOverlapWithin { + + private RangeOverlapWithin() { } + + /** + * Report identical, overlapping or touching ranges between elements of rangeHolders. + * + * For overlapping and touching, only report consecutive ranges and only with a single sample of + * otherwise identical ranges. + * + * @param rangeHolders represent the ranges to be checked. + * + * Note: for each rangeHolder, rangeHolder.getRange() is called multiple times. + * To avoid repeated work, they should keep a copy of the range instead of + * getting it each time from the document. + * + * @param reportAtMost Limit the number of records returned to atMost. + * Zero {@code reportAtMost} means no limit. + * + * @param includeTouching Should the result contain ranges sharing only a boundary? + */ + public static + List> findOverlappingRanges(XTextDocument doc, + List rangeHolders, + boolean includeTouching, + int reportAtMost) { + + RangeSort.RangePartitions partitions = RangeSort.partitionAndSortRanges(rangeHolders); + + return findOverlappingRanges(partitions, reportAtMost, includeTouching); + } + + /** + * Report identical, overlapping or touching ranges. + * + * For overlapping and touching, only report consecutive ranges and only with a single sample of + * otherwise identical ranges. + * + * @param atMost Limit the number of records returned to atMost. + * Zero {@code atMost} means no limit. + * + * @param includeTouching Should the result contain ranges sharing only a boundary? + */ + public static + List> findOverlappingRanges(RangeSort.RangePartitions input, + int atMost, + boolean includeTouching) { + assert atMost >= 0; + + List> result = new ArrayList<>(); + + for (List partition : input.getPartitions()) { + if (partition.isEmpty()) { + continue; + } + XTextRangeCompare cmp = UnoCast.cast(XTextRangeCompare.class, + partition.get(0).getRange().getText()).get(); + + for (int i = 0; i < (partition.size() - 1); i++) { + V aHolder = partition.get(i); + V bHolder = partition.get(i + 1); + XTextRange aRange = aHolder.getRange(); + XTextRange bRange = bHolder.getRange(); + + // check equal values + int cmpResult = UnoTextRange.compareStartsThenEndsUnsafe(cmp, aRange, bRange); + if (cmpResult == 0) { + List aValues = new ArrayList<>(); + aValues.add(aHolder); + // aValues.add(bHolder); + // collect those equal + while (i < (partition.size() - 1) && + UnoTextRange.compareStartsThenEndsUnsafe( + cmp, + aRange, + partition.get(i + 1).getRange()) == 0) { + bHolder = partition.get(i + 1); + aValues.add(bHolder); + i++; + } + result.add(new RangeOverlap(RangeOverlapKind.EQUAL_RANGE, aValues)); + if (atMost > 0 && result.size() >= atMost) { + return result; + } + continue; + } + + // Not equal, and (a <= b) since sorted. + // Check if a.end >= b.start + cmpResult = UnoTextRange.compareStartsUnsafe(cmp, aRange.getEnd(), bRange.getStart()); + if (cmpResult > 0 || (includeTouching && (cmpResult == 0))) { + // found overlap or touch + List valuesForOverlappingRanges = new ArrayList<>(); + valuesForOverlappingRanges.add(aHolder); + valuesForOverlappingRanges.add(bHolder); + result.add(new RangeOverlap((cmpResult == 0) + ? RangeOverlapKind.TOUCH + : RangeOverlapKind.OVERLAP, + valuesForOverlappingRanges)); + } + if (atMost > 0 && result.size() >= atMost) { + return result; + } + } + } + return result; + } + +} diff --git a/src/main/java/org/jabref/model/openoffice/rangesort/RangeSort.java b/src/main/java/org/jabref/model/openoffice/rangesort/RangeSort.java new file mode 100644 index 00000000000..99ace5154e1 --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/rangesort/RangeSort.java @@ -0,0 +1,106 @@ +package org.jabref.model.openoffice.rangesort; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.jabref.model.openoffice.uno.UnoCast; +import org.jabref.model.openoffice.uno.UnoTextRange; + +import com.sun.star.text.XText; +import com.sun.star.text.XTextRangeCompare; + +/** + * RangeSort provides sorting based on XTextRangeCompare, which only provides comparison + * between XTextRange values within the same XText. + */ +public class RangeSort { + + private RangeSort() { + /**/ + } + + /** + * Compare two RangeHolders (using RangeHolder.getRange()) within an XText. + * + * Note: since we only look at the ranges, this comparison is generally not consistent with + * `equals` on the RangeHolders. Probably should not be used for key comparison in + * {@code TreeMap} or {@code Set} + * + */ + private static class HolderComparatorWithinPartition implements Comparator { + + private final XTextRangeCompare cmp; + + HolderComparatorWithinPartition(XText text) { + cmp = (UnoCast.cast(XTextRangeCompare.class, text) + .orElseThrow(java.lang.IllegalArgumentException::new)); + } + + /** + * Assumes a and b belong to the same XText as cmp. + */ + @Override + public int compare(RangeHolder a, RangeHolder b) { + return UnoTextRange.compareStartsThenEndsUnsafe(cmp, a.getRange(), b.getRange()); + } + } + + /** + * Sort a list of RangeHolder values known to share the same getText(). + * + * Note: RangeHolder.getRange() is called many times. + */ + public static void sortWithinPartition(List rangeHolders) { + if (rangeHolders.isEmpty()) { + return; + } + XText text = rangeHolders.get(0).getRange().getText(); + rangeHolders.sort(new HolderComparatorWithinPartition(text)); + } + + /** + * Represent a partitioning of RangeHolders by XText + */ + public static class RangePartitions { + private final Map> partitions; + + public RangePartitions() { + this.partitions = new HashMap<>(); + } + + public void add(V holder) { + XText partitionKey = holder.getRange().getText(); + List partition = partitions.computeIfAbsent(partitionKey, unused -> new ArrayList<>()); + partition.add(holder); + } + + public List> getPartitions() { + return new ArrayList<>(partitions.values()); + } + } + + /** + * Partition RangeHolders by the corresponding XText. + */ + public static RangePartitions partitionRanges(List holders) { + RangePartitions result = new RangePartitions<>(); + for (V holder : holders) { + result.add(holder); + } + return result; + } + + /** + * Note: RangeHolder.getRange() is called many times. + */ + public static RangePartitions partitionAndSortRanges(List holders) { + RangePartitions result = partitionRanges(holders); + for (List partition : result.getPartitions()) { + sortWithinPartition(partition); + } + return result; + } +} diff --git a/src/main/java/org/jabref/model/openoffice/rangesort/RangeSortEntry.java b/src/main/java/org/jabref/model/openoffice/rangesort/RangeSortEntry.java new file mode 100644 index 00000000000..0ed686ee901 --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/rangesort/RangeSortEntry.java @@ -0,0 +1,42 @@ +package org.jabref.model.openoffice.rangesort; + +import com.sun.star.text.XTextRange; + +/** + * A simple implementation of {@code RangeSortable} + */ +public class RangeSortEntry implements RangeSortable { + + private XTextRange range; + private int indexInPosition; + private T content; + + public RangeSortEntry(XTextRange range, int indexInPosition, T content) { + this.range = range; + this.indexInPosition = indexInPosition; + this.content = content; + } + + @Override + public XTextRange getRange() { + return range; + } + + @Override + public int getIndexInPosition() { + return indexInPosition; + } + + @Override + public T getContent() { + return content; + } + + public void setRange(XTextRange range) { + this.range = range; + } + + public void setIndexInPosition(int indexInPosition) { + this.indexInPosition = indexInPosition; + } +} diff --git a/src/main/java/org/jabref/model/openoffice/rangesort/RangeSortVisual.java b/src/main/java/org/jabref/model/openoffice/rangesort/RangeSortVisual.java new file mode 100644 index 00000000000..e412f4ade6c --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/rangesort/RangeSortVisual.java @@ -0,0 +1,144 @@ +package org.jabref.model.openoffice.rangesort; + +import java.util.ArrayList; +import java.util.List; + +import org.jabref.model.openoffice.uno.UnoScreenRefresh; + +import com.sun.star.awt.Point; +import com.sun.star.text.XTextDocument; +import com.sun.star.text.XTextRange; +import com.sun.star.text.XTextViewCursor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Sort XTextRange values visually (top-down,left-to-right). + * + * Requires functional XTextViewCursor. + * + * Problem: for multicolumn layout and when viewing pages side-by-side in LO, the + * (top-down,left-to-right) order interpreted as-on-the-screen: an XTextRange at the top of + * the second column or second page is sorted before an XTextRange at the bottom of the + * first column of the first page. + */ +public class RangeSortVisual { + + private static final Logger LOGGER = LoggerFactory.getLogger(RangeSortVisual.class); + + private RangeSortVisual() { + /**/ + } + + /** + * Sort the input {@code inputs} visually. + * + * Requires a functional {@code XTextViewCursor}. + * + * @return The input, sorted by the elements XTextRange and getIndexInPosition. + */ + public static List> visualSort(List> inputs, + XTextDocument doc, + FunctionalTextViewCursor fcursor) { + + if (UnoScreenRefresh.hasControllersLocked(doc)) { + final String msg = "visualSort: with ControllersLocked, viewCursor.gotoRange is probably useless"; + LOGGER.warn(msg); + throw new IllegalStateException(msg); + } + + XTextViewCursor viewCursor = fcursor.getViewCursor(); + + final int inputSize = inputs.size(); + + // find coordinates + List positions = new ArrayList<>(inputSize); + for (RangeSortable v : inputs) { + positions.add(findPositionOfTextRange(v.getRange(), viewCursor)); + } + fcursor.restore(doc); + + // order by position + ArrayList>> comparableMarks = new ArrayList<>(inputSize); + for (int i = 0; i < inputSize; i++) { + RangeSortable input = inputs.get(i); + comparableMarks.add(new ComparableMark<>(positions.get(i), + input.getIndexInPosition(), + input)); + } + comparableMarks.sort(RangeSortVisual::compareTopToBottomLeftToRight); + + // collect ordered result + List> result = new ArrayList<>(comparableMarks.size()); + for (ComparableMark> mark : comparableMarks) { + result.add(mark.getContent()); + } + + if (result.size() != inputSize) { + throw new IllegalStateException("visualSort: result.size() != inputSize"); + } + + return result; + } + + /** + * Given a location, return its position: coordinates relative to the top left position of the + * first page of the document. + * + * Note: for text layouts with two or more columns, this gives the wrong order: + * top-down/left-to-right does not match reading order. + * + * Note: The "relative to the top left position of the first page" is meant "as it appears on + * the screen". + * + * In particular: when viewing pages side-by-side, the top half of the right page is + * higher than the lower half of the left page. Again, top-down/left-to-right does not + * match reading order. + * + * @param range Location. + * @param cursor To get the position, we need az XTextViewCursor. + * It will be moved to the range. + */ + private static Point findPositionOfTextRange(XTextRange range, XTextViewCursor cursor) { + cursor.gotoRange(range, false); + return cursor.getPosition(); + } + + private static int compareTopToBottomLeftToRight(ComparableMark a, ComparableMark b) { + + if (a.position.Y != b.position.Y) { + return a.position.Y - b.position.Y; + } + if (a.position.X != b.position.X) { + return a.position.X - b.position.X; + } + return a.indexInPosition - b.indexInPosition; + } + + /** + * A reference mark name paired with its visual position. + * + * Comparison is based on (Y,X,indexInPosition): vertical compared first, horizontal second, + * indexInPosition third. + * + * Used for sorting reference marks by their visual positions. + */ + private static class ComparableMark { + + private final Point position; + private final int indexInPosition; + private final T content; + + public ComparableMark(Point position, int indexInPosition, T content) { + this.position = position; + this.indexInPosition = indexInPosition; + this.content = content; + } + + public T getContent() { + return content; + } + + } + +} diff --git a/src/main/java/org/jabref/model/openoffice/rangesort/RangeSortable.java b/src/main/java/org/jabref/model/openoffice/rangesort/RangeSortable.java new file mode 100644 index 00000000000..59a4c3fa9af --- /dev/null +++ b/src/main/java/org/jabref/model/openoffice/rangesort/RangeSortable.java @@ -0,0 +1,22 @@ +package org.jabref.model.openoffice.rangesort; + +import com.sun.star.text.XTextRange; + +/** + * This is what {@code visualSort} needs in its input. + */ +public interface RangeSortable extends RangeHolder { + + /** The XTextRange + * + * For citation marks in footnotes this may be the range of the footnote mark. + */ + XTextRange getRange(); + + /** + * For citation marks in footnotes this may provide order within the footnote. + */ + int getIndexInPosition(); + + T getContent(); +} diff --git a/src/main/java/org/jabref/model/pdf/search/EnglishStemAnalyzer.java b/src/main/java/org/jabref/model/pdf/search/EnglishStemAnalyzer.java new file mode 100644 index 00000000000..1dcccbb6583 --- /dev/null +++ b/src/main/java/org/jabref/model/pdf/search/EnglishStemAnalyzer.java @@ -0,0 +1,25 @@ +package org.jabref.model.pdf.search; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.LowerCaseFilter; +import org.apache.lucene.analysis.StopFilter; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.Tokenizer; +import org.apache.lucene.analysis.core.DecimalDigitFilter; +import org.apache.lucene.analysis.en.EnglishAnalyzer; +import org.apache.lucene.analysis.en.PorterStemFilter; +import org.apache.lucene.analysis.standard.StandardTokenizer; + +public class EnglishStemAnalyzer extends Analyzer { + + @Override + protected TokenStreamComponents createComponents(String fieldName) { + Tokenizer source = new StandardTokenizer(); + TokenStream filter = new LowerCaseFilter(source); + filter = new StopFilter(filter, EnglishAnalyzer.ENGLISH_STOP_WORDS_SET); + filter = new DecimalDigitFilter(filter); + filter = new PorterStemFilter(filter); + return new TokenStreamComponents(source, filter); + } +} + diff --git a/src/main/java/org/jabref/model/pdf/search/PdfSearchResults.java b/src/main/java/org/jabref/model/pdf/search/PdfSearchResults.java new file mode 100644 index 00000000000..2c6ad8e9d89 --- /dev/null +++ b/src/main/java/org/jabref/model/pdf/search/PdfSearchResults.java @@ -0,0 +1,32 @@ +package org.jabref.model.pdf.search; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public final class PdfSearchResults { + + private final List searchResults; + + public PdfSearchResults(List search) { + this.searchResults = Collections.unmodifiableList(search); + } + + public PdfSearchResults() { + this.searchResults = Collections.emptyList(); + } + + public List getSortedByScore() { + List sortedList = new ArrayList<>(searchResults); + sortedList.sort((searchResult, t1) -> Float.compare(searchResult.getLuceneScore(), t1.getLuceneScore())); + return Collections.unmodifiableList(sortedList); + } + + public List getSearchResults() { + return this.searchResults; + } + + public int numSearchResults() { + return this.searchResults.size(); + } +} diff --git a/src/main/java/org/jabref/model/pdf/search/SearchFieldConstants.java b/src/main/java/org/jabref/model/pdf/search/SearchFieldConstants.java new file mode 100644 index 00000000000..ca72a1fac1b --- /dev/null +++ b/src/main/java/org/jabref/model/pdf/search/SearchFieldConstants.java @@ -0,0 +1,13 @@ +package org.jabref.model.pdf.search; + +public class SearchFieldConstants { + + public static final String PATH = "path"; + public static final String CONTENT = "content"; + public static final String ANNOTATIONS = "annotations"; + public static final String MODIFIED = "modified"; + + public static final String[] PDF_FIELDS = new String[]{PATH, CONTENT, MODIFIED, ANNOTATIONS}; + + public static final String VERSION = "0.3a"; +} diff --git a/src/main/java/org/jabref/model/pdf/search/SearchResult.java b/src/main/java/org/jabref/model/pdf/search/SearchResult.java new file mode 100644 index 00000000000..ea3dd319e6c --- /dev/null +++ b/src/main/java/org/jabref/model/pdf/search/SearchResult.java @@ -0,0 +1,83 @@ +package org.jabref.model.pdf.search; + +import java.io.IOException; + +import org.jabref.model.entry.BibEntry; + +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.index.IndexableField; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.search.highlight.Highlighter; +import org.apache.lucene.search.highlight.InvalidTokenOffsetsException; +import org.apache.lucene.search.highlight.QueryScorer; +import org.apache.lucene.search.highlight.SimpleHTMLFormatter; +import org.apache.lucene.search.highlight.TextFragment; + +import static org.jabref.model.pdf.search.SearchFieldConstants.CONTENT; +import static org.jabref.model.pdf.search.SearchFieldConstants.MODIFIED; +import static org.jabref.model.pdf.search.SearchFieldConstants.PATH; + +public final class SearchResult { + + private final String path; + private final String content; + private final long modified; + + private final float luceneScore; + private String html; + + public SearchResult(IndexSearcher searcher, Query query, ScoreDoc scoreDoc) throws IOException { + this.path = getFieldContents(searcher, scoreDoc, PATH); + this.content = getFieldContents(searcher, scoreDoc, CONTENT); + this.modified = Long.parseLong(getFieldContents(searcher, scoreDoc, MODIFIED)); + this.luceneScore = scoreDoc.score; + + TokenStream stream = new EnglishStemAnalyzer().tokenStream(CONTENT, content); + + Highlighter highlighter = new Highlighter(new SimpleHTMLFormatter(), new QueryScorer(query)); + try { + + TextFragment[] frags = highlighter.getBestTextFragments(stream, content, true, 10); + this.html = ""; + for (TextFragment frag : frags) { + html += "

" + frag.toString() + "

"; + } + } catch (InvalidTokenOffsetsException e) { + this.html = ""; + } + } + + private String getFieldContents(IndexSearcher searcher, ScoreDoc scoreDoc, String field) throws IOException { + IndexableField indexableField = searcher.doc(scoreDoc.doc).getField(field); + if (indexableField == null) { + return ""; + } + return indexableField.stringValue(); + } + + public boolean isResultFor(BibEntry entry) { + return entry.getFiles().stream().anyMatch(linkedFile -> path.equals(linkedFile.getLink())); + } + + public String getPath() { + return path; + } + + public String getContent() { + return content; + } + + public long getModified() { + return modified; + } + + public float getLuceneScore() { + return luceneScore; + } + + public String getHtml() { + return html; + } +} diff --git a/src/main/java/org/jabref/model/search/GroupSearchQuery.java b/src/main/java/org/jabref/model/search/GroupSearchQuery.java index 3eea63f99d9..90056d9b0f7 100644 --- a/src/main/java/org/jabref/model/search/GroupSearchQuery.java +++ b/src/main/java/org/jabref/model/search/GroupSearchQuery.java @@ -1,22 +1,22 @@ package org.jabref.model.search; +import java.util.EnumSet; import java.util.Objects; import org.jabref.model.entry.BibEntry; import org.jabref.model.search.rules.SearchRule; import org.jabref.model.search.rules.SearchRules; +import org.jabref.model.search.rules.SearchRules.SearchFlags; public class GroupSearchQuery implements SearchMatcher { private final String query; - private final boolean caseSensitive; - private final boolean regularExpression; + private final EnumSet searchFlags; private final SearchRule rule; - public GroupSearchQuery(String query, boolean caseSensitive, boolean regularExpression) { + public GroupSearchQuery(String query, EnumSet searchFlags) { this.query = Objects.requireNonNull(query); - this.caseSensitive = caseSensitive; - this.regularExpression = regularExpression; + this.searchFlags = searchFlags; this.rule = Objects.requireNonNull(getSearchRule()); } @@ -32,11 +32,11 @@ public boolean isMatch(BibEntry entry) { } private SearchRule getSearchRule() { - return SearchRules.getSearchRuleByQuery(query, caseSensitive, regularExpression); + return SearchRules.getSearchRuleByQuery(query, searchFlags); } private String getCaseSensitiveDescription() { - if (caseSensitive) { + if (searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { return "case sensitive"; } else { return "case insensitive"; @@ -44,7 +44,7 @@ private String getCaseSensitiveDescription() { } private String getRegularExpressionDescription() { - if (regularExpression) { + if (searchFlags.contains(SearchRules.SearchFlags.REGULAR_EXPRESSION)) { return "regular expression"; } else { return "plain text"; @@ -59,11 +59,7 @@ public String getSearchExpression() { return query; } - public boolean isCaseSensitive() { - return caseSensitive; - } - - public boolean isRegularExpression() { - return regularExpression; + public EnumSet getSearchFlags() { + return searchFlags; } } diff --git a/src/main/java/org/jabref/model/search/rules/ContainBasedSearchRule.java b/src/main/java/org/jabref/model/search/rules/ContainBasedSearchRule.java index f77c57324d2..cf7c7568368 100644 --- a/src/main/java/org/jabref/model/search/rules/ContainBasedSearchRule.java +++ b/src/main/java/org/jabref/model/search/rules/ContainBasedSearchRule.java @@ -1,25 +1,48 @@ package org.jabref.model.search.rules; +import java.io.IOException; +import java.util.EnumSet; import java.util.Iterator; import java.util.List; import java.util.Locale; +import java.util.Vector; +import java.util.stream.Collectors; +import org.jabref.architecture.AllowedToUseLogic; +import org.jabref.gui.Globals; +import org.jabref.gui.LibraryTab; +import org.jabref.logic.pdf.search.retrieval.PdfSearcher; +import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.Field; +import org.jabref.model.pdf.search.PdfSearchResults; +import org.jabref.model.pdf.search.SearchResult; +import org.jabref.model.search.rules.SearchRules.SearchFlags; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Search rule for contain-based search. */ +@AllowedToUseLogic("Because access to the lucene index is needed") public class ContainBasedSearchRule implements SearchRule { - private final boolean caseSensitive; + private static final Logger LOGGER = LoggerFactory.getLogger(LibraryTab.class); - public ContainBasedSearchRule(boolean caseSensitive) { - this.caseSensitive = caseSensitive; - } + private final EnumSet searchFlags; + + private String lastQuery; + private List lastSearchResults; + + private final BibDatabaseContext databaseContext; - public boolean isCaseSensitive() { - return caseSensitive; + public ContainBasedSearchRule(EnumSet searchFlags) { + this.searchFlags = searchFlags; + this.lastQuery = ""; + lastSearchResults = new Vector<>(); + + databaseContext = Globals.stateManager.getActiveDatabase().orElse(null); } @Override @@ -31,7 +54,7 @@ public boolean validateSearchStrings(String query) { public boolean applyRule(String query, BibEntry bibEntry) { String searchString = query; - if (!caseSensitive) { + if (!searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { searchString = searchString.toLowerCase(Locale.ROOT); } @@ -39,7 +62,7 @@ public boolean applyRule(String query, BibEntry bibEntry) { for (Field fieldKey : bibEntry.getFields()) { String formattedFieldContent = bibEntry.getLatexFreeField(fieldKey).get(); - if (!caseSensitive) { + if (!searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { formattedFieldContent = formattedFieldContent.toLowerCase(Locale.ROOT); } @@ -56,6 +79,32 @@ public boolean applyRule(String query, BibEntry bibEntry) { } } - return false; // Didn't match all words. + return getFulltextResults(query, bibEntry).numSearchResults() > 0; // Didn't match all words. + } + + @Override + public PdfSearchResults getFulltextResults(String query, BibEntry bibEntry) { + + if (!searchFlags.contains(SearchRules.SearchFlags.FULLTEXT) || databaseContext == null) { + return new PdfSearchResults(List.of()); + } + + if (!query.equals(this.lastQuery)) { + this.lastQuery = query; + lastSearchResults = List.of(); + try { + PdfSearcher searcher = PdfSearcher.of(databaseContext); + PdfSearchResults results = searcher.search(query, 5); + lastSearchResults = results.getSortedByScore(); + } catch (IOException e) { + LOGGER.error("Could not retrieve search results!", e); + } + } + + return new PdfSearchResults(lastSearchResults.stream().filter(searchResult -> searchResult.isResultFor(bibEntry)).collect(Collectors.toList())); + } + + public EnumSet getSearchFlags() { + return searchFlags; } } diff --git a/src/main/java/org/jabref/model/search/rules/GrammarBasedSearchRule.java b/src/main/java/org/jabref/model/search/rules/GrammarBasedSearchRule.java index 98e12978963..5b0b5c40ea3 100644 --- a/src/main/java/org/jabref/model/search/rules/GrammarBasedSearchRule.java +++ b/src/main/java/org/jabref/model/search/rules/GrammarBasedSearchRule.java @@ -1,5 +1,8 @@ package org.jabref.model.search.rules; +import java.io.IOException; +import java.util.EnumSet; +import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -8,10 +11,17 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; +import org.jabref.architecture.AllowedToUseLogic; +import org.jabref.gui.Globals; +import org.jabref.logic.pdf.search.retrieval.PdfSearcher; +import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.Keyword; import org.jabref.model.entry.field.Field; import org.jabref.model.entry.field.InternalField; +import org.jabref.model.pdf.search.PdfSearchResults; +import org.jabref.model.pdf.search.SearchResult; +import org.jabref.model.search.rules.SearchRules.SearchFlags; import org.jabref.search.SearchBaseVisitor; import org.jabref.search.SearchLexer; import org.jabref.search.SearchParser; @@ -32,15 +42,18 @@ *

* This class implements the "Advanced Search Mode" described in the help */ +@AllowedToUseLogic("Because access to the lucene index is needed") public class GrammarBasedSearchRule implements SearchRule { private static final Logger LOGGER = LoggerFactory.getLogger(GrammarBasedSearchRule.class); - private final boolean caseSensitiveSearch; - private final boolean regExpSearch; + private final EnumSet searchFlags; private ParseTree tree; private String query; + private List searchResults; + + private final BibDatabaseContext databaseContext; public static class ThrowingErrorListener extends BaseErrorListener { @@ -54,21 +67,13 @@ public void syntaxError(Recognizer recognizer, Object offendingSymbol, } } - public GrammarBasedSearchRule(boolean caseSensitiveSearch, boolean regExpSearch) throws RecognitionException { - this.caseSensitiveSearch = caseSensitiveSearch; - this.regExpSearch = regExpSearch; - } - - public static boolean isValid(boolean caseSensitive, boolean regExp, String query) { - return new GrammarBasedSearchRule(caseSensitive, regExp).validateSearchStrings(query); - } - - public boolean isCaseSensitiveSearch() { - return this.caseSensitiveSearch; + public GrammarBasedSearchRule(EnumSet searchFlags) throws RecognitionException { + this.searchFlags = searchFlags; + databaseContext = Globals.stateManager.getActiveDatabase().orElse(null); } - public boolean isRegExpSearch() { - return this.regExpSearch; + public static boolean isValid(EnumSet searchFlags, String query) { + return new GrammarBasedSearchRule(searchFlags).validateSearchStrings(query); } public ParseTree getTree() { @@ -93,18 +98,34 @@ private void init(String query) throws ParseCancellationException { parser.setErrorHandler(new BailErrorStrategy()); // ParseCancelationException on parse errors tree = parser.start(); this.query = query; + + if (!searchFlags.contains(SearchRules.SearchFlags.FULLTEXT) || databaseContext == null) { + return; + } + try { + PdfSearcher searcher = PdfSearcher.of(databaseContext); + PdfSearchResults results = searcher.search(query, 5); + searchResults = results.getSortedByScore(); + } catch (IOException e) { + LOGGER.error("Could not retrieve search results!", e); + } } @Override public boolean applyRule(String query, BibEntry bibEntry) { try { - return new BibtexSearchVisitor(caseSensitiveSearch, regExpSearch, bibEntry).visit(tree); + return new BibtexSearchVisitor(searchFlags, bibEntry).visit(tree); } catch (Exception e) { LOGGER.debug("Search failed", e); - return false; + return getFulltextResults(query, bibEntry).numSearchResults() > 0; } } + @Override + public PdfSearchResults getFulltextResults(String query, BibEntry bibEntry) { + return new PdfSearchResults(searchResults.stream().filter(searchResult -> searchResult.isResultFor(bibEntry)).collect(Collectors.toList())); + } + @Override public boolean validateSearchStrings(String query) { try { @@ -116,6 +137,10 @@ public boolean validateSearchStrings(String query) { } } + public EnumSet getSearchFlags() { + return searchFlags; + } + public enum ComparisonOperator { EXACT, CONTAINS, DOES_NOT_CONTAIN; @@ -136,12 +161,12 @@ public static class Comparator { private final Pattern fieldPattern; private final Pattern valuePattern; - public Comparator(String field, String value, ComparisonOperator operator, boolean caseSensitive, boolean regex) { + public Comparator(String field, String value, ComparisonOperator operator, EnumSet searchFlags) { this.operator = operator; - int option = caseSensitive ? 0 : Pattern.CASE_INSENSITIVE; - this.fieldPattern = Pattern.compile(regex ? field : "\\Q" + field + "\\E", option); - this.valuePattern = Pattern.compile(regex ? value : "\\Q" + value + "\\E", option); + int option = searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE) ? 0 : Pattern.CASE_INSENSITIVE; + this.fieldPattern = Pattern.compile(searchFlags.contains(SearchRules.SearchFlags.REGULAR_EXPRESSION) ? field : "\\Q" + field + "\\E", option); + this.valuePattern = Pattern.compile(searchFlags.contains(SearchRules.SearchFlags.REGULAR_EXPRESSION) ? value : "\\Q" + value + "\\E", option); } public boolean compare(BibEntry entry) { @@ -200,19 +225,17 @@ public boolean matchFieldValue(String content) { */ static class BibtexSearchVisitor extends SearchBaseVisitor { - private final boolean caseSensitive; - private final boolean regex; + private final EnumSet searchFlags; private final BibEntry entry; - public BibtexSearchVisitor(boolean caseSensitive, boolean regex, BibEntry bibEntry) { - this.caseSensitive = caseSensitive; - this.regex = regex; + public BibtexSearchVisitor(EnumSet searchFlags, BibEntry bibEntry) { + this.searchFlags = searchFlags; this.entry = bibEntry; } public boolean comparison(String field, ComparisonOperator operator, String value) { - return new Comparator(field, value, operator, caseSensitive, regex).compare(entry); + return new Comparator(field, value, operator, searchFlags).compare(entry); } @Override @@ -232,7 +255,7 @@ public Boolean visitComparison(SearchParser.ComparisonContext context) { if (fieldDescriptor.isPresent()) { return comparison(fieldDescriptor.get().getText(), ComparisonOperator.build(context.operator.getText()), right); } else { - return SearchRules.getSearchRule(caseSensitive, regex).applyRule(right, entry); + return SearchRules.getSearchRule(searchFlags).applyRule(right, entry); } } diff --git a/src/main/java/org/jabref/model/search/rules/RegexBasedSearchRule.java b/src/main/java/org/jabref/model/search/rules/RegexBasedSearchRule.java index 9d43710c655..0cc4bcb1f0a 100644 --- a/src/main/java/org/jabref/model/search/rules/RegexBasedSearchRule.java +++ b/src/main/java/org/jabref/model/search/rules/RegexBasedSearchRule.java @@ -1,38 +1,62 @@ package org.jabref.model.search.rules; +import java.io.IOException; +import java.util.EnumSet; +import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; +import java.util.stream.Collectors; +import org.jabref.architecture.AllowedToUseLogic; +import org.jabref.gui.Globals; +import org.jabref.logic.pdf.search.retrieval.PdfSearcher; +import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.Field; +import org.jabref.model.pdf.search.PdfSearchResults; +import org.jabref.model.pdf.search.SearchResult; +import org.jabref.model.search.rules.SearchRules.SearchFlags; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Search rule for regex-based search. */ +@AllowedToUseLogic("Because access to the lucene index is needed") public class RegexBasedSearchRule implements SearchRule { - private final boolean caseSensitive; + private static final Logger LOGGER = LoggerFactory.getLogger(GrammarBasedSearchRule.class); + + private final EnumSet searchFlags; + + private String lastQuery; + private List lastSearchResults; + + private final BibDatabaseContext databaseContext; - public RegexBasedSearchRule(boolean caseSensitive) { - this.caseSensitive = caseSensitive; + public RegexBasedSearchRule(EnumSet searchFlags) { + this.searchFlags = searchFlags; + + databaseContext = Globals.stateManager.getActiveDatabase().orElse(null); } - public boolean isCaseSensitive() { - return caseSensitive; + public EnumSet getSearchFlags() { + return searchFlags; } @Override public boolean validateSearchStrings(String query) { String searchString = query; - if (!caseSensitive) { + if (!searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { searchString = searchString.toLowerCase(Locale.ROOT); } try { - Pattern.compile(searchString, caseSensitive ? 0 : Pattern.CASE_INSENSITIVE); + Pattern.compile(searchString, searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE) ? 0 : Pattern.CASE_INSENSITIVE); } catch (PatternSyntaxException ex) { return false; } @@ -44,7 +68,7 @@ public boolean applyRule(String query, BibEntry bibEntry) { Pattern pattern; try { - pattern = Pattern.compile(query, caseSensitive ? 0 : Pattern.CASE_INSENSITIVE); + pattern = Pattern.compile(query, searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE) ? 0 : Pattern.CASE_INSENSITIVE); } catch (PatternSyntaxException ex) { return false; } @@ -59,6 +83,27 @@ public boolean applyRule(String query, BibEntry bibEntry) { } } } - return false; + return getFulltextResults(query, bibEntry).numSearchResults() > 0; + } + + @Override + public PdfSearchResults getFulltextResults(String query, BibEntry bibEntry) { + + if (!searchFlags.contains(SearchRules.SearchFlags.FULLTEXT) || databaseContext == null) { + return new PdfSearchResults(List.of()); + } + + if (!query.equals(this.lastQuery)) { + this.lastQuery = query; + lastSearchResults = List.of(); + try { + PdfSearcher searcher = PdfSearcher.of(databaseContext); + PdfSearchResults results = searcher.search(query, 5); + lastSearchResults = results.getSortedByScore(); + } catch (IOException e) { + LOGGER.error("Could not retrieve search results!", e); + } + } + return new PdfSearchResults(lastSearchResults.stream().filter(searchResult -> searchResult.isResultFor(bibEntry)).collect(Collectors.toList())); } } diff --git a/src/main/java/org/jabref/model/search/rules/SearchRule.java b/src/main/java/org/jabref/model/search/rules/SearchRule.java index ffc4dced699..1be2b05b342 100644 --- a/src/main/java/org/jabref/model/search/rules/SearchRule.java +++ b/src/main/java/org/jabref/model/search/rules/SearchRule.java @@ -1,10 +1,13 @@ package org.jabref.model.search.rules; import org.jabref.model.entry.BibEntry; +import org.jabref.model.pdf.search.PdfSearchResults; public interface SearchRule { boolean applyRule(String query, BibEntry bibEntry); + PdfSearchResults getFulltextResults(String query, BibEntry bibEntry); + boolean validateSearchStrings(String query); } diff --git a/src/main/java/org/jabref/model/search/rules/SearchRules.java b/src/main/java/org/jabref/model/search/rules/SearchRules.java index dcde2b66227..9b6ffb51b77 100644 --- a/src/main/java/org/jabref/model/search/rules/SearchRules.java +++ b/src/main/java/org/jabref/model/search/rules/SearchRules.java @@ -1,5 +1,6 @@ package org.jabref.model.search.rules; +import java.util.EnumSet; import java.util.regex.Pattern; public class SearchRules { @@ -12,18 +13,18 @@ private SearchRules() { /** * Returns the appropriate search rule that fits best to the given parameter. */ - public static SearchRule getSearchRuleByQuery(String query, boolean caseSensitive, boolean regex) { + public static SearchRule getSearchRuleByQuery(String query, EnumSet searchFlags) { if (isSimpleQuery(query)) { - return new ContainBasedSearchRule(caseSensitive); + return new ContainBasedSearchRule(searchFlags); } // this searches specified fields if specified, // and all fields otherwise - SearchRule searchExpression = new GrammarBasedSearchRule(caseSensitive, regex); + SearchRule searchExpression = new GrammarBasedSearchRule(searchFlags); if (searchExpression.validateSearchStrings(query)) { return searchExpression; } else { - return getSearchRule(caseSensitive, regex); + return getSearchRule(searchFlags); } } @@ -31,11 +32,15 @@ private static boolean isSimpleQuery(String query) { return SIMPLE_EXPRESSION.matcher(query).matches(); } - static SearchRule getSearchRule(boolean caseSensitive, boolean regex) { - if (regex) { - return new RegexBasedSearchRule(caseSensitive); + static SearchRule getSearchRule(EnumSet searchFlags) { + if (searchFlags.contains(SearchFlags.REGULAR_EXPRESSION)) { + return new RegexBasedSearchRule(searchFlags); } else { - return new ContainBasedSearchRule(caseSensitive); + return new ContainBasedSearchRule(searchFlags); } } + + public enum SearchFlags { + CASE_SENSITIVE, REGULAR_EXPRESSION, FULLTEXT; + } } diff --git a/src/main/java/org/jabref/model/study/Study.java b/src/main/java/org/jabref/model/study/Study.java index 382268ce39a..cc117f0160e 100644 --- a/src/main/java/org/jabref/model/study/Study.java +++ b/src/main/java/org/jabref/model/study/Study.java @@ -127,6 +127,31 @@ public boolean equals(Object o) { return getDatabases() != null ? getDatabases().equals(study.getDatabases()) : study.getDatabases() == null; } + public boolean equalsBesideLastSearchDate(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Study study = (Study) o; + + if (getAuthors() != null ? !getAuthors().equals(study.getAuthors()) : study.getAuthors() != null) { + return false; + } + if (getTitle() != null ? !getTitle().equals(study.getTitle()) : study.getTitle() != null) { + return false; + } + if (getResearchQuestions() != null ? !getResearchQuestions().equals(study.getResearchQuestions()) : study.getResearchQuestions() != null) { + return false; + } + if (getQueries() != null ? !getQueries().equals(study.getQueries()) : study.getQueries() != null) { + return false; + } + return getDatabases() != null ? getDatabases().equals(study.getDatabases()) : study.getDatabases() == null; + } + @Override public int hashCode() { return Objects.hashCode(this); diff --git a/src/main/java/org/jabref/preferences/JabRefPreferences.java b/src/main/java/org/jabref/preferences/JabRefPreferences.java index ba5df0fa216..8a459c260ea 100644 --- a/src/main/java/org/jabref/preferences/JabRefPreferences.java +++ b/src/main/java/org/jabref/preferences/JabRefPreferences.java @@ -236,6 +236,7 @@ public class JabRefPreferences implements PreferencesService { public static final String SEARCH_DISPLAY_MODE = "searchDisplayMode"; public static final String SEARCH_CASE_SENSITIVE = "caseSensitiveSearch"; public static final String SEARCH_REG_EXP = "regExpSearch"; + public static final String SEARCH_FULLTEXT = "fulltextSearch"; public static final String GENERATE_KEY_ON_IMPORT = "generateKeyOnImport"; @@ -438,6 +439,7 @@ private JabRefPreferences() { defaults.put(SEARCH_DISPLAY_MODE, SearchDisplayMode.FILTER.toString()); defaults.put(SEARCH_CASE_SENSITIVE, Boolean.FALSE); defaults.put(SEARCH_REG_EXP, Boolean.FALSE); + defaults.put(SEARCH_FULLTEXT, Boolean.TRUE); defaults.put(GENERATE_KEY_ON_IMPORT, Boolean.TRUE); @@ -2532,7 +2534,8 @@ public SearchPreferences getSearchPreferences() { return new SearchPreferences( searchDisplayMode, getBoolean(SEARCH_CASE_SENSITIVE), - getBoolean(SEARCH_REG_EXP)); + getBoolean(SEARCH_REG_EXP), + getBoolean(SEARCH_FULLTEXT)); } @Override @@ -2540,6 +2543,7 @@ public void storeSearchPreferences(SearchPreferences preferences) { put(SEARCH_DISPLAY_MODE, Objects.requireNonNull(preferences.getSearchDisplayMode()).toString()); putBoolean(SEARCH_CASE_SENSITIVE, preferences.isCaseSensitive()); putBoolean(SEARCH_REG_EXP, preferences.isRegularExpression()); + putBoolean(SEARCH_FULLTEXT, preferences.isFulltext()); } //************************************************************************************************************* diff --git a/src/main/java/org/jabref/preferences/SearchPreferences.java b/src/main/java/org/jabref/preferences/SearchPreferences.java index e85594dd7b2..2674c0e897f 100644 --- a/src/main/java/org/jabref/preferences/SearchPreferences.java +++ b/src/main/java/org/jabref/preferences/SearchPreferences.java @@ -1,17 +1,33 @@ package org.jabref.preferences; +import java.util.EnumSet; + import org.jabref.gui.search.SearchDisplayMode; +import org.jabref.model.search.rules.SearchRules; +import org.jabref.model.search.rules.SearchRules.SearchFlags; public class SearchPreferences { private final SearchDisplayMode searchDisplayMode; - private final boolean isCaseSensitive; - private final boolean isRegularExpression; + private final EnumSet searchFlags; - public SearchPreferences(SearchDisplayMode searchDisplayMode, boolean isCaseSensitive, boolean isRegularExpression) { + public SearchPreferences(SearchDisplayMode searchDisplayMode, boolean isCaseSensitive, boolean isRegularExpression, boolean isFulltext) { this.searchDisplayMode = searchDisplayMode; - this.isCaseSensitive = isCaseSensitive; - this.isRegularExpression = isRegularExpression; + searchFlags = EnumSet.noneOf(SearchFlags.class); + if (isCaseSensitive) { + searchFlags.add(SearchFlags.CASE_SENSITIVE); + } + if (isRegularExpression) { + searchFlags.add(SearchFlags.REGULAR_EXPRESSION); + } + if (isFulltext) { + searchFlags.add(SearchFlags.FULLTEXT); + } + } + + public SearchPreferences(SearchDisplayMode searchDisplayMode, EnumSet searchFlags) { + this.searchDisplayMode = searchDisplayMode; + this.searchFlags = searchFlags; } public SearchDisplayMode getSearchDisplayMode() { @@ -19,22 +35,44 @@ public SearchDisplayMode getSearchDisplayMode() { } public boolean isCaseSensitive() { - return isCaseSensitive; + return searchFlags.contains(SearchFlags.CASE_SENSITIVE); } public boolean isRegularExpression() { - return isRegularExpression; + return searchFlags.contains(SearchFlags.REGULAR_EXPRESSION); + } + + public boolean isFulltext() { + return searchFlags.contains(SearchFlags.FULLTEXT); + } + + public EnumSet getSearchFlags() { + EnumSet searchFlags = EnumSet.noneOf(SearchFlags.class); + if (isCaseSensitive()) { + searchFlags.add(SearchRules.SearchFlags.CASE_SENSITIVE); + } + if (isRegularExpression()) { + searchFlags.add(SearchRules.SearchFlags.REGULAR_EXPRESSION); + } + if (isFulltext()) { + searchFlags.add(SearchRules.SearchFlags.FULLTEXT); + } + return searchFlags; } public SearchPreferences withSearchDisplayMode(SearchDisplayMode newSearchDisplayMode) { - return new SearchPreferences(newSearchDisplayMode, isCaseSensitive, isRegularExpression); + return new SearchPreferences(newSearchDisplayMode, isCaseSensitive(), isRegularExpression(), isFulltext()); } public SearchPreferences withCaseSensitive(boolean newCaseSensitive) { - return new SearchPreferences(searchDisplayMode, newCaseSensitive, isRegularExpression); + return new SearchPreferences(searchDisplayMode, newCaseSensitive, isRegularExpression(), isFulltext()); } public SearchPreferences withRegularExpression(boolean newRegularExpression) { - return new SearchPreferences(searchDisplayMode, isCaseSensitive, newRegularExpression); + return new SearchPreferences(searchDisplayMode, isCaseSensitive(), newRegularExpression, isFulltext()); + } + + public SearchPreferences withFulltext(boolean newFulltext) { + return new SearchPreferences(searchDisplayMode, isCaseSensitive(), isRegularExpression(), newFulltext); } } diff --git a/src/main/resources/journals/journalList.mv b/src/main/resources/journals/journalList.mv index a61ae466218..1d4b99c5787 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_da.properties b/src/main/resources/l10n/JabRef_da.properties index 714785a8daa..d91b22833bd 100644 --- a/src/main/resources/l10n/JabRef_da.properties +++ b/src/main/resources/l10n/JabRef_da.properties @@ -1060,6 +1060,8 @@ Default\ pattern=Standardmønster + + diff --git a/src/main/resources/l10n/JabRef_de.properties b/src/main/resources/l10n/JabRef_de.properties index d0704582b6f..7b623188610 100644 --- a/src/main/resources/l10n/JabRef_de.properties +++ b/src/main/resources/l10n/JabRef_de.properties @@ -629,7 +629,6 @@ Previous\ preview\ layout=Voriger Vorschaustil Available=Verfügbar Selected=Ausgewählt Selected\ Layouts\ can\ not\ be\ empty=Ausgewählte Layouts können nicht leer sein -Start\ systematic\ literature\ review=Systematische Literaturrecherche starten Reset\ default\ preview\ style=Standard-Vorschau-Stil zurücksetzen Previous\ entry=Vorheriger Eintrag Primary\ sort\ criterion=Primäres Sortierkriterium @@ -1099,6 +1098,16 @@ Expand\ all=Alle aufklappen Collapse\ all=Alle einklappen Searches\ the\ selected\ directory\ for\ unlinked\ files.=Sucht im ausgewählten Ordner nach nicht-verlinkten Dateien. Starts\ the\ import\ of\ BibTeX\ entries.=Startet den Import von BibTeX-Einträgen. +Last\ edited\:=Zuletzt bearbeitet\: +All\ time=Gesamte Zeit +last\ edited=zuletzt bearbeitet +Last\ day=Lerzter Tag +Last\ week=Letzte Woche +Last\ month=Letzter Monat +Last\ year=Letztes Jahr +Sort\ by\:=Sortieren nach\: +Newest\ first=Neueste zuerst +Oldest\ first=Älteste zuerst Directory=Verzeichnis Search\ results=Suchergebnisse Import\ result=Importergebnis @@ -1487,7 +1496,7 @@ character=Zeichen word=Wort Show\ symmetric\ diff=Zeige Änderungen symmetisch Copy\ Version=Kopiere Version -Maintainers=Betreuer*innen +Maintainers=Maintainer Contributors=Mitwirkende License=Lizenz JabRef\ would\ not\ have\ been\ possible\ without\ the\ help\ of\ our\ contributors.=JabRef wäre ohne die Hilfe unserer Mitwirkenden nicht möglich gewesen. @@ -2309,4 +2318,13 @@ New\ entry\ by\ type=Neuer Eintrag nach Typ File\ '%1'\ is\ a\ duplicate\ of\ '%0'.\ Keeping\ '%0'=Datei '%1' ist ein Duplikat von '%0'. Behalte '%0' File\ '%1'\ is\ a\ duplicate\ of\ '%0'.\ Keeping\ both\ due\ to\ deletion\ error=Datei '%1' ist ein Duplikat von '%0'. Beide werden aufgrund eines Löschfehlers beibehalten +Enable\ field\ formatters=Feldformatierer aktivieren +Entry\ Type=Eintragstyp +Entry\ types=Eintragstypen +Field\ names=Feldnamen +Others=Andere +Overwrite\ existing\ field\ values=Bestehende Feldwerte überschreiben +Recommended=Empfohlen + + diff --git a/src/main/resources/l10n/JabRef_el.properties b/src/main/resources/l10n/JabRef_el.properties index 47d6d865d67..c6bd750c712 100644 --- a/src/main/resources/l10n/JabRef_el.properties +++ b/src/main/resources/l10n/JabRef_el.properties @@ -1687,6 +1687,8 @@ Default\ pattern=Προεπιλεγμένο μοτίβο + + diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties index 6073699969c..8abf5a998e1 100644 --- a/src/main/resources/l10n/JabRef_en.properties +++ b/src/main/resources/l10n/JabRef_en.properties @@ -363,6 +363,8 @@ Formatter\ name=Formatter name found\ in\ AUX\ file=found in AUX file +Fulltext\ search=Fulltext search + Fulltext\ for=Fulltext for Further\ information\ about\ Mr.\ DLib\ for\ JabRef\ users.=Further information about Mr. DLib for JabRef users. @@ -440,6 +442,8 @@ Include\ subgroups\:\ When\ selected,\ view\ entries\ contained\ in\ this\ group Independent\ group\:\ When\ selected,\ view\ only\ this\ group's\ entries=Independent group: When selected, view only this group's entries I\ Agree=I Agree +Indexing\ pdf\ files=Indexing pdf files + Invalid\ citation\ key=Invalid citation key Invalid\ URL=Invalid URL @@ -629,7 +633,6 @@ Previous\ preview\ layout=Previous preview layout Available=Available Selected=Selected Selected\ Layouts\ can\ not\ be\ empty=Selected Layouts can not be empty -Start\ systematic\ literature\ review=Start systematic literature review Reset\ default\ preview\ style=Reset default preview style Previous\ entry=Previous entry Primary\ sort\ criterion=Primary sort criterion @@ -2319,7 +2322,6 @@ New\ entry\ by\ type=New entry by type File\ '%1'\ is\ a\ duplicate\ of\ '%0'.\ Keeping\ '%0'=File '%1' is a duplicate of '%0'. Keeping '%0' File\ '%1'\ is\ a\ duplicate\ of\ '%0'.\ Keeping\ both\ due\ to\ deletion\ error=File '%1' is a duplicate of '%0'. Keeping both due to deletion error - Enable\ field\ formatters=Enable field formatters Entry\ Type=Entry Type Entry\ types=Entry types @@ -2327,3 +2329,37 @@ Field\ names=Field names Others=Others Overwrite\ existing\ field\ values=Overwrite existing field values Recommended=Recommended + +Authors\ and\ Title=Authors and Title +Database=Database +Databases=Databases +Manage\ study\ definition=Manage study definition +Add\ Author\:=Add Author\: +Add\ Database\:=Add Database\: +Add\ Query\:=Add Query\: +Add\ Research\ Question\:=Add Research Question\: +Perform\ search\ for\ existing\ systematic\ literature\ review=Perform search for existing systematic literature review +Queries=Queries +Research\ Questions=Research Questions +Searching=Searching +Start\ new\ systematic\ literature\ review=Start new systematic literature review +Study\ Title\:=Study Title\: +Study\ repository\ could\ not\ be\ created=Study repository could not be created + +All\ query\ terms\ are\ joined\ using\ the\ logical\ AND,\ and\ OR\ operators=All query terms are joined using the logical AND, and OR operators +Finalize=Finalize +If\ the\ sequence\ of\ terms\ is\ relevant\ wrap\ them\ in\ double\ quotes =If the sequence of terms is relevant wrap them in double quotes +Query\ terms\ are\ separated\ by\ spaces.=Query terms are separated by spaces. +Select\ the\ study\ directory\:=Select the study directory\: +An\ example\:=An example\: +Define\ study\ parameters=Define study parameters +Start\ survey=Start survey +Query=Query +Question=Question +Select\ directory=Select directory + +Rebuild\ fulltext\ search\ index=Rebuild fulltext search index +Rebuild\ fulltext\ search\ index\ for\ current\ library?=Rebuild fulltext search index for current library? +Rebuilding\ fulltext\ search\ index...=Rebuilding fulltext search index... +Failed\ to\ access\ fulltext\ search\ index=Failed to access fulltext search index +Found\ match\ in\ %0=Found match in %0 diff --git a/src/main/resources/l10n/JabRef_es.properties b/src/main/resources/l10n/JabRef_es.properties index b69ab73139b..64282ad69bb 100644 --- a/src/main/resources/l10n/JabRef_es.properties +++ b/src/main/resources/l10n/JabRef_es.properties @@ -629,7 +629,6 @@ Previous\ preview\ layout=Plantilla de previsualización previa Available=Disponible Selected=Seleccionado Selected\ Layouts\ can\ not\ be\ empty=Los diseños seleccionados no pueden estar vacíos -Start\ systematic\ literature\ review=Iniciar revisión documental sistemática Reset\ default\ preview\ style=Restablecer el estilo de vista previa predeterminado Previous\ entry=Entrada anterior Primary\ sort\ criterion=Criterio de ordenación primario @@ -2283,7 +2282,6 @@ Customization=Personalización New\ entry\ by\ type=Entrada nueva por tipo - Enable\ field\ formatters=Habilitar formateadores de campos Entry\ Type=Tipo de entrada Entry\ types=Tipos de entrada @@ -2291,3 +2289,6 @@ Field\ names=Nombre del campo Others=Otros Overwrite\ existing\ field\ values=Sobreescribir valores de campo existentes Recommended=Recomendado + + + diff --git a/src/main/resources/l10n/JabRef_fa.properties b/src/main/resources/l10n/JabRef_fa.properties index 480f9b0500f..564c7f5dda1 100644 --- a/src/main/resources/l10n/JabRef_fa.properties +++ b/src/main/resources/l10n/JabRef_fa.properties @@ -609,6 +609,8 @@ Auto\ complete\ enabled.=تکمیل خودکار غیرفعال شد. + + diff --git a/src/main/resources/l10n/JabRef_fr.properties b/src/main/resources/l10n/JabRef_fr.properties index f5478814b79..24309352b74 100644 --- a/src/main/resources/l10n/JabRef_fr.properties +++ b/src/main/resources/l10n/JabRef_fr.properties @@ -629,7 +629,6 @@ Previous\ preview\ layout=Mode d'aperçu précédent Available=Disponible Selected=Sélectionné Selected\ Layouts\ can\ not\ be\ empty=Les mises en page sélectionnées ne peuvent pas être vides -Start\ systematic\ literature\ review=Lancer une revue systématique de la littérature Reset\ default\ preview\ style=Restaurer le style d'aperçu par défaut Previous\ entry=Entrée précédente Primary\ sort\ criterion=Critère de tri principal @@ -2319,7 +2318,6 @@ New\ entry\ by\ type=Nouvelle entrée par type File\ '%1'\ is\ a\ duplicate\ of\ '%0'.\ Keeping\ '%0'=Le fichier '%1' est un doublon de '%0'. Conservation de '%0' File\ '%1'\ is\ a\ duplicate\ of\ '%0'.\ Keeping\ both\ due\ to\ deletion\ error=Le fichier '%1' est un doublon de '%0'. Conservation des deux en raison d'une erreur de suppression - Enable\ field\ formatters=Activer les formateurs de champs Entry\ Type=Type d’entrée Entry\ types=Types d'entrée @@ -2327,3 +2325,6 @@ Field\ names=Noms de champs Others=Autres Overwrite\ existing\ field\ values=Écraser les valeurs des champs existants Recommended=Usuels + + + diff --git a/src/main/resources/l10n/JabRef_in.properties b/src/main/resources/l10n/JabRef_in.properties index ee10cdf24bd..6fec89f503f 100644 --- a/src/main/resources/l10n/JabRef_in.properties +++ b/src/main/resources/l10n/JabRef_in.properties @@ -1629,6 +1629,8 @@ Default\ pattern=Pola bawaan + + diff --git a/src/main/resources/l10n/JabRef_it.properties b/src/main/resources/l10n/JabRef_it.properties index 843f3943a67..1c3deb9c75b 100644 --- a/src/main/resources/l10n/JabRef_it.properties +++ b/src/main/resources/l10n/JabRef_it.properties @@ -629,7 +629,6 @@ Previous\ preview\ layout=Successivo layout di anteprima Available=Disponibile Selected=Selezionato Selected\ Layouts\ can\ not\ be\ empty=I layout selezionati non possono essere vuoti -Start\ systematic\ literature\ review=Inizia la revisione sistematica della letteratura Reset\ default\ preview\ style=Ripristina lo stile di anteprima predefinito Previous\ entry=Voce precedente Primary\ sort\ criterion=Criterio di ordinamento principale @@ -2319,7 +2318,6 @@ New\ entry\ by\ type=Nuova voce per tipo File\ '%1'\ is\ a\ duplicate\ of\ '%0'.\ Keeping\ '%0'=Il file '%1' è un duplicato di '%0'. Mantenere '%0' File\ '%1'\ is\ a\ duplicate\ of\ '%0'.\ Keeping\ both\ due\ to\ deletion\ error=Il file '%1' è un duplicato di '%0'. Mantenere entrambi a causa di errore di cancellazione - Enable\ field\ formatters=Abilita formatori di campo Entry\ Type=Tipo di voce Entry\ types=Tipi di voce @@ -2327,3 +2325,6 @@ Field\ names=Nome del campo Others=Altri Overwrite\ existing\ field\ values=Sovrascrivi i valori esistenti del campo Recommended=Consigliato + + + diff --git a/src/main/resources/l10n/JabRef_ja.properties b/src/main/resources/l10n/JabRef_ja.properties index 67d09f7fd11..c93ab4bb5ac 100644 --- a/src/main/resources/l10n/JabRef_ja.properties +++ b/src/main/resources/l10n/JabRef_ja.properties @@ -1944,6 +1944,8 @@ Required=必須 + + diff --git a/src/main/resources/l10n/JabRef_nl.properties b/src/main/resources/l10n/JabRef_nl.properties index 0dd609d771d..4cfb3e8ab87 100644 --- a/src/main/resources/l10n/JabRef_nl.properties +++ b/src/main/resources/l10n/JabRef_nl.properties @@ -1748,6 +1748,8 @@ Default\ pattern=Standaard patroon + + diff --git a/src/main/resources/l10n/JabRef_no.properties b/src/main/resources/l10n/JabRef_no.properties index 6bccc6dd079..837ef24f7ab 100644 --- a/src/main/resources/l10n/JabRef_no.properties +++ b/src/main/resources/l10n/JabRef_no.properties @@ -1177,6 +1177,8 @@ Default\ pattern=Standardmønster + + diff --git a/src/main/resources/l10n/JabRef_pl.properties b/src/main/resources/l10n/JabRef_pl.properties index 5178c08f6c0..a994fb1876e 100644 --- a/src/main/resources/l10n/JabRef_pl.properties +++ b/src/main/resources/l10n/JabRef_pl.properties @@ -1038,3 +1038,5 @@ Reset=Reset + + diff --git a/src/main/resources/l10n/JabRef_pt.properties b/src/main/resources/l10n/JabRef_pt.properties index 8d8ef3efb51..bf85f2f85c2 100644 --- a/src/main/resources/l10n/JabRef_pt.properties +++ b/src/main/resources/l10n/JabRef_pt.properties @@ -1336,6 +1336,8 @@ Default\ pattern=Ppadrão predefinido + + diff --git a/src/main/resources/l10n/JabRef_pt_BR.properties b/src/main/resources/l10n/JabRef_pt_BR.properties index b6f99d2a76c..97c863ec250 100644 --- a/src/main/resources/l10n/JabRef_pt_BR.properties +++ b/src/main/resources/l10n/JabRef_pt_BR.properties @@ -623,7 +623,6 @@ Previous\ preview\ layout=Anterior Layout de visualização Available=Disponível Selected=Selecionado Selected\ Layouts\ can\ not\ be\ empty=Os Layouts selecionados não podem estar vazios -Start\ systematic\ literature\ review=Iniciar revisão sistemática da literatura Reset\ default\ preview\ style=Redefinir estilo de pré-visualização padrão Previous\ entry=Referência anterior Primary\ sort\ criterion=Critério de ordenação primário @@ -2178,3 +2177,5 @@ Removes\ digits.=Remove dígitos. + + diff --git a/src/main/resources/l10n/JabRef_ru.properties b/src/main/resources/l10n/JabRef_ru.properties index 200b8f3c632..75609bfb0e9 100644 --- a/src/main/resources/l10n/JabRef_ru.properties +++ b/src/main/resources/l10n/JabRef_ru.properties @@ -629,7 +629,6 @@ Previous\ preview\ layout=Предыдущий макет предварител Available=Доступно Selected=Выбрано Selected\ Layouts\ can\ not\ be\ empty=Выбранные макеты не могут быть пустыми -Start\ systematic\ literature\ review=Начать систематический обзор литературы Reset\ default\ preview\ style=Сброс стиль пред. просмотра по умолчанию Previous\ entry=Предыдущая запись Primary\ sort\ criterion=Первичный критерий сортировки @@ -2318,7 +2317,6 @@ New\ entry\ by\ type=Новая запись по типу File\ '%1'\ is\ a\ duplicate\ of\ '%0'.\ Keeping\ '%0'=Файл '%1' является дубликатом '%0'. Оставлен '%0' File\ '%1'\ is\ a\ duplicate\ of\ '%0'.\ Keeping\ both\ due\ to\ deletion\ error=Файл '%1' является дубликатом '%0'. Оставлены оба из-за ошибки удаления - Enable\ field\ formatters=Включить форматирование полей Entry\ Type=Тип записи Entry\ types=Типы записей @@ -2326,3 +2324,6 @@ Field\ names=Названия полей Others=Другие Overwrite\ existing\ field\ values=Перезаписать текущие значения полей Recommended=Рекомендованный + + + diff --git a/src/main/resources/l10n/JabRef_sv.properties b/src/main/resources/l10n/JabRef_sv.properties index 666e7ab65b2..be4032b0623 100644 --- a/src/main/resources/l10n/JabRef_sv.properties +++ b/src/main/resources/l10n/JabRef_sv.properties @@ -1544,3 +1544,5 @@ Previous\ preview\ style=Föregående förhandsgranskningsstil + + diff --git a/src/main/resources/l10n/JabRef_tl.properties b/src/main/resources/l10n/JabRef_tl.properties index 12e9a755173..347bc32f6bb 100644 --- a/src/main/resources/l10n/JabRef_tl.properties +++ b/src/main/resources/l10n/JabRef_tl.properties @@ -1343,6 +1343,8 @@ Default\ pattern=Default na pattern + + diff --git a/src/main/resources/l10n/JabRef_tr.properties b/src/main/resources/l10n/JabRef_tr.properties index ab2a06e11bb..5db5e6bfbc5 100644 --- a/src/main/resources/l10n/JabRef_tr.properties +++ b/src/main/resources/l10n/JabRef_tr.properties @@ -629,7 +629,6 @@ Previous\ preview\ layout=Önceki önizleme düzeni Available=Mevcut Selected=Seçili Selected\ Layouts\ can\ not\ be\ empty=Seçili Yerleşim Planları boş olamaz -Start\ systematic\ literature\ review=Sistematik literatür derlemesini başlat Reset\ default\ preview\ style=Öntanımlı ön izleme stilini en başa döndür Previous\ entry=Önceki girdi Primary\ sort\ criterion=Birincil sıralama kriteri @@ -2295,3 +2294,5 @@ Error\ reading\ PDF\ content\:\ %0=PDF içeriğini okumada hata\: %0 + + diff --git a/src/main/resources/l10n/JabRef_vi.properties b/src/main/resources/l10n/JabRef_vi.properties index 5b45a184b76..32e539787f1 100644 --- a/src/main/resources/l10n/JabRef_vi.properties +++ b/src/main/resources/l10n/JabRef_vi.properties @@ -1104,6 +1104,8 @@ Default\ pattern=Kiểu mặc định + + diff --git a/src/main/resources/l10n/JabRef_zh_CN.properties b/src/main/resources/l10n/JabRef_zh_CN.properties index bae09bc73c0..2299f670c3b 100644 --- a/src/main/resources/l10n/JabRef_zh_CN.properties +++ b/src/main/resources/l10n/JabRef_zh_CN.properties @@ -2231,3 +2231,5 @@ Error\ importing.\ See\ the\ error\ log\ for\ details.=导入错误。具体内 + + diff --git a/src/main/resources/l10n/JabRef_zh_TW.properties b/src/main/resources/l10n/JabRef_zh_TW.properties index d54a04b2d87..e59a0361f3c 100644 --- a/src/main/resources/l10n/JabRef_zh_TW.properties +++ b/src/main/resources/l10n/JabRef_zh_TW.properties @@ -988,3 +988,5 @@ Error\ importing.\ See\ the\ error\ log\ for\ details.=匯入錯誤。詳細內 + + diff --git a/src/main/resources/luceneIndex/.gitignore b/src/main/resources/luceneIndex/.gitignore new file mode 100644 index 00000000000..72e8ffc0db8 --- /dev/null +++ b/src/main/resources/luceneIndex/.gitignore @@ -0,0 +1 @@ +* diff --git a/src/main/resources/luceneIndex/.gitkeep b/src/main/resources/luceneIndex/.gitkeep new file mode 100644 index 00000000000..ef056e1e450 --- /dev/null +++ b/src/main/resources/luceneIndex/.gitkeep @@ -0,0 +1,2 @@ +# For ensuring this directory is not deleted. +# This directory is used for the Lucene indices. diff --git a/src/main/resources/org/jabref/logic/git/git.gitignore b/src/main/resources/org/jabref/logic/git/git.gitignore new file mode 100644 index 00000000000..dcff6d2d473 --- /dev/null +++ b/src/main/resources/org/jabref/logic/git/git.gitignore @@ -0,0 +1,4 @@ +### JabRef ### +# JabRef database specific files for persistence, not relevant to persist +*.sav +*.bak diff --git a/src/test/java/org/jabref/architecture/MainArchitectureTests.java b/src/test/java/org/jabref/architecture/MainArchitectureTests.java index a6459ed2fe3..40e3d945f2b 100644 --- a/src/test/java/org/jabref/architecture/MainArchitectureTests.java +++ b/src/test/java/org/jabref/architecture/MainArchitectureTests.java @@ -117,7 +117,12 @@ public static void doNotUseLogicInModel(JavaClasses classes) { @ArchTest public static void restrictUsagesInModel(JavaClasses classes) { - noClasses().that().resideInAPackage(PACKAGE_ORG_JABREF_MODEL) + // Until we switch to Lucene, we need to access Globals.stateManager().getActiveDatabase() from the search classes, + // because the PDFSearch needs to access the index of the corresponding database + noClasses().that().areNotAssignableFrom("org.jabref.model.search.rules.ContainBasedSearchRule") + .and().areNotAssignableFrom("org.jabref.model.search.rules.RegexBasedSearchRule") + .and().areNotAssignableFrom("org.jabref.model.search.rules.GrammarBasedSearchRule") + .and().resideInAPackage(PACKAGE_ORG_JABREF_MODEL) .should().dependOnClassesThat().resideInAPackage(PACKAGE_JAVAX_SWING) .orShould().dependOnClassesThat().haveFullyQualifiedName(CLASS_ORG_JABREF_GLOBALS) .check(classes); diff --git a/src/test/java/org/jabref/gui/duplicationFinder/DuplicateSearchTest.java b/src/test/java/org/jabref/gui/duplicationFinder/DuplicateSearchTest.java deleted file mode 100644 index fb82989a8ff..00000000000 --- a/src/test/java/org/jabref/gui/duplicationFinder/DuplicateSearchTest.java +++ /dev/null @@ -1,103 +0,0 @@ -package org.jabref.gui.duplicationFinder; - -import java.util.Arrays; -import java.util.Collections; -import java.util.Optional; - -import org.jabref.gui.DialogService; -import org.jabref.gui.JabRefFrame; -import org.jabref.gui.LibraryTab; -import org.jabref.gui.StateManager; -import org.jabref.gui.undo.CountingUndoManager; -import org.jabref.gui.undo.NamedCompound; -import org.jabref.gui.util.OptionalObjectProperty; -import org.jabref.logic.l10n.Localization; -import org.jabref.model.database.BibDatabase; -import org.jabref.model.database.BibDatabaseContext; -import org.jabref.model.database.BibDatabaseMode; -import org.jabref.model.entry.BibEntry; -import org.jabref.model.entry.field.StandardField; -import org.jabref.model.entry.types.StandardEntryType; -import org.jabref.testutils.category.GUITest; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.testfx.framework.junit5.ApplicationExtension; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@GUITest -@ExtendWith(ApplicationExtension.class) -public class DuplicateSearchTest { - - private final DialogService dialogService = spy(DialogService.class); - private final StateManager stateManager = mock(StateManager.class); - private final JabRefFrame jabRefFrame = mock(JabRefFrame.class); - private final LibraryTab libraryTab = mock(LibraryTab.class); - private final BibDatabaseContext bibDatabaseContext = mock(BibDatabaseContext.class); - private final CountingUndoManager undoManager = mock(CountingUndoManager.class); - - private DuplicateSearch duplicateSearch; - private BibEntry entry1; - - @BeforeEach - void setupDuplicateSearchInstance() { - entry1 = new BibEntry(StandardEntryType.InProceedings) - .withField(StandardField.AUTHOR, "Souti Chattopadhyay and Nicholas Nelson and Audrey Au and Natalia Morales and Christopher Sanchez and Rahul Pandita and Anita Sarma") - .withField(StandardField.TITLE, "A tale from the trenches") - .withField(StandardField.YEAR, "2020") - .withField(StandardField.DOI, "10.1145/3377811.3380330") - .withField(StandardField.SUBTITLE, "cognitive biases and software development") - .withCitationKey("Chattopadhyay2020"); - - when(jabRefFrame.getCurrentLibraryTab()).thenReturn(libraryTab); - when(stateManager.activeDatabaseProperty()).thenReturn(OptionalObjectProperty.empty()); - duplicateSearch = new DuplicateSearch(jabRefFrame, dialogService, stateManager); - } - - @Test - public void executeWithNoEntries() { - when(stateManager.getActiveDatabase()).thenReturn(Optional.of(bibDatabaseContext)); - when(bibDatabaseContext.getEntries()).thenReturn(Collections.emptyList()); - - duplicateSearch.execute(); - verify(dialogService, times(1)).notify(Localization.lang("Searching for duplicates...")); - } - - @Test - public void executeWithOneEntry() { - when(stateManager.getActiveDatabase()).thenReturn(Optional.of(bibDatabaseContext)); - when(bibDatabaseContext.getEntries()).thenReturn(Collections.singletonList(entry1)); - - duplicateSearch.execute(); - verify(dialogService, times(1)).notify(Localization.lang("Searching for duplicates...")); - } - - @Test - public void executeWithNoDuplicates() { - BibEntry entry2 = new BibEntry(StandardEntryType.InProceedings) - .withField(StandardField.AUTHOR, "Tale S Sastad and Karl Thomas Hjelmervik") - .withField(StandardField.TITLE, "Synthesizing Realistic, High-Resolution Anti-Submarine Sonar Data\n") - .withField(StandardField.YEAR, "2018") - .withField(StandardField.DOI, "10.1109/OCEANSKOBE.2018.8558837") - .withCitationKey("Sastad2018"); - - when(stateManager.getActiveDatabase()).thenReturn(Optional.of(bibDatabaseContext)); - when(bibDatabaseContext.getEntries()).thenReturn(Arrays.asList(entry1, entry2)); - when(bibDatabaseContext.getMode()).thenReturn(BibDatabaseMode.BIBTEX); - when(libraryTab.getBibDatabaseContext()).thenReturn(bibDatabaseContext); - when(libraryTab.getDatabase()).thenReturn(mock(BibDatabase.class)); - when(libraryTab.getUndoManager()).thenReturn(undoManager); - when(undoManager.addEdit(mock(NamedCompound.class))).thenReturn(true); - - duplicateSearch.execute(); - verify(dialogService, times(1)).notify(Localization.lang("Searching for duplicates...")); - verify(dialogService, times(1)).notify(Localization.lang("Duplicates found") + ": " + String.valueOf(0) + ' ' - + Localization.lang("pairs processed") + ": " + String.valueOf(0)); - } -} diff --git a/src/test/java/org/jabref/gui/search/ContainsAndRegexBasedSearchRuleDescriberTest.java b/src/test/java/org/jabref/gui/search/ContainsAndRegexBasedSearchRuleDescriberTest.java index b67da25d1ca..bc3406ec43c 100644 --- a/src/test/java/org/jabref/gui/search/ContainsAndRegexBasedSearchRuleDescriberTest.java +++ b/src/test/java/org/jabref/gui/search/ContainsAndRegexBasedSearchRuleDescriberTest.java @@ -1,5 +1,6 @@ package org.jabref.gui.search; +import java.util.EnumSet; import java.util.List; import javafx.scene.text.Text; @@ -8,6 +9,8 @@ import org.jabref.gui.search.rules.describer.ContainsAndRegexBasedSearchRuleDescriber; import org.jabref.gui.util.TooltipTextUtil; +import org.jabref.model.search.rules.SearchRules; +import org.jabref.model.search.rules.SearchRules.SearchFlags; import org.jabref.testutils.category.GUITest; import org.junit.jupiter.api.Test; @@ -32,7 +35,7 @@ void testSimpleTerm() { TooltipTextUtil.createText("This search contains entries in which any field contains the term "), TooltipTextUtil.createText("test", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" (case insensitive). ")); - TextFlow description = new ContainsAndRegexBasedSearchRuleDescriber(false, false, query).getDescription(); + TextFlow description = new ContainsAndRegexBasedSearchRuleDescriber(EnumSet.noneOf(SearchFlags.class), query).getDescription(); TextFlowEqualityHelper.assertEquals(expectedTexts, description); } @@ -46,7 +49,7 @@ void testNoAst() { TooltipTextUtil.createText(" and "), TooltipTextUtil.createText("b", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" (case insensitive). ")); - TextFlow description = new ContainsAndRegexBasedSearchRuleDescriber(false, false, query).getDescription(); + TextFlow description = new ContainsAndRegexBasedSearchRuleDescriber(EnumSet.noneOf(SearchFlags.class), query).getDescription(); TextFlowEqualityHelper.assertEquals(expectedTexts, description); } @@ -60,7 +63,7 @@ void testNoAstRegex() { TooltipTextUtil.createText(" and "), TooltipTextUtil.createText("b", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" (case insensitive). ")); - TextFlow description = new ContainsAndRegexBasedSearchRuleDescriber(false, true, query).getDescription(); + TextFlow description = new ContainsAndRegexBasedSearchRuleDescriber(EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION), query).getDescription(); TextFlowEqualityHelper.assertEquals(expectedTexts, description); } @@ -74,7 +77,7 @@ void testNoAstRegexCaseSensitive() { TooltipTextUtil.createText(" and "), TooltipTextUtil.createText("b", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" (case sensitive). ")); - TextFlow description = new ContainsAndRegexBasedSearchRuleDescriber(true, true, query).getDescription(); + TextFlow description = new ContainsAndRegexBasedSearchRuleDescriber(EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION), query).getDescription(); TextFlowEqualityHelper.assertEquals(expectedTexts, description); } @@ -88,7 +91,7 @@ void testNoAstCaseSensitive() { TooltipTextUtil.createText(" and "), TooltipTextUtil.createText("b", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" (case sensitive). ")); - TextFlow description = new ContainsAndRegexBasedSearchRuleDescriber(true, false, query).getDescription(); + TextFlow description = new ContainsAndRegexBasedSearchRuleDescriber(EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE), query).getDescription(); TextFlowEqualityHelper.assertEquals(expectedTexts, description); } diff --git a/src/test/java/org/jabref/gui/search/GrammarBasedSearchRuleDescriberTest.java b/src/test/java/org/jabref/gui/search/GrammarBasedSearchRuleDescriberTest.java index 36ae0dbea71..cad5aa79e89 100644 --- a/src/test/java/org/jabref/gui/search/GrammarBasedSearchRuleDescriberTest.java +++ b/src/test/java/org/jabref/gui/search/GrammarBasedSearchRuleDescriberTest.java @@ -1,6 +1,7 @@ package org.jabref.gui.search; import java.util.Arrays; +import java.util.EnumSet; import java.util.List; import javafx.scene.text.Text; @@ -10,6 +11,8 @@ import org.jabref.gui.search.rules.describer.GrammarBasedSearchRuleDescriber; import org.jabref.gui.util.TooltipTextUtil; import org.jabref.model.search.rules.GrammarBasedSearchRule; +import org.jabref.model.search.rules.SearchRules; +import org.jabref.model.search.rules.SearchRules.SearchFlags; import org.jabref.testutils.category.GUITest; import org.junit.jupiter.api.Test; @@ -29,10 +32,10 @@ void onStart(Stage stage) { stage.show(); } - private TextFlow createDescription(String query, boolean caseSensitive, boolean regExp) { - GrammarBasedSearchRule grammarBasedSearchRule = new GrammarBasedSearchRule(caseSensitive, regExp); + private TextFlow createDescription(String query, EnumSet searchFlags) { + GrammarBasedSearchRule grammarBasedSearchRule = new GrammarBasedSearchRule(searchFlags); assertTrue(grammarBasedSearchRule.validateSearchStrings(query)); - GrammarBasedSearchRuleDescriber describer = new GrammarBasedSearchRuleDescriber(caseSensitive, regExp, grammarBasedSearchRule.getTree()); + GrammarBasedSearchRuleDescriber describer = new GrammarBasedSearchRuleDescriber(searchFlags, grammarBasedSearchRule.getTree()); return describer.getDescription(); } @@ -42,7 +45,7 @@ void testSimpleQueryCaseSensitiveRegex() { List expectedTexts = Arrays.asList(TooltipTextUtil.createText("This search contains entries in which "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("a", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" contains the regular expression "), TooltipTextUtil.createText("b", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(". "), TooltipTextUtil.createText("The search is case sensitive.")); - TextFlow description = createDescription(query, true, true); + TextFlow description = createDescription(query, EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); TextFlowEqualityHelper.assertEquals(expectedTexts, description); } @@ -53,7 +56,7 @@ void testSimpleQueryCaseSensitive() { List expectedTexts = Arrays.asList(TooltipTextUtil.createText("This search contains entries in which "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("a", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" contains the term "), TooltipTextUtil.createText("b", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(". "), TooltipTextUtil.createText("The search is case sensitive.")); - TextFlow description = createDescription(query, true, false); + TextFlow description = createDescription(query, EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)); TextFlowEqualityHelper.assertEquals(expectedTexts, description); } @@ -64,7 +67,7 @@ void testSimpleQuery() { List expectedTexts = Arrays.asList(TooltipTextUtil.createText("This search contains entries in which "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("a", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" contains the term "), TooltipTextUtil.createText("b", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(". "), TooltipTextUtil.createText("The search is case insensitive.")); - TextFlow description = createDescription(query, false, false); + TextFlow description = createDescription(query, EnumSet.noneOf(SearchFlags.class)); TextFlowEqualityHelper.assertEquals(expectedTexts, description); } @@ -75,7 +78,7 @@ void testSimpleQueryRegex() { List expectedTexts = Arrays.asList(TooltipTextUtil.createText("This search contains entries in which "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("a", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" contains the regular expression "), TooltipTextUtil.createText("b", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(". "), TooltipTextUtil.createText("The search is case insensitive.")); - TextFlow description = createDescription(query, false, true); + TextFlow description = createDescription(query, EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); TextFlowEqualityHelper.assertEquals(expectedTexts, description); } @@ -87,7 +90,7 @@ void testComplexQueryCaseSensitiveRegex() { TooltipTextUtil.createText(" contains the regular expression "), TooltipTextUtil.createText("b", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" and "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("c", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" contains the regular expression "), TooltipTextUtil.createText("e", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" or "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("e", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" contains the regular expression "), TooltipTextUtil.createText("x", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(". "), TooltipTextUtil.createText("The search is case sensitive.")); - TextFlow description = createDescription(query, true, true); + TextFlow description = createDescription(query, EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); TextFlowEqualityHelper.assertEquals(expectedTexts, description); } @@ -99,7 +102,7 @@ void testComplexQueryRegex() { TooltipTextUtil.createText(" contains the regular expression "), TooltipTextUtil.createText("b", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" and "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("c", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" contains the regular expression "), TooltipTextUtil.createText("e", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" or "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("e", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" contains the regular expression "), TooltipTextUtil.createText("x", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(". "), TooltipTextUtil.createText("The search is case insensitive.")); - TextFlow description = createDescription(query, false, true); + TextFlow description = createDescription(query, EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); TextFlowEqualityHelper.assertEquals(expectedTexts, description); } @@ -110,7 +113,7 @@ void testComplexQueryCaseSensitive() { List expectedTexts = Arrays.asList(TooltipTextUtil.createText("This search contains entries in which "), TooltipTextUtil.createText("not "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("a", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" contains the term "), TooltipTextUtil.createText("b", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" and "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("c", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" contains the term "), TooltipTextUtil.createText("e", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" or "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("e", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" contains the term "), TooltipTextUtil.createText("x", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(". "), TooltipTextUtil.createText("The search is case sensitive.")); - TextFlow description = createDescription(query, true, false); + TextFlow description = createDescription(query, EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)); TextFlowEqualityHelper.assertEquals(expectedTexts, description); } @@ -121,7 +124,7 @@ void testComplexQuery() { List expectedTexts = Arrays.asList(TooltipTextUtil.createText("This search contains entries in which "), TooltipTextUtil.createText("not "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("a", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" contains the term "), TooltipTextUtil.createText("b", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" and "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("c", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" contains the term "), TooltipTextUtil.createText("e", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" or "), TooltipTextUtil.createText("the field "), TooltipTextUtil.createText("e", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(" contains the term "), TooltipTextUtil.createText("x", TooltipTextUtil.TextType.BOLD), TooltipTextUtil.createText(". "), TooltipTextUtil.createText("The search is case insensitive.")); - TextFlow description = createDescription(query, false, false); + TextFlow description = createDescription(query, EnumSet.noneOf(SearchFlags.class)); TextFlowEqualityHelper.assertEquals(expectedTexts, description); } diff --git a/src/test/java/org/jabref/logic/crawler/CrawlerTest.java b/src/test/java/org/jabref/logic/crawler/CrawlerTest.java index b07dc48c59f..6dcc82f08ac 100644 --- a/src/test/java/org/jabref/logic/crawler/CrawlerTest.java +++ b/src/test/java/org/jabref/logic/crawler/CrawlerTest.java @@ -1,6 +1,7 @@ package org.jabref.logic.crawler; import java.net.URL; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -8,8 +9,8 @@ import org.jabref.logic.bibtex.FieldContentFormatterPreferences; import org.jabref.logic.citationkeypattern.CitationKeyPatternPreferences; import org.jabref.logic.citationkeypattern.GlobalCitationKeyPattern; -import org.jabref.logic.crawler.git.GitHandler; import org.jabref.logic.exporter.SavePreferences; +import org.jabref.logic.git.SlrGitHandler; import org.jabref.logic.importer.ImportFormatPreferences; import org.jabref.logic.preferences.TimestampPreferences; import org.jabref.logic.util.io.FileUtil; @@ -37,22 +38,14 @@ class CrawlerTest { SavePreferences savePreferences; TimestampPreferences timestampPreferences; BibEntryTypesManager entryTypesManager; - GitHandler gitHandler = mock(GitHandler.class, Answers.RETURNS_DEFAULTS); + SlrGitHandler gitHandler = mock(SlrGitHandler.class, Answers.RETURNS_DEFAULTS); String hashCodeQuantum = String.valueOf("Quantum".hashCode()); String hashCodeCloudComputing = String.valueOf("Cloud Computing".hashCode()); - String hashCodeSoftwareEngineering = String.valueOf("\"Software Engineering\"".hashCode()); @Test public void testWhetherAllFilesAreCreated() throws Exception { setUp(); - Crawler testCrawler = new Crawler(getPathToStudyDefinitionFile(), - gitHandler, - new DummyFileUpdateMonitor(), - importFormatPreferences, - savePreferences, - timestampPreferences, - entryTypesManager - ); + Crawler testCrawler = new Crawler(getPathToStudyDefinitionFile(), gitHandler, importFormatPreferences, savePreferences, timestampPreferences, entryTypesManager, new DummyFileUpdateMonitor()); testCrawler.performCrawl(); @@ -71,7 +64,7 @@ public void testWhetherAllFilesAreCreated() throws Exception { } private Path getPathToStudyDefinitionFile() { - return tempRepositoryDirectory.resolve("study.yml"); + return tempRepositoryDirectory; } /** @@ -98,6 +91,7 @@ private void setUp() throws Exception { when(savePreferences.getEncoding()).thenReturn(null); when(savePreferences.takeMetadataSaveOrderInAccount()).thenReturn(true); when(savePreferences.getCitationKeyPatternPreferences()).thenReturn(citationKeyPatternPreferences); + when(savePreferences.getEncoding()).thenReturn(Charset.defaultCharset()); when(importFormatPreferences.getKeywordSeparator()).thenReturn(','); when(importFormatPreferences.getFieldContentFormatterPreferences()).thenReturn(new FieldContentFormatterPreferences()); when(importFormatPreferences.isKeywordSyncEnabled()).thenReturn(false); diff --git a/src/test/java/org/jabref/logic/crawler/StudyDatabaseToFetcherConverterTest.java b/src/test/java/org/jabref/logic/crawler/StudyDatabaseToFetcherConverterTest.java index f850658fefb..74ba78baa1a 100644 --- a/src/test/java/org/jabref/logic/crawler/StudyDatabaseToFetcherConverterTest.java +++ b/src/test/java/org/jabref/logic/crawler/StudyDatabaseToFetcherConverterTest.java @@ -6,8 +6,8 @@ import java.util.List; import org.jabref.logic.bibtex.FieldContentFormatterPreferences; -import org.jabref.logic.crawler.git.GitHandler; import org.jabref.logic.exporter.SavePreferences; +import org.jabref.logic.git.SlrGitHandler; import org.jabref.logic.importer.ImportFormatPreferences; import org.jabref.logic.importer.SearchBasedFetcher; import org.jabref.logic.preferences.TimestampPreferences; @@ -30,7 +30,7 @@ class StudyDatabaseToFetcherConverterTest { SavePreferences savePreferences; TimestampPreferences timestampPreferences; BibEntryTypesManager entryTypesManager; - GitHandler gitHandler; + SlrGitHandler gitHandler; @TempDir Path tempRepositoryDirectory; @@ -47,7 +47,7 @@ void setUpMocks() { when(importFormatPreferences.isKeywordSyncEnabled()).thenReturn(false); when(importFormatPreferences.getEncoding()).thenReturn(StandardCharsets.UTF_8); entryTypesManager = new BibEntryTypesManager(); - gitHandler = mock(GitHandler.class, Answers.RETURNS_DEFAULTS); + gitHandler = mock(SlrGitHandler.class, Answers.RETURNS_DEFAULTS); } @Test diff --git a/src/test/java/org/jabref/logic/crawler/StudyRepositoryTest.java b/src/test/java/org/jabref/logic/crawler/StudyRepositoryTest.java index 8bba87c2a38..d4becff0a1d 100644 --- a/src/test/java/org/jabref/logic/crawler/StudyRepositoryTest.java +++ b/src/test/java/org/jabref/logic/crawler/StudyRepositoryTest.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.net.URL; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -14,9 +15,9 @@ import org.jabref.logic.citationkeypattern.CitationKeyGenerator; import org.jabref.logic.citationkeypattern.CitationKeyPatternPreferences; import org.jabref.logic.citationkeypattern.GlobalCitationKeyPattern; -import org.jabref.logic.crawler.git.GitHandler; import org.jabref.logic.database.DatabaseMerger; import org.jabref.logic.exporter.SavePreferences; +import org.jabref.logic.git.SlrGitHandler; import org.jabref.logic.importer.ImportFormatPreferences; import org.jabref.logic.preferences.TimestampPreferences; import org.jabref.logic.util.io.FileUtil; @@ -53,7 +54,7 @@ class StudyRepositoryTest { @TempDir Path tempRepositoryDirectory; StudyRepository studyRepository; - GitHandler gitHandler = mock(GitHandler.class, Answers.RETURNS_DEFAULTS); + SlrGitHandler gitHandler = mock(SlrGitHandler.class, Answers.RETURNS_DEFAULTS); String hashCodeQuantum = String.valueOf("Quantum".hashCode()); String hashCodeCloudComputing = String.valueOf("Cloud Computing".hashCode()); String hashCodeSoftwareEngineering = String.valueOf("\"Software Engineering\"".hashCode()); @@ -79,6 +80,7 @@ public void setUpMocks() throws Exception { when(savePreferences.getSaveOrder()).thenReturn(new SaveOrderConfig()); when(savePreferences.getEncoding()).thenReturn(null); when(savePreferences.takeMetadataSaveOrderInAccount()).thenReturn(true); + when(savePreferences.getEncoding()).thenReturn(Charset.defaultCharset()); when(savePreferences.getCitationKeyPatternPreferences()).thenReturn(citationKeyPatternPreferences); when(importFormatPreferences.getKeywordSeparator()).thenReturn(','); when(importFormatPreferences.getFieldContentFormatterPreferences()).thenReturn(new FieldContentFormatterPreferences()); diff --git a/src/test/java/org/jabref/logic/exporter/GroupSerializerTest.java b/src/test/java/org/jabref/logic/exporter/GroupSerializerTest.java index 4c1bc3147fb..1753495506a 100644 --- a/src/test/java/org/jabref/logic/exporter/GroupSerializerTest.java +++ b/src/test/java/org/jabref/logic/exporter/GroupSerializerTest.java @@ -3,6 +3,7 @@ import java.nio.file.Path; import java.util.Arrays; import java.util.Collections; +import java.util.EnumSet; import java.util.List; import javafx.scene.paint.Color; @@ -24,6 +25,7 @@ import org.jabref.model.groups.TexGroup; import org.jabref.model.groups.WordKeywordGroup; import org.jabref.model.metadata.MetaData; +import org.jabref.model.search.rules.SearchRules; import org.jabref.model.util.DummyFileUpdateMonitor; import org.junit.jupiter.api.BeforeEach; @@ -89,14 +91,14 @@ void serializeSingleRegexKeywordGroup() { @Test void serializeSingleSearchGroup() { - SearchGroup group = new SearchGroup("myExplicitGroup", GroupHierarchyType.INDEPENDENT, "author=harrer", true, true); + SearchGroup group = new SearchGroup("myExplicitGroup", GroupHierarchyType.INDEPENDENT, "author=harrer", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); List serialization = groupSerializer.serializeTree(GroupTreeNode.fromGroup(group)); assertEquals(Collections.singletonList("0 SearchGroup:myExplicitGroup;0;author=harrer;1;1;1;;;;"), serialization); } @Test void serializeSingleSearchGroupWithRegex() { - SearchGroup group = new SearchGroup("myExplicitGroup", GroupHierarchyType.INCLUDING, "author=\"harrer\"", true, false); + SearchGroup group = new SearchGroup("myExplicitGroup", GroupHierarchyType.INCLUDING, "author=\"harrer\"", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)); List serialization = groupSerializer.serializeTree(GroupTreeNode.fromGroup(group)); assertEquals(Collections.singletonList("0 SearchGroup:myExplicitGroup;2;author=\"harrer\";1;0;1;;;;"), serialization); } diff --git a/src/test/java/org/jabref/logic/git/GitHandlerTest.java b/src/test/java/org/jabref/logic/git/GitHandlerTest.java new file mode 100644 index 00000000000..6f5e30052cf --- /dev/null +++ b/src/test/java/org/jabref/logic/git/GitHandlerTest.java @@ -0,0 +1,58 @@ +package org.jabref.logic.git; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Iterator; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class GitHandlerTest { + @TempDir + Path repositoryPath; + private GitHandler gitHandler; + + @BeforeEach + public void setUpGitHandler() { + gitHandler = new GitHandler(repositoryPath); + } + + @Test + void checkoutNewBranch() throws IOException, GitAPIException { + gitHandler.checkoutBranch("testBranch"); + + try (Git git = Git.open(repositoryPath.toFile())) { + assertEquals("testBranch", git.getRepository().getBranch()); + } + } + + @Test + void createCommitOnCurrentBranch() throws IOException, GitAPIException { + try (Git git = Git.open(repositoryPath.toFile())) { + // Create commit + Files.createFile(Path.of(repositoryPath.toString(), "Test.txt")); + gitHandler.createCommitOnCurrentBranch("TestCommit", false); + + AnyObjectId head = git.getRepository().resolve(Constants.HEAD); + Iterator log = git.log() + .add(head) + .call().iterator(); + assertEquals("TestCommit", log.next().getFullMessage()); + assertEquals("Initial commit", log.next().getFullMessage()); + } + } + + @Test + void getCurrentlyCheckedOutBranch() throws IOException { + assertEquals("master", gitHandler.getCurrentlyCheckedOutBranch()); + } +} diff --git a/src/test/java/org/jabref/logic/git/SlrGitHandlerTest.java b/src/test/java/org/jabref/logic/git/SlrGitHandlerTest.java new file mode 100644 index 00000000000..d18c3374e7c --- /dev/null +++ b/src/test/java/org/jabref/logic/git/SlrGitHandlerTest.java @@ -0,0 +1,83 @@ +package org.jabref.logic.git; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jgit.api.errors.GitAPIException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class SlrGitHandlerTest { + @TempDir + Path repositoryPath; + private SlrGitHandler gitHandler; + + @BeforeEach + public void setUpGitHandler() { + gitHandler = new SlrGitHandler(repositoryPath); + } + + @Test + void calculateDiffOnBranch() throws IOException, GitAPIException { + String expectedPatch = + "diff --git a/TestFolder/Test1.txt b/TestFolder/Test1.txt\n" + + "index 74809e3..2ae1945 100644\n" + + "--- a/TestFolder/Test1.txt\n" + + "+++ b/TestFolder/Test1.txt\n" + + "@@ -1 +1,2 @@\n" + + "+This is a new line of text 2\n" + + " This is a new line of text\n"; + + gitHandler.checkoutBranch("branch1"); + Files.createDirectory(Path.of(repositoryPath.toString(), "TestFolder")); + Files.createFile(Path.of(repositoryPath.toString(), "TestFolder", "Test1.txt")); + Files.writeString(Path.of(repositoryPath.toString(), "TestFolder", "Test1.txt"), "This is a new line of text\n"); + gitHandler.createCommitOnCurrentBranch("Commit 1 on branch1", false); + + Files.createFile(Path.of(repositoryPath.toString(), "Test2.txt")); + Files.writeString(Path.of(repositoryPath.toString(), "TestFolder", "Test1.txt"), "This is a new line of text 2\n" + Files.readString(Path.of(repositoryPath.toString(), "TestFolder", "Test1.txt"))); + gitHandler.createCommitOnCurrentBranch("Commit 2 on branch1", false); + + System.out.println(gitHandler.calculatePatchOfNewSearchResults("branch1")); + assertEquals(expectedPatch, gitHandler.calculatePatchOfNewSearchResults("branch1")); + } + + @Test + void calculatePatch() throws IOException, GitAPIException { + Map expected = new HashMap<>(); + expected.put(Path.of(repositoryPath.toString(), "TestFolder", "Test1.txt"), "This is a new line of text 2"); + + Map result = gitHandler.parsePatchForAddedEntries( + "diff --git a/TestFolder/Test1.txt b/TestFolder/Test1.txt\n" + + "index 74809e3..2ae1945 100644\n" + + "--- a/TestFolder/Test1.txt\n" + + "+++ b/TestFolder/Test1.txt\n" + + "@@ -1 +1,2 @@\n" + + "+This is a new line of text 2\n" + + " This is a new line of text"); + + assertEquals(expected, result); + } + + @Test + void applyPatch() throws IOException, GitAPIException { + gitHandler.checkoutBranch("branch1"); + Files.createFile(Path.of(repositoryPath.toString(), "Test1.txt")); + gitHandler.createCommitOnCurrentBranch("Commit on branch1", false); + gitHandler.checkoutBranch("branch2"); + Files.createFile(Path.of(repositoryPath.toString(), "Test2.txt")); + Files.writeString(Path.of(repositoryPath.toString(), "Test1.txt"), "This is a new line of text"); + gitHandler.createCommitOnCurrentBranch("Commit on branch2.", false); + + gitHandler.checkoutBranch("branch1"); + gitHandler.appendLatestSearchResultsOntoCurrentBranch("TestMessage", "branch2"); + + assertEquals("This is a new line of text", Files.readString(Path.of(repositoryPath.toString(), "Test1.txt"))); + } +} diff --git a/src/test/java/org/jabref/logic/importer/FulltextFetchersTest.java b/src/test/java/org/jabref/logic/importer/FulltextFetchersTest.java index 724c0b52c80..47308002d0b 100644 --- a/src/test/java/org/jabref/logic/importer/FulltextFetchersTest.java +++ b/src/test/java/org/jabref/logic/importer/FulltextFetchersTest.java @@ -8,10 +8,9 @@ import org.jabref.logic.importer.fetcher.TrustLevel; import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; import org.jabref.testutils.category.FetcherTest; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -20,17 +19,8 @@ @FetcherTest public class FulltextFetchersTest { - private BibEntry entry; - @BeforeEach - public void setUp() { - entry = new BibEntry(); - } - - @AfterEach - public void tearDown() { - entry = null; - } + private BibEntry entry = new BibEntry(); @Test public void acceptPdfUrls() throws MalformedURLException { @@ -60,18 +50,21 @@ public void noTrustLevel() throws MalformedURLException { @Test public void higherTrustLevelWins() throws IOException, FetcherException { - final URL lowUrl = new URL("http://docs.oasis-open.org/opencsa/sca-bpel/sca-bpel-1.1-spec-cd-01.pdf"); + FulltextFetcher finderHigh = mock(FulltextFetcher.class); + when(finderHigh.getTrustLevel()).thenReturn(TrustLevel.SOURCE); final URL highUrl = new URL("http://docs.oasis-open.org/wsbpel/2.0/OS/wsbpel-v2.0-OS.pdf"); + when(finderHigh.findFullText(entry)).thenReturn(Optional.of(highUrl)); - FulltextFetcher finderHigh = mock(FulltextFetcher.class); FulltextFetcher finderLow = mock(FulltextFetcher.class); - when(finderHigh.getTrustLevel()).thenReturn(TrustLevel.SOURCE); when(finderLow.getTrustLevel()).thenReturn(TrustLevel.UNKNOWN); - when(finderHigh.findFullText(entry)).thenReturn(Optional.of(highUrl)); + final URL lowUrl = new URL("http://docs.oasis-open.org/opencsa/sca-bpel/sca-bpel-1.1-spec-cd-01.pdf"); when(finderLow.findFullText(entry)).thenReturn(Optional.of(lowUrl)); FulltextFetchers fetcher = new FulltextFetchers(Set.of(finderLow, finderHigh)); + // set an (arbitrary) DOI to the test entry to skip side effects inside the "findFullTextPDF" method + entry.setField(StandardField.DOI, "10.5220/0007903201120130"); + assertEquals(Optional.of(highUrl), fetcher.findFullTextPDF(entry)); } } diff --git a/src/test/java/org/jabref/logic/importer/fetcher/AstrophysicsDataSystemTest.java b/src/test/java/org/jabref/logic/importer/fetcher/AstrophysicsDataSystemTest.java index 7f5c708047f..f7854d13f53 100644 --- a/src/test/java/org/jabref/logic/importer/fetcher/AstrophysicsDataSystemTest.java +++ b/src/test/java/org/jabref/logic/importer/fetcher/AstrophysicsDataSystemTest.java @@ -144,6 +144,7 @@ public void testGetName() { @Test public void searchByQueryFindsEntry() throws Exception { List fetchedEntries = fetcher.performSearch("Diez slice theorem Lie"); + assertFalse(fetchedEntries.isEmpty()); assertTrue(fetchedEntries.contains(diezSliceTheoremEntry)); } @@ -154,8 +155,10 @@ public void searchByEntryFindsEntry() throws Exception { searchEntry.setField(StandardField.AUTHOR, "Diez"); List fetchedEntries = fetcher.performSearch(searchEntry); + + // The list contains more than one element, thus we need to check in two steps and cannot use assertEquals(List.of(diezSliceTheoremEntry, fetchedEntries)) assertFalse(fetchedEntries.isEmpty()); - assertEquals(diezSliceTheoremEntry, fetchedEntries.get(0)); + assertTrue(fetchedEntries.contains(diezSliceTheoremEntry)); } @Test diff --git a/src/test/java/org/jabref/logic/pdf/search/indexing/DocumentReaderTest.java b/src/test/java/org/jabref/logic/pdf/search/indexing/DocumentReaderTest.java new file mode 100644 index 00000000000..d8da1977f29 --- /dev/null +++ b/src/test/java/org/jabref/logic/pdf/search/indexing/DocumentReaderTest.java @@ -0,0 +1,50 @@ +package org.jabref.logic.pdf.search.indexing; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.LinkedFile; +import org.jabref.preferences.FilePreferences; + +import org.apache.lucene.document.Document; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class DocumentReaderTest { + + private BibDatabaseContext databaseContext; + private FilePreferences filePreferences; + + @BeforeEach + public void setup() { + this.databaseContext = mock(BibDatabaseContext.class); + when(databaseContext.getFileDirectories(Mockito.any())).thenReturn(Collections.singletonList(Path.of("src/test/resources/pdfs"))); + this.filePreferences = mock(FilePreferences.class); + when(filePreferences.getUser()).thenReturn("test"); + when(filePreferences.getFileDirectory()).thenReturn(Optional.empty()); + when(filePreferences.shouldStoreFilesRelativeToBib()).thenReturn(true); + } + + @Test + public void unknownFileTestShouldReturnEmptyList() throws IOException { + // given + BibEntry entry = new BibEntry(); + entry.setFiles(Collections.singletonList(new LinkedFile("Wrong path", "NOT_PRESENT.pdf", "Type"))); + + // when + final List emptyDocumentList = new DocumentReader(entry, filePreferences).readLinkedPdfs(databaseContext); + + // then + assertEquals(Collections.emptyList(), emptyDocumentList); + } +} diff --git a/src/test/java/org/jabref/logic/pdf/search/indexing/PdfIndexerTest.java b/src/test/java/org/jabref/logic/pdf/search/indexing/PdfIndexerTest.java new file mode 100644 index 00000000000..9dc45525ac8 --- /dev/null +++ b/src/test/java/org/jabref/logic/pdf/search/indexing/PdfIndexerTest.java @@ -0,0 +1,147 @@ +package org.jabref.logic.pdf.search.indexing; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Optional; + +import org.jabref.logic.util.StandardFileType; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.LinkedFile; +import org.jabref.model.entry.types.StandardEntryType; +import org.jabref.preferences.FilePreferences; + +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.store.NIOFSDirectory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class PdfIndexerTest { + + private PdfIndexer indexer; + private BibDatabase database; + private BibDatabaseContext context = mock(BibDatabaseContext.class); + + @BeforeEach + public void setUp(@TempDir Path indexDir) throws IOException { + FilePreferences filePreferences = mock(FilePreferences.class); + this.database = new BibDatabase(); + + this.context = mock(BibDatabaseContext.class); + when(context.getDatabasePath()).thenReturn(Optional.of(Path.of("src/test/resources/pdfs/"))); + when(context.getFileDirectories(Mockito.any())).thenReturn(Collections.singletonList(Path.of("src/test/resources/pdfs"))); + when(context.getFulltextIndexPath()).thenReturn(indexDir); + when(context.getDatabase()).thenReturn(database); + this.indexer = PdfIndexer.of(context, filePreferences); + } + + @Test + public void exampleThesisIndex() throws IOException { + // given + BibEntry entry = new BibEntry(StandardEntryType.PhdThesis); + entry.setFiles(Collections.singletonList(new LinkedFile("Example Thesis", "thesis-example.pdf", StandardFileType.PDF.getName()))); + database.insertEntry(entry); + + // when + indexer.createIndex(database, context); + + // then + try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { + assertEquals(1, reader.numDocs()); + } + } + + @Test + public void exampleThesisIndexWithKey() throws IOException { + // given + BibEntry entry = new BibEntry(StandardEntryType.PhdThesis); + entry.setCitationKey("Example2017"); + entry.setFiles(Collections.singletonList(new LinkedFile("Example Thesis", "thesis-example.pdf", StandardFileType.PDF.getName()))); + database.insertEntry(entry); + + // when + indexer.createIndex(database, context); + + // then + try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { + assertEquals(1, reader.numDocs()); + } + } + + @Test + public void metaDataIndex() throws IOException { + // given + BibEntry entry = new BibEntry(StandardEntryType.Article); + entry.setFiles(Collections.singletonList(new LinkedFile("Example Thesis", "metaData.pdf", StandardFileType.PDF.getName()))); + + database.insertEntry(entry); + + // when + indexer.createIndex(database, context); + + // then + try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { + assertEquals(1, reader.numDocs()); + } + } + + @Test + public void testFlushIndex() throws IOException { + // given + BibEntry entry = new BibEntry(StandardEntryType.PhdThesis); + entry.setCitationKey("Example2017"); + entry.setFiles(Collections.singletonList(new LinkedFile("Example Thesis", "thesis-example.pdf", StandardFileType.PDF.getName()))); + database.insertEntry(entry); + + indexer.createIndex(database, context); + // index actually exists + try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { + assertEquals(1, reader.numDocs()); + } + + // when + indexer.flushIndex(); + + // then + try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { + assertEquals(0, reader.numDocs()); + } + } + + @Test + public void exampleThesisIndexAppendMetaData() throws IOException { + // given + BibEntry exampleThesis = new BibEntry(StandardEntryType.PhdThesis); + exampleThesis.setCitationKey("ExampleThesis2017"); + exampleThesis.setFiles(Collections.singletonList(new LinkedFile("Example Thesis", "thesis-example.pdf", StandardFileType.PDF.getName()))); + database.insertEntry(exampleThesis); + indexer.createIndex(database, context); + + // index with first entry + try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { + assertEquals(1, reader.numDocs()); + } + + BibEntry metadata = new BibEntry(StandardEntryType.Article); + metadata.setCitationKey("MetaData2017"); + metadata.setFiles(Collections.singletonList(new LinkedFile("Metadata file", "metaData.pdf", StandardFileType.PDF.getName()))); + + // when + indexer.addToIndex(metadata, null); + + // then + try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { + assertEquals(2, reader.numDocs()); + } + } +} + diff --git a/src/test/java/org/jabref/logic/pdf/search/retrieval/PdfSearcherTest.java b/src/test/java/org/jabref/logic/pdf/search/retrieval/PdfSearcherTest.java new file mode 100644 index 00000000000..4be7e5fc2cd --- /dev/null +++ b/src/test/java/org/jabref/logic/pdf/search/retrieval/PdfSearcherTest.java @@ -0,0 +1,106 @@ +package org.jabref.logic.pdf.search.retrieval; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; + +import org.jabref.logic.pdf.search.indexing.PdfIndexer; +import org.jabref.logic.util.StandardFileType; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.LinkedFile; +import org.jabref.model.entry.types.StandardEntryType; +import org.jabref.model.pdf.search.PdfSearchResults; +import org.jabref.preferences.FilePreferences; + +import org.apache.lucene.queryparser.classic.ParseException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class PdfSearcherTest { + + private PdfSearcher search; + + @BeforeEach + public void setUp(@TempDir Path indexDir) throws IOException { + FilePreferences filePreferences = mock(FilePreferences.class); + // given + BibDatabase database = new BibDatabase(); + BibDatabaseContext context = mock(BibDatabaseContext.class); + when(context.getFileDirectories(Mockito.any())).thenReturn(Collections.singletonList(Path.of("src/test/resources/pdfs"))); + when(context.getFulltextIndexPath()).thenReturn(indexDir); + when(context.getDatabase()).thenReturn(database); + BibEntry examplePdf = new BibEntry(StandardEntryType.Article); + examplePdf.setFiles(Collections.singletonList(new LinkedFile("Example Entry", "example.pdf", StandardFileType.PDF.getName()))); + database.insertEntry(examplePdf); + + BibEntry metaDataEntry = new BibEntry(StandardEntryType.Article); + metaDataEntry.setFiles(Collections.singletonList(new LinkedFile("Metadata Entry", "metaData.pdf", StandardFileType.PDF.getName()))); + metaDataEntry.setCitationKey("MetaData2017"); + database.insertEntry(metaDataEntry); + + BibEntry exampleThesis = new BibEntry(StandardEntryType.PhdThesis); + exampleThesis.setFiles(Collections.singletonList(new LinkedFile("Example Thesis", "thesis-example.pdf", StandardFileType.PDF.getName()))); + exampleThesis.setCitationKey("ExampleThesis"); + database.insertEntry(exampleThesis); + + PdfIndexer indexer = PdfIndexer.of(context, filePreferences); + search = PdfSearcher.of(context); + + indexer.createIndex(database, context); + } + + @Test + public void searchForTest() throws IOException, ParseException { + PdfSearchResults result = search.search("test", 10); + assertEquals(2, result.numSearchResults()); + } + + @Test + public void searchForUniversity() throws IOException, ParseException { + PdfSearchResults result = search.search("University", 10); + assertEquals(1, result.numSearchResults()); + } + + @Test + public void searchForStopWord() throws IOException, ParseException { + PdfSearchResults result = search.search("and", 10); + assertEquals(0, result.numSearchResults()); + } + + @Test + public void searchForSecond() throws IOException, ParseException { + PdfSearchResults result = search.search("second", 10); + assertEquals(2, result.numSearchResults()); + } + + @Test + public void searchForAnnotation() throws IOException, ParseException { + PdfSearchResults result = search.search("annotation", 10); + assertEquals(2, result.numSearchResults()); + } + + @Test + public void searchForEmptyString() throws IOException { + PdfSearchResults result = search.search("", 10); + assertEquals(0, result.numSearchResults()); + } + + @Test + public void searchWithNullString() throws IOException { + assertThrows(NullPointerException.class, () -> search.search(null, 10)); + } + + @Test + public void searchForZeroResults() throws IOException { + assertThrows(IllegalArgumentException.class, () -> search.search("test", 0)); + } +} diff --git a/src/test/java/org/jabref/logic/search/DatabaseSearcherTest.java b/src/test/java/org/jabref/logic/search/DatabaseSearcherTest.java index 22b48a39759..f276f9ddfaf 100644 --- a/src/test/java/org/jabref/logic/search/DatabaseSearcherTest.java +++ b/src/test/java/org/jabref/logic/search/DatabaseSearcherTest.java @@ -1,12 +1,14 @@ package org.jabref.logic.search; import java.util.Collections; +import java.util.EnumSet; import java.util.List; 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.jabref.model.search.rules.SearchRules; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -15,7 +17,7 @@ public class DatabaseSearcherTest { - public static final SearchQuery INVALID_SEARCH_QUERY = new SearchQuery("\\asd123{}asdf", true, true); + public static final SearchQuery INVALID_SEARCH_QUERY = new SearchQuery("\\asd123{}asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); private BibDatabase database; @@ -26,7 +28,7 @@ public void setUp() { @Test public void testNoMatchesFromEmptyDatabase() { - List matches = new DatabaseSearcher(new SearchQuery("whatever", true, true), database).getMatches(); + List matches = new DatabaseSearcher(new SearchQuery("whatever", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)), database).getMatches(); assertEquals(Collections.emptyList(), matches); } @@ -39,7 +41,7 @@ public void testNoMatchesFromEmptyDatabaseWithInvalidSearchExpression() { @Test public void testGetDatabaseFromMatchesDatabaseWithEmptyEntries() { database.insertEntry(new BibEntry()); - List matches = new DatabaseSearcher(new SearchQuery("whatever", true, true), database).getMatches(); + List matches = new DatabaseSearcher(new SearchQuery("whatever", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)), database).getMatches(); assertEquals(Collections.emptyList(), matches); } @@ -48,7 +50,7 @@ public void testNoMatchesFromDatabaseWithArticleTypeEntry() { BibEntry entry = new BibEntry(StandardEntryType.Article); entry.setField(StandardField.AUTHOR, "harrer"); database.insertEntry(entry); - List matches = new DatabaseSearcher(new SearchQuery("whatever", true, true), database).getMatches(); + List matches = new DatabaseSearcher(new SearchQuery("whatever", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)), database).getMatches(); assertEquals(Collections.emptyList(), matches); } @@ -57,13 +59,13 @@ public void testCorrectMatchFromDatabaseWithArticleTypeEntry() { BibEntry entry = new BibEntry(StandardEntryType.Article); entry.setField(StandardField.AUTHOR, "harrer"); database.insertEntry(entry); - List matches = new DatabaseSearcher(new SearchQuery("harrer", true, true), database).getMatches(); + List matches = new DatabaseSearcher(new SearchQuery("harrer", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)), database).getMatches(); assertEquals(Collections.singletonList(entry), matches); } @Test public void testNoMatchesFromEmptyDatabaseWithInvalidQuery() { - SearchQuery query = new SearchQuery("asdf[", true, true); + SearchQuery query = new SearchQuery("asdf[", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); DatabaseSearcher databaseSearcher = new DatabaseSearcher(query, database); @@ -76,7 +78,7 @@ public void testCorrectMatchFromDatabaseWithIncollectionTypeEntry() { entry.setField(StandardField.AUTHOR, "tonho"); database.insertEntry(entry); - SearchQuery query = new SearchQuery("tonho", true, true); + SearchQuery query = new SearchQuery("tonho", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); List matches = new DatabaseSearcher(query, database).getMatches(); assertEquals(Collections.singletonList(entry), matches); @@ -91,7 +93,7 @@ public void testNoMatchesFromDatabaseWithTwoEntries() { entry.setField(StandardField.AUTHOR, "tonho"); database.insertEntry(entry); - SearchQuery query = new SearchQuery("tonho", true, true); + SearchQuery query = new SearchQuery("tonho", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); DatabaseSearcher databaseSearcher = new DatabaseSearcher(query, database); assertEquals(Collections.singletonList(entry), databaseSearcher.getMatches()); @@ -103,7 +105,7 @@ public void testNoMatchesFromDabaseWithIncollectionTypeEntry() { entry.setField(StandardField.AUTHOR, "tonho"); database.insertEntry(entry); - SearchQuery query = new SearchQuery("asdf", true, true); + SearchQuery query = new SearchQuery("asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); DatabaseSearcher databaseSearcher = new DatabaseSearcher(query, database); assertEquals(Collections.emptyList(), databaseSearcher.getMatches()); @@ -114,7 +116,7 @@ public void testNoMatchFromDatabaseWithEmptyEntry() { BibEntry entry = new BibEntry(); database.insertEntry(entry); - SearchQuery query = new SearchQuery("tonho", true, true); + SearchQuery query = new SearchQuery("tonho", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); DatabaseSearcher databaseSearcher = new DatabaseSearcher(query, database); assertEquals(Collections.emptyList(), databaseSearcher.getMatches()); diff --git a/src/test/java/org/jabref/logic/search/SearchQueryTest.java b/src/test/java/org/jabref/logic/search/SearchQueryTest.java index 2be23fd98f4..4bf01fadb73 100644 --- a/src/test/java/org/jabref/logic/search/SearchQueryTest.java +++ b/src/test/java/org/jabref/logic/search/SearchQueryTest.java @@ -1,11 +1,14 @@ package org.jabref.logic.search; +import java.util.EnumSet; import java.util.Optional; import java.util.regex.Pattern; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.types.StandardEntryType; +import org.jabref.model.search.rules.SearchRules; +import org.jabref.model.search.rules.SearchRules.SearchFlags; import org.junit.jupiter.api.Test; @@ -17,29 +20,29 @@ public class SearchQueryTest { @Test public void testToString() { - assertEquals("\"asdf\" (case sensitive, regular expression)", new SearchQuery("asdf", true, true).toString()); - assertEquals("\"asdf\" (case insensitive, plain text)", new SearchQuery("asdf", false, false).toString()); + assertEquals("\"asdf\" (case sensitive, regular expression)", new SearchQuery("asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).toString()); + assertEquals("\"asdf\" (case insensitive, plain text)", new SearchQuery("asdf", EnumSet.noneOf(SearchFlags.class)).toString()); } @Test public void testIsContainsBasedSearch() { - assertTrue(new SearchQuery("asdf", true, false).isContainsBasedSearch()); - assertTrue(new SearchQuery("asdf", true, true).isContainsBasedSearch()); - assertFalse(new SearchQuery("author=asdf", true, false).isContainsBasedSearch()); + assertTrue(new SearchQuery("asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)).isContainsBasedSearch()); + assertTrue(new SearchQuery("asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).isContainsBasedSearch()); + assertFalse(new SearchQuery("author=asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)).isContainsBasedSearch()); } @Test public void testIsGrammarBasedSearch() { - assertFalse(new SearchQuery("asdf", true, false).isGrammarBasedSearch()); - assertFalse(new SearchQuery("asdf", true, true).isGrammarBasedSearch()); - assertTrue(new SearchQuery("author=asdf", true, false).isGrammarBasedSearch()); + assertFalse(new SearchQuery("asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)).isGrammarBasedSearch()); + assertFalse(new SearchQuery("asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).isGrammarBasedSearch()); + assertTrue(new SearchQuery("author=asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)).isGrammarBasedSearch()); } @Test public void testGrammarSearch() { BibEntry entry = new BibEntry(); entry.addKeyword("one two", ','); - SearchQuery searchQuery = new SearchQuery("keywords=\"one two\"", false, false); + SearchQuery searchQuery = new SearchQuery("keywords=\"one two\"", EnumSet.noneOf(SearchFlags.class)); assertTrue(searchQuery.isMatch(entry)); } @@ -47,7 +50,7 @@ public void testGrammarSearch() { public void testGrammarSearchFullEntryLastCharMissing() { BibEntry entry = new BibEntry(); entry.setField(StandardField.TITLE, "systematic revie"); - SearchQuery searchQuery = new SearchQuery("title=\"systematic review\"", false, false); + SearchQuery searchQuery = new SearchQuery("title=\"systematic review\"", EnumSet.noneOf(SearchFlags.class)); assertFalse(searchQuery.isMatch(entry)); } @@ -55,7 +58,7 @@ public void testGrammarSearchFullEntryLastCharMissing() { public void testGrammarSearchFullEntry() { BibEntry entry = new BibEntry(); entry.setField(StandardField.TITLE, "systematic review"); - SearchQuery searchQuery = new SearchQuery("title=\"systematic review\"", false, false); + SearchQuery searchQuery = new SearchQuery("title=\"systematic review\"", EnumSet.noneOf(SearchFlags.class)); assertTrue(searchQuery.isMatch(entry)); } @@ -64,7 +67,7 @@ public void testSearchingForOpenBraketInBooktitle() { BibEntry e = new BibEntry(StandardEntryType.InProceedings); e.setField(StandardField.BOOKTITLE, "Super Conference (SC)"); - SearchQuery searchQuery = new SearchQuery("booktitle=\"(\"", false, false); + SearchQuery searchQuery = new SearchQuery("booktitle=\"(\"", EnumSet.noneOf(SearchFlags.class)); assertTrue(searchQuery.isMatch(e)); } @@ -73,7 +76,7 @@ public void testSearchMatchesSingleKeywordNotPart() { BibEntry e = new BibEntry(StandardEntryType.InProceedings); e.setField(StandardField.KEYWORDS, "banana, pineapple, orange"); - SearchQuery searchQuery = new SearchQuery("anykeyword==apple", false, false); + SearchQuery searchQuery = new SearchQuery("anykeyword==apple", EnumSet.noneOf(SearchFlags.class)); assertFalse(searchQuery.isMatch(e)); } @@ -82,7 +85,7 @@ public void testSearchMatchesSingleKeyword() { BibEntry e = new BibEntry(StandardEntryType.InProceedings); e.setField(StandardField.KEYWORDS, "banana, pineapple, orange"); - SearchQuery searchQuery = new SearchQuery("anykeyword==pineapple", false, false); + SearchQuery searchQuery = new SearchQuery("anykeyword==pineapple", EnumSet.noneOf(SearchFlags.class)); assertTrue(searchQuery.isMatch(e)); } @@ -92,7 +95,7 @@ public void testSearchAllFields() { e.setField(StandardField.TITLE, "Fruity features"); e.setField(StandardField.KEYWORDS, "banana, pineapple, orange"); - SearchQuery searchQuery = new SearchQuery("anyfield==\"fruity features\"", false, false); + SearchQuery searchQuery = new SearchQuery("anyfield==\"fruity features\"", EnumSet.noneOf(SearchFlags.class)); assertTrue(searchQuery.isMatch(e)); } @@ -102,7 +105,7 @@ public void testSearchAllFieldsNotForSpecificField() { e.setField(StandardField.TITLE, "Fruity features"); e.setField(StandardField.KEYWORDS, "banana, pineapple, orange"); - SearchQuery searchQuery = new SearchQuery("anyfield=fruit and keywords!=banana", false, false); + SearchQuery searchQuery = new SearchQuery("anyfield=fruit and keywords!=banana", EnumSet.noneOf(SearchFlags.class)); assertFalse(searchQuery.isMatch(e)); } @@ -112,7 +115,7 @@ public void testSearchAllFieldsAndSpecificField() { e.setField(StandardField.TITLE, "Fruity features"); e.setField(StandardField.KEYWORDS, "banana, pineapple, orange"); - SearchQuery searchQuery = new SearchQuery("anyfield=fruit and keywords=apple", false, false); + SearchQuery searchQuery = new SearchQuery("anyfield=fruit and keywords=apple", EnumSet.noneOf(SearchFlags.class)); assertTrue(searchQuery.isMatch(e)); } @@ -122,59 +125,59 @@ public void testIsMatch() { entry.setType(StandardEntryType.Article); entry.setField(StandardField.AUTHOR, "asdf"); - assertFalse(new SearchQuery("BiblatexEntryType", true, true).isMatch(entry)); - assertTrue(new SearchQuery("asdf", true, true).isMatch(entry)); - assertTrue(new SearchQuery("author=asdf", true, true).isMatch(entry)); + assertFalse(new SearchQuery("BiblatexEntryType", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).isMatch(entry)); + assertTrue(new SearchQuery("asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).isMatch(entry)); + assertTrue(new SearchQuery("author=asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).isMatch(entry)); } @Test public void testIsValidQueryNotAsRegEx() { - assertTrue(new SearchQuery("asdf", true, false).isValid()); + assertTrue(new SearchQuery("asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)).isValid()); } @Test public void testIsValidQueryContainsBracketNotAsRegEx() { - assertTrue(new SearchQuery("asdf[", true, false).isValid()); + assertTrue(new SearchQuery("asdf[", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)).isValid()); } @Test public void testIsNotValidQueryContainsBracketNotAsRegEx() { - assertTrue(new SearchQuery("asdf[", true, true).isValid()); + assertTrue(new SearchQuery("asdf[", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).isValid()); } @Test public void testIsValidQueryAsRegEx() { - assertTrue(new SearchQuery("asdf", true, true).isValid()); + assertTrue(new SearchQuery("asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).isValid()); } @Test public void testIsValidQueryWithNumbersAsRegEx() { - assertTrue(new SearchQuery("123", true, true).isValid()); + assertTrue(new SearchQuery("123", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).isValid()); } @Test public void testIsValidQueryContainsBracketAsRegEx() { - assertTrue(new SearchQuery("asdf[", true, true).isValid()); + assertTrue(new SearchQuery("asdf[", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).isValid()); } @Test public void testIsValidQueryWithEqualSignAsRegEx() { - assertTrue(new SearchQuery("author=asdf", true, true).isValid()); + assertTrue(new SearchQuery("author=asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).isValid()); } @Test public void testIsValidQueryWithNumbersAndEqualSignAsRegEx() { - assertTrue(new SearchQuery("author=123", true, true).isValid()); + assertTrue(new SearchQuery("author=123", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).isValid()); } @Test public void testIsValidQueryWithEqualSignNotAsRegEx() { - assertTrue(new SearchQuery("author=asdf", true, false).isValid()); + assertTrue(new SearchQuery("author=asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)).isValid()); } @Test public void testIsValidQueryWithNumbersAndEqualSignNotAsRegEx() { - assertTrue(new SearchQuery("author=123", true, false).isValid()); + assertTrue(new SearchQuery("author=123", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)).isValid()); } @Test @@ -184,21 +187,21 @@ public void isMatchedForNormalAndFieldBasedSearchMixed() { entry.setField(StandardField.AUTHOR, "asdf"); entry.setField(StandardField.ABSTRACT, "text"); - assertTrue(new SearchQuery("text AND author=asdf", true, true).isMatch(entry)); + assertTrue(new SearchQuery("text AND author=asdf", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)).isMatch(entry)); } @Test public void testSimpleTerm() { String query = "progress"; - SearchQuery result = new SearchQuery(query, false, false); + SearchQuery result = new SearchQuery(query, EnumSet.noneOf(SearchFlags.class)); assertFalse(result.isGrammarBasedSearch()); } @Test public void testGetPattern() { String query = "progress"; - SearchQuery result = new SearchQuery(query, false, false); + SearchQuery result = new SearchQuery(query, EnumSet.noneOf(SearchFlags.class)); Pattern pattern = Pattern.compile("(\\Qprogress\\E)"); // We can't directly compare the pattern objects assertEquals(Optional.of(pattern.toString()), result.getPatternForWords().map(Pattern::toString)); @@ -207,7 +210,7 @@ public void testGetPattern() { @Test public void testGetRegexpPattern() { String queryText = "[a-c]\\d* \\d*"; - SearchQuery regexQuery = new SearchQuery(queryText, false, true); + SearchQuery regexQuery = new SearchQuery(queryText, EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); Pattern pattern = Pattern.compile("([a-c]\\d* \\d*)"); assertEquals(Optional.of(pattern.toString()), regexQuery.getPatternForWords().map(Pattern::toString)); } @@ -215,7 +218,7 @@ public void testGetRegexpPattern() { @Test public void testGetRegexpJavascriptPattern() { String queryText = "[a-c]\\d* \\d*"; - SearchQuery regexQuery = new SearchQuery(queryText, false, true); + SearchQuery regexQuery = new SearchQuery(queryText, EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); Pattern pattern = Pattern.compile("([a-c]\\d* \\d*)"); assertEquals(Optional.of(pattern.toString()), regexQuery.getJavaScriptPatternForWords().map(Pattern::toString)); } @@ -224,7 +227,7 @@ public void testGetRegexpJavascriptPattern() { public void testEscapingInPattern() { // first word contain all java special regex characters String queryText = "<([{\\\\^-=$!|]})?*+.> word1 word2."; - SearchQuery textQueryWithSpecialChars = new SearchQuery(queryText, false, false); + SearchQuery textQueryWithSpecialChars = new SearchQuery(queryText, EnumSet.noneOf(SearchFlags.class)); String pattern = "(\\Q<([{\\^-=$!|]})?*+.>\\E)|(\\Qword1\\E)|(\\Qword2.\\E)"; assertEquals(Optional.of(pattern), textQueryWithSpecialChars.getPatternForWords().map(Pattern::toString)); } @@ -233,7 +236,7 @@ public void testEscapingInPattern() { public void testEscapingInJavascriptPattern() { // first word contain all javascript special regex characters that should be escaped individually in text based search String queryText = "([{\\\\^$|]})?*+./ word1 word2."; - SearchQuery textQueryWithSpecialChars = new SearchQuery(queryText, false, false); + SearchQuery textQueryWithSpecialChars = new SearchQuery(queryText, EnumSet.noneOf(SearchFlags.class)); String pattern = "(\\(\\[\\{\\\\\\^\\$\\|\\]\\}\\)\\?\\*\\+\\.\\/)|(word1)|(word2\\.)"; assertEquals(Optional.of(pattern), textQueryWithSpecialChars.getJavaScriptPatternForWords().map(Pattern::toString)); } diff --git a/src/test/java/org/jabref/logic/util/VersionTest.java b/src/test/java/org/jabref/logic/util/VersionTest.java index f0b12ad01be..438225fa712 100644 --- a/src/test/java/org/jabref/logic/util/VersionTest.java +++ b/src/test/java/org/jabref/logic/util/VersionTest.java @@ -1,13 +1,18 @@ package org.jabref.logic.util; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Optional; +import org.jabref.support.DisabledOnCIServer; +import org.jabref.testutils.category.FetcherTest; + import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; public class VersionTest { @@ -294,4 +299,11 @@ public void ciSuffixShouldBeRemovedIfDateIsPresent() { Version v50ci = Version.parse("5.0-ci.1--2020-03-06--289142f"); assertEquals("5.0--2020-03-06--289142f", v50ci.getFullVersion()); } + + @Test + @FetcherTest + @DisabledOnCIServer("GitHub puts a low rate limit on unauthenticated calls") + public void getAllAvailableVersionsReturnsSomething() throws Exception { + assertNotEquals(Collections.emptyList(), Version.getAllAvailableVersions()); + } } diff --git a/src/test/java/org/jabref/model/groups/GroupTreeNodeTest.java b/src/test/java/org/jabref/model/groups/GroupTreeNodeTest.java index faa4d9d3d56..8878219ecda 100644 --- a/src/test/java/org/jabref/model/groups/GroupTreeNodeTest.java +++ b/src/test/java/org/jabref/model/groups/GroupTreeNodeTest.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.EnumSet; import java.util.List; import java.util.Optional; @@ -11,6 +12,8 @@ import org.jabref.model.entry.field.StandardField; import org.jabref.model.search.matchers.AndMatcher; import org.jabref.model.search.matchers.OrMatcher; +import org.jabref.model.search.rules.SearchRules; +import org.jabref.model.search.rules.SearchRules.SearchFlags; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -81,7 +84,7 @@ private static AbstractGroup getKeywordGroup(String name) { } private static AbstractGroup getSearchGroup(String name) { - return new SearchGroup(name, GroupHierarchyType.INCLUDING, "searchExpression", true, false); + return new SearchGroup(name, GroupHierarchyType.INCLUDING, "searchExpression", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)); } private static AbstractGroup getExplict(String name) { @@ -253,7 +256,7 @@ void setGroupExplicitToSearchDoesNotKeepPreviousAssignments() { ExplicitGroup oldGroup = new ExplicitGroup("OldGroup", GroupHierarchyType.INDEPENDENT, ','); oldGroup.add(entry); GroupTreeNode node = GroupTreeNode.fromGroup(oldGroup); - AbstractGroup newGroup = new SearchGroup("NewGroup", GroupHierarchyType.INDEPENDENT, "test", false, false); + AbstractGroup newGroup = new SearchGroup("NewGroup", GroupHierarchyType.INDEPENDENT, "test", EnumSet.noneOf(SearchFlags.class)); node.setGroup(newGroup, true, true, entries); @@ -331,7 +334,7 @@ void onlySubgroupsContainAllEntries() { @Test void addEntriesToGroupWorksNotForGroupsNotSupportingExplicitAddingOfEntries() { - GroupTreeNode searchGroup = new GroupTreeNode(new SearchGroup("Search A", GroupHierarchyType.INCLUDING, "searchExpression", true, false)); + GroupTreeNode searchGroup = new GroupTreeNode(new SearchGroup("Search A", GroupHierarchyType.INCLUDING, "searchExpression", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE))); List fieldChanges = searchGroup.addEntriesToGroup(entries); assertEquals(Collections.emptyList(), fieldChanges); @@ -339,7 +342,7 @@ void addEntriesToGroupWorksNotForGroupsNotSupportingExplicitAddingOfEntries() { @Test void removeEntriesFromGroupWorksNotForGroupsNotSupportingExplicitRemovalOfEntries() { - GroupTreeNode searchGroup = new GroupTreeNode(new SearchGroup("Search A", GroupHierarchyType.INCLUDING, "searchExpression", true, false)); + GroupTreeNode searchGroup = new GroupTreeNode(new SearchGroup("Search A", GroupHierarchyType.INCLUDING, "searchExpression", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE))); List fieldChanges = searchGroup.removeEntriesFromGroup(entries); assertEquals(Collections.emptyList(), fieldChanges); diff --git a/src/test/java/org/jabref/model/groups/SearchGroupTest.java b/src/test/java/org/jabref/model/groups/SearchGroupTest.java index bdc24d22b56..9b499ccf6ee 100644 --- a/src/test/java/org/jabref/model/groups/SearchGroupTest.java +++ b/src/test/java/org/jabref/model/groups/SearchGroupTest.java @@ -1,6 +1,9 @@ package org.jabref.model.groups; +import java.util.EnumSet; + import org.jabref.model.entry.BibEntry; +import org.jabref.model.search.rules.SearchRules; import org.junit.jupiter.api.Test; @@ -10,7 +13,7 @@ public class SearchGroupTest { @Test public void containsFindsWordWithRegularExpression() { - SearchGroup group = new SearchGroup("myExplicitGroup", GroupHierarchyType.INDEPENDENT, "anyfield=rev*", true, true); + SearchGroup group = new SearchGroup("myExplicitGroup", GroupHierarchyType.INDEPENDENT, "anyfield=rev*", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); BibEntry entry = new BibEntry(); entry.addKeyword("review", ','); diff --git a/src/test/java/org/jabref/model/search/rules/ContainBasedSearchRuleTest.java b/src/test/java/org/jabref/model/search/rules/ContainBasedSearchRuleTest.java index ad7b0f645ad..337263b0dbc 100644 --- a/src/test/java/org/jabref/model/search/rules/ContainBasedSearchRuleTest.java +++ b/src/test/java/org/jabref/model/search/rules/ContainBasedSearchRuleTest.java @@ -1,5 +1,7 @@ package org.jabref.model.search.rules; +import java.util.EnumSet; + import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.types.StandardEntryType; @@ -17,10 +19,10 @@ public class ContainBasedSearchRuleTest { @Test public void testBasicSearchParsing() { BibEntry be = makeBibtexEntry(); - ContainBasedSearchRule bsCaseSensitive = new ContainBasedSearchRule(true); - ContainBasedSearchRule bsCaseInsensitive = new ContainBasedSearchRule(false); - RegexBasedSearchRule bsCaseSensitiveRegexp = new RegexBasedSearchRule(true); - RegexBasedSearchRule bsCaseInsensitiveRegexp = new RegexBasedSearchRule(false); + ContainBasedSearchRule bsCaseSensitive = new ContainBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); + ContainBasedSearchRule bsCaseInsensitive = new ContainBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); + RegexBasedSearchRule bsCaseSensitiveRegexp = new RegexBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); + RegexBasedSearchRule bsCaseInsensitiveRegexp = new RegexBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); String query = "marine 2001 shields"; diff --git a/src/test/java/org/jabref/model/search/rules/GrammarBasedSearchRuleTest.java b/src/test/java/org/jabref/model/search/rules/GrammarBasedSearchRuleTest.java index 28f045b07fb..a32394078ab 100644 --- a/src/test/java/org/jabref/model/search/rules/GrammarBasedSearchRuleTest.java +++ b/src/test/java/org/jabref/model/search/rules/GrammarBasedSearchRuleTest.java @@ -1,5 +1,7 @@ package org.jabref.model.search.rules; +import java.util.EnumSet; + import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.types.StandardEntryType; @@ -16,7 +18,7 @@ public class GrammarBasedSearchRuleTest { @Test void applyRuleMatchesSingleTermWithRegex() { - GrammarBasedSearchRule searchRule = new GrammarBasedSearchRule(true, true); + GrammarBasedSearchRule searchRule = new GrammarBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); String query = "M[a-z]+e"; assertTrue(searchRule.validateSearchStrings(query)); @@ -25,7 +27,7 @@ void applyRuleMatchesSingleTermWithRegex() { @Test void applyRuleDoesNotMatchSingleTermWithRegex() { - GrammarBasedSearchRule searchRule = new GrammarBasedSearchRule(true, true); + GrammarBasedSearchRule searchRule = new GrammarBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); String query = "M[0-9]+e"; assertTrue(searchRule.validateSearchStrings(query)); diff --git a/src/test/resources/.gitignore b/src/test/resources/.gitignore new file mode 100644 index 00000000000..aad78953eb0 --- /dev/null +++ b/src/test/resources/.gitignore @@ -0,0 +1 @@ +luceneTestIndex diff --git a/src/test/resources/org/jabref/logic/importer/fileformat/MsBibImporterTestTranslator.xml b/src/test/resources/org/jabref/logic/importer/fileformat/MsBibImporterTestTranslator.xml index 69b03d0a6dc..65d00f3f7f6 100644 --- a/src/test/resources/org/jabref/logic/importer/fileformat/MsBibImporterTestTranslator.xml +++ b/src/test/resources/org/jabref/logic/importer/fileformat/MsBibImporterTestTranslator.xml @@ -1,30 +1,31 @@ - - - Nac16 - Misc - {BD524449-102F-470B-951F-CFE852BA526D} - MeinArtikel - 2016 - 17 - MeineZeitung - - - Nachname, Vorname MIddleName; Nachname2, Vorname2 MiddleName21 - - - - - TestÜbersetzer - - - - - 1 - 07 - 1 - 2018 - 07 - 1 - + + + Nac16 + Misc + {BD524449-102F-470B-951F-CFE852BA526D} + MeinArtikel + 2016 + 17 + MeineZeitung + + + Nachname, Vorname MIddleName; Nachname2, Vorname2 MiddleName21 + + + + + TestÜbersetzer + + + + + 1 + 07 + 1 + 2018 + 07 + 1 + diff --git a/src/test/resources/pdfs/example.pdf b/src/test/resources/pdfs/example.pdf new file mode 100644 index 00000000000..19ae584cebd Binary files /dev/null and b/src/test/resources/pdfs/example.pdf differ diff --git a/src/test/resources/pdfs/metaData.pdf b/src/test/resources/pdfs/metaData.pdf new file mode 100644 index 00000000000..b7ed86bdf9c Binary files /dev/null and b/src/test/resources/pdfs/metaData.pdf differ