From b7990ea3e80eae4ab7cc8cf8d508fea5d2e0ebe8 Mon Sep 17 00:00:00 2001 From: Christoph Date: Wed, 3 Jan 2024 21:39:12 +0100 Subject: [PATCH 01/25] Use custom DOI now in template exporter (#10745) Fixes https://discourse.jabref.org/t/export-html-disregards-custom-doi-base-uri/4084 Fixes https://github.com/JabRef/jabref-issue-melting-pot/issues/341 --- CHANGELOG.md | 2 ++ .../logic/importer/fetcher/DoiResolution.java | 2 +- .../org/jabref/logic/layout/LayoutEntry.java | 2 +- .../layout/LayoutFormatterPreferences.java | 9 ++++++++ .../jabref/logic/layout/format/DOICheck.java | 21 ++++++++++++++++++- .../jabref/model/entry/identifier/DOI.java | 2 +- .../jabref/preferences/JabRefPreferences.java | 1 + .../logic/layout/format/DOICheckTest.java | 14 ++++++++++++- 8 files changed, 48 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 678f8dbf011..ede9ae9a686 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Changed +- The Custom export format now uses the custom DOI base URI in the preferences for the `DOICheck`, if activated [forum#4084](https://discourse.jabref.org/t/export-html-disregards-custom-doi-base-uri/4084) + ### Fixed - We fixed an issue where attempting to cancel the importing/generation of an entry from id is ignored. [#10508](https://github.com/JabRef/jabref/issues/10508) diff --git a/src/main/java/org/jabref/logic/importer/fetcher/DoiResolution.java b/src/main/java/org/jabref/logic/importer/fetcher/DoiResolution.java index db14fad1dc3..b7a63e3961f 100644 --- a/src/main/java/org/jabref/logic/importer/fetcher/DoiResolution.java +++ b/src/main/java/org/jabref/logic/importer/fetcher/DoiResolution.java @@ -40,7 +40,7 @@ public class DoiResolution implements FulltextFetcher { private static final Logger LOGGER = LoggerFactory.getLogger(DoiResolution.class); - private DOIPreferences doiPreferences; + private final DOIPreferences doiPreferences; public DoiResolution(DOIPreferences doiPreferences) { super(); diff --git a/src/main/java/org/jabref/logic/layout/LayoutEntry.java b/src/main/java/org/jabref/logic/layout/LayoutEntry.java index b739393a982..cf4fd8e1c7f 100644 --- a/src/main/java/org/jabref/logic/layout/LayoutEntry.java +++ b/src/main/java/org/jabref/logic/layout/LayoutEntry.java @@ -443,7 +443,7 @@ private LayoutFormatter getLayoutFormatterByName(String name) { case "CreateDocBook5Editors" -> new CreateDocBook5Editors(); case "CurrentDate" -> new CurrentDate(); case "DateFormatter" -> new DateFormatter(); - case "DOICheck" -> new DOICheck(); + case "DOICheck" -> new DOICheck(preferences.getDoiPreferences()); case "DOIStrip" -> new DOIStrip(); case "EntryTypeFormatter" -> new EntryTypeFormatter(); case "FirstPage" -> new FirstPage(); diff --git a/src/main/java/org/jabref/logic/layout/LayoutFormatterPreferences.java b/src/main/java/org/jabref/logic/layout/LayoutFormatterPreferences.java index b2177cbd543..8aecd278608 100644 --- a/src/main/java/org/jabref/logic/layout/LayoutFormatterPreferences.java +++ b/src/main/java/org/jabref/logic/layout/LayoutFormatterPreferences.java @@ -7,17 +7,22 @@ import javafx.beans.property.StringProperty; import org.jabref.logic.layout.format.NameFormatterPreferences; +import org.jabref.logic.preferences.DOIPreferences; public class LayoutFormatterPreferences { private final NameFormatterPreferences nameFormatterPreferences; + + private final DOIPreferences doiPreferences; private final StringProperty mainFileDirectoryProperty; private final Map customExportNameFormatters = new HashMap<>(); public LayoutFormatterPreferences(NameFormatterPreferences nameFormatterPreferences, + DOIPreferences doiPreferences, StringProperty mainFileDirectoryProperty) { this.nameFormatterPreferences = nameFormatterPreferences; this.mainFileDirectoryProperty = mainFileDirectoryProperty; + this.doiPreferences = doiPreferences; } public NameFormatterPreferences getNameFormatterPreferences() { @@ -39,4 +44,8 @@ public void clearCustomExportNameFormatters() { public void putCustomExportNameFormatter(String formatterName, String contents) { customExportNameFormatters.put(formatterName, contents); } + + public DOIPreferences getDoiPreferences() { + return doiPreferences; + } } diff --git a/src/main/java/org/jabref/logic/layout/format/DOICheck.java b/src/main/java/org/jabref/logic/layout/format/DOICheck.java index 0ae0483c698..ed9f2a6fd7b 100644 --- a/src/main/java/org/jabref/logic/layout/format/DOICheck.java +++ b/src/main/java/org/jabref/logic/layout/format/DOICheck.java @@ -1,14 +1,25 @@ package org.jabref.logic.layout.format; +import java.net.URI; + import org.jabref.logic.layout.LayoutFormatter; +import org.jabref.logic.preferences.DOIPreferences; import org.jabref.model.entry.identifier.DOI; /** * Used to fix [ 1588028 ] export HTML table DOI URL. *

- * Will prepend "http://doi.org/" if only DOI and not an URL is given. + * Will prepend "http://doi.org/" or the DOI url with a custom base URL defined in the {@link DOIPreferences} + * if only DOI and not an URL is given. */ public class DOICheck implements LayoutFormatter { + + private final DOIPreferences doiPreferences; + + public DOICheck(DOIPreferences doiPreferences) { + this.doiPreferences = doiPreferences; + } + @Override public String format(String fieldText) { if (fieldText == null) { @@ -18,6 +29,14 @@ public String format(String fieldText) { if (result.startsWith("/")) { result = result.substring(1); } + + if (doiPreferences.isUseCustom()) { + var base = URI.create(doiPreferences.getDefaultBaseURI()); + return DOI.parse(result).flatMap(doi -> doi.getExternalURIFromBase(base)) + .map(URI::toASCIIString) + .orElse(result); + } + return DOI.parse(result).map(DOI::getURIAsASCIIString).orElse(result); } } diff --git a/src/main/java/org/jabref/model/entry/identifier/DOI.java b/src/main/java/org/jabref/model/entry/identifier/DOI.java index b2fc250b739..aa7d2d2e66e 100644 --- a/src/main/java/org/jabref/model/entry/identifier/DOI.java +++ b/src/main/java/org/jabref/model/entry/identifier/DOI.java @@ -262,7 +262,7 @@ public Optional getExternalURIWithCustomBase(String customBase) { return getExternalURIFromBase(URI.create(customBase)); } - private Optional getExternalURIFromBase(URI base) { + public Optional getExternalURIFromBase(URI base) { try { URI uri = new URI(base.getScheme(), base.getHost(), "/" + doi, null); return Optional.of(uri); diff --git a/src/main/java/org/jabref/preferences/JabRefPreferences.java b/src/main/java/org/jabref/preferences/JabRefPreferences.java index 90c6bd7b87f..65b320e5971 100644 --- a/src/main/java/org/jabref/preferences/JabRefPreferences.java +++ b/src/main/java/org/jabref/preferences/JabRefPreferences.java @@ -1178,6 +1178,7 @@ public void importPreferences(Path file) throws JabRefException { public LayoutFormatterPreferences getLayoutFormatterPreferences() { return new LayoutFormatterPreferences( getNameFormatterPreferences(), + getDOIPreferences(), getFilePreferences().mainFileDirectoryProperty()); } diff --git a/src/test/java/org/jabref/logic/layout/format/DOICheckTest.java b/src/test/java/org/jabref/logic/layout/format/DOICheckTest.java index 2c7fb18598c..f1252c770ea 100644 --- a/src/test/java/org/jabref/logic/layout/format/DOICheckTest.java +++ b/src/test/java/org/jabref/logic/layout/format/DOICheckTest.java @@ -3,16 +3,21 @@ import java.util.stream.Stream; import org.jabref.logic.layout.LayoutFormatter; +import org.jabref.logic.preferences.DOIPreferences; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class DOICheckTest { - private LayoutFormatter layoutFormatter = new DOICheck(); + private final DOIPreferences doiPreferences = mock(DOIPreferences.class); + private final LayoutFormatter layoutFormatter = new DOICheck(doiPreferences); @ParameterizedTest @MethodSource("provideDOI") @@ -20,6 +25,13 @@ void formatDOI(String formattedDOI, String originalDOI) { assertEquals(formattedDOI, layoutFormatter.format(originalDOI)); } + @Test + void formatDOIWithCustomBase() { + when(doiPreferences.isUseCustom()).thenReturn(true); + when(doiPreferences.getDefaultBaseURI()).thenReturn("http://example.org"); + assertEquals("http://example.org/10.1000/ISBN1-900512-44-0", layoutFormatter.format("10.1000/ISBN1-900512-44-0")); + } + private static Stream provideDOI() { return Stream.of( Arguments.of("", ""), From b797c5546ec7288b27f9e0e9cc6780cdb50aef75 Mon Sep 17 00:00:00 2001 From: Siedlerchr Date: Wed, 3 Jan 2024 21:46:32 +0100 Subject: [PATCH 02/25] downgrade javafx to check if it works --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 51b2b754f80..96cda73c2fd 100644 --- a/build.gradle +++ b/build.gradle @@ -101,7 +101,7 @@ dependencyLocking { } javafx { - version = "21" + version = "20" modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.web', 'javafx.swing' ] } From 5b755ebe6d9bb872c06acfa78befb5a012a23696 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Sun, 7 Jan 2024 09:32:13 +0100 Subject: [PATCH 03/25] Refine howto for copying --- .../intellij-89-run-with-intellij.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/getting-into-the-code/guidelines-for-setting-up-a-local-workspace/intellij-89-run-with-intellij.md b/docs/getting-into-the-code/guidelines-for-setting-up-a-local-workspace/intellij-89-run-with-intellij.md index 17ec4d6ea85..ab626193e43 100644 --- a/docs/getting-into-the-code/guidelines-for-setting-up-a-local-workspace/intellij-89-run-with-intellij.md +++ b/docs/getting-into-the-code/guidelines-for-setting-up-a-local-workspace/intellij-89-run-with-intellij.md @@ -18,10 +18,12 @@ Due to [IDEA-119280](https://youtrack.jetbrains.com/issue/IDEA-119280), it is a 6. Open the project view (Alt+1 , on mac cmd>+1) 7. Copy all build resources to the folder of the build classes 1. Navigate to the folder `build/resoruces/main` - 2. Select all folders below (`bst`, `csl-locales`, ...) - 3. Press Ctrl+C to mark them for copying - 4. Select the folder `out/production/classes` - 5. Press Ctrl+V to start the copy process + 1. Right click -> "Open In" -> "Explorer" + 1. Navigate into directory "main" + 1. Select the folder `out/production/classes` + 1. Right click -> "Open In" -> "Explorer" + 1. Navigate into directory "classes" + 1. Now you have two Explorer windows opened. Copy all files and directories from the first one to the second one. 8. Locate the class `Launcher` (e.g., by ctrl+N and then typing `Launcher`). Press Enter to jump to that class.

IntelliJ search for class “Launcher” From 960507097e5729da60cfce6c3e1784d1b6b37037 Mon Sep 17 00:00:00 2001 From: ThiloteE <73715071+ThiloteE@users.noreply.github.com> Date: Sun, 7 Jan 2024 14:23:04 +0100 Subject: [PATCH 04/25] Add chocolate.bib MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Library that can be used as (example) material for the JabRef website and other public relations matters. The particular topic "chocolate" was chosen following a suggestion by Tobias Dietz: "For jabref online, I spend some time thinking about what kind of articles / subjects one should choose for user-faced presentation. In the end, I went for the theme "Chocolate" because: 1. It's positively received across many cultures 2. It's easy to understand and relate too, even if you are not an expert in the field 3. It has enough variation to e.g. demonstrate the group tree 4. while still being homogenous enough to at least make the impression to be a plausible "real" library of someone 5. It's not such a dry subject as computer science or mathematics, while at the same time being serious and scientific enough Was hard to find a topic that scored well on all these points. Another close contender was "Old-time classics" (Newton, Einstein, Hilbert, ...), but then you get the cliché old western guys narrative. So maybe the blog series could use "Chocolate" as well?" --- src/test/resources/testbib/Chocolate.bib | 225 +++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 src/test/resources/testbib/Chocolate.bib diff --git a/src/test/resources/testbib/Chocolate.bib b/src/test/resources/testbib/Chocolate.bib new file mode 100644 index 00000000000..b9ab2091276 --- /dev/null +++ b/src/test/resources/testbib/Chocolate.bib @@ -0,0 +1,225 @@ +@Article{Corti_2009, + author = {Corti, Roberto and Flammer, Andreas J. and Hollenberg, Norman K. and Lüscher, Thomas F.}, + date = {2009-03}, + journaltitle = {Circulation}, + title = {Cocoa and Cardiovascular Health}, + doi = {10.1161/circulationaha.108.827022}, + issn = {1524-4539}, + number = {10}, + pages = {1433--1441}, + volume = {119}, + file = {:DownloadedViaJabRef/Corti et al. (2009-03) Cocoa Cardiovascular Health.pdf:PDF}, + publisher = {Ovid Technologies (Wolters Kluwer Health)}, +} + +@Article{Cooper_2007, + author = {Cooper, Karen A. and Donovan, Jennifer L. and Waterhouse, Andrew L. and Williamson, Gary}, + date = {2007-08}, + journaltitle = {British Journal of Nutrition}, + title = {Cocoa and health: a decade of research}, + doi = {10.1017/s0007114507795296}, + issn = {1475-2662}, + number = {1}, + pages = {1--11}, + volume = {99}, + file = {:DownloadedViaJabRef/Cooper et al. (2007-08) Cocoa health_ decade.pdf:PDF}, + publisher = {Cambridge University Press (CUP)}, +} + +@Article{Ding_2006, + author = {Ding, Eric L and Hutfless, Susan M and Ding, Xin and Girotra, Saket}, + date = {2006-01}, + journaltitle = {Nutrition & Metabolism}, + title = {Chocolate and Prevention of Cardiovascular Disease: A Systematic Review}, + doi = {10.1186/1743-7075-3-2}, + issn = {1743-7075}, + number = {1}, + volume = {3}, + file = {:DownloadedViaJabRef/Ding et al. (2006-01) Chocolate Prevention Cardiovascular.pdf:PDF}, + publisher = {Springer Science and Business Media LLC}, +} + +@Article{Keen_2001, + author = {Keen, Carl L.}, + date = {2001-10}, + journaltitle = {Journal of the American College of Nutrition}, + title = {Chocolate: Food as Medicine/Medicine as Food}, + doi = {10.1080/07315724.2001.10719181}, + issn = {1541-1087}, + number = {sup5}, + pages = {436S--439S}, + volume = {20}, + publisher = {Informa UK Limited}, +} + +@Article{Katz_2011, + author = {Katz, David L. and Doughty, Kim and Ali, Ather}, + date = {2011-11}, + journaltitle = {Antioxidants & Redox Signaling}, + title = {Cocoa and Chocolate in Human Health and Disease}, + doi = {10.1089/ars.2010.3697}, + issn = {1557-7716}, + number = {10}, + pages = {2779--2811}, + volume = {15}, + file = {:DownloadedViaJabRef/Katz et al. (2011-11) Cocoa Chocolate Human.pdf:PDF}, + publisher = {Mary Ann Liebert Inc}, + readstatus = {skimmed}, +} + +@Article{Parker_2006, + author = {Parker, Gordon and Parker, Isabella and Brotchie, Heather}, + date = {2006-06}, + journaltitle = {Journal of Affective Disorders}, + title = {Mood state effects of chocolate}, + doi = {10.1016/j.jad.2006.02.007}, + issn = {0165-0327}, + number = {2–3}, + pages = {149--159}, + volume = {92}, + groups = {Used}, + publisher = {Elsevier BV}, + readstatus = {read}, +} + +@Article{Scholey_2013, + author = {Scholey, Andrew and Owen, Lauren}, + date = {2013-10}, + journaltitle = {Nutrition Reviews}, + title = {Effects of chocolate on cognitive function and mood: a systematic review}, + doi = {10.1111/nure.12065}, + issn = {0029-6643}, + number = {10}, + pages = {665--681}, + volume = {71}, + file = {:Used/Scholey & Owen (2013-10) Effects chocolate cognitive.pdf:PDF}, + groups = {Used}, + publisher = {Oxford University Press (OUP)}, + readstatus = {read}, +} + +@Article{Tan_2021, + author = {Tan, Terence Yew Chin and Lim, Xin Yi and Yeo, Julie Hsiao Hui and Lee, Shaun Wen Huey and Lai, Nai Ming}, + date = {2021-08}, + journaltitle = {Nutrients}, + title = {The Health Effects of Chocolate and Cocoa: A Systematic Review}, + doi = {10.3390/nu13092909}, + issn = {2072-6643}, + number = {9}, + pages = {2909}, + volume = {13}, + file = {:DownloadedViaJabRef/Tan et al. (2021-08) Health Effects Chocolate.pdf:PDF}, + publisher = {MDPI AG}, + readstatus = {skimmed}, +} + +@Article{Macht_2007, + author = {Macht, Michael and Mueller, Jochen}, + date = {2007-11}, + journaltitle = {Appetite}, + title = {Immediate effects of chocolate on experimentally induced mood states}, + doi = {10.1016/j.appet.2007.05.004}, + issn = {0195-6663}, + number = {3}, + pages = {667--674}, + volume = {49}, + groups = {Used}, + publisher = {Elsevier BV}, + readstatus = {read}, +} + +@Article{Tokede_2011, + author = {Tokede, O A and Gaziano, J M and Djoussé, L}, + date = {2011-05}, + journaltitle = {European Journal of Clinical Nutrition}, + title = {Effects of cocoa products/dark chocolate on serum lipids: a meta-analysis}, + doi = {10.1038/ejcn.2011.64}, + issn = {1476-5640}, + number = {8}, + pages = {879--886}, + volume = {65}, + publisher = {Springer Science and Business Media LLC}, +} + +@Article{Garcia_2018, + author = {Garcia, Jose P and Santana, Adrian and Baruqui, Diego Lugo and Suraci, Nicholas}, + date = {2018-12}, + journaltitle = {Reviews in Cardiovascular Medicine}, + title = {The Cardiovascular effects of chocolate.}, + doi = {10.31083/j.rcm.2018.04.3187}, + issn = {2153-8174}, + number = {4}, + volume = {19}, + publisher = {IMR Press}, +} + +@Article{Hooper_2012, + author = {Hooper, Lee and Kay, Colin and Abdelhamid, Asmaa and Kroon, Paul A and Cohn, Jeffrey S and Rimm, Eric B and Cassidy, Aedín}, + date = {2012-03}, + journaltitle = {The American Journal of Clinical Nutrition}, + title = {Effects of chocolate, cocoa, and flavan-3-ols on cardiovascular health: a systematic review and meta-analysis of randomized trials}, + doi = {10.3945/ajcn.111.023457}, + issn = {0002-9165}, + number = {3}, + pages = {740--751}, + volume = {95}, + publisher = {Elsevier BV}, + readstatus = {skimmed}, +} + +@Article{Di_Renzo_2012, + author = {Di Renzo, Gian Carlo and Brillo, Eleonora and Romanelli, Maila and Porcaro, Giuseppina and Capanna, Federica and Kanninen, Tomi T and Gerli, Sandro and Clerici, Graziano}, + date = {2012-06}, + journaltitle = {The Journal of Maternal-Fetal & Neonatal Medicine}, + title = {Potential effects of chocolate on human pregnancy: a randomized controlled trial}, + doi = {10.3109/14767058.2012.683085}, + issn = {1476-4954}, + number = {10}, + pages = {1860--1867}, + volume = {25}, + groups = {Used}, + publisher = {Informa UK Limited}, + readstatus = {read}, +} + +@Article{Richard_2017, + author = {Richard, Anna and Meule, Adrian and Friese, Malte and Blechert, Jens}, + date = {2017-09}, + journaltitle = {Frontiers in Psychology}, + title = {Effects of Chocolate Deprivation on Implicit and Explicit Evaluation of Chocolate in High and Low Trait Chocolate Cravers}, + doi = {10.3389/fpsyg.2017.01591}, + issn = {1664-1078}, + volume = {8}, + groups = {Used}, + publisher = {Frontiers Media SA}, + readstatus = {read}, +} + +@Article{Fulton_1969, + author = {Fulton, James E., Jr. and Plewig, Gerd and Kligman, Albert M.}, + date = {1969-12}, + journaltitle = {JAMA: The Journal of the American Medical Association}, + title = {{Effect of Chocolate on Acne Vulgaris}}, + doi = {10.1001/jama.1969.03160370055011}, + eprint = {https://jamanetwork.com/journals/jama/articlepdf/350738/jama\_210\_11\_011.pdf}, + issn = {0098-7484}, + number = {11}, + pages = {2071-2074}, + url = {https://doi.org/10.1001/jama.1969.03160370055011}, + volume = {210}, + abstract = {{To test the widespread idea that chocolate is harmful in instances of acne vulgaris, 65 subjects with moderate acne ate either a bar containing ten times the amount of chocolate in a typical bar, or an identical-appearing bar which contained no chocolate. Counting of all the lesions on one side of the face before and after each ingestion period indicated no difference between the bars. Five normal subjects ingested two enriched chocolate bars daily for one month; this represented a daily addition of the diet of 1,200 calories, of which about half was vegetable fat. This excessive intake of chocolate and fat did not alter the composition or output of sebum. A review of studies purporting to show that diets high in carbohydrate or fat stimulate sebaceous secretion and adversely affect acne vulgaris indicates that these claims are unproved.}}, + publisher = {American Medical Association (AMA)}, +} + +@Comment{jabref-meta: databaseType:biblatex;} + +@Comment{jabref-meta: grouping: +0 AllEntriesGroup:; +1 SearchGroup:Entries without a group\;0\;groups != .+\;0\;1\;1\;\;\;\;; +1 SearchGroup:Entries without a linked file\;0\;file != .+\;0\;1\;1\;\;\;\;; +1 StaticGroup:Paywalled\;0\;1\;\;\;\;; +1 SearchGroup:To read\;0\;groups != .+ and readstatus != .+\;0\;1\;1\;0x008080ff\;\;\;; +1 KeywordGroup:Skimmed\;0\;readstatus\;skimmed\;0\;0\;1\;0xffff00ff\;\;\;; +1 KeywordGroup:Read\;0\;readstatus\;read\;0\;0\;0\;0x00ff00ff\;\;\;; +1 StaticGroup:Used\;0\;1\;0x0000ffff\;\;\;; +} From 053a7d0243be3232738d2f3e78b282ca246eaa59 Mon Sep 17 00:00:00 2001 From: Christoph Date: Sun, 7 Jan 2024 15:14:20 +0100 Subject: [PATCH 05/25] Update intellij-89-run-with-intellij.md add finder --- .../intellij-89-run-with-intellij.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/getting-into-the-code/guidelines-for-setting-up-a-local-workspace/intellij-89-run-with-intellij.md b/docs/getting-into-the-code/guidelines-for-setting-up-a-local-workspace/intellij-89-run-with-intellij.md index ab626193e43..67240227082 100644 --- a/docs/getting-into-the-code/guidelines-for-setting-up-a-local-workspace/intellij-89-run-with-intellij.md +++ b/docs/getting-into-the-code/guidelines-for-setting-up-a-local-workspace/intellij-89-run-with-intellij.md @@ -18,10 +18,10 @@ Due to [IDEA-119280](https://youtrack.jetbrains.com/issue/IDEA-119280), it is a 6. Open the project view (Alt+1 , on mac cmd>+1) 7. Copy all build resources to the folder of the build classes 1. Navigate to the folder `build/resoruces/main` - 1. Right click -> "Open In" -> "Explorer" + 1. Right click -> "Open In" -> "Explorer (Finder on macOS)" 1. Navigate into directory "main" 1. Select the folder `out/production/classes` - 1. Right click -> "Open In" -> "Explorer" + 1. Right click -> "Open In" -> "Explorer (Finder on macOS)" 1. Navigate into directory "classes" 1. Now you have two Explorer windows opened. Copy all files and directories from the first one to the second one. 8. Locate the class `Launcher` (e.g., by ctrl+N and then typing `Launcher`). Press Enter to jump to that class. From 8c27a62cb26a2c78f4f0337e1b278704432d1542 Mon Sep 17 00:00:00 2001 From: ThiloteE <73715071+ThiloteE@users.noreply.github.com> Date: Sun, 7 Jan 2024 15:31:10 +0100 Subject: [PATCH 06/25] Remove abstract --- src/test/resources/testbib/Chocolate.bib | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/resources/testbib/Chocolate.bib b/src/test/resources/testbib/Chocolate.bib index b9ab2091276..4bd7eb4df95 100644 --- a/src/test/resources/testbib/Chocolate.bib +++ b/src/test/resources/testbib/Chocolate.bib @@ -207,7 +207,6 @@ @Article{Fulton_1969 pages = {2071-2074}, url = {https://doi.org/10.1001/jama.1969.03160370055011}, volume = {210}, - abstract = {{To test the widespread idea that chocolate is harmful in instances of acne vulgaris, 65 subjects with moderate acne ate either a bar containing ten times the amount of chocolate in a typical bar, or an identical-appearing bar which contained no chocolate. Counting of all the lesions on one side of the face before and after each ingestion period indicated no difference between the bars. Five normal subjects ingested two enriched chocolate bars daily for one month; this represented a daily addition of the diet of 1,200 calories, of which about half was vegetable fat. This excessive intake of chocolate and fat did not alter the composition or output of sebum. A review of studies purporting to show that diets high in carbohydrate or fat stimulate sebaceous secretion and adversely affect acne vulgaris indicates that these claims are unproved.}}, publisher = {American Medical Association (AMA)}, } From 17c87b4880e64aee385f606979aee2e8b44867e9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jan 2024 14:16:24 +0000 Subject: [PATCH 07/25] Bump org.mockito:mockito-core from 5.7.0 to 5.8.0 (#10757) Bumps [org.mockito:mockito-core](https://github.com/mockito/mockito) from 5.7.0 to 5.8.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v5.7.0...v5.8.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 96cda73c2fd..86759997f7b 100644 --- a/build.gradle +++ b/build.gradle @@ -244,7 +244,7 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' testImplementation 'org.junit.platform:junit-platform-launcher:1.10.1' - testImplementation 'org.mockito:mockito-core:5.7.0' + testImplementation 'org.mockito:mockito-core:5.8.0' testImplementation 'org.xmlunit:xmlunit-core:2.9.1' testImplementation 'org.xmlunit:xmlunit-matchers:2.9.1' testRuntimeOnly 'com.tngtech.archunit:archunit-junit5-engine:1.2.1' From b0bdeb4632836dcde4cb76fe8340bd1703395c75 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jan 2024 14:17:08 +0000 Subject: [PATCH 08/25] Bump org.jsoup:jsoup from 1.16.2 to 1.17.2 (#10756) Bumps [org.jsoup:jsoup](https://github.com/jhy/jsoup) from 1.16.2 to 1.17.2. - [Release notes](https://github.com/jhy/jsoup/releases) - [Changelog](https://github.com/jhy/jsoup/blob/master/CHANGES.md) - [Commits](https://github.com/jhy/jsoup/compare/jsoup-1.16.2...jsoup-1.17.2) --- updated-dependencies: - dependency-name: org.jsoup:jsoup dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 86759997f7b..7ea16772103 100644 --- a/build.gradle +++ b/build.gradle @@ -184,7 +184,7 @@ dependencies { implementation 'org.controlsfx:controlsfx:11.2.0' implementation 'com.github.Dansoftowner:jSystemThemeDetector:3.8' - implementation 'org.jsoup:jsoup:1.16.2' + implementation 'org.jsoup:jsoup:1.17.2' implementation 'com.konghq:unirest-java:3.14.5' implementation 'org.slf4j:slf4j-api:2.0.9' From 7df5cfc8c7187b6c65e75a55cfd2e559c8cbc24c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jan 2024 14:18:28 +0000 Subject: [PATCH 09/25] Bump org.glassfish.jersey.containers:jersey-container-grizzly2-http (#10755) Bumps org.glassfish.jersey.containers:jersey-container-grizzly2-http from 3.1.4 to 3.1.5. --- updated-dependencies: - dependency-name: org.glassfish.jersey.containers:jersey-container-grizzly2-http dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 7ea16772103..e3a8c4e51a0 100644 --- a/build.gradle +++ b/build.gradle @@ -232,7 +232,7 @@ dependencies { // testImplementation 'org.glassfish.hk2:hk2-junitrunner:3.0.4' // HTTP server // implementation 'org.glassfish.jersey.containers:jersey-container-netty-http:3.1.1' - implementation 'org.glassfish.jersey.containers:jersey-container-grizzly2-http:3.1.4' + implementation 'org.glassfish.jersey.containers:jersey-container-grizzly2-http:3.1.5' testImplementation 'org.glassfish.jersey.test-framework.providers:jersey-test-framework-provider-grizzly2:3.1.5' // Allow objects "magically" to be mapped to JSON using GSON // implementation 'org.glassfish.jersey.media:jersey-media-json-gson:3.1.1' From 9e0c3d71c94a73c8a6472dfcf39cc4e71ad24e05 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jan 2024 14:19:16 +0000 Subject: [PATCH 10/25] Bump org.openrewrite.rewrite from 6.5.4 to 6.6.3 (#10758) Bumps org.openrewrite.rewrite from 6.5.4 to 6.6.3. --- updated-dependencies: - dependency-name: org.openrewrite.rewrite dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e3a8c4e51a0..af0f5c9e96f 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ plugins { id 'idea' - id 'org.openrewrite.rewrite' version '6.5.4' + id 'org.openrewrite.rewrite' version '6.6.3' } // Enable following for debugging From 3a8c937fb272b453ce12c620813c4566e1b93a27 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jan 2024 14:19:19 +0000 Subject: [PATCH 11/25] Bump org.apache.lucene:lucene-core from 9.9.0 to 9.9.1 (#10759) Bumps org.apache.lucene:lucene-core from 9.9.0 to 9.9.1. --- updated-dependencies: - dependency-name: org.apache.lucene:lucene-core dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index af0f5c9e96f..256b3d85bee 100644 --- a/build.gradle +++ b/build.gradle @@ -119,7 +119,7 @@ dependencies { exclude group: 'org.junit.jupiter' } - implementation 'org.apache.lucene:lucene-core:9.9.0' + implementation 'org.apache.lucene:lucene-core:9.9.1' implementation 'org.apache.lucene:lucene-queryparser:9.9.1' implementation 'org.apache.lucene:lucene-queries:9.9.0' implementation 'org.apache.lucene:lucene-analysis-common:9.9.1' From 2f40641b5761373e0409591a52cf235772864c38 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jan 2024 14:49:08 +0000 Subject: [PATCH 12/25] Bump appleboy/ssh-action from 1.0.2 to 1.0.3 (#10761) Bumps [appleboy/ssh-action](https://github.com/appleboy/ssh-action) from 1.0.2 to 1.0.3. - [Release notes](https://github.com/appleboy/ssh-action/releases) - [Commits](https://github.com/appleboy/ssh-action/compare/v1.0.2...v1.0.3) --- updated-dependencies: - dependency-name: appleboy/ssh-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/cleanup-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cleanup-pr.yml b/.github/workflows/cleanup-pr.yml index c5ffae5f848..9444fd882b6 100644 --- a/.github/workflows/cleanup-pr.yml +++ b/.github/workflows/cleanup-pr.yml @@ -26,7 +26,7 @@ jobs: BUILDJABREFPRIVATEKEY: ${{ secrets.buildJabRefPrivateKey }} - name: Delete folder on builds.jabref.org if: steps.checksecrets.outputs.secretspresent == 'YES' - uses: appleboy/ssh-action@v1.0.2 + uses: appleboy/ssh-action@v1.0.3 with: script: rm -rf /var/www/builds.jabref.org/www/pull/${{ github.event.pull_request.number }} || true host: build-upload.jabref.org From 2e0605c5122aff0d5a1cbd764cbdb0e254efed60 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jan 2024 14:52:44 +0000 Subject: [PATCH 13/25] Bump lycheeverse/lychee-action from 1.8.0 to 1.9.0 (#10760) Bumps [lycheeverse/lychee-action](https://github.com/lycheeverse/lychee-action) from 1.8.0 to 1.9.0. - [Release notes](https://github.com/lycheeverse/lychee-action/releases) - [Commits](https://github.com/lycheeverse/lychee-action/compare/v1.8.0...v1.9.0) --- updated-dependencies: - dependency-name: lycheeverse/lychee-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/check-links.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index dccd1957003..0fa9a5f1670 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -29,7 +29,7 @@ jobs: restore-keys: cache-lychee- - name: Link Checker id: lychee - uses: lycheeverse/lychee-action@v1.8.0 + uses: lycheeverse/lychee-action@v1.9.0 with: fail: true args: --accept '200,201,202,203,204,429,500' --max-concurrency 1 --cache --no-progress --exclude-all-private './**/*.md' From 3fda9dcfba77234ee20bee7b0d44d05288f07611 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Mon, 8 Jan 2024 21:09:38 +0100 Subject: [PATCH 14/25] Fix line endings --- src/test/resources/testbib/Chocolate.bib | 448 +++++++++++------------ 1 file changed, 224 insertions(+), 224 deletions(-) diff --git a/src/test/resources/testbib/Chocolate.bib b/src/test/resources/testbib/Chocolate.bib index 4bd7eb4df95..5fbb0f7af03 100644 --- a/src/test/resources/testbib/Chocolate.bib +++ b/src/test/resources/testbib/Chocolate.bib @@ -1,224 +1,224 @@ -@Article{Corti_2009, - author = {Corti, Roberto and Flammer, Andreas J. and Hollenberg, Norman K. and Lüscher, Thomas F.}, - date = {2009-03}, - journaltitle = {Circulation}, - title = {Cocoa and Cardiovascular Health}, - doi = {10.1161/circulationaha.108.827022}, - issn = {1524-4539}, - number = {10}, - pages = {1433--1441}, - volume = {119}, - file = {:DownloadedViaJabRef/Corti et al. (2009-03) Cocoa Cardiovascular Health.pdf:PDF}, - publisher = {Ovid Technologies (Wolters Kluwer Health)}, -} - -@Article{Cooper_2007, - author = {Cooper, Karen A. and Donovan, Jennifer L. and Waterhouse, Andrew L. and Williamson, Gary}, - date = {2007-08}, - journaltitle = {British Journal of Nutrition}, - title = {Cocoa and health: a decade of research}, - doi = {10.1017/s0007114507795296}, - issn = {1475-2662}, - number = {1}, - pages = {1--11}, - volume = {99}, - file = {:DownloadedViaJabRef/Cooper et al. (2007-08) Cocoa health_ decade.pdf:PDF}, - publisher = {Cambridge University Press (CUP)}, -} - -@Article{Ding_2006, - author = {Ding, Eric L and Hutfless, Susan M and Ding, Xin and Girotra, Saket}, - date = {2006-01}, - journaltitle = {Nutrition & Metabolism}, - title = {Chocolate and Prevention of Cardiovascular Disease: A Systematic Review}, - doi = {10.1186/1743-7075-3-2}, - issn = {1743-7075}, - number = {1}, - volume = {3}, - file = {:DownloadedViaJabRef/Ding et al. (2006-01) Chocolate Prevention Cardiovascular.pdf:PDF}, - publisher = {Springer Science and Business Media LLC}, -} - -@Article{Keen_2001, - author = {Keen, Carl L.}, - date = {2001-10}, - journaltitle = {Journal of the American College of Nutrition}, - title = {Chocolate: Food as Medicine/Medicine as Food}, - doi = {10.1080/07315724.2001.10719181}, - issn = {1541-1087}, - number = {sup5}, - pages = {436S--439S}, - volume = {20}, - publisher = {Informa UK Limited}, -} - -@Article{Katz_2011, - author = {Katz, David L. and Doughty, Kim and Ali, Ather}, - date = {2011-11}, - journaltitle = {Antioxidants & Redox Signaling}, - title = {Cocoa and Chocolate in Human Health and Disease}, - doi = {10.1089/ars.2010.3697}, - issn = {1557-7716}, - number = {10}, - pages = {2779--2811}, - volume = {15}, - file = {:DownloadedViaJabRef/Katz et al. (2011-11) Cocoa Chocolate Human.pdf:PDF}, - publisher = {Mary Ann Liebert Inc}, - readstatus = {skimmed}, -} - -@Article{Parker_2006, - author = {Parker, Gordon and Parker, Isabella and Brotchie, Heather}, - date = {2006-06}, - journaltitle = {Journal of Affective Disorders}, - title = {Mood state effects of chocolate}, - doi = {10.1016/j.jad.2006.02.007}, - issn = {0165-0327}, - number = {2–3}, - pages = {149--159}, - volume = {92}, - groups = {Used}, - publisher = {Elsevier BV}, - readstatus = {read}, -} - -@Article{Scholey_2013, - author = {Scholey, Andrew and Owen, Lauren}, - date = {2013-10}, - journaltitle = {Nutrition Reviews}, - title = {Effects of chocolate on cognitive function and mood: a systematic review}, - doi = {10.1111/nure.12065}, - issn = {0029-6643}, - number = {10}, - pages = {665--681}, - volume = {71}, - file = {:Used/Scholey & Owen (2013-10) Effects chocolate cognitive.pdf:PDF}, - groups = {Used}, - publisher = {Oxford University Press (OUP)}, - readstatus = {read}, -} - -@Article{Tan_2021, - author = {Tan, Terence Yew Chin and Lim, Xin Yi and Yeo, Julie Hsiao Hui and Lee, Shaun Wen Huey and Lai, Nai Ming}, - date = {2021-08}, - journaltitle = {Nutrients}, - title = {The Health Effects of Chocolate and Cocoa: A Systematic Review}, - doi = {10.3390/nu13092909}, - issn = {2072-6643}, - number = {9}, - pages = {2909}, - volume = {13}, - file = {:DownloadedViaJabRef/Tan et al. (2021-08) Health Effects Chocolate.pdf:PDF}, - publisher = {MDPI AG}, - readstatus = {skimmed}, -} - -@Article{Macht_2007, - author = {Macht, Michael and Mueller, Jochen}, - date = {2007-11}, - journaltitle = {Appetite}, - title = {Immediate effects of chocolate on experimentally induced mood states}, - doi = {10.1016/j.appet.2007.05.004}, - issn = {0195-6663}, - number = {3}, - pages = {667--674}, - volume = {49}, - groups = {Used}, - publisher = {Elsevier BV}, - readstatus = {read}, -} - -@Article{Tokede_2011, - author = {Tokede, O A and Gaziano, J M and Djoussé, L}, - date = {2011-05}, - journaltitle = {European Journal of Clinical Nutrition}, - title = {Effects of cocoa products/dark chocolate on serum lipids: a meta-analysis}, - doi = {10.1038/ejcn.2011.64}, - issn = {1476-5640}, - number = {8}, - pages = {879--886}, - volume = {65}, - publisher = {Springer Science and Business Media LLC}, -} - -@Article{Garcia_2018, - author = {Garcia, Jose P and Santana, Adrian and Baruqui, Diego Lugo and Suraci, Nicholas}, - date = {2018-12}, - journaltitle = {Reviews in Cardiovascular Medicine}, - title = {The Cardiovascular effects of chocolate.}, - doi = {10.31083/j.rcm.2018.04.3187}, - issn = {2153-8174}, - number = {4}, - volume = {19}, - publisher = {IMR Press}, -} - -@Article{Hooper_2012, - author = {Hooper, Lee and Kay, Colin and Abdelhamid, Asmaa and Kroon, Paul A and Cohn, Jeffrey S and Rimm, Eric B and Cassidy, Aedín}, - date = {2012-03}, - journaltitle = {The American Journal of Clinical Nutrition}, - title = {Effects of chocolate, cocoa, and flavan-3-ols on cardiovascular health: a systematic review and meta-analysis of randomized trials}, - doi = {10.3945/ajcn.111.023457}, - issn = {0002-9165}, - number = {3}, - pages = {740--751}, - volume = {95}, - publisher = {Elsevier BV}, - readstatus = {skimmed}, -} - -@Article{Di_Renzo_2012, - author = {Di Renzo, Gian Carlo and Brillo, Eleonora and Romanelli, Maila and Porcaro, Giuseppina and Capanna, Federica and Kanninen, Tomi T and Gerli, Sandro and Clerici, Graziano}, - date = {2012-06}, - journaltitle = {The Journal of Maternal-Fetal & Neonatal Medicine}, - title = {Potential effects of chocolate on human pregnancy: a randomized controlled trial}, - doi = {10.3109/14767058.2012.683085}, - issn = {1476-4954}, - number = {10}, - pages = {1860--1867}, - volume = {25}, - groups = {Used}, - publisher = {Informa UK Limited}, - readstatus = {read}, -} - -@Article{Richard_2017, - author = {Richard, Anna and Meule, Adrian and Friese, Malte and Blechert, Jens}, - date = {2017-09}, - journaltitle = {Frontiers in Psychology}, - title = {Effects of Chocolate Deprivation on Implicit and Explicit Evaluation of Chocolate in High and Low Trait Chocolate Cravers}, - doi = {10.3389/fpsyg.2017.01591}, - issn = {1664-1078}, - volume = {8}, - groups = {Used}, - publisher = {Frontiers Media SA}, - readstatus = {read}, -} - -@Article{Fulton_1969, - author = {Fulton, James E., Jr. and Plewig, Gerd and Kligman, Albert M.}, - date = {1969-12}, - journaltitle = {JAMA: The Journal of the American Medical Association}, - title = {{Effect of Chocolate on Acne Vulgaris}}, - doi = {10.1001/jama.1969.03160370055011}, - eprint = {https://jamanetwork.com/journals/jama/articlepdf/350738/jama\_210\_11\_011.pdf}, - issn = {0098-7484}, - number = {11}, - pages = {2071-2074}, - url = {https://doi.org/10.1001/jama.1969.03160370055011}, - volume = {210}, - publisher = {American Medical Association (AMA)}, -} - -@Comment{jabref-meta: databaseType:biblatex;} - -@Comment{jabref-meta: grouping: -0 AllEntriesGroup:; -1 SearchGroup:Entries without a group\;0\;groups != .+\;0\;1\;1\;\;\;\;; -1 SearchGroup:Entries without a linked file\;0\;file != .+\;0\;1\;1\;\;\;\;; -1 StaticGroup:Paywalled\;0\;1\;\;\;\;; -1 SearchGroup:To read\;0\;groups != .+ and readstatus != .+\;0\;1\;1\;0x008080ff\;\;\;; -1 KeywordGroup:Skimmed\;0\;readstatus\;skimmed\;0\;0\;1\;0xffff00ff\;\;\;; -1 KeywordGroup:Read\;0\;readstatus\;read\;0\;0\;0\;0x00ff00ff\;\;\;; -1 StaticGroup:Used\;0\;1\;0x0000ffff\;\;\;; -} +@Article{Corti_2009, + author = {Corti, Roberto and Flammer, Andreas J. and Hollenberg, Norman K. and Lüscher, Thomas F.}, + date = {2009-03}, + journaltitle = {Circulation}, + title = {Cocoa and Cardiovascular Health}, + doi = {10.1161/circulationaha.108.827022}, + issn = {1524-4539}, + number = {10}, + pages = {1433--1441}, + volume = {119}, + file = {:DownloadedViaJabRef/Corti et al. (2009-03) Cocoa Cardiovascular Health.pdf:PDF}, + publisher = {Ovid Technologies (Wolters Kluwer Health)}, +} + +@Article{Cooper_2007, + author = {Cooper, Karen A. and Donovan, Jennifer L. and Waterhouse, Andrew L. and Williamson, Gary}, + date = {2007-08}, + journaltitle = {British Journal of Nutrition}, + title = {Cocoa and health: a decade of research}, + doi = {10.1017/s0007114507795296}, + issn = {1475-2662}, + number = {1}, + pages = {1--11}, + volume = {99}, + file = {:DownloadedViaJabRef/Cooper et al. (2007-08) Cocoa health_ decade.pdf:PDF}, + publisher = {Cambridge University Press (CUP)}, +} + +@Article{Ding_2006, + author = {Ding, Eric L and Hutfless, Susan M and Ding, Xin and Girotra, Saket}, + date = {2006-01}, + journaltitle = {Nutrition & Metabolism}, + title = {Chocolate and Prevention of Cardiovascular Disease: A Systematic Review}, + doi = {10.1186/1743-7075-3-2}, + issn = {1743-7075}, + number = {1}, + volume = {3}, + file = {:DownloadedViaJabRef/Ding et al. (2006-01) Chocolate Prevention Cardiovascular.pdf:PDF}, + publisher = {Springer Science and Business Media LLC}, +} + +@Article{Keen_2001, + author = {Keen, Carl L.}, + date = {2001-10}, + journaltitle = {Journal of the American College of Nutrition}, + title = {Chocolate: Food as Medicine/Medicine as Food}, + doi = {10.1080/07315724.2001.10719181}, + issn = {1541-1087}, + number = {sup5}, + pages = {436S--439S}, + volume = {20}, + publisher = {Informa UK Limited}, +} + +@Article{Katz_2011, + author = {Katz, David L. and Doughty, Kim and Ali, Ather}, + date = {2011-11}, + journaltitle = {Antioxidants & Redox Signaling}, + title = {Cocoa and Chocolate in Human Health and Disease}, + doi = {10.1089/ars.2010.3697}, + issn = {1557-7716}, + number = {10}, + pages = {2779--2811}, + volume = {15}, + file = {:DownloadedViaJabRef/Katz et al. (2011-11) Cocoa Chocolate Human.pdf:PDF}, + publisher = {Mary Ann Liebert Inc}, + readstatus = {skimmed}, +} + +@Article{Parker_2006, + author = {Parker, Gordon and Parker, Isabella and Brotchie, Heather}, + date = {2006-06}, + journaltitle = {Journal of Affective Disorders}, + title = {Mood state effects of chocolate}, + doi = {10.1016/j.jad.2006.02.007}, + issn = {0165-0327}, + number = {2–3}, + pages = {149--159}, + volume = {92}, + groups = {Used}, + publisher = {Elsevier BV}, + readstatus = {read}, +} + +@Article{Scholey_2013, + author = {Scholey, Andrew and Owen, Lauren}, + date = {2013-10}, + journaltitle = {Nutrition Reviews}, + title = {Effects of chocolate on cognitive function and mood: a systematic review}, + doi = {10.1111/nure.12065}, + issn = {0029-6643}, + number = {10}, + pages = {665--681}, + volume = {71}, + file = {:Used/Scholey & Owen (2013-10) Effects chocolate cognitive.pdf:PDF}, + groups = {Used}, + publisher = {Oxford University Press (OUP)}, + readstatus = {read}, +} + +@Article{Tan_2021, + author = {Tan, Terence Yew Chin and Lim, Xin Yi and Yeo, Julie Hsiao Hui and Lee, Shaun Wen Huey and Lai, Nai Ming}, + date = {2021-08}, + journaltitle = {Nutrients}, + title = {The Health Effects of Chocolate and Cocoa: A Systematic Review}, + doi = {10.3390/nu13092909}, + issn = {2072-6643}, + number = {9}, + pages = {2909}, + volume = {13}, + file = {:DownloadedViaJabRef/Tan et al. (2021-08) Health Effects Chocolate.pdf:PDF}, + publisher = {MDPI AG}, + readstatus = {skimmed}, +} + +@Article{Macht_2007, + author = {Macht, Michael and Mueller, Jochen}, + date = {2007-11}, + journaltitle = {Appetite}, + title = {Immediate effects of chocolate on experimentally induced mood states}, + doi = {10.1016/j.appet.2007.05.004}, + issn = {0195-6663}, + number = {3}, + pages = {667--674}, + volume = {49}, + groups = {Used}, + publisher = {Elsevier BV}, + readstatus = {read}, +} + +@Article{Tokede_2011, + author = {Tokede, O A and Gaziano, J M and Djoussé, L}, + date = {2011-05}, + journaltitle = {European Journal of Clinical Nutrition}, + title = {Effects of cocoa products/dark chocolate on serum lipids: a meta-analysis}, + doi = {10.1038/ejcn.2011.64}, + issn = {1476-5640}, + number = {8}, + pages = {879--886}, + volume = {65}, + publisher = {Springer Science and Business Media LLC}, +} + +@Article{Garcia_2018, + author = {Garcia, Jose P and Santana, Adrian and Baruqui, Diego Lugo and Suraci, Nicholas}, + date = {2018-12}, + journaltitle = {Reviews in Cardiovascular Medicine}, + title = {The Cardiovascular effects of chocolate.}, + doi = {10.31083/j.rcm.2018.04.3187}, + issn = {2153-8174}, + number = {4}, + volume = {19}, + publisher = {IMR Press}, +} + +@Article{Hooper_2012, + author = {Hooper, Lee and Kay, Colin and Abdelhamid, Asmaa and Kroon, Paul A and Cohn, Jeffrey S and Rimm, Eric B and Cassidy, Aedín}, + date = {2012-03}, + journaltitle = {The American Journal of Clinical Nutrition}, + title = {Effects of chocolate, cocoa, and flavan-3-ols on cardiovascular health: a systematic review and meta-analysis of randomized trials}, + doi = {10.3945/ajcn.111.023457}, + issn = {0002-9165}, + number = {3}, + pages = {740--751}, + volume = {95}, + publisher = {Elsevier BV}, + readstatus = {skimmed}, +} + +@Article{Di_Renzo_2012, + author = {Di Renzo, Gian Carlo and Brillo, Eleonora and Romanelli, Maila and Porcaro, Giuseppina and Capanna, Federica and Kanninen, Tomi T and Gerli, Sandro and Clerici, Graziano}, + date = {2012-06}, + journaltitle = {The Journal of Maternal-Fetal & Neonatal Medicine}, + title = {Potential effects of chocolate on human pregnancy: a randomized controlled trial}, + doi = {10.3109/14767058.2012.683085}, + issn = {1476-4954}, + number = {10}, + pages = {1860--1867}, + volume = {25}, + groups = {Used}, + publisher = {Informa UK Limited}, + readstatus = {read}, +} + +@Article{Richard_2017, + author = {Richard, Anna and Meule, Adrian and Friese, Malte and Blechert, Jens}, + date = {2017-09}, + journaltitle = {Frontiers in Psychology}, + title = {Effects of Chocolate Deprivation on Implicit and Explicit Evaluation of Chocolate in High and Low Trait Chocolate Cravers}, + doi = {10.3389/fpsyg.2017.01591}, + issn = {1664-1078}, + volume = {8}, + groups = {Used}, + publisher = {Frontiers Media SA}, + readstatus = {read}, +} + +@Article{Fulton_1969, + author = {Fulton, James E., Jr. and Plewig, Gerd and Kligman, Albert M.}, + date = {1969-12}, + journaltitle = {JAMA: The Journal of the American Medical Association}, + title = {{Effect of Chocolate on Acne Vulgaris}}, + doi = {10.1001/jama.1969.03160370055011}, + eprint = {https://jamanetwork.com/journals/jama/articlepdf/350738/jama\_210\_11\_011.pdf}, + issn = {0098-7484}, + number = {11}, + pages = {2071-2074}, + url = {https://doi.org/10.1001/jama.1969.03160370055011}, + volume = {210}, + publisher = {American Medical Association (AMA)}, +} + +@Comment{jabref-meta: databaseType:biblatex;} + +@Comment{jabref-meta: grouping: +0 AllEntriesGroup:; +1 SearchGroup:Entries without a group\;0\;groups != .+\;0\;1\;1\;\;\;\;; +1 SearchGroup:Entries without a linked file\;0\;file != .+\;0\;1\;1\;\;\;\;; +1 StaticGroup:Paywalled\;0\;1\;\;\;\;; +1 SearchGroup:To read\;0\;groups != .+ and readstatus != .+\;0\;1\;1\;0x008080ff\;\;\;; +1 KeywordGroup:Skimmed\;0\;readstatus\;skimmed\;0\;0\;1\;0xffff00ff\;\;\;; +1 KeywordGroup:Read\;0\;readstatus\;read\;0\;0\;0\;0x00ff00ff\;\;\;; +1 StaticGroup:Used\;0\;1\;0x0000ffff\;\;\;; +} From e55e7055c84505cb22acc35937a7a3e086306694 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Mon, 8 Jan 2024 21:49:53 +0100 Subject: [PATCH 15/25] Ignore submodule changes (for normal devs) (#10754) --- .github/workflows/refresh-csl-subtrees.yml | 5 +++++ .github/workflows/refresh-journal-lists.yml | 4 ++++ .gitmodules | 12 +++++++++--- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/.github/workflows/refresh-csl-subtrees.yml b/.github/workflows/refresh-csl-subtrees.yml index 3e86ac97267..789bd4b6ba0 100644 --- a/.github/workflows/refresh-csl-subtrees.yml +++ b/.github/workflows/refresh-csl-subtrees.yml @@ -31,6 +31,11 @@ jobs: cd src/main/resources/csl-locales git checkout master git pull + - name: Persist changes + run: | + git add -f src/main/resources/csl-styles + git add -f src/main/resources/csl-locales + git diff-index --quiet HEAD || git commit -m 'Update CSL styles' - uses: peter-evans/create-pull-request@v5 with: token: ${{ secrets.GH_TOKEN_UPDATE_GRADLE_WRAPPER }} diff --git a/.github/workflows/refresh-journal-lists.yml b/.github/workflows/refresh-journal-lists.yml index 3ee5042adf2..367c905031c 100644 --- a/.github/workflows/refresh-journal-lists.yml +++ b/.github/workflows/refresh-journal-lists.yml @@ -37,6 +37,10 @@ jobs: - name: Check whether journal-list.mv can be generated (the "real" generation is done inside JabRef's build process) run: | ./gradlew generateJournalListMV + - name: Persist changes + run: | + git add -f buildres/abbrv.jabref.org + git diff-index --quiet HEAD || git commit -m 'Update journal abbreviation lists' - uses: peter-evans/create-pull-request@v5 with: token: ${{ secrets.GH_TOKEN_UPDATE_GRADLE_WRAPPER }} diff --git a/.gitmodules b/.gitmodules index 39f867d9511..10944a3d5a0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,15 @@ -[submodule "buildres/abbrv.jabref.org"] +[submodule "abbrv.jabref.org"] path = buildres/abbrv.jabref.org url = https://github.com/JabRef/abbrv.jabref.org.git -[submodule "buildres/csl-styles"] + ignore = all + shallow = true +[submodule "csl-styles"] path = src/main/resources/csl-styles url = https://github.com/citation-style-language/styles.git -[submodule "buildres/csl-locales"] + ignore = all + shallow = true +[submodule "csl-locales"] path = src/main/resources/csl-locales url = https://github.com/citation-style-language/locales.git + ignore = all + shallow = true From bae698a58bd34db0ef5b146593e6d35d884143d4 Mon Sep 17 00:00:00 2001 From: Christoph Date: Mon, 8 Jan 2024 21:58:08 +0100 Subject: [PATCH 16/25] Add cites field to bib entries for citation relation (#10752) * Add cites field to bib entries for citation relation Change list view order Fixes https://github.com/JabRef/jabref-issue-melting-pot/issues/345 * add changelog fix l10n * remove * fix * add viewmodel and tests * fix checkstyle * fix * Minor updates ^^ --------- Co-authored-by: Oliver Kopp --- CHANGELOG.md | 3 + .../jabref/gui/entryeditor/EntryEditor.java | 2 +- .../CitationRelationItem.java | 19 +- .../CitationRelationsTab.java | 71 +++-- .../CitationsRelationsTabViewModel.java | 115 ++++++++ .../semanticscholar/AuthorResponse.java | 3 + .../semanticscholar/CitationDataItem.java | 3 + .../semanticscholar/CitationFetcher.java | 2 +- .../semanticscholar/CitationsResponse.java | 3 + .../semanticscholar/ReferenceDataItem.java | 7 +- .../semanticscholar/ReferencesResponse.java | 3 + .../jabref/model/database/BibDatabase.java | 15 +- .../model/entry/field/StandardField.java | 1 + src/main/resources/l10n/JabRef_en.properties | 2 +- .../CitationsRelationsTabViewModelTest.java | 131 ++++++++++ .../OpenOfficeDocumentCreatorTest.java | 1 - ...ficeCalcExportFormatContentSingleEntry.xml | 245 +++++++++--------- 17 files changed, 437 insertions(+), 189 deletions(-) create mode 100644 src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModel.java create mode 100644 src/test/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModelTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index ede9ae9a686..8506cd4d882 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,12 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Added +- When importing entries form the "Citation relations" tab, the field [cites](https://docs.jabref.org/advanced/entryeditor/entrylinks) is now filled according to the relationship between the entries. [#10572](https://github.com/JabRef/jabref/pull/10752) + ### Changed - The Custom export format now uses the custom DOI base URI in the preferences for the `DOICheck`, if activated [forum#4084](https://discourse.jabref.org/t/export-html-disregards-custom-doi-base-uri/4084) +- We changed the order of the lists in the "Citation relations" tab. `Cites` are now on the left and `Cited by` on the right [#10572](https://github.com/JabRef/jabref/pull/10752) ### Fixed diff --git a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java index 7ee6f1b6968..802c04194b4 100644 --- a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java +++ b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java @@ -285,7 +285,7 @@ private List createTabs() { entryEditorTabs.add(new FileAnnotationTab(libraryTab.getAnnotationCache())); entryEditorTabs.add(new RelatedArticlesTab(entryEditorPreferences, preferencesService, dialogService, taskExecutor)); entryEditorTabs.add(new CitationRelationsTab(entryEditorPreferences, dialogService, databaseContext, - undoManager, stateManager, fileMonitor, preferencesService, libraryTab)); + undoManager, stateManager, fileMonitor, preferencesService, libraryTab, taskExecutor)); sourceTab = new SourceTab( databaseContext, diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationItem.java b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationItem.java index aacbc90cc5f..0b0fe6acbd6 100644 --- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationItem.java +++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationItem.java @@ -5,20 +5,7 @@ /** * Class to hold a BibEntry and a boolean value whether it is already in the current database or not. */ -public class CitationRelationItem { - private final BibEntry entry; - private final boolean isLocal; - - public CitationRelationItem(BibEntry entry, boolean isLocal) { - this.entry = entry; - this.isLocal = isLocal; - } - - public BibEntry getEntry() { - return entry; - } - - public boolean isLocal() { - return isLocal; - } +public record CitationRelationItem( + BibEntry entry, + boolean isLocal) { } diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java index 01e2249d1b0..6be605c61c1 100644 --- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java @@ -32,10 +32,10 @@ import org.jabref.gui.entryeditor.EntryEditorTab; import org.jabref.gui.entryeditor.citationrelationtab.semanticscholar.CitationFetcher; import org.jabref.gui.entryeditor.citationrelationtab.semanticscholar.SemanticScholarFetcher; -import org.jabref.gui.externalfiles.ImportHandler; import org.jabref.gui.icon.IconTheme; import org.jabref.gui.util.BackgroundTask; import org.jabref.gui.util.NoSelectionModel; +import org.jabref.gui.util.TaskExecutor; import org.jabref.gui.util.ViewModelListCellFactory; import org.jabref.logic.l10n.Localization; import org.jabref.model.database.BibDatabaseContext; @@ -66,12 +66,14 @@ public class CitationRelationsTab extends EntryEditorTab { private final FileUpdateMonitor fileUpdateMonitor; private final PreferencesService preferencesService; private final LibraryTab libraryTab; + private final TaskExecutor taskExecutor; private final BibEntryRelationsRepository bibEntryRelationsRepository; + private final CitationsRelationsTabViewModel citationsRelationsTabViewModel; public CitationRelationsTab(EntryEditorPreferences preferences, DialogService dialogService, BibDatabaseContext databaseContext, UndoManager undoManager, StateManager stateManager, FileUpdateMonitor fileUpdateMonitor, - PreferencesService preferencesService, LibraryTab lTab) { + PreferencesService preferencesService, LibraryTab lTab, TaskExecutor taskExecutor) { this.preferences = preferences; this.dialogService = dialogService; this.databaseContext = databaseContext; @@ -80,11 +82,13 @@ public CitationRelationsTab(EntryEditorPreferences preferences, DialogService di this.fileUpdateMonitor = fileUpdateMonitor; this.preferencesService = preferencesService; this.libraryTab = lTab; + this.taskExecutor = taskExecutor; setText(Localization.lang("Citation relations")); setTooltip(new Tooltip(Localization.lang("Show articles related by citation"))); this.bibEntryRelationsRepository = new BibEntryRelationsRepository(new SemanticScholarFetcher(preferencesService.getImporterPreferences()), new BibEntryRelationsCache()); + citationsRelationsTabViewModel = new CitationsRelationsTabViewModel(databaseContext, preferencesService, undoManager, stateManager, dialogService, fileUpdateMonitor, Globals.TASK_EXECUTOR); } /** @@ -107,7 +111,7 @@ private SplitPane getPaneAndStartSearch(BibEntry entry) { citedByHBox.setPrefHeight(40); // Create Heading Lab - Label citingLabel = new Label(Localization.lang("Citing")); + Label citingLabel = new Label(Localization.lang("Cites")); styleLabel(citingLabel); Label citedByLabel = new Label(Localization.lang("Cited By")); styleLabel(citedByLabel); @@ -160,20 +164,19 @@ private SplitPane getPaneAndStartSearch(BibEntry entry) { refreshCitingButton.setOnMouseClicked(event -> { searchForRelations(entry, citingListView, abortCitingButton, - refreshCitingButton, CitationFetcher.SearchType.CITING, importCitingButton, citingProgress, true); + refreshCitingButton, CitationFetcher.SearchType.CITES, importCitingButton, citingProgress, true); }); refreshCitedByButton.setOnMouseClicked(event -> searchForRelations(entry, citedByListView, abortCitedButton, refreshCitedByButton, CitationFetcher.SearchType.CITED_BY, importCitedByButton, citedByProgress, true)); // Create SplitPane to hold all nodes above - SplitPane container = new SplitPane(citedByVBox, citingVBox); - - styleFetchedListView(citingListView); + SplitPane container = new SplitPane(citingVBox, citedByVBox); styleFetchedListView(citedByListView); + styleFetchedListView(citingListView); searchForRelations(entry, citingListView, abortCitingButton, refreshCitingButton, - CitationFetcher.SearchType.CITING, importCitingButton, citingProgress, false); + CitationFetcher.SearchType.CITES, importCitingButton, citingProgress, false); searchForRelations(entry, citedByListView, abortCitedButton, refreshCitedByButton, CitationFetcher.SearchType.CITED_BY, importCitedByButton, citedByProgress, false); @@ -193,7 +196,7 @@ private void styleFetchedListView(CheckListView listView) HBox separator = new HBox(); HBox.setHgrow(separator, Priority.SOMETIMES); - Node entryNode = BibEntryView.getEntryNode(entry.getEntry()); + Node entryNode = BibEntryView.getEntryNode(entry.entry()); HBox.setHgrow(entryNode, Priority.ALWAYS); HBox hContainer = new HBox(); hContainer.prefWidthProperty().bind(listView.widthProperty().subtract(25)); @@ -203,8 +206,8 @@ private void styleFetchedListView(CheckListView listView) jumpTo.setTooltip(new Tooltip(Localization.lang("Jump to entry in database"))); jumpTo.getStyleClass().add("addEntryButton"); jumpTo.setOnMouseClicked(event -> { - libraryTab.showAndEdit(entry.getEntry()); - libraryTab.clearAndSelect(entry.getEntry()); + libraryTab.showAndEdit(entry.entry()); + libraryTab.clearAndSelect(entry.entry()); citingTask.cancel(); citedByTask.cancel(); }); @@ -285,7 +288,7 @@ protected void bindToEntry(BibEntry entry) { * * @param entry BibEntry currently selected in Jabref Database * @param listView ListView to use - * @param abortButton Button to stop the search + * @param abortButton Button to stop the search * @param refreshButton refresh Button to use * @param searchType type of search (CITING / CITEDBY) */ @@ -305,7 +308,7 @@ private void searchForRelations(BibEntry entry, CheckListView> task; - if (searchType == CitationFetcher.SearchType.CITING) { + if (searchType == CitationFetcher.SearchType.CITES) { task = BackgroundTask.wrap(() -> { if (shouldRefresh) { bibEntryRelationsRepository.forceRefreshReferences(entry); @@ -332,18 +335,18 @@ private void searchForRelations(BibEntry entry, CheckListView prepareToSearchForRelations(abortButton, refreshButton, importButton, progress, task)) - .onSuccess(fetchedList -> onSearchForRelationsSucceed(entry, listView, abortButton, refreshButton, - searchType, importButton, progress, fetchedList, observableList)) - .onFailure(exception -> { - LOGGER.error("Error while fetching citing Articles", exception); - hideNodes(abortButton, progress, importButton); - listView.setPlaceholder(new Label(Localization.lang("Error while fetching citing entries: %0", - exception.getMessage()))); - - refreshButton.setVisible(true); - dialogService.notify(exception.getMessage()); - }) - .executeWith(Globals.TASK_EXECUTOR); + .onSuccess(fetchedList -> onSearchForRelationsSucceed(entry, listView, abortButton, refreshButton, + searchType, importButton, progress, fetchedList, observableList)) + .onFailure(exception -> { + LOGGER.error("Error while fetching citing Articles", exception); + hideNodes(abortButton, progress, importButton); + listView.setPlaceholder(new Label(Localization.lang("Error while fetching citing entries: %0", + exception.getMessage()))); + + refreshButton.setVisible(true); + dialogService.notify(exception.getMessage()); + }) + .executeWith(taskExecutor); } private void onSearchForRelationsSucceed(BibEntry entry, CheckListView listView, @@ -354,7 +357,7 @@ private void onSearchForRelationsSucceed(BibEntry entry, CheckListView new CitationRelationItem(entr, false)) - .collect(Collectors.toList())); + .collect(Collectors.toList())); if (!observableList.isEmpty()) { listView.refresh(); @@ -396,19 +399,11 @@ private void showNodes(Node... nodes) { * * @param entriesToImport entries to import */ - private void importEntries(List entriesToImport, CitationFetcher.SearchType searchType, BibEntry entry) { + private void importEntries(List entriesToImport, CitationFetcher.SearchType searchType, BibEntry existingEntry) { citingTask.cancel(); citedByTask.cancel(); - List entries = entriesToImport.stream().map(CitationRelationItem::getEntry).collect(Collectors.toList()); - ImportHandler importHandler = new ImportHandler( - databaseContext, - preferencesService, - fileUpdateMonitor, - undoManager, - stateManager, - dialogService, - Globals.TASK_EXECUTOR); - importHandler.importEntries(entries); + + citationsRelationsTabViewModel.importEntries(entriesToImport, searchType, existingEntry); dialogService.notify(Localization.lang("Number of entries successfully imported") + ": " + entriesToImport.size()); } diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModel.java b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModel.java new file mode 100644 index 00000000000..dd53b44ef58 --- /dev/null +++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModel.java @@ -0,0 +1,115 @@ +package org.jabref.gui.entryeditor.citationrelationtab; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import javax.swing.undo.UndoManager; + +import org.jabref.gui.DialogService; +import org.jabref.gui.StateManager; +import org.jabref.gui.entryeditor.citationrelationtab.semanticscholar.CitationFetcher; +import org.jabref.gui.externalfiles.ImportHandler; +import org.jabref.gui.util.TaskExecutor; +import org.jabref.logic.citationkeypattern.CitationKeyGenerator; +import org.jabref.logic.citationkeypattern.CitationKeyPatternPreferences; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.util.FileUpdateMonitor; +import org.jabref.preferences.PreferencesService; + +public class CitationsRelationsTabViewModel { + + private final BibDatabaseContext databaseContext; + private final PreferencesService preferencesService; + private final UndoManager undoManager; + private final StateManager stateManager; + private final DialogService dialogService; + private final FileUpdateMonitor fileUpdateMonitor; + private final TaskExecutor taskExecutor; + + public CitationsRelationsTabViewModel(BibDatabaseContext databaseContext, PreferencesService preferencesService, UndoManager undoManager, StateManager stateManager, DialogService dialogService, FileUpdateMonitor fileUpdateMonitor, TaskExecutor taskExecutor) { + this.databaseContext = databaseContext; + this.preferencesService = preferencesService; + this.undoManager = undoManager; + this.stateManager = stateManager; + this.dialogService = dialogService; + this.fileUpdateMonitor = fileUpdateMonitor; + this.taskExecutor = taskExecutor; + } + + public void importEntries(List entriesToImport, CitationFetcher.SearchType searchType, BibEntry existingEntry) { + List entries = entriesToImport.stream().map(CitationRelationItem::entry).toList(); + + ImportHandler importHandler = new ImportHandler( + databaseContext, + preferencesService, + fileUpdateMonitor, + undoManager, + stateManager, + dialogService, + taskExecutor); + + switch (searchType) { + case CITES -> importCites(entries, existingEntry, importHandler); + case CITED_BY -> importCitedBy(entries, existingEntry, importHandler); + } + } + + private void importCites(List entries, BibEntry existingEntry, ImportHandler importHandler) { + CitationKeyPatternPreferences citationKeyPatternPreferences = preferencesService.getCitationKeyPatternPreferences(); + CitationKeyGenerator generator = new CitationKeyGenerator(databaseContext, citationKeyPatternPreferences); + boolean generateNewKeyOnImport = preferencesService.getImporterPreferences().generateNewKeyOnImportProperty().get(); + + List citeKeys = getExistingEntriesFromCiteField(existingEntry); + citeKeys.removeIf(String::isEmpty); + for (BibEntry entryToCite : entries) { + if (generateNewKeyOnImport || entryToCite.getCitationKey().isEmpty()) { + String key = generator.generateKey(entryToCite); + entryToCite.setCitationKey(key); + addToKeyToList(citeKeys, key); + } else { + addToKeyToList(citeKeys, entryToCite.getCitationKey().get()); + } + } + existingEntry.setField(StandardField.CITES, toCommaSeparatedString(citeKeys)); + importHandler.importEntries(entries); + } + + private void importCitedBy(List entries, BibEntry existingEntry, ImportHandler importHandler) { + CitationKeyPatternPreferences citationKeyPatternPreferences = preferencesService.getCitationKeyPatternPreferences(); + CitationKeyGenerator generator = new CitationKeyGenerator(databaseContext, citationKeyPatternPreferences); + boolean generateNewKeyOnImport = preferencesService.getImporterPreferences().generateNewKeyOnImportProperty().get(); + + for (BibEntry entryThatCitesOurExistingEntry : entries) { + List existingCites = getExistingEntriesFromCiteField(entryThatCitesOurExistingEntry); + existingCites.removeIf(String::isEmpty); + String key; + if (generateNewKeyOnImport || entryThatCitesOurExistingEntry.getCitationKey().isEmpty()) { + key = generator.generateKey(entryThatCitesOurExistingEntry); + entryThatCitesOurExistingEntry.setCitationKey(key); + } else { + key = existingEntry.getCitationKey().get(); + } + addToKeyToList(existingCites, key); + entryThatCitesOurExistingEntry.setField(StandardField.CITES, toCommaSeparatedString(existingCites)); + } + + importHandler.importEntries(entries); + } + + private void addToKeyToList(List list, String key) { + if (!list.contains(key)) { + list.add(key); + } + } + + private List getExistingEntriesFromCiteField(BibEntry entry) { + return Arrays.stream(entry.getField(StandardField.CITES).orElse("").split(",")).collect(Collectors.toList()); + } + + private String toCommaSeparatedString(List citeentries) { + return String.join(",", citeentries); + } +} diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/AuthorResponse.java b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/AuthorResponse.java index f23ba10a54f..539b99cc39d 100644 --- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/AuthorResponse.java +++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/AuthorResponse.java @@ -1,5 +1,8 @@ package org.jabref.gui.entryeditor.citationrelationtab.semanticscholar; +/** + * Used for GSON + */ public class AuthorResponse { private String authorId; private String name; diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationDataItem.java b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationDataItem.java index 50a3143bf6c..684285b46df 100644 --- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationDataItem.java +++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationDataItem.java @@ -1,5 +1,8 @@ package org.jabref.gui.entryeditor.citationrelationtab.semanticscholar; +/** + * Used for GSON + */ public class CitationDataItem { private PaperDetails citingPaper; diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationFetcher.java b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationFetcher.java index 4b10cadce4f..1b87c7ab0bb 100644 --- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationFetcher.java +++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationFetcher.java @@ -14,7 +14,7 @@ public interface CitationFetcher { * Possible search methods */ enum SearchType { - CITING("reference"), + CITES("reference"), CITED_BY("citation"); public final String label; diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationsResponse.java b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationsResponse.java index c87eb9346c2..999eb7eca2a 100644 --- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationsResponse.java +++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/CitationsResponse.java @@ -2,6 +2,9 @@ import java.util.List; +/** + * Used for GSON + */ public class CitationsResponse { private int offset; private int next; diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/ReferenceDataItem.java b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/ReferenceDataItem.java index 4df4c64ea6a..b9c53c355e9 100644 --- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/ReferenceDataItem.java +++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/ReferenceDataItem.java @@ -1,13 +1,12 @@ package org.jabref.gui.entryeditor.citationrelationtab.semanticscholar; +/** + * Used for GSON + */ public class ReferenceDataItem { private PaperDetails citedPaper; public PaperDetails getCitedPaper() { return citedPaper; } - - public void setCitedPaper(PaperDetails citedPaper) { - this.citedPaper = citedPaper; - } } diff --git a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/ReferencesResponse.java b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/ReferencesResponse.java index 8f5506d9cb6..0a6ac34af07 100644 --- a/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/ReferencesResponse.java +++ b/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/semanticscholar/ReferencesResponse.java @@ -2,6 +2,9 @@ import java.util.List; +/** + * Used for GSON + */ public class ReferencesResponse { private int offset; private int next; diff --git a/src/main/java/org/jabref/model/database/BibDatabase.java b/src/main/java/org/jabref/model/database/BibDatabase.java index d8ab4556482..d1bb17b098d 100644 --- a/src/main/java/org/jabref/model/database/BibDatabase.java +++ b/src/main/java/org/jabref/model/database/BibDatabase.java @@ -148,12 +148,7 @@ public Set getAllVisibleFields() { * Returns the entry with the given citation key. */ public synchronized Optional getEntryByCitationKey(String key) { - for (BibEntry entry : entries) { - if (key.equals(entry.getCitationKey().orElse(null))) { - return Optional.of(entry); - } - } - return Optional.empty(); + return entries.stream().filter(entry -> Objects.equals(entry.getCitationKey().orElse(null), key)).findFirst(); } /** @@ -402,9 +397,9 @@ public Collection getUsedStrings(Collection entries) { * references. * * @param entriesToResolve A collection of BibtexEntries in which all strings of the form - * #xxx# will be resolved against the hash map of string - * references stored in the database. - * @param inPlace If inPlace is true then the given BibtexEntries will be modified, if false then copies of the BibtexEntries are made before resolving the strings. + * #xxx# will be resolved against the hash map of string + * references stored in the database. + * @param inPlace If inPlace is true then the given BibtexEntries will be modified, if false then copies of the BibtexEntries are made before resolving the strings. * @return a list of bibtexentries, with all strings resolved. It is dependent on the value of inPlace whether copies are made or the given BibtexEntries are modified. */ public List resolveForStrings(Collection entriesToResolve, boolean inPlace) { @@ -548,7 +543,7 @@ public void setEpilog(String epilog) { /** * Registers a listener object (subscriber) to the internal event bus. * The following events are posted: - * + *

* - {@link EntriesAddedEvent} * - {@link EntryChangedEvent} * - {@link EntriesRemovedEvent} diff --git a/src/main/java/org/jabref/model/entry/field/StandardField.java b/src/main/java/org/jabref/model/entry/field/StandardField.java index c286ca6ea3c..2d277de57b7 100644 --- a/src/main/java/org/jabref/model/entry/field/StandardField.java +++ b/src/main/java/org/jabref/model/entry/field/StandardField.java @@ -34,6 +34,7 @@ public enum StandardField implements Field { // Comments of users are handled at {@link org.jabref.model.entry.field.UserSpecificCommentField} COMMENT("comment", FieldProperty.COMMENT, FieldProperty.MULTILINE_TEXT, FieldProperty.VERBATIM), CROSSREF("crossref", FieldProperty.SINGLE_ENTRY_LINK), + CITES("cites", FieldProperty.MULTIPLE_ENTRY_LINK), DATE("date", FieldProperty.DATE), DAY("day"), DAYFILED("dayfiled"), diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties index e9edd71f8d8..3e766da2048 100644 --- a/src/main/resources/l10n/JabRef_en.properties +++ b/src/main/resources/l10n/JabRef_en.properties @@ -2605,7 +2605,7 @@ Get\ more\ themes...=Get more themes... Add\ selected\ entries\ to\ database=Add selected entries to database The\ selected\ entry\ doesn't\ have\ a\ DOI\ linked\ to\ it.\ Lookup\ a\ DOI\ and\ try\ again.=The selected entry doesn't have a DOI linked to it. Lookup a DOI and try again. Cited\ By=Cited By -Citing=Citing +Cites=Cites No\ articles\ found=No articles found Restart\ search=Restart search Cancel\ search=Cancel search diff --git a/src/test/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModelTest.java b/src/test/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModelTest.java new file mode 100644 index 00000000000..e3037b2edc6 --- /dev/null +++ b/src/test/java/org/jabref/gui/entryeditor/citationrelationtab/CitationsRelationsTabViewModelTest.java @@ -0,0 +1,131 @@ +package org.jabref.gui.entryeditor.citationrelationtab; + +import java.util.List; +import java.util.Optional; + +import javax.swing.undo.UndoManager; + +import org.jabref.gui.DialogService; +import org.jabref.gui.StateManager; +import org.jabref.gui.entryeditor.citationrelationtab.semanticscholar.CitationFetcher; +import org.jabref.gui.externalfiles.ImportHandler; +import org.jabref.gui.util.CurrentThreadTaskExecutor; +import org.jabref.logic.citationkeypattern.CitationKeyPatternPreferences; +import org.jabref.logic.citationkeypattern.GlobalCitationKeyPattern; +import org.jabref.logic.database.DuplicateCheck; +import org.jabref.logic.importer.ImportFormatPreferences; +import org.jabref.logic.importer.ImporterPreferences; +import org.jabref.logic.preferences.OwnerPreferences; +import org.jabref.logic.preferences.TimestampPreferences; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.StandardEntryType; +import org.jabref.model.util.DummyFileUpdateMonitor; +import org.jabref.preferences.FilePreferences; +import org.jabref.preferences.PreferencesService; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class CitationsRelationsTabViewModelTest { + private ImportHandler importHandler; + private BibDatabaseContext bibDatabaseContext; + private BibEntry testEntry; + + @Mock + private PreferencesService preferencesService; + @Mock + private DuplicateCheck duplicateCheck; + private BibEntry existingEntry; + private BibEntry firstEntryToImport; + private BibEntry secondEntryToImport; + private CitationsRelationsTabViewModel viewModel; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + + ImportFormatPreferences importFormatPreferences = mock(ImportFormatPreferences.class, Answers.RETURNS_DEEP_STUBS); + when(preferencesService.getImportFormatPreferences()).thenReturn(importFormatPreferences); + + ImporterPreferences importerPreferences = mock(ImporterPreferences.class, Answers.RETURNS_DEEP_STUBS); + when(importerPreferences.isGenerateNewKeyOnImport()).thenReturn(false); + when(preferencesService.getImporterPreferences()).thenReturn(importerPreferences); + + when(preferencesService.getFilePreferences()).thenReturn(mock(FilePreferences.class)); + when(preferencesService.getOwnerPreferences()).thenReturn(mock(OwnerPreferences.class, Answers.RETURNS_DEEP_STUBS)); + when(preferencesService.getTimestampPreferences()).thenReturn(mock(TimestampPreferences.class, Answers.RETURNS_DEEP_STUBS)); + + CitationKeyPatternPreferences citationKeyPatternPreferences = mock(CitationKeyPatternPreferences.class); + GlobalCitationKeyPattern pattern = GlobalCitationKeyPattern.fromPattern("[auth][year]"); + when(citationKeyPatternPreferences.getKeyPattern()).thenReturn(pattern); + when(preferencesService.getCitationKeyPatternPreferences()).thenReturn(citationKeyPatternPreferences); + + bibDatabaseContext = new BibDatabaseContext(new BibDatabase()); + when(duplicateCheck.isDuplicate(any(), any(), any())).thenReturn(false); + + viewModel = new CitationsRelationsTabViewModel( + bibDatabaseContext, + preferencesService, + mock(UndoManager.class), + mock(StateManager.class, Answers.RETURNS_DEEP_STUBS), + mock(DialogService.class), + new DummyFileUpdateMonitor(), + new CurrentThreadTaskExecutor()); + + existingEntry = new BibEntry(StandardEntryType.Article) + .withCitationKey("Test2023") + .withField(StandardField.AUTHOR, "Test Author"); + + bibDatabaseContext.getDatabase().insertEntry(existingEntry); + + firstEntryToImport = new BibEntry(StandardEntryType.Article).withField(StandardField.AUTHOR, "First Author") + .withField(StandardField.YEAR, "2022") + .withCitationKey("FirstAuthorCitationKey2022"); + + secondEntryToImport = new BibEntry(StandardEntryType.Article).withField(StandardField.AUTHOR, "Second Author") + .withField(StandardField.YEAR, "2021") + .withCitationKey("SecondAuthorCitationKey20221"); + } + + @Test + void testExistingEntryCitesOtherPaperWithCitationKeys() { + var citationItems = List.of(new CitationRelationItem(firstEntryToImport, false), + new CitationRelationItem(secondEntryToImport, false)); + + viewModel.importEntries(citationItems, CitationFetcher.SearchType.CITES, existingEntry); + assertEquals(Optional.of("FirstAuthorCitationKey2022,SecondAuthorCitationKey20221"), existingEntry.getField(StandardField.CITES)); + assertEquals(List.of(existingEntry, firstEntryToImport, secondEntryToImport), bibDatabaseContext.getEntries()); + } + + @Test + void testImportedEntriesWithExistingCitationKeysCiteExistingEntry() { + var citationItems = List.of(new CitationRelationItem(firstEntryToImport, false), + new CitationRelationItem(secondEntryToImport, false)); + + viewModel.importEntries(citationItems, CitationFetcher.SearchType.CITED_BY, existingEntry); + assertEquals(Optional.of("Test2023"), firstEntryToImport.getField(StandardField.CITES)); + assertEquals(List.of(existingEntry, firstEntryToImport, secondEntryToImport), bibDatabaseContext.getEntries()); + } + + @Test + void testExistingEntryCitesOtherPaperWithCitationKeysAndExistingCiteField() { + existingEntry.setField(StandardField.CITES, "Asdf1222"); + var citationItems = List.of(new CitationRelationItem(firstEntryToImport, false), + new CitationRelationItem(secondEntryToImport, false)); + + viewModel.importEntries(citationItems, CitationFetcher.SearchType.CITES, existingEntry); + assertEquals(Optional.of("Asdf1222,FirstAuthorCitationKey2022,SecondAuthorCitationKey20221"), existingEntry.getField(StandardField.CITES)); + assertEquals(List.of(existingEntry, firstEntryToImport, secondEntryToImport), bibDatabaseContext.getEntries()); + } +} diff --git a/src/test/java/org/jabref/logic/exporter/OpenOfficeDocumentCreatorTest.java b/src/test/java/org/jabref/logic/exporter/OpenOfficeDocumentCreatorTest.java index 53c0d088a45..db1cffa4ab1 100644 --- a/src/test/java/org/jabref/logic/exporter/OpenOfficeDocumentCreatorTest.java +++ b/src/test/java/org/jabref/logic/exporter/OpenOfficeDocumentCreatorTest.java @@ -73,7 +73,6 @@ void testPerformExportForSingleEntry(@TempDir Path testFolder) throws Exception Input.Builder control = Input.from(Files.newInputStream(xmlFile)); Input.Builder test = Input.from(Files.newInputStream(contentXmlPath)); - // for debugging purposes // Path testPath = xmlFile.resolveSibling("test.xml"); // Files.copy(Files.newInputStream(contentXmlPath), testPath, StandardCopyOption.REPLACE_EXISTING); diff --git a/src/test/resources/org/jabref/logic/exporter/OldOpenOfficeCalcExportFormatContentSingleEntry.xml b/src/test/resources/org/jabref/logic/exporter/OldOpenOfficeCalcExportFormatContentSingleEntry.xml index 0594bb222be..618b78d30a2 100644 --- a/src/test/resources/org/jabref/logic/exporter/OldOpenOfficeCalcExportFormatContentSingleEntry.xml +++ b/src/test/resources/org/jabref/logic/exporter/OldOpenOfficeCalcExportFormatContentSingleEntry.xml @@ -1,12 +1,16 @@ - - + + - + - + @@ -75,6 +79,9 @@ Crossref + + Cites + Date @@ -387,372 +394,376 @@ 7 - + - + - + New York, NY, USA - + - + - + - + - + - + Tony Clear - + + + + - + - + - + - + - + - + - + - + - + - + - + http://doi.acm.org/10.1145/820127.820136 - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + 0097-8418 - + - + - + SIGCSE Bull. - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + 4 - + - + - + 13--14 - + - + - + - + - + - + ACM - + - + - + - + - + - + - + - + - + - + - + - + - + - + - Design and usability in security systems: daily life as a context of use? + Design and usability in security systems: daily life as a context of + use? - + - + - + - + - + - + - + - + 34 - + 2002 - + - + - + - + - + - + - + - + - + - + - + - + \ No newline at end of file From 0c26a550c252438b7a0696eaab5c83fa8a148d29 Mon Sep 17 00:00:00 2001 From: Luggas <127773292+Luggas4you@users.noreply.github.com> Date: Mon, 8 Jan 2024 23:58:40 +0100 Subject: [PATCH 17/25] Implement test cases for search (#10193) * Move search folder to logic * Add testEmptyLibrarySearch * Add initializeDatabaseFromPath * Add TestLibraryA & testUpperAndLowerWordSearch * Add testSimpleSingleFieldSearch * Add testSimpleMultipleFieldSearch * Add testSensitiveWordSearch * Add testSensitiveMutipleFieldSearch * Add BibEntries for test-library-B * Add testSimpleRegularExpression * Add testSensitiveRegularExpression * SimplePDFFulltextSearch WIP * Change set.Files to add.File * Add testSimplePDFNoteFulltextSearch WIP * Move indexer * Add missing @Tests to SearchFunctionalityTest.java WIP * WIP * Exclude non-functional @Tests * Add withFiles * Remove empty lines * @NullMarked for LinkedFile * Fix Logger parameter * Streamline tests * Fix checkstyle * Refine tests * Get rid of missing "identity" formatter * Minimize test files * Remove obsolete method (and make indexer class variable to speedup) * Fix filenames * Some refactorings * Revert global indexer mapping (does not work) ERROR: Could not retrieve search results.: org.apache.lucene.index.IndexNotFoundException: no segments* file found in NIOFSDirectory@C:\Users\koppor\AppData\Local\Temp\junit11457118934853053051 lockFactory=org.apache.lucene.store.NativeFSLockFactory@5d6751d7: files: [write.lock] * Revert "Revert global indexer mapping (does not work)" This reverts commit e0810441971ad7d456683903ce9bb7d0fbeb729e. * Small code updates * Rename test files * Some more logging * Refine transaction boundaries (and some minor tweaks) * Add TODO * Fix off-by-one error * Remove non-used .bib file * Refine comments * Fix variable assignment * test-library-A -> test-library-title-casing * Merge test cases of library-B.bib into title-casing.bib * Increase transaction boundary for index addition/removal * More readable directory names for index directory * WIP: Introduce PdfIndexerManager * Preparation for "fulltext search not checking all attached files upon start" * Use "right" factory (and rename factor getter) * Fix FullTextSeachRule (refactoring introduced a bug) * Revert property for recheck of attached files * Fix linting issues * Fix Formatters optimization * Fix test * Exception for architecture * Refine .gitignore * Add missing } * Do not search for PDF files in case of an exception of a search * Remove duplicate code (and unneccsary pre-fetch of search results) * Add exception for test * Remove ".getMessage()" * Move comment and remove obsolete variable * Fix typo * Fix name * Add dot * Add comment * Call splitting method * Add JavaDocComment * Revert lambdas --------- Co-authored-by: Oliver Kopp --- CHANGELOG.md | 3 + .../org/jabref/gui/JabRefExecutorService.java | 4 + src/main/java/org/jabref/gui/JabRefFrame.java | 2 + src/main/java/org/jabref/gui/JabRefGUI.java | 2 +- src/main/java/org/jabref/gui/LibraryTab.java | 20 +- .../java/org/jabref/gui/StateManager.java | 7 - .../jabref/gui/entryeditor/CommentsTab.java | 2 +- .../gui/entryeditor/DeprecatedFieldsTab.java | 2 +- .../gui/entryeditor/FieldsEditorTab.java | 2 +- .../gui/entryeditor/OptionalFields2Tab.java | 2 +- .../gui/entryeditor/OptionalFieldsTab.java | 2 +- .../entryeditor/OptionalFieldsTabBase.java | 2 +- .../gui/entryeditor/OtherFieldsTab.java | 2 +- .../jabref/gui/entryeditor/PreviewTab.java | 2 +- .../gui/entryeditor/RequiredFieldsTab.java | 2 +- .../gui/entryeditor/UserDefinedFieldsTab.java | 2 +- .../gui/exporter/SaveDatabaseAction.java | 7 +- .../ExternalFilesEntryLinker.java | 10 +- .../org/jabref/gui/preview/PreviewPanel.java | 2 +- .../RebuildFulltextSearchIndexAction.java | 7 +- .../logic/cleanup/FieldFormatterCleanups.java | 17 +- .../jabref/logic/formatter/Formatters.java | 15 +- .../importer/fileformat/BibtexParser.java | 6 +- .../logic/importer/util/MetaDataParser.java | 4 +- .../search/{indexing => }/DocumentReader.java | 29 +-- .../{indexing => }/IndexingTaskManager.java | 54 +++-- .../pdf/search/{indexing => }/PdfIndexer.java | 203 ++++++++++++------ .../logic/pdf/search/PdfIndexerManager.java | 79 +++++++ .../search/{retrieval => }/PdfSearcher.java | 41 ++-- .../jabref/logic/search/DatabaseSearcher.java | 10 +- .../org/jabref/logic/search/SearchQuery.java | 2 +- .../jabref/logic/util/StandardFileType.java | 3 - .../model/database/BibDatabaseContext.java | 10 +- .../jabref/model/database/BibDatabases.java | 2 +- .../java/org/jabref/model/entry/BibEntry.java | 24 ++- .../org/jabref/model/entry/LinkedFile.java | 19 +- .../search/rules/ContainsBasedSearchRule.java | 6 +- .../search/rules/FullTextSearchRule.java | 45 ++-- .../search/rules/GrammarBasedSearchRule.java | 33 +-- .../architecture/TestArchitectureTest.java | 1 + .../gui/entryeditor/CommentsTabTest.java | 2 +- .../cleanup/FieldFormatterCleanupsTest.java | 39 +++- .../importer/util/MetaDataParserTest.java | 21 ++ .../{indexing => }/DocumentReaderTest.java | 2 +- .../search/{indexing => }/PdfIndexerTest.java | 58 ++--- .../{retrieval => }/PdfSearcherTest.java | 37 ++-- .../logic/search/DatabaseSearcherTest.java | 2 - .../DatabaseSearcherWithBibFilesTest.java | 172 +++++++++++++++ .../jabref/logic/search/SearchQueryTest.java | 4 +- .../database/BibDatabaseContextTest.java | 7 +- .../jabref/model/groups/SearchGroupTest.java | 28 +++ .../org/jabref/logic/search}/.gitignore | 2 + .../org/jabref/logic/search/README.md | 11 + .../org/jabref/logic/search}/empty.bib | 0 .../logic/search/minimal-all-upper-case.pdf} | Bin 15486 -> 14598 bytes .../logic/search/minimal-all-upper-case.tex} | 6 +- .../logic/search/minimal-mixed-case.pdf} | Bin 15117 -> 15117 bytes .../logic/search/minimal-mixed-case.tex} | 4 - .../search/minimal-note-all-upper-case.pdf | Bin 0 -> 15771 bytes .../search/minimal-note-all-upper-case.tex} | 8 +- .../logic/search/minimal-note-mixed-case.pdf | Bin 0 -> 15771 bytes .../logic/search/minimal-note-mixed-case.tex} | 8 +- .../search/minimal-note-sentence-case.pdf | Bin 0 -> 15771 bytes .../search/minimal-note-sentence-case.tex} | 8 +- .../logic/search/minimal-sentence-case.pdf} | Bin 14866 -> 14867 bytes .../logic/search/minimal-sentence-case.tex} | 4 - .../search/test-library-title-casing.bib | 13 ++ .../test-library-with-attached-files.bib | 27 +++ src/test/search/README.md | 9 - src/test/search/resources/minimal-note.pdf | Bin 15754 -> 0 bytes src/test/search/resources/minimal-note2.pdf | Bin 16010 -> 0 bytes src/test/search/resources/minimal1.pdf | Bin 15621 -> 0 bytes src/test/search/resources/test-library-A.bib | 26 --- src/test/search/resources/test-library-B.bib | 21 -- src/test/search/resources/test-library-C.bib | 34 --- src/test/search/resources/test-library-D.bib | 55 ----- 76 files changed, 790 insertions(+), 505 deletions(-) rename src/main/java/org/jabref/logic/pdf/search/{indexing => }/DocumentReader.java (87%) rename src/main/java/org/jabref/logic/pdf/search/{indexing => }/IndexingTaskManager.java (63%) rename src/main/java/org/jabref/logic/pdf/search/{indexing => }/PdfIndexer.java (50%) create mode 100644 src/main/java/org/jabref/logic/pdf/search/PdfIndexerManager.java rename src/main/java/org/jabref/logic/pdf/search/{retrieval => }/PdfSearcher.java (64%) rename src/test/java/org/jabref/logic/pdf/search/{indexing => }/DocumentReaderTest.java (98%) rename src/test/java/org/jabref/logic/pdf/search/{indexing => }/PdfIndexerTest.java (74%) rename src/test/java/org/jabref/logic/pdf/search/{retrieval => }/PdfSearcherTest.java (76%) create mode 100644 src/test/java/org/jabref/logic/search/DatabaseSearcherWithBibFilesTest.java rename src/test/{search/resources => resources/org/jabref/logic/search}/.gitignore (83%) create mode 100644 src/test/resources/org/jabref/logic/search/README.md rename src/test/{search/resources => resources/org/jabref/logic/search}/empty.bib (100%) rename src/test/{search/resources/minimal-note1.pdf => resources/org/jabref/logic/search/minimal-all-upper-case.pdf} (89%) rename src/test/{search/resources/minimal1.tex => resources/org/jabref/logic/search/minimal-all-upper-case.tex} (53%) rename src/test/{search/resources/minimal2.pdf => resources/org/jabref/logic/search/minimal-mixed-case.pdf} (97%) rename src/test/{search/resources/minimal2.tex => resources/org/jabref/logic/search/minimal-mixed-case.tex} (80%) create mode 100644 src/test/resources/org/jabref/logic/search/minimal-note-all-upper-case.pdf rename src/test/{search/resources/minimal-note1.tex => resources/org/jabref/logic/search/minimal-note-all-upper-case.tex} (63%) create mode 100644 src/test/resources/org/jabref/logic/search/minimal-note-mixed-case.pdf rename src/test/{search/resources/minimal-note2.tex => resources/org/jabref/logic/search/minimal-note-mixed-case.tex} (63%) create mode 100644 src/test/resources/org/jabref/logic/search/minimal-note-sentence-case.pdf rename src/test/{search/resources/minimal-note.tex => resources/org/jabref/logic/search/minimal-note-sentence-case.tex} (63%) rename src/test/{search/resources/minimal.pdf => resources/org/jabref/logic/search/minimal-sentence-case.pdf} (96%) rename src/test/{search/resources/minimal.tex => resources/org/jabref/logic/search/minimal-sentence-case.tex} (97%) create mode 100644 src/test/resources/org/jabref/logic/search/test-library-title-casing.bib create mode 100644 src/test/resources/org/jabref/logic/search/test-library-with-attached-files.bib delete mode 100644 src/test/search/README.md delete mode 100644 src/test/search/resources/minimal-note.pdf delete mode 100644 src/test/search/resources/minimal-note2.pdf delete mode 100644 src/test/search/resources/minimal1.pdf delete mode 100644 src/test/search/resources/test-library-A.bib delete mode 100644 src/test/search/resources/test-library-B.bib delete mode 100644 src/test/search/resources/test-library-C.bib delete mode 100644 src/test/search/resources/test-library-D.bib diff --git a/CHANGELOG.md b/CHANGELOG.md index 8506cd4d882..ddbd81a149c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,12 +16,15 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Changed - The Custom export format now uses the custom DOI base URI in the preferences for the `DOICheck`, if activated [forum#4084](https://discourse.jabref.org/t/export-html-disregards-custom-doi-base-uri/4084) +- The index directories for full text search have now more readable names to increase debugging possibilities using Apache Lucense's Lurk. [#10193](https://github.com/JabRef/jabref/issues/10193) +- The fulltext search also indexes files ending with .pdf (but do not having an explicit file type set). [#10193](https://github.com/JabRef/jabref/issues/10193) - We changed the order of the lists in the "Citation relations" tab. `Cites` are now on the left and `Cited by` on the right [#10572](https://github.com/JabRef/jabref/pull/10752) ### Fixed - We fixed an issue where attempting to cancel the importing/generation of an entry from id is ignored. [#10508](https://github.com/JabRef/jabref/issues/10508) - We fixed an issue where the preview panel showing the wrong entry (an entry that is not selected in the entry table). [#9172](https://github.com/JabRef/jabref/issues/9172) +- The last page of a PDF is now indexed by the full text search. [#10193](https://github.com/JabRef/jabref/issues/10193) - We fixed an issue where the duplicate check did not take umlauts or other LaTeX-encoded characters into account. [#10744](https://github.com/JabRef/jabref/pull/10744) - We fixed the colors of the icon on hover for unset special fields. [#10431](https://github.com/JabRef/jabref/issues/10431) diff --git a/src/main/java/org/jabref/gui/JabRefExecutorService.java b/src/main/java/org/jabref/gui/JabRefExecutorService.java index b23beeb8d2c..90c3b31bdae 100644 --- a/src/main/java/org/jabref/gui/JabRefExecutorService.java +++ b/src/main/java/org/jabref/gui/JabRefExecutorService.java @@ -13,6 +13,8 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import org.jabref.logic.pdf.search.PdfIndexerManager; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -146,6 +148,8 @@ public void shutdownEverything() { gracefullyShutdown(this.executorService); gracefullyShutdown(this.lowPriorityExecutorService); + PdfIndexerManager.shutdownAllIndexers(); + timer.cancel(); } diff --git a/src/main/java/org/jabref/gui/JabRefFrame.java b/src/main/java/org/jabref/gui/JabRefFrame.java index 926dc41d6ef..0631c7fedc5 100644 --- a/src/main/java/org/jabref/gui/JabRefFrame.java +++ b/src/main/java/org/jabref/gui/JabRefFrame.java @@ -61,6 +61,7 @@ import org.jabref.logic.importer.ImportCleanup; import org.jabref.logic.importer.ParserResult; import org.jabref.logic.l10n.Localization; +import org.jabref.logic.pdf.search.PdfIndexerManager; import org.jabref.logic.shared.DatabaseLocation; import org.jabref.logic.undo.AddUndoableActionEvent; import org.jabref.logic.undo.UndoChangeEvent; @@ -862,6 +863,7 @@ public void closeTab(LibraryTab libraryTab) { } AutosaveManager.shutdown(context); BackupManager.shutdown(context, prefs.getFilePreferences().getBackupDirectory(), prefs.getFilePreferences().shouldCreateBackup()); + PdfIndexerManager.shutdownAllIndexers(); } private void removeTab(LibraryTab libraryTab) { diff --git a/src/main/java/org/jabref/gui/JabRefGUI.java b/src/main/java/org/jabref/gui/JabRefGUI.java index 134ff626773..657429b2e3c 100644 --- a/src/main/java/org/jabref/gui/JabRefGUI.java +++ b/src/main/java/org/jabref/gui/JabRefGUI.java @@ -262,7 +262,7 @@ private void openDatabases() { for (int tabNumber = 0; tabNumber < parserResults.size(); tabNumber++) { // ToDo: Method needs to be rewritten, because the index of the parser result and of the libraryTab may not // be identical, if there are also other tabs opened, that are not libraryTabs. Currently there are none, - // therefor for now this ok. + // therefore for now this ok. ParserResult pr = parserResults.get(tabNumber); if (pr.hasWarnings()) { ParserResultWarningDialog.showParserResultWarningDialog(pr, mainFrame.getDialogService()); diff --git a/src/main/java/org/jabref/gui/LibraryTab.java b/src/main/java/org/jabref/gui/LibraryTab.java index 341476d2098..658117d8483 100644 --- a/src/main/java/org/jabref/gui/LibraryTab.java +++ b/src/main/java/org/jabref/gui/LibraryTab.java @@ -51,8 +51,9 @@ 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.pdf.search.IndexingTaskManager; +import org.jabref.logic.pdf.search.PdfIndexer; +import org.jabref.logic.pdf.search.PdfIndexerManager; import org.jabref.logic.search.SearchQuery; import org.jabref.logic.shared.DatabaseLocation; import org.jabref.logic.util.UpdateField; @@ -233,7 +234,7 @@ public void onDatabaseLoadingSucceed(ParserResult result) { if (preferencesService.getFilePreferences().shouldFulltextIndexLinkedFiles()) { try { - indexingTaskManager.updateIndex(PdfIndexer.of(bibDatabaseContext, preferencesService.getFilePreferences()), bibDatabaseContext); + indexingTaskManager.updateIndex(PdfIndexerManager.getIndexer(bibDatabaseContext, preferencesService.getFilePreferences()), bibDatabaseContext); } catch (IOException e) { LOGGER.error("Cannot access lucene index", e); } @@ -912,10 +913,8 @@ private class IndexUpdateListener { public void listen(EntriesAddedEvent addedEntryEvent) { if (preferencesService.getFilePreferences().shouldFulltextIndexLinkedFiles()) { try { - PdfIndexer pdfIndexer = PdfIndexer.of(bibDatabaseContext, preferencesService.getFilePreferences()); - for (BibEntry addedEntry : addedEntryEvent.getBibEntries()) { - indexingTaskManager.addToIndex(pdfIndexer, addedEntry, bibDatabaseContext); - } + PdfIndexer pdfIndexer = PdfIndexerManager.getIndexer(bibDatabaseContext, preferencesService.getFilePreferences()); + indexingTaskManager.addToIndex(pdfIndexer, addedEntryEvent.getBibEntries()); } catch (IOException e) { LOGGER.error("Cannot access lucene index", e); } @@ -926,7 +925,7 @@ public void listen(EntriesAddedEvent addedEntryEvent) { public void listen(EntriesRemovedEvent removedEntriesEvent) { if (preferencesService.getFilePreferences().shouldFulltextIndexLinkedFiles()) { try { - PdfIndexer pdfIndexer = PdfIndexer.of(bibDatabaseContext, preferencesService.getFilePreferences()); + PdfIndexer pdfIndexer = PdfIndexerManager.getIndexer(bibDatabaseContext, preferencesService.getFilePreferences()); for (BibEntry removedEntry : removedEntriesEvent.getBibEntries()) { indexingTaskManager.removeFromIndex(pdfIndexer, removedEntry); } @@ -949,8 +948,9 @@ public void listen(FieldChangedEvent fieldChangedEvent) { 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); + PdfIndexer indexer = PdfIndexerManager.getIndexer(bibDatabaseContext, preferencesService.getFilePreferences()); + indexingTaskManager.addToIndex(indexer, fieldChangedEvent.getBibEntry(), addedFiles); + indexingTaskManager.removeFromIndex(indexer, removedFiles); } catch (IOException e) { LOGGER.warn("I/O error when writing lucene index", e); } diff --git a/src/main/java/org/jabref/gui/StateManager.java b/src/main/java/org/jabref/gui/StateManager.java index f1ff42c5785..743ad0ef473 100644 --- a/src/main/java/org/jabref/gui/StateManager.java +++ b/src/main/java/org/jabref/gui/StateManager.java @@ -4,7 +4,6 @@ import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.stream.Collectors; import javafx.beans.Observable; import javafx.beans.binding.Bindings; @@ -31,7 +30,6 @@ import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.groups.GroupTreeNode; -import org.jabref.model.util.OptionalUtil; import com.tobiasdiez.easybind.EasyBind; import com.tobiasdiez.easybind.EasyBinding; @@ -144,11 +142,6 @@ public void setActiveDatabase(BibDatabaseContext database) { } } - public List getEntriesInCurrentDatabase() { - return OptionalUtil.flatMap(activeDatabase.get(), BibDatabaseContext::getEntries) - .collect(Collectors.toList()); - } - public void clearSearchQuery() { activeSearchQuery.setValue(Optional.empty()); } diff --git a/src/main/java/org/jabref/gui/entryeditor/CommentsTab.java b/src/main/java/org/jabref/gui/entryeditor/CommentsTab.java index dc4434ce5d4..4aa92a3520d 100644 --- a/src/main/java/org/jabref/gui/entryeditor/CommentsTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/CommentsTab.java @@ -25,7 +25,7 @@ import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.journals.JournalAbbreviationRepository; import org.jabref.logic.l10n.Localization; -import org.jabref.logic.pdf.search.indexing.IndexingTaskManager; +import org.jabref.logic.pdf.search.IndexingTaskManager; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.Field; diff --git a/src/main/java/org/jabref/gui/entryeditor/DeprecatedFieldsTab.java b/src/main/java/org/jabref/gui/entryeditor/DeprecatedFieldsTab.java index ca15fe49b7c..a66339ea7b6 100644 --- a/src/main/java/org/jabref/gui/entryeditor/DeprecatedFieldsTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/DeprecatedFieldsTab.java @@ -17,7 +17,7 @@ import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.journals.JournalAbbreviationRepository; import org.jabref.logic.l10n.Localization; -import org.jabref.logic.pdf.search.indexing.IndexingTaskManager; +import org.jabref.logic.pdf.search.IndexingTaskManager; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.database.BibDatabaseMode; import org.jabref.model.entry.BibEntry; diff --git a/src/main/java/org/jabref/gui/entryeditor/FieldsEditorTab.java b/src/main/java/org/jabref/gui/entryeditor/FieldsEditorTab.java index bdaf404b4c1..20e54396bfe 100644 --- a/src/main/java/org/jabref/gui/entryeditor/FieldsEditorTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/FieldsEditorTab.java @@ -34,7 +34,7 @@ import org.jabref.gui.theme.ThemeManager; import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.journals.JournalAbbreviationRepository; -import org.jabref.logic.pdf.search.indexing.IndexingTaskManager; +import org.jabref.logic.pdf.search.IndexingTaskManager; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.Field; diff --git a/src/main/java/org/jabref/gui/entryeditor/OptionalFields2Tab.java b/src/main/java/org/jabref/gui/entryeditor/OptionalFields2Tab.java index da37540e513..c63c145cfba 100644 --- a/src/main/java/org/jabref/gui/entryeditor/OptionalFields2Tab.java +++ b/src/main/java/org/jabref/gui/entryeditor/OptionalFields2Tab.java @@ -9,7 +9,7 @@ import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.journals.JournalAbbreviationRepository; import org.jabref.logic.l10n.Localization; -import org.jabref.logic.pdf.search.indexing.IndexingTaskManager; +import org.jabref.logic.pdf.search.IndexingTaskManager; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntryTypesManager; import org.jabref.preferences.PreferencesService; diff --git a/src/main/java/org/jabref/gui/entryeditor/OptionalFieldsTab.java b/src/main/java/org/jabref/gui/entryeditor/OptionalFieldsTab.java index d511ef268cf..ad522d2411f 100644 --- a/src/main/java/org/jabref/gui/entryeditor/OptionalFieldsTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/OptionalFieldsTab.java @@ -9,7 +9,7 @@ import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.journals.JournalAbbreviationRepository; import org.jabref.logic.l10n.Localization; -import org.jabref.logic.pdf.search.indexing.IndexingTaskManager; +import org.jabref.logic.pdf.search.IndexingTaskManager; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntryTypesManager; import org.jabref.preferences.PreferencesService; diff --git a/src/main/java/org/jabref/gui/entryeditor/OptionalFieldsTabBase.java b/src/main/java/org/jabref/gui/entryeditor/OptionalFieldsTabBase.java index 2bfdc10b92b..5f61a0279ab 100644 --- a/src/main/java/org/jabref/gui/entryeditor/OptionalFieldsTabBase.java +++ b/src/main/java/org/jabref/gui/entryeditor/OptionalFieldsTabBase.java @@ -16,7 +16,7 @@ import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.journals.JournalAbbreviationRepository; import org.jabref.logic.l10n.Localization; -import org.jabref.logic.pdf.search.indexing.IndexingTaskManager; +import org.jabref.logic.pdf.search.IndexingTaskManager; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.database.BibDatabaseMode; import org.jabref.model.entry.BibEntry; diff --git a/src/main/java/org/jabref/gui/entryeditor/OtherFieldsTab.java b/src/main/java/org/jabref/gui/entryeditor/OtherFieldsTab.java index ed388027b69..4ed6b1434ad 100644 --- a/src/main/java/org/jabref/gui/entryeditor/OtherFieldsTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/OtherFieldsTab.java @@ -20,7 +20,7 @@ import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.journals.JournalAbbreviationRepository; import org.jabref.logic.l10n.Localization; -import org.jabref.logic.pdf.search.indexing.IndexingTaskManager; +import org.jabref.logic.pdf.search.IndexingTaskManager; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.database.BibDatabaseMode; import org.jabref.model.entry.BibEntry; diff --git a/src/main/java/org/jabref/gui/entryeditor/PreviewTab.java b/src/main/java/org/jabref/gui/entryeditor/PreviewTab.java index 1555f577c45..648cbe003a8 100644 --- a/src/main/java/org/jabref/gui/entryeditor/PreviewTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/PreviewTab.java @@ -7,7 +7,7 @@ import org.jabref.gui.theme.ThemeManager; import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.l10n.Localization; -import org.jabref.logic.pdf.search.indexing.IndexingTaskManager; +import org.jabref.logic.pdf.search.IndexingTaskManager; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.preferences.PreferencesService; diff --git a/src/main/java/org/jabref/gui/entryeditor/RequiredFieldsTab.java b/src/main/java/org/jabref/gui/entryeditor/RequiredFieldsTab.java index fb856f62f5e..9e7d03eae64 100644 --- a/src/main/java/org/jabref/gui/entryeditor/RequiredFieldsTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/RequiredFieldsTab.java @@ -16,7 +16,7 @@ import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.journals.JournalAbbreviationRepository; import org.jabref.logic.l10n.Localization; -import org.jabref.logic.pdf.search.indexing.IndexingTaskManager; +import org.jabref.logic.pdf.search.IndexingTaskManager; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.BibEntryType; diff --git a/src/main/java/org/jabref/gui/entryeditor/UserDefinedFieldsTab.java b/src/main/java/org/jabref/gui/entryeditor/UserDefinedFieldsTab.java index ce94dcca516..064855d593d 100644 --- a/src/main/java/org/jabref/gui/entryeditor/UserDefinedFieldsTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/UserDefinedFieldsTab.java @@ -13,7 +13,7 @@ import org.jabref.gui.theme.ThemeManager; import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.journals.JournalAbbreviationRepository; -import org.jabref.logic.pdf.search.indexing.IndexingTaskManager; +import org.jabref.logic.pdf.search.IndexingTaskManager; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.Field; diff --git a/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java b/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java index 87cb097b734..954fabcf47f 100644 --- a/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java +++ b/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java @@ -34,6 +34,7 @@ import org.jabref.logic.exporter.SelfContainedSaveConfiguration; import org.jabref.logic.l10n.Encodings; import org.jabref.logic.l10n.Localization; +import org.jabref.logic.pdf.search.PdfIndexerManager; import org.jabref.logic.shared.DatabaseLocation; import org.jabref.logic.shared.prefs.SharedDatabasePreferences; import org.jabref.logic.util.StandardFileType; @@ -137,13 +138,12 @@ public void saveSelectedAsPlain() { boolean saveAs(Path file, SaveDatabaseMode mode) { BibDatabaseContext context = libraryTab.getBibDatabaseContext(); - // Close AutosaveManager and BackupManager for original library Optional databasePath = context.getDatabasePath(); if (databasePath.isPresent()) { - final Path oldFile = databasePath.get(); - context.setDatabasePath(oldFile); + // Close AutosaveManager, BackupManager, and PdfIndexer for original library AutosaveManager.shutdown(context); BackupManager.shutdown(context, this.preferences.getFilePreferences().getBackupDirectory(), preferences.getFilePreferences().shouldCreateBackup()); + PdfIndexerManager.shutdownIndexer(context); } // Set new location @@ -164,6 +164,7 @@ boolean saveAs(Path file, SaveDatabaseMode mode) { // Reset (here: uninstall and install again) AutosaveManager and BackupManager for the new file name libraryTab.resetChangeMonitor(); libraryTab.installAutosaveManagerAndBackupManager(); + // PdfIndexerManager does not need to be called; the method {@link org.jabref.logic.pdf.search.PdfIndexerManager.get()} is called if a new indexer is needed preferences.getGuiPreferences().getFileHistory().newFile(file); } diff --git a/src/main/java/org/jabref/gui/externalfiles/ExternalFilesEntryLinker.java b/src/main/java/org/jabref/gui/externalfiles/ExternalFilesEntryLinker.java index 57df1e395c8..8e15ff4c474 100644 --- a/src/main/java/org/jabref/gui/externalfiles/ExternalFilesEntryLinker.java +++ b/src/main/java/org/jabref/gui/externalfiles/ExternalFilesEntryLinker.java @@ -15,8 +15,8 @@ import org.jabref.logic.cleanup.MoveFilesCleanup; import org.jabref.logic.cleanup.RenamePdfCleanup; import org.jabref.logic.l10n.Localization; -import org.jabref.logic.pdf.search.indexing.IndexingTaskManager; -import org.jabref.logic.pdf.search.indexing.PdfIndexer; +import org.jabref.logic.pdf.search.IndexingTaskManager; +import org.jabref.logic.pdf.search.PdfIndexerManager; import org.jabref.logic.util.io.FileNameCleaner; import org.jabref.logic.util.io.FileUtil; import org.jabref.model.database.BibDatabaseContext; @@ -87,7 +87,7 @@ public void moveFilesToFileDirRenameAndAddToEntry(BibEntry entry, List fil } try { - indexingTaskManager.addToIndex(PdfIndexer.of(bibDatabaseContext, filePreferences), entry, bibDatabaseContext); + indexingTaskManager.addToIndex(PdfIndexerManager.getIndexer(bibDatabaseContext, filePreferences), entry); } catch (IOException e) { LOGGER.error("Could not access Fulltext-Index", e); } @@ -105,9 +105,9 @@ public void copyFilesToFileDirAndAddToEntry(BibEntry entry, List files, In } try { - indexingTaskManager.addToIndex(PdfIndexer.of(bibDatabaseContext, filePreferences), entry, bibDatabaseContext); + indexingTaskManager.addToIndex(PdfIndexerManager.getIndexer(bibDatabaseContext, filePreferences), entry); } catch (IOException e) { - LOGGER.error("Could not access Fulltext-Index", e); + LOGGER.error("Could not access fulltext index", e); } } diff --git a/src/main/java/org/jabref/gui/preview/PreviewPanel.java b/src/main/java/org/jabref/gui/preview/PreviewPanel.java index 3a332dba0c7..e2c6d3c4ada 100644 --- a/src/main/java/org/jabref/gui/preview/PreviewPanel.java +++ b/src/main/java/org/jabref/gui/preview/PreviewPanel.java @@ -25,7 +25,7 @@ import org.jabref.gui.theme.ThemeManager; import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.l10n.Localization; -import org.jabref.logic.pdf.search.indexing.IndexingTaskManager; +import org.jabref.logic.pdf.search.IndexingTaskManager; import org.jabref.logic.preview.PreviewLayout; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; diff --git a/src/main/java/org/jabref/gui/search/RebuildFulltextSearchIndexAction.java b/src/main/java/org/jabref/gui/search/RebuildFulltextSearchIndexAction.java index 4fb2a575ab6..3a686979b35 100644 --- a/src/main/java/org/jabref/gui/search/RebuildFulltextSearchIndexAction.java +++ b/src/main/java/org/jabref/gui/search/RebuildFulltextSearchIndexAction.java @@ -9,7 +9,8 @@ import org.jabref.gui.util.BackgroundTask; import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.l10n.Localization; -import org.jabref.logic.pdf.search.indexing.PdfIndexer; +import org.jabref.logic.pdf.search.PdfIndexer; +import org.jabref.logic.pdf.search.PdfIndexerManager; import org.jabref.model.database.BibDatabaseContext; import org.jabref.preferences.FilePreferences; @@ -74,8 +75,8 @@ private void rebuildIndex() { return; } try { - currentLibraryTab.get().getIndexingTaskManager().createIndex(PdfIndexer.of(databaseContext, filePreferences)); - currentLibraryTab.get().getIndexingTaskManager().updateIndex(PdfIndexer.of(databaseContext, filePreferences), databaseContext); + PdfIndexer indexer = PdfIndexerManager.getIndexer(databaseContext, filePreferences); + currentLibraryTab.get().getIndexingTaskManager().rebuildIndex(indexer); } catch (IOException e) { dialogService.notify(Localization.lang("Failed to access fulltext search index")); LOGGER.error("Failed to access fulltext search index", e); diff --git a/src/main/java/org/jabref/logic/cleanup/FieldFormatterCleanups.java b/src/main/java/org/jabref/logic/cleanup/FieldFormatterCleanups.java index bb9651f9145..e37274328c0 100644 --- a/src/main/java/org/jabref/logic/cleanup/FieldFormatterCleanups.java +++ b/src/main/java/org/jabref/logic/cleanup/FieldFormatterCleanups.java @@ -204,13 +204,16 @@ public static FieldFormatterCleanups parse(List formatterMetaList) { } static Formatter getFormatterFromString(String formatterName) { - for (Formatter formatter : Formatters.getAll()) { - if (formatterName.equals(formatter.getKey())) { - return formatter; - } - } - LOGGER.info("Formatter {} not found.", formatterName); - return new IdentityFormatter(); + return Formatters + .getFormatterForKey(formatterName) + .orElseGet(() -> { + if (!"identity".equals(formatterName)) { + // The identity formatter is not listed in the formatters list, but is still valid + // Therefore, we log errors in other cases only + LOGGER.info("Formatter {} not found.", formatterName); + } + return new IdentityFormatter(); + }); } @Override diff --git a/src/main/java/org/jabref/logic/formatter/Formatters.java b/src/main/java/org/jabref/logic/formatter/Formatters.java index ad57b7f6202..82fb14932e8 100644 --- a/src/main/java/org/jabref/logic/formatter/Formatters.java +++ b/src/main/java/org/jabref/logic/formatter/Formatters.java @@ -3,9 +3,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.regex.Pattern; +import java.util.stream.Collectors; import org.jabref.logic.cleanup.Formatter; import org.jabref.logic.formatter.bibtexfields.CleanupUrlFormatter; @@ -40,6 +42,12 @@ public class Formatters { private static final Pattern TRUNCATE_PATTERN = Pattern.compile("\\Atruncate\\d+\\z"); + private static Map keyToFormatterMap; + + static { + keyToFormatterMap = getAll().stream().collect(Collectors.toMap(Formatter::getKey, f -> f)); + } + private Formatters() { } @@ -92,6 +100,11 @@ public static List getAll() { return all; } + public static Optional getFormatterForKey(String name) { + Objects.requireNonNull(name); + return keyToFormatterMap.containsKey(name) ? Optional.of(keyToFormatterMap.get(name)) : Optional.empty(); + } + public static Optional getFormatterForModifier(String modifier) { Objects.requireNonNull(modifier); @@ -115,7 +128,7 @@ public static Optional getFormatterForModifier(String modifier) { int truncateAfter = Integer.parseInt(modifier.substring(8)); return Optional.of(new TruncateFormatter(truncateAfter)); } else { - return getAll().stream().filter(f -> f.getKey().equals(modifier)).findAny(); + return getFormatterForKey(modifier); } } } diff --git a/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java b/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java index a609d68ca8d..3057071e82a 100644 --- a/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java +++ b/src/main/java/org/jabref/logic/importer/fileformat/BibtexParser.java @@ -301,6 +301,8 @@ private void parseJabRefComment(Map meta) { return; } + // We remove all line breaks in the metadata + // These have been inserted to prevent too long lines when the file was saved, and are not part of the data. String comment = buffer.toString().replaceAll("[\\x0d\\x0a]", ""); if (comment.substring(0, Math.min(comment.length(), MetaData.META_FLAG.length())).equals(MetaData.META_FLAG)) { if (comment.startsWith(MetaData.META_FLAG)) { @@ -309,10 +311,6 @@ private void parseJabRefComment(Map meta) { int pos = rest.indexOf(':'); if (pos > 0) { - // We remove all line breaks in the metadata - these - // will have been inserted - // to prevent too long lines when the file was - // saved, and are not part of the data. meta.put(rest.substring(0, pos), rest.substring(pos + 1)); // meta comments are always re-written by JabRef and not stored in the file diff --git a/src/main/java/org/jabref/logic/importer/util/MetaDataParser.java b/src/main/java/org/jabref/logic/importer/util/MetaDataParser.java index b5b733f7973..cccf64afe31 100644 --- a/src/main/java/org/jabref/logic/importer/util/MetaDataParser.java +++ b/src/main/java/org/jabref/logic/importer/util/MetaDataParser.java @@ -78,6 +78,8 @@ public MetaData parse(Map data, Character keywordSeparator) thro /** * Parses the data map and changes the given {@link MetaData} instance respectively. + * + * @return the given metaData instance (which is modified, too) */ public MetaData parse(MetaData metaData, Map data, Character keywordSeparator) throws ParseException { List defaultCiteKeyPattern = new ArrayList<>(); @@ -103,7 +105,7 @@ public MetaData parse(MetaData metaData, Map data, Character key String user = entry.getKey().substring(MetaData.FILE_DIRECTORY.length() + 1); metaData.setUserFileDirectory(user, parseDirectory(entry.getValue())); } else if (entry.getKey().startsWith(MetaData.FILE_DIRECTORY_LATEX)) { - // The user name starts directly after FILE_DIRECTORY_LATEX" + '-' + // The user name starts directly after FILE_DIRECTORY_LATEX + '-' String user = entry.getKey().substring(MetaData.FILE_DIRECTORY_LATEX.length() + 1); Path path = Path.of(parseDirectory(entry.getValue())).normalize(); metaData.setLatexFileDirectory(user, path); diff --git a/src/main/java/org/jabref/logic/pdf/search/indexing/DocumentReader.java b/src/main/java/org/jabref/logic/pdf/search/DocumentReader.java similarity index 87% rename from src/main/java/org/jabref/logic/pdf/search/indexing/DocumentReader.java rename to src/main/java/org/jabref/logic/pdf/search/DocumentReader.java index e2e1c0f5b7e..8431db88b52 100644 --- a/src/main/java/org/jabref/logic/pdf/search/indexing/DocumentReader.java +++ b/src/main/java/org/jabref/logic/pdf/search/DocumentReader.java @@ -1,4 +1,4 @@ -package org.jabref.logic.pdf.search.indexing; +package org.jabref.logic.pdf.search; import java.io.IOException; import java.nio.file.Files; @@ -57,7 +57,7 @@ public final class DocumentReader { 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!"); + throw new IllegalStateException("There are no linked PDF files to this BibEntry."); } this.entry = bibEntry; @@ -93,17 +93,17 @@ public List readLinkedPdfs(BibDatabaseContext databaseContext) { private List readPdfContents(LinkedFile pdf, Path resolvedPdfPath) { List pages = new ArrayList<>(); try (PDDocument pdfDocument = Loader.loadPDF(resolvedPdfPath.toFile())) { - for (int pageNumber = 0; pageNumber < pdfDocument.getNumberOfPages(); pageNumber++) { - Document newDocument = new Document(); - addIdentifiers(newDocument, pdf.getLink()); - addMetaData(newDocument, resolvedPdfPath, pageNumber); - try { - addContentIfNotEmpty(pdfDocument, newDocument, pageNumber); - } catch (IOException e) { - LOGGER.warn("Could not read page {} of {}", pageNumber, resolvedPdfPath.toAbsolutePath(), e); - } - pages.add(newDocument); + for (int pageNumber = 0; pageNumber < pdfDocument.getNumberOfPages(); pageNumber++) { + Document newDocument = new Document(); + addIdentifiers(newDocument, pdf.getLink()); + addMetaData(newDocument, resolvedPdfPath, pageNumber); + try { + addContentIfNotEmpty(pdfDocument, newDocument, pageNumber); + } catch (IOException e) { + LOGGER.warn("Could not read page {} of {}", pageNumber, resolvedPdfPath.toAbsolutePath(), e); } + pages.add(newDocument); + } } catch (IOException e) { LOGGER.warn("Could not read {}", resolvedPdfPath.toAbsolutePath(), e); } @@ -145,8 +145,9 @@ public static String mergeLines(String text) { private void addContentIfNotEmpty(PDDocument pdfDocument, Document newDocument, int pageNumber) throws IOException { PDFTextStripper pdfTextStripper = new PDFTextStripper(); pdfTextStripper.setLineSeparator("\n"); - pdfTextStripper.setStartPage(pageNumber); - pdfTextStripper.setEndPage(pageNumber); + // Apache PDFTextStripper is 1-based. See {@link org.apache.pdfbox.text.PDFTextStripper.processPages} + pdfTextStripper.setStartPage(pageNumber + 1); + pdfTextStripper.setEndPage(pageNumber + 1); String pdfContent = pdfTextStripper.getText(pdfDocument); if (StringUtil.isNotBlank(pdfContent)) { diff --git a/src/main/java/org/jabref/logic/pdf/search/indexing/IndexingTaskManager.java b/src/main/java/org/jabref/logic/pdf/search/IndexingTaskManager.java similarity index 63% rename from src/main/java/org/jabref/logic/pdf/search/indexing/IndexingTaskManager.java rename to src/main/java/org/jabref/logic/pdf/search/IndexingTaskManager.java index ba8bc5e5d12..8ae4117e72d 100644 --- a/src/main/java/org/jabref/logic/pdf/search/indexing/IndexingTaskManager.java +++ b/src/main/java/org/jabref/logic/pdf/search/IndexingTaskManager.java @@ -1,9 +1,11 @@ -package org.jabref.logic.pdf.search.indexing; +package org.jabref.logic.pdf.search; import java.util.List; import java.util.Queue; import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; import org.jabref.gui.util.BackgroundTask; import org.jabref.gui.util.DefaultTaskExecutor; @@ -85,41 +87,47 @@ public AutoCloseable blockNewTasks() { }; } - public void createIndex(PdfIndexer indexer) { - enqueueTask(indexer::createIndex); + public void rebuildIndex(PdfIndexer indexer) { + enqueueTask(indexer::rebuildIndex); } + /** + * Updates the index by performing a delta analysis of the files already existing in the index and the files in the library. + */ public void updateIndex(PdfIndexer indexer, BibDatabaseContext databaseContext) { Set pathsToRemove = indexer.getListOfFilePaths(); - for (BibEntry entry : databaseContext.getEntries()) { - for (LinkedFile file : entry.getFiles()) { - enqueueTask(() -> indexer.addToIndex(entry, file, databaseContext)); - pathsToRemove.remove(file.getLink()); - } - } - for (String pathToRemove : pathsToRemove) { - enqueueTask(() -> indexer.removeFromIndex(pathToRemove)); - } + databaseContext.getEntries().stream() + .flatMap(entry -> entry.getFiles().stream()) + .map(LinkedFile::getLink) + .forEach(pathsToRemove::remove); + // The indexer checks the attached PDFs for modifications (based on the timestamp of the PDF) and reindexes the PDF if it is newer than the index. Therefore, we need to pass the whole library to the indexer for re-indexing. + addToIndex(indexer, databaseContext.getEntries()); + enqueueTask(() -> indexer.removePathsFromIndex(pathsToRemove)); } - public void addToIndex(PdfIndexer indexer, BibEntry entry, BibDatabaseContext databaseContext) { - addToIndex(indexer, entry, entry.getFiles(), databaseContext); + public void addToIndex(PdfIndexer indexer, List entries) { + AtomicInteger counter = new AtomicInteger(); + // To enable seeing progress in the UI, we group the entries in chunks of 50 + // Solution inspired by https://stackoverflow.com/a/27595803/873282 + entries.stream().collect(Collectors.groupingBy(x -> counter.getAndIncrement() / 50)) + .values() + .forEach(list -> enqueueTask(() -> indexer.addToIndex(list))); } - public void addToIndex(PdfIndexer indexer, BibEntry entry, List linkedFiles, BibDatabaseContext databaseContext) { - for (LinkedFile file : linkedFiles) { - enqueueTask(() -> indexer.addToIndex(entry, file, databaseContext)); - } + public void addToIndex(PdfIndexer indexer, BibEntry entry) { + enqueueTask(() -> indexer.addToIndex(entry)); } - public void removeFromIndex(PdfIndexer indexer, BibEntry entry, List linkedFiles) { - for (LinkedFile file : linkedFiles) { - enqueueTask(() -> indexer.removeFromIndex(file.getLink())); - } + public void addToIndex(PdfIndexer indexer, BibEntry entry, List linkedFiles) { + enqueueTask(() -> indexer.addToIndex(entry, linkedFiles)); } public void removeFromIndex(PdfIndexer indexer, BibEntry entry) { - enqueueTask(() -> removeFromIndex(indexer, entry, entry.getFiles())); + enqueueTask(() -> indexer.removeFromIndex(entry)); + } + + public void removeFromIndex(PdfIndexer indexer, List linkedFiles) { + enqueueTask(() -> indexer.removeFromIndex(linkedFiles)); } public void updateDatabaseName(String name) { diff --git a/src/main/java/org/jabref/logic/pdf/search/indexing/PdfIndexer.java b/src/main/java/org/jabref/logic/pdf/search/PdfIndexer.java similarity index 50% rename from src/main/java/org/jabref/logic/pdf/search/indexing/PdfIndexer.java rename to src/main/java/org/jabref/logic/pdf/search/PdfIndexer.java index d10a6c0221a..a973831e134 100644 --- a/src/main/java/org/jabref/logic/pdf/search/indexing/PdfIndexer.java +++ b/src/main/java/org/jabref/logic/pdf/search/PdfIndexer.java @@ -1,16 +1,16 @@ -package org.jabref.logic.pdf.search.indexing; +package org.jabref.logic.pdf.search; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; -import org.jabref.gui.LibraryTab; import org.jabref.logic.util.StandardFileType; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; @@ -19,6 +19,7 @@ import org.jabref.model.pdf.search.SearchFieldConstants; import org.jabref.preferences.FilePreferences; +import com.google.common.annotations.VisibleForTesting; import org.apache.lucene.document.Document; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexNotFoundException; @@ -41,75 +42,138 @@ */ public class PdfIndexer { - private static final Logger LOGGER = LoggerFactory.getLogger(LibraryTab.class); + private static final Logger LOGGER = LoggerFactory.getLogger(PdfIndexer.class); - private final Directory directoryToIndex; - private BibDatabaseContext databaseContext; + @VisibleForTesting + IndexWriter indexWriter; + private final BibDatabaseContext databaseContext; private final FilePreferences filePreferences; + private final Directory indexDirectory; + private IndexReader reader; - public PdfIndexer(Directory indexDirectory, FilePreferences filePreferences) { - this.directoryToIndex = indexDirectory; + private PdfIndexer(BibDatabaseContext databaseContext, Directory indexDirectory, FilePreferences filePreferences) { + this.databaseContext = databaseContext; + this.indexDirectory = indexDirectory; this.filePreferences = filePreferences; } + /** + * Method is public, because DatabaseSearcherWithBibFilesTest resides in another package + */ + @VisibleForTesting + public static PdfIndexer of(BibDatabaseContext databaseContext, Path indexDirectory, FilePreferences filePreferences) throws IOException { + return new PdfIndexer(databaseContext, new NIOFSDirectory(indexDirectory), filePreferences); + } + + /** + * Method is public, because DatabaseSearcherWithBibFilesTest resides in another package + */ + @VisibleForTesting public static PdfIndexer of(BibDatabaseContext databaseContext, FilePreferences filePreferences) throws IOException { - return new PdfIndexer(new NIOFSDirectory(databaseContext.getFulltextIndexPath()), filePreferences); + return new PdfIndexer(databaseContext, 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! + * Creates (and thus resets) the PDF index. No re-indexing will be done. + * Any previous state of the Lucene search is deleted. */ public void createIndex() { - // Create new index by creating IndexWriter but not writing anything. - try (IndexWriter indexWriter = new IndexWriter(directoryToIndex, new IndexWriterConfig(new EnglishStemAnalyzer()).setOpenMode(IndexWriterConfig.OpenMode.CREATE))) { - // empty comment for checkstyle + LOGGER.debug("Creating new index for directory {}.", indexDirectory); + initializeIndexWriterAndReader(IndexWriterConfig.OpenMode.CREATE); + } + + /** + * Needs to be accessed by {@link PdfSearcher} + */ + IndexWriter getIndexWriter() { + LOGGER.trace("Getting the index writer"); + if (indexWriter == null) { + LOGGER.trace("Initializing the index writer"); + initializeIndexWriterAndReader(IndexWriterConfig.OpenMode.CREATE_OR_APPEND); + } else { + LOGGER.trace("Using existing index writer"); + } + return indexWriter; + } + + private void initializeIndexWriterAndReader(IndexWriterConfig.OpenMode mode) { + try { + indexWriter = new IndexWriter( + indexDirectory, + new IndexWriterConfig( + new EnglishStemAnalyzer()).setOpenMode(mode)); + } catch (IOException e) { + LOGGER.error("Could not initialize the IndexWriter", e); + } + try { + reader = DirectoryReader.open(indexWriter); } catch (IOException e) { - LOGGER.warn("Could not create new Index!", e); + LOGGER.error("Could not initialize the IndexReader", e); } } - public void addToIndex(BibDatabaseContext databaseContext) { - for (BibEntry entry : databaseContext.getEntries()) { - addToIndex(entry, databaseContext); + /** + * Rebuilds the PDF index. All PDF files linked to entries in the database will be re-indexed. + */ + public void rebuildIndex() { + LOGGER.debug("Rebuilding index."); + createIndex(); + addToIndex(databaseContext.getEntries()); + } + + public void addToIndex(List entries) { + int count = 0; + for (BibEntry entry : entries) { + addToIndex(entry, false); + count++; + if (count % 100 == 0) { + doCommit(); + } } + doCommit(); + LOGGER.debug("Added {} documents to the index.", count); } /** - * Adds all the pdf files linked to one entry in the database to an existing (or new) Lucene search index + * Adds all 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); + public void addToIndex(BibEntry entry) { + addToIndex(entry, entry.getFiles(), true); + } + + private void addToIndex(BibEntry entry, boolean shouldCommit) { + addToIndex(entry, entry.getFiles(), false); + if (shouldCommit) { + doCommit(); + } } /** * 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) { + public void addToIndex(BibEntry entry, Collection linkedFiles) { + addToIndex(entry, linkedFiles, true); + } + + public void addToIndex(BibEntry entry, Collection linkedFiles, boolean shouldCommit) { for (LinkedFile linkedFile : linkedFiles) { - addToIndex(entry, linkedFile, databaseContext); + addToIndex(entry, linkedFile, false); + } + if (shouldCommit) { + doCommit(); } } - /** - * 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()) { - addToIndex(entry, linkedFile); + private void doCommit() { + try { + getIndexWriter().commit(); + } catch (IOException e) { + LOGGER.warn("Could not commit changes to the index.", e); } } @@ -119,14 +183,11 @@ public void addToIndex(BibEntry entry, LinkedFile linkedFile, BibDatabaseContext * @param linkedFilePath the path to the file to be removed */ public void removeFromIndex(String linkedFilePath) { - try (IndexWriter indexWriter = new IndexWriter( - directoryToIndex, - new IndexWriterConfig( - new EnglishStemAnalyzer()).setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND))) { - indexWriter.deleteDocuments(new Term(SearchFieldConstants.PATH, linkedFilePath)); - indexWriter.commit(); + try { + getIndexWriter().deleteDocuments(new Term(SearchFieldConstants.PATH, linkedFilePath)); + getIndexWriter().commit(); } catch (IOException e) { - LOGGER.warn("Could not initialize the IndexWriter!", e); + LOGGER.debug("Could not remove document {} from the index.", linkedFilePath, e); } } @@ -136,30 +197,21 @@ public void removeFromIndex(String linkedFilePath) { * @param entry the entry documents are linked to */ public void removeFromIndex(BibEntry entry) { - removeFromIndex(entry, entry.getFiles()); + removeFromIndex(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) { + public void removeFromIndex(Collection linkedFiles) { for (LinkedFile linkedFile : linkedFiles) { removeFromIndex(linkedFile.getLink()); } } - /** - * 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); + public void removePathsFromIndex(Collection linkedFiles) { + for (String linkedFile : linkedFiles) { + removeFromIndex(linkedFile); } } @@ -170,8 +222,15 @@ public void flushIndex() { * @param entry the entry associated with the file * @param linkedFile the file to write to the index */ - private void addToIndex(BibEntry entry, LinkedFile linkedFile) { - if (linkedFile.isOnlineLink() || !StandardFileType.PDF.getName().equals(linkedFile.getFileType())) { + public void addToIndex(BibEntry entry, LinkedFile linkedFile) { + this.addToIndex(entry, linkedFile, true); + } + + private void addToIndex(BibEntry entry, LinkedFile linkedFile, boolean shouldCommit) { + if (linkedFile.isOnlineLink() || + (!StandardFileType.PDF.getName().equals(linkedFile.getFileType()) && + // We do not require the file type to be set + (!linkedFile.getLink().endsWith(".pdf") && !linkedFile.getLink().endsWith(".PDF")))) { return; } Optional resolvedPath = linkedFile.findIn(databaseContext, filePreferences); @@ -182,7 +241,7 @@ private void addToIndex(BibEntry entry, LinkedFile linkedFile) { LOGGER.debug("Adding {} to index", linkedFile.getLink()); try { // Check if a document with this path is already in the index - try (IndexReader reader = DirectoryReader.open(directoryToIndex)) { + try { IndexSearcher searcher = new IndexSearcher(reader); TermQuery query = new TermQuery(new Term(SearchFieldConstants.PATH, linkedFile.getLink())); TopDocs topDocs = searcher.search(query, 1); @@ -190,28 +249,27 @@ private void addToIndex(BibEntry entry, LinkedFile linkedFile) { 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)) { + LOGGER.debug("File {} is already indexed", linkedFile.getLink()); return; } } } catch (IndexNotFoundException e) { - // if there is no index yet, don't need to check anything! + LOGGER.debug("Index not found. Continuing.", e); } // If no document was found, add the new one Optional> pages = new DocumentReader(entry, filePreferences).readLinkedPdf(this.databaseContext, linkedFile); if (pages.isPresent()) { - try (IndexWriter indexWriter = new IndexWriter(directoryToIndex, - new IndexWriterConfig( - new EnglishStemAnalyzer()).setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND))) { - indexWriter.addDocuments(pages.get()); - indexWriter.commit(); + getIndexWriter().addDocuments(pages.get()); + if (shouldCommit) { + getIndexWriter().commit(); } + } else { + LOGGER.debug("No content found in file {}", linkedFile.getLink()); } } catch (IOException e) { - LOGGER.warn("Could not add the document {} to the index!", linkedFile.getLink(), e); + LOGGER.warn("Could not add document {} to the index.", linkedFile.getLink(), e); } } @@ -222,7 +280,7 @@ private void addToIndex(BibEntry entry, LinkedFile linkedFile) { */ public Set getListOfFilePaths() { Set paths = new HashSet<>(); - try (IndexReader reader = DirectoryReader.open(directoryToIndex)) { + try (IndexReader reader = DirectoryReader.open(indexWriter)) { IndexSearcher searcher = new IndexSearcher(reader); MatchAllDocsQuery query = new MatchAllDocsQuery(); TopDocs allDocs = searcher.search(query, Integer.MAX_VALUE); @@ -231,8 +289,13 @@ public Set getListOfFilePaths() { paths.add(doc.getField(SearchFieldConstants.PATH).stringValue()); } } catch (IOException e) { + LOGGER.debug("Could not read from index. Returning intermediate result.", e); return paths; } return paths; } + + public void close() throws IOException { + indexWriter.close(); + } } diff --git a/src/main/java/org/jabref/logic/pdf/search/PdfIndexerManager.java b/src/main/java/org/jabref/logic/pdf/search/PdfIndexerManager.java new file mode 100644 index 00000000000..3f4bca38c49 --- /dev/null +++ b/src/main/java/org/jabref/logic/pdf/search/PdfIndexerManager.java @@ -0,0 +1,79 @@ +package org.jabref.logic.pdf.search; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.preferences.FilePreferences; + +import org.jspecify.annotations.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A PdfIndexer takes a long time to build. Caching it speeds up. + *

+ * The PdfIndexer is related to the BibDatabaseContext and the FilePreferences. If the user changes the path of the library + * or the file preferences, we need to create a new PdfIndexer. Otherwise, we can reuse the existing one. + *

+ * This manager implements a Object Pool pattern for {@link PdfIndexer}. + */ +public class PdfIndexerManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(PdfIndexerManager.class); + + // Map from the path of the library index to the respective indexer + private static Map indexerMap = new HashMap<>(); + + // We store the file preferences for each path, so that we can update the indexer when the preferences change + private static Map pathFilePreferencesMap = new HashMap<>(); + + public static @NonNull PdfIndexer getIndexer(BibDatabaseContext context, FilePreferences filePreferences) throws IOException { + Path fulltextIndexPath = context.getFulltextIndexPath(); + PdfIndexer indexer = indexerMap.get(fulltextIndexPath); + if (indexer != null) { + // Check if the file preferences have changed + FilePreferences storedFilePreferences = pathFilePreferencesMap.get(fulltextIndexPath); + if (storedFilePreferences.equals(filePreferences)) { + LOGGER.trace("Found existing indexer for context {}", context); + return indexer; + } + LOGGER.debug("File preferences have changed, updating indexer"); + indexer.close(); + indexer = PdfIndexer.of(context, filePreferences); + indexerMap.put(fulltextIndexPath, indexer); + pathFilePreferencesMap.put(fulltextIndexPath, filePreferences); + return indexer; + } + LOGGER.debug("No indexer found for context {}, creating new one", context); + indexer = PdfIndexer.of(context, filePreferences); + indexerMap.put(fulltextIndexPath, indexer); + pathFilePreferencesMap.put(fulltextIndexPath, filePreferences); + return indexer; + } + + public static void shutdownAllIndexers() { + indexerMap.values().forEach(indexer -> { + try { + indexer.close(); + } catch (Exception e) { + LOGGER.debug("Problem closing PDF indexer", e); + } + }); + } + + public static void shutdownIndexer(BibDatabaseContext context) { + PdfIndexer indexer = indexerMap.get(context.getFulltextIndexPath()); + if (indexer != null) { + try { + indexer.close(); + } catch (IOException e) { + LOGGER.debug("Could not close indexer", e); + } + } else { + LOGGER.debug("No indexer found for context {}", context); + } + } +} diff --git a/src/main/java/org/jabref/logic/pdf/search/retrieval/PdfSearcher.java b/src/main/java/org/jabref/logic/pdf/search/PdfSearcher.java similarity index 64% rename from src/main/java/org/jabref/logic/pdf/search/retrieval/PdfSearcher.java rename to src/main/java/org/jabref/logic/pdf/search/PdfSearcher.java index 2fd7e54e5b0..3906341ecb5 100644 --- a/src/main/java/org/jabref/logic/pdf/search/retrieval/PdfSearcher.java +++ b/src/main/java/org/jabref/logic/pdf/search/PdfSearcher.java @@ -1,12 +1,11 @@ -package org.jabref.logic.pdf.search.retrieval; +package org.jabref.logic.pdf.search; import java.io.IOException; -import java.util.LinkedList; +import java.util.ArrayList; 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; @@ -20,8 +19,6 @@ 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; @@ -31,14 +28,15 @@ public final class PdfSearcher { private static final Logger LOGGER = LoggerFactory.getLogger(LibraryTab.class); - private final Directory indexDirectory; + private final PdfIndexer indexer; + private EnglishStemAnalyzer englishStemAnalyzer = new EnglishStemAnalyzer(); - private PdfSearcher(Directory indexDirectory) { - this.indexDirectory = indexDirectory; + private PdfSearcher(PdfIndexer indexer) { + this.indexer = indexer; } - public static PdfSearcher of(BibDatabaseContext databaseContext) throws IOException { - return new PdfSearcher(new NIOFSDirectory(databaseContext.getFulltextIndexPath())); + public static PdfSearcher of(PdfIndexer indexer) throws IOException { + return new PdfSearcher(indexer); } /** @@ -48,32 +46,27 @@ public static PdfSearcher of(BibDatabaseContext databaseContext) throws IOExcept * @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!"))) { + 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); + throw new IllegalArgumentException("Must be called with at least 1 maxHits, was " + maxHits); } - List resultDocs = new LinkedList<>(); - - if (!DirectoryReader.indexExists(indexDirectory)) { - LOGGER.debug("Index directory {} does not yet exist", indexDirectory); - return new PdfSearchResults(); - } - - try (IndexReader reader = DirectoryReader.open(indexDirectory)) { + List resultDocs = new ArrayList<>(); + // We need to point the DirectoryReader to the indexer, because we get errors otherwise + // Hint from https://stackoverflow.com/a/63673753/873282. + try (IndexReader reader = DirectoryReader.open(indexer.getIndexWriter())) { + Query query = new MultiFieldQueryParser(PDF_FIELDS, englishStemAnalyzer).parse(searchString); 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: '{}'!\n{}", searchString, e.getMessage()); + LOGGER.warn("Could not parse query: '{}'", searchString, e); return new PdfSearchResults(); } } diff --git a/src/main/java/org/jabref/logic/search/DatabaseSearcher.java b/src/main/java/org/jabref/logic/search/DatabaseSearcher.java index 422d8d4d35c..d5db1b6309f 100644 --- a/src/main/java/org/jabref/logic/search/DatabaseSearcher.java +++ b/src/main/java/org/jabref/logic/search/DatabaseSearcher.java @@ -3,7 +3,6 @@ import java.util.Collections; import java.util.List; import java.util.Objects; -import java.util.stream.Collectors; import org.jabref.model.database.BibDatabase; import org.jabref.model.database.BibDatabases; @@ -24,15 +23,18 @@ public DatabaseSearcher(SearchQuery query, BibDatabase database) { this.database = Objects.requireNonNull(database); } + /** + * @return The matches in the order they appear in the library. + */ public List getMatches() { - LOGGER.debug("Search term: " + query); + LOGGER.debug("Search term: {}", query); if (!query.isValid()) { - LOGGER.warn("Search failed: illegal search expression"); + LOGGER.warn("Search failed: invalid search expression"); return Collections.emptyList(); } - List matchEntries = database.getEntries().stream().filter(query::isMatch).collect(Collectors.toList()); + List matchEntries = database.getEntries().stream().filter(query::isMatch).toList(); return BibDatabases.purgeEmptyEntries(matchEntries); } } diff --git a/src/main/java/org/jabref/logic/search/SearchQuery.java b/src/main/java/org/jabref/logic/search/SearchQuery.java index 4dc9fd40a58..ef1bb0479c3 100644 --- a/src/main/java/org/jabref/logic/search/SearchQuery.java +++ b/src/main/java/org/jabref/logic/search/SearchQuery.java @@ -69,7 +69,7 @@ public SearchQuery(String query, EnumSet searchFlags) { @Override public String toString() { - return String.format("\"%s\" (%s, %s)", getQuery(), getCaseSensitiveDescription(), getRegularExpressionDescription()); + return String.format("\"%s\" (%s, %s) %s", getQuery(), getCaseSensitiveDescription(), getRegularExpressionDescription(), searchFlags); } @Override diff --git a/src/main/java/org/jabref/logic/util/StandardFileType.java b/src/main/java/org/jabref/logic/util/StandardFileType.java index d71f5b50dec..904212a5d2a 100644 --- a/src/main/java/org/jabref/logic/util/StandardFileType.java +++ b/src/main/java/org/jabref/logic/util/StandardFileType.java @@ -47,9 +47,6 @@ public enum StandardFileType implements FileType { CITAVI("Citavi", "ctv6bak", "ctv5bak"), MARKDOWN("Markdown", "md"); - - - private final List extensions; private final String name; diff --git a/src/main/java/org/jabref/model/database/BibDatabaseContext.java b/src/main/java/org/jabref/model/database/BibDatabaseContext.java index 12d67a71a2f..55869680353 100644 --- a/src/main/java/org/jabref/model/database/BibDatabaseContext.java +++ b/src/main/java/org/jabref/model/database/BibDatabaseContext.java @@ -16,6 +16,7 @@ import org.jabref.logic.shared.DatabaseSynchronizer; import org.jabref.logic.util.CoarseChangeFilter; import org.jabref.logic.util.OS; +import org.jabref.logic.util.io.BackupFileUtil; import org.jabref.model.entry.BibEntry; import org.jabref.model.metadata.MetaData; import org.jabref.model.study.Study; @@ -161,7 +162,7 @@ public List getFileDirectories(FilePreferences preferences) { metaData.getDefaultFileDirectory() .ifPresent(metaDataDirectory -> fileDirs.add(getFileDirectoryPath(metaDataDirectory))); - // 3. BIB file directory or Main file directory + // 3. BIB file directory or main file directory // fileDirs.isEmpty in the case, 1) no user-specific file directory and 2) no general file directory is set // (in the metadata of the bib file) if (fileDirs.isEmpty() && preferences.shouldStoreFilesRelativeToBibFile()) { @@ -237,12 +238,17 @@ public List getEntries() { return database.getEntries(); } + /** + * @return The path to store the lucene index files. One directory for each library. + */ public Path getFulltextIndexPath() { Path appData = OS.getNativeDesktop().getFulltextIndexBaseDirectory(); Path indexPath; if (getDatabasePath().isPresent()) { - indexPath = appData.resolve(String.valueOf(this.getDatabasePath().get().hashCode())); + Path databaseFileName = getDatabasePath().get().getFileName(); + String fileName = BackupFileUtil.getUniqueFilePrefix(databaseFileName) + "--" + databaseFileName; + indexPath = appData.resolve(fileName); LOGGER.debug("Index path for {} is {}", getDatabasePath().get(), indexPath); return indexPath; } diff --git a/src/main/java/org/jabref/model/database/BibDatabases.java b/src/main/java/org/jabref/model/database/BibDatabases.java index 750454c84d1..6ca03178637 100644 --- a/src/main/java/org/jabref/model/database/BibDatabases.java +++ b/src/main/java/org/jabref/model/database/BibDatabases.java @@ -14,7 +14,7 @@ private BibDatabases() { /** * Receives a Collection of BibEntry instances, iterates through them, and * removes all entries that have no fields set. This is useful for rooting out - * an unsucessful import (wrong format) that returns a number of empty entries. + * an unsuccessful import (wrong format) that returns a number of empty entries. */ public static List purgeEmptyEntries(Collection entries) { return entries.stream() diff --git a/src/main/java/org/jabref/model/entry/BibEntry.java b/src/main/java/org/jabref/model/entry/BibEntry.java index 2b7519438eb..8a61da21ca4 100644 --- a/src/main/java/org/jabref/model/entry/BibEntry.java +++ b/src/main/java/org/jabref/model/entry/BibEntry.java @@ -54,25 +54,23 @@ /** * Represents a Bib(La)TeX entry, which can be BibTeX or BibLaTeX. *

- * Example: + * Example: * - *

{@code
+ * 
{@code
  * Some commment
  * @misc{key,
  *   fieldName = {fieldValue},
- *   otherFieldName = {otherVieldValue}
+ *   otherFieldName = {otherFieldValue}
  * }
- *     }
- * - * Then, + * }
+ *

+ * Then, *

    *
  • "Some comment" is the comment before the entry,
  • *
  • "misc" is the entry type
  • *
  • "key" the citation key
  • *
  • "fieldName" and "otherFieldName" the fields of the BibEntry
  • *
- *

- *

* A BibTeX entry has following properties: *

    *
  • comments before entry
  • @@ -89,7 +87,7 @@ *
*

*

- * In case you search for a builder as described in Item 2 of the book "Effective Java", you won't find one. Please use the methods {@link #withCitationKey(String)} and {@link #withField(Field, String)}. + * In case you search for a builder as described in Item 2 of the book "Effective Java", you won't find one. Please use the methods {@link #withCitationKey(String)} and {@link #withField(Field, String)}. All these methods set {@link #hasChanged()} to false. In case changed, use {@link #withChanged(boolean)}. *

*/ @AllowedToUseLogic("because it needs access to parser and writers") @@ -945,6 +943,7 @@ public BibEntry withField(Field field, String value) { */ public BibEntry withFields(Map content) { this.fields = FXCollections.observableMap(new HashMap<>(content)); + this.setChanged(false); return this; } @@ -969,6 +968,7 @@ public String getUserComments() { public BibEntry withUserComments(String commentsBeforeEntry) { this.commentsBeforeEntry = commentsBeforeEntry; + this.setChanged(false); return this; } @@ -1039,6 +1039,12 @@ public Optional setFiles(List files) { return this.setField(StandardField.FILE, newValue); } + public BibEntry withFiles(List files) { + setFiles(files); + this.setChanged(false); + return this; + } + /** * Gets a list of linked files. * diff --git a/src/main/java/org/jabref/model/entry/LinkedFile.java b/src/main/java/org/jabref/model/entry/LinkedFile.java index e3c296f5c90..0eeb1c60bc0 100644 --- a/src/main/java/org/jabref/model/entry/LinkedFile.java +++ b/src/main/java/org/jabref/model/entry/LinkedFile.java @@ -18,15 +18,20 @@ import javafx.beans.property.StringProperty; import org.jabref.architecture.AllowedToUseLogic; +import org.jabref.logic.util.FileType; import org.jabref.logic.util.io.FileUtil; import org.jabref.model.database.BibDatabaseContext; import org.jabref.preferences.FilePreferences; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + /** * Represents the link to an external file (e.g. associated PDF file). * This class is {@link Serializable} which is needed for drag and drop in gui */ @AllowedToUseLogic("Uses FileUtil from logic") +@NullMarked public class LinkedFile implements Serializable { private static final String REGEX_URL = "^((?:https?\\:\\/\\/|www\\.)(?:[-a-z0-9]+\\.)*[-a-z0-9]+.*)"; @@ -43,8 +48,12 @@ public LinkedFile(String description, Path link, String fileType) { this(Objects.requireNonNull(description), Objects.requireNonNull(link).toString(), Objects.requireNonNull(fileType)); } + public LinkedFile(String description, String link, FileType fileType) { + this(description, link, fileType.getName()); + } + /** - * Constructor for non-valid paths. We need to parse them, because the GUI needs to render it. + * Constructor can also be used for non-valid paths. We need to parse them, because the GUI needs to render it. */ public LinkedFile(String description, String link, String fileType) { this.description.setValue(Objects.requireNonNull(description)); @@ -80,6 +89,10 @@ public void setFileType(String fileType) { this.fileType.setValue(fileType); } + public void setFileType(FileType fileType) { + this.setFileType(fileType.getName()); + } + public String getDescription() { return description.get(); } @@ -105,7 +118,7 @@ public Observable[] getObservables() { } @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; } @@ -128,7 +141,7 @@ private void writeObject(ObjectOutputStream out) throws IOException { } /** - * Reads serialized object from ObjectInputStreamm, automatically called + * Reads serialized object from {@link ObjectInputStream}, automatically called */ private void readObject(ObjectInputStream in) throws IOException { fileType = new SimpleStringProperty(in.readUTF()); diff --git a/src/main/java/org/jabref/model/search/rules/ContainsBasedSearchRule.java b/src/main/java/org/jabref/model/search/rules/ContainsBasedSearchRule.java index 618d1771b95..8f4e7aad63c 100644 --- a/src/main/java/org/jabref/model/search/rules/ContainsBasedSearchRule.java +++ b/src/main/java/org/jabref/model/search/rules/ContainsBasedSearchRule.java @@ -54,6 +54,10 @@ public boolean applyRule(String query, BibEntry bibEntry) { } } - return getFulltextResults(query, bibEntry).numSearchResults() > 0; // Didn't match all words. + if (!searchFlags.contains(SearchRules.SearchFlags.FULLTEXT)) { + return false; + } + + return getFulltextResults(query, bibEntry).numSearchResults() > 0; } } diff --git a/src/main/java/org/jabref/model/search/rules/FullTextSearchRule.java b/src/main/java/org/jabref/model/search/rules/FullTextSearchRule.java index 27cfbf65b8b..110d4128bb8 100644 --- a/src/main/java/org/jabref/model/search/rules/FullTextSearchRule.java +++ b/src/main/java/org/jabref/model/search/rules/FullTextSearchRule.java @@ -4,12 +4,12 @@ import java.util.Collections; import java.util.EnumSet; import java.util.List; -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.logic.pdf.search.PdfIndexer; +import org.jabref.logic.pdf.search.PdfIndexerManager; +import org.jabref.logic.pdf.search.PdfSearcher; import org.jabref.model.entry.BibEntry; import org.jabref.model.pdf.search.PdfSearchResults; import org.jabref.model.pdf.search.SearchResult; @@ -30,16 +30,12 @@ public abstract class FullTextSearchRule implements SearchRule { protected final EnumSet searchFlags; protected String lastQuery; - protected List lastSearchResults; - - private final BibDatabaseContext databaseContext; + protected List lastPdfSearchResults; public FullTextSearchRule(EnumSet searchFlags) { this.searchFlags = searchFlags; this.lastQuery = ""; - lastSearchResults = Collections.emptyList(); - - databaseContext = Globals.stateManager.getActiveDatabase().orElse(null); + lastPdfSearchResults = Collections.emptyList(); } public EnumSet getSearchFlags() { @@ -48,24 +44,37 @@ public EnumSet getSearchFlags() { @Override public PdfSearchResults getFulltextResults(String query, BibEntry bibEntry) { - if (!searchFlags.contains(SearchRules.SearchFlags.FULLTEXT) || databaseContext == null) { + if (!searchFlags.contains(SearchRules.SearchFlags.FULLTEXT)) { + LOGGER.debug("Fulltext search results called even though fulltext search flag is missing."); return new PdfSearchResults(); } - if (!query.equals(this.lastQuery)) { + if (query.equals(this.lastQuery)) { + LOGGER.trace("Reusing fulltext search results (query={}, lastQuery={}).", query, this.lastQuery); + } else { + LOGGER.trace("Performing full query {}.", query); + PdfIndexer pdfIndexer; + try { + pdfIndexer = PdfIndexerManager.getIndexer(Globals.stateManager.getActiveDatabase().get(), Globals.prefs.getFilePreferences()); + } catch (IOException e) { + LOGGER.error("Could not access full text index.", e); + return new PdfSearchResults(); + } this.lastQuery = query; - lastSearchResults = Collections.emptyList(); + lastPdfSearchResults = Collections.emptyList(); try { - PdfSearcher searcher = PdfSearcher.of(databaseContext); + PdfSearcher searcher = PdfSearcher.of(pdfIndexer); PdfSearchResults results = searcher.search(query, 5); - lastSearchResults = results.getSortedByScore(); + lastPdfSearchResults = results.getSortedByScore(); } catch (IOException e) { - LOGGER.error("Could not retrieve search results!", e); + LOGGER.error("Could not retrieve search results.", e); + return new PdfSearchResults(); } } - return new PdfSearchResults(lastSearchResults.stream() - .filter(searchResult -> searchResult.isResultFor(bibEntry)) - .collect(Collectors.toList())); + // We found a number of PDF files, now we need to relate it to the current BibEntry + return new PdfSearchResults(lastPdfSearchResults.stream() + .filter(searchResult -> searchResult.isResultFor(bibEntry)) + .toList()); } } 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 fd281a11a0f..768c88f0303 100644 --- a/src/main/java/org/jabref/model/search/rules/GrammarBasedSearchRule.java +++ b/src/main/java/org/jabref/model/search/rules/GrammarBasedSearchRule.java @@ -1,6 +1,5 @@ package org.jabref.model.search.rules; -import java.io.IOException; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; @@ -13,14 +12,10 @@ 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.model.strings.StringUtil; @@ -45,7 +40,7 @@ * 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 { +public class GrammarBasedSearchRule extends FullTextSearchRule { private static final Logger LOGGER = LoggerFactory.getLogger(GrammarBasedSearchRule.class); @@ -55,8 +50,6 @@ public class GrammarBasedSearchRule implements SearchRule { private String query; private List searchResults = new ArrayList<>(); - private final BibDatabaseContext databaseContext; - public static class ThrowingErrorListener extends BaseErrorListener { public static final ThrowingErrorListener INSTANCE = new ThrowingErrorListener(); @@ -70,8 +63,8 @@ public void syntaxError(Recognizer recognizer, Object offendingSymbol, } public GrammarBasedSearchRule(EnumSet searchFlags) throws RecognitionException { + super(searchFlags); this.searchFlags = searchFlags; - databaseContext = Globals.stateManager.getActiveDatabase().orElse(null); } public static boolean isValid(EnumSet searchFlags, String query) { @@ -97,20 +90,9 @@ private void init(String query) throws ParseCancellationException { SearchParser parser = new SearchParser(new CommonTokenStream(lexer)); parser.removeErrorListeners(); // no infos on file system parser.addErrorListener(ThrowingErrorListener.INSTANCE); - parser.setErrorHandler(new BailErrorStrategy()); // ParseCancelationException on parse errors + parser.setErrorHandler(new BailErrorStrategy()); // ParseCancellationException 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 @@ -118,16 +100,11 @@ public boolean applyRule(String query, BibEntry bibEntry) { try { return new BibtexSearchVisitor(searchFlags, bibEntry).visit(tree); } catch (Exception e) { - LOGGER.debug("Search failed", e); - return getFulltextResults(query, bibEntry).numSearchResults() > 0; + LOGGER.info("Search failed", e); + return false; } } - @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 { diff --git a/src/test/java/org/jabref/architecture/TestArchitectureTest.java b/src/test/java/org/jabref/architecture/TestArchitectureTest.java index 81f73e5a117..9df4d5b2652 100644 --- a/src/test/java/org/jabref/architecture/TestArchitectureTest.java +++ b/src/test/java/org/jabref/architecture/TestArchitectureTest.java @@ -25,6 +25,7 @@ public void testsAreIndependent(JavaClasses classes) { .and().doNotHaveSimpleName("PreferencesMigrationsTest") .and().doNotHaveSimpleName("SaveDatabaseActionTest") .and().doNotHaveSimpleName("UpdateTimestampListenerTest") + .and().doNotHaveSimpleName("DatabaseSearcherWithBibFilesTest") .and().doNotHaveFullyQualifiedName("org.jabref.benchmarks.Benchmarks") .and().doNotHaveFullyQualifiedName("org.jabref.testutils.interactive.styletester.StyleTesterMain") .should().dependOnClassesThat().haveFullyQualifiedName(CLASS_ORG_JABREF_GLOBALS) diff --git a/src/test/java/org/jabref/gui/entryeditor/CommentsTabTest.java b/src/test/java/org/jabref/gui/entryeditor/CommentsTabTest.java index 06a4cfc361f..be3f9274d4a 100644 --- a/src/test/java/org/jabref/gui/entryeditor/CommentsTabTest.java +++ b/src/test/java/org/jabref/gui/entryeditor/CommentsTabTest.java @@ -12,7 +12,7 @@ import org.jabref.gui.theme.ThemeManager; import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.journals.JournalAbbreviationRepository; -import org.jabref.logic.pdf.search.indexing.IndexingTaskManager; +import org.jabref.logic.pdf.search.IndexingTaskManager; import org.jabref.logic.preferences.OwnerPreferences; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.database.BibDatabaseMode; diff --git a/src/test/java/org/jabref/logic/cleanup/FieldFormatterCleanupsTest.java b/src/test/java/org/jabref/logic/cleanup/FieldFormatterCleanupsTest.java index b6737e8c924..90676249146 100644 --- a/src/test/java/org/jabref/logic/cleanup/FieldFormatterCleanupsTest.java +++ b/src/test/java/org/jabref/logic/cleanup/FieldFormatterCleanupsTest.java @@ -5,6 +5,7 @@ import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.stream.Stream; import org.jabref.logic.formatter.IdentityFormatter; import org.jabref.logic.formatter.bibtexfields.EscapeAmpersandsFormatter; @@ -18,11 +19,15 @@ import org.jabref.logic.layout.format.ReplaceUnicodeLigaturesFormatter; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.InternalField; import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.types.StandardEntryType; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -72,7 +77,7 @@ public void checkLowerCaseSaveAction() { FieldFormatterCleanups actions = new FieldFormatterCleanups(true, FieldFormatterCleanups.parse("title[lower_case]")); FieldFormatterCleanup lowerCaseTitle = new FieldFormatterCleanup(StandardField.TITLE, new LowerCaseFormatter()); - assertEquals(Collections.singletonList(lowerCaseTitle), actions.getConfiguredActions()); + assertEquals(List.of(lowerCaseTitle), actions.getConfiguredActions()); actions.applySaveActions(entry); @@ -309,6 +314,24 @@ void parserTest() { fieldFormatterCleanups); } + @Test + void identityCanBeParsed() { + List fieldFormatterCleanups = FieldFormatterCleanups.parse(""" + all-text-fields[identity] + date[normalize_date] + month[normalize_month] + pages[normalize_page_numbers] + """); + assertEquals( + List.of( + new FieldFormatterCleanup(InternalField.INTERNAL_ALL_TEXT_FIELDS_FIELD, new IdentityFormatter()), + new FieldFormatterCleanup(StandardField.DATE, new NormalizeDateFormatter()), + new FieldFormatterCleanup(StandardField.MONTH, new NormalizeMonthFormatter()), + new FieldFormatterCleanup(StandardField.PAGES, new NormalizePagesFormatter()) + ), + fieldFormatterCleanups); + } + @Test void getMetaDataStringWorks() { assertEquals(""" @@ -329,8 +352,16 @@ void parsingOfDefaultSaveActions() { """)); } - @Test - void formatterFromString() { - assertEquals(new ReplaceUnicodeLigaturesFormatter(), FieldFormatterCleanups.getFormatterFromString("replace_unicode_ligatures")); + public static Stream formatterFromString() { + return Stream.of( + Arguments.of(new ReplaceUnicodeLigaturesFormatter(), "replace_unicode_ligatures"), + Arguments.of(new LowerCaseFormatter(), "lower_case") + ); + } + + @ParameterizedTest + @MethodSource + void formatterFromString(Formatter expected, String input) { + assertEquals(expected, FieldFormatterCleanups.getFormatterFromString(input)); } } diff --git a/src/test/java/org/jabref/logic/importer/util/MetaDataParserTest.java b/src/test/java/org/jabref/logic/importer/util/MetaDataParserTest.java index 251b3d10279..d043c51fe79 100644 --- a/src/test/java/org/jabref/logic/importer/util/MetaDataParserTest.java +++ b/src/test/java/org/jabref/logic/importer/util/MetaDataParserTest.java @@ -1,13 +1,22 @@ package org.jabref.logic.importer.util; +import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.stream.Stream; +import org.jabref.logic.cleanup.FieldFormatterCleanup; +import org.jabref.logic.cleanup.FieldFormatterCleanups; import org.jabref.logic.exporter.MetaDataSerializerTest; +import org.jabref.logic.formatter.casechanger.LowerCaseFormatter; import org.jabref.model.entry.BibEntryTypeBuilder; +import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.field.UnknownField; import org.jabref.model.entry.types.UnknownEntryType; +import org.jabref.model.metadata.MetaData; +import org.jabref.model.util.DummyFileUpdateMonitor; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.CsvSource; @@ -56,4 +65,16 @@ public static Stream parseCustomizedEntryType() { void parseCustomizedEntryType(BibEntryTypeBuilder expected, String source) { assertEquals(Optional.of(expected.build()), MetaDataParser.parseCustomEntryType(source)); } + + @Test + public void saveActions() throws Exception { + Map data = Map.of("saveActions", "enabled;title[lower_case]"); + MetaDataParser metaDataParser = new MetaDataParser(new DummyFileUpdateMonitor()); + MetaData parsed = metaDataParser.parse(new MetaData(), data, ','); + + MetaData expected = new MetaData(); + FieldFormatterCleanups fieldFormatterCleanups = new FieldFormatterCleanups(true, List.of(new FieldFormatterCleanup(StandardField.TITLE, new LowerCaseFormatter()))); + expected.setSaveActions(fieldFormatterCleanups); + assertEquals(expected, parsed); + } } diff --git a/src/test/java/org/jabref/logic/pdf/search/indexing/DocumentReaderTest.java b/src/test/java/org/jabref/logic/pdf/search/DocumentReaderTest.java similarity index 98% rename from src/test/java/org/jabref/logic/pdf/search/indexing/DocumentReaderTest.java rename to src/test/java/org/jabref/logic/pdf/search/DocumentReaderTest.java index 982504c9938..135944c722c 100644 --- a/src/test/java/org/jabref/logic/pdf/search/indexing/DocumentReaderTest.java +++ b/src/test/java/org/jabref/logic/pdf/search/DocumentReaderTest.java @@ -1,4 +1,4 @@ -package org.jabref.logic.pdf.search.indexing; +package org.jabref.logic.pdf.search; import java.nio.file.Path; import java.util.Collections; diff --git a/src/test/java/org/jabref/logic/pdf/search/indexing/PdfIndexerTest.java b/src/test/java/org/jabref/logic/pdf/search/PdfIndexerTest.java similarity index 74% rename from src/test/java/org/jabref/logic/pdf/search/indexing/PdfIndexerTest.java rename to src/test/java/org/jabref/logic/pdf/search/PdfIndexerTest.java index 629bb293778..187f01ce656 100644 --- a/src/test/java/org/jabref/logic/pdf/search/indexing/PdfIndexerTest.java +++ b/src/test/java/org/jabref/logic/pdf/search/PdfIndexerTest.java @@ -1,4 +1,4 @@ -package org.jabref.logic.pdf.search.indexing; +package org.jabref.logic.pdf.search; import java.io.IOException; import java.nio.file.Path; @@ -53,8 +53,7 @@ public void exampleThesisIndex() throws IOException { database.insertEntry(entry); // when - indexer.createIndex(); - indexer.addToIndex(context); + indexer.rebuildIndex(); // then try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { @@ -63,18 +62,17 @@ public void exampleThesisIndex() throws IOException { } @Test - public void dontIndexNonPdf() throws IOException { + public void doNotIndexNonPdf() throws IOException { // given - BibEntry entry = new BibEntry(StandardEntryType.PhdThesis); - entry.setFiles(Collections.singletonList(new LinkedFile("Example Thesis", "thesis-example.pdf", StandardFileType.AUX.getName()))); + BibEntry entry = new BibEntry(StandardEntryType.PhdThesis) + .withFiles(Collections.singletonList(new LinkedFile("Example Thesis", "thesis-example.aux", StandardFileType.AUX.getName()))); database.insertEntry(entry); // when - indexer.createIndex(); - indexer.addToIndex(context); + indexer.rebuildIndex(); // then - try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { + try (IndexReader reader = DirectoryReader.open(indexer.indexWriter)) { assertEquals(0, reader.numDocs()); } } @@ -87,11 +85,10 @@ public void dontIndexOnlineLinks() throws IOException { database.insertEntry(entry); // when - indexer.createIndex(); - indexer.addToIndex(context); + indexer.rebuildIndex(); // then - try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { + try (IndexReader reader = DirectoryReader.open(indexer.indexWriter)) { assertEquals(0, reader.numDocs()); } } @@ -105,8 +102,7 @@ public void exampleThesisIndexWithKey() throws IOException { database.insertEntry(entry); // when - indexer.createIndex(); - indexer.addToIndex(context); + indexer.rebuildIndex(); // then try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { @@ -123,8 +119,7 @@ public void metaDataIndex() throws IOException { database.insertEntry(entry); // when - indexer.createIndex(); - indexer.addToIndex(context); + indexer.rebuildIndex(); // then try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { @@ -132,30 +127,6 @@ public void metaDataIndex() throws IOException { } } - @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(); - indexer.addToIndex(context); - // index actually exists - try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { - assertEquals(33, 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 @@ -163,8 +134,8 @@ public void exampleThesisIndexAppendMetaData() throws IOException { exampleThesis.setCitationKey("ExampleThesis2017"); exampleThesis.setFiles(Collections.singletonList(new LinkedFile("Example Thesis", "thesis-example.pdf", StandardFileType.PDF.getName()))); database.insertEntry(exampleThesis); - indexer.createIndex(); - indexer.addToIndex(context); + + indexer.rebuildIndex(); // index with first entry try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { @@ -176,7 +147,7 @@ public void exampleThesisIndexAppendMetaData() throws IOException { metadata.setFiles(Collections.singletonList(new LinkedFile("Metadata file", "metaData.pdf", StandardFileType.PDF.getName()))); // when - indexer.addToIndex(metadata, null); + indexer.addToIndex(metadata); // then try (IndexReader reader = DirectoryReader.open(new NIOFSDirectory(context.getFulltextIndexPath()))) { @@ -184,4 +155,3 @@ public void exampleThesisIndexAppendMetaData() throws IOException { } } } - diff --git a/src/test/java/org/jabref/logic/pdf/search/retrieval/PdfSearcherTest.java b/src/test/java/org/jabref/logic/pdf/search/PdfSearcherTest.java similarity index 76% rename from src/test/java/org/jabref/logic/pdf/search/retrieval/PdfSearcherTest.java rename to src/test/java/org/jabref/logic/pdf/search/PdfSearcherTest.java index e005c271ff1..b06bda0d328 100644 --- a/src/test/java/org/jabref/logic/pdf/search/retrieval/PdfSearcherTest.java +++ b/src/test/java/org/jabref/logic/pdf/search/PdfSearcherTest.java @@ -1,10 +1,10 @@ -package org.jabref.logic.pdf.search.retrieval; +package org.jabref.logic.pdf.search; import java.io.IOException; import java.nio.file.Path; import java.util.Collections; +import java.util.List; -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; @@ -12,6 +12,7 @@ import org.jabref.model.entry.LinkedFile; import org.jabref.model.entry.types.StandardEntryType; import org.jabref.model.pdf.search.PdfSearchResults; +import org.jabref.model.pdf.search.SearchResult; import org.jabref.preferences.FilePreferences; import org.apache.lucene.queryparser.classic.ParseException; @@ -32,44 +33,48 @@ public class PdfSearcherTest { @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); when(context.getEntries()).thenReturn(database.getEntries()); - BibEntry examplePdf = new BibEntry(StandardEntryType.Article); - examplePdf.setFiles(Collections.singletonList(new LinkedFile("Example Entry", "example.pdf", StandardFileType.PDF.getName()))); + + BibEntry examplePdf = new BibEntry(StandardEntryType.Article) + .withFiles(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"); + BibEntry metaDataEntry = new BibEntry(StandardEntryType.Article) + .withCitationKey("MetaData2017") + .withFiles(Collections.singletonList(new LinkedFile("Metadata Entry", "metaData.pdf", StandardFileType.PDF.getName()))); 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"); + BibEntry exampleThesis = new BibEntry(StandardEntryType.PhdThesis) + .withCitationKey("ExampleThesis") + .withFiles(Collections.singletonList(new LinkedFile("Example Thesis", "thesis-example.pdf", StandardFileType.PDF.getName()))); database.insertEntry(exampleThesis); PdfIndexer indexer = PdfIndexer.of(context, filePreferences); - search = PdfSearcher.of(context); + search = PdfSearcher.of(indexer); - indexer.createIndex(); - indexer.addToIndex(context); + indexer.rebuildIndex(); } @Test public void searchForTest() throws IOException, ParseException { PdfSearchResults result = search.search("test", 10); - assertEquals(8, result.numSearchResults()); + assertEquals(10, result.numSearchResults()); } @Test public void searchForUniversity() throws IOException, ParseException { PdfSearchResults result = search.search("University", 10); - assertEquals(1, result.numSearchResults()); + assertEquals(2, result.numSearchResults()); + List searchResults = result.getSearchResults(); + assertEquals(0, searchResults.get(0).getPageNumber()); + assertEquals(9, searchResults.get(1).getPageNumber()); } @Test diff --git a/src/test/java/org/jabref/logic/search/DatabaseSearcherTest.java b/src/test/java/org/jabref/logic/search/DatabaseSearcherTest.java index f276f9ddfaf..fa38b68cc03 100644 --- a/src/test/java/org/jabref/logic/search/DatabaseSearcherTest.java +++ b/src/test/java/org/jabref/logic/search/DatabaseSearcherTest.java @@ -66,9 +66,7 @@ public void testCorrectMatchFromDatabaseWithArticleTypeEntry() { @Test public void testNoMatchesFromEmptyDatabaseWithInvalidQuery() { 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()); } diff --git a/src/test/java/org/jabref/logic/search/DatabaseSearcherWithBibFilesTest.java b/src/test/java/org/jabref/logic/search/DatabaseSearcherWithBibFilesTest.java new file mode 100644 index 00000000000..84a54d6b3c1 --- /dev/null +++ b/src/test/java/org/jabref/logic/search/DatabaseSearcherWithBibFilesTest.java @@ -0,0 +1,172 @@ +package org.jabref.logic.search; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +import org.jabref.architecture.AllowedToUseSwing; +import org.jabref.gui.Globals; +import org.jabref.logic.importer.ImportFormatPreferences; +import org.jabref.logic.importer.ParserResult; +import org.jabref.logic.importer.fileformat.BibtexImporter; +import org.jabref.logic.pdf.search.PdfIndexer; +import org.jabref.logic.pdf.search.PdfIndexerManager; +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.field.StandardField; +import org.jabref.model.entry.types.StandardEntryType; +import org.jabref.model.search.rules.SearchRules; +import org.jabref.model.util.DummyFileUpdateMonitor; +import org.jabref.preferences.FilePreferences; +import org.jabref.preferences.PreferencesService; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Answers; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@AllowedToUseSwing("Makes use of Globals because of FullTextSearchRule") +public class DatabaseSearcherWithBibFilesTest { + + private static BibEntry titleSentenceCased = new BibEntry(StandardEntryType.Misc) + .withCitationKey("title-sentence-cased") + .withField(StandardField.TITLE, "Title Sentence Cased"); + private static BibEntry titleMixedCased = new BibEntry(StandardEntryType.Misc) + .withCitationKey("title-mixed-cased") + .withField(StandardField.TITLE, "TiTle MiXed CaSed"); + private static BibEntry titleUpperCased = new BibEntry(StandardEntryType.Misc) + .withCitationKey("title-upper-cased") + .withField(StandardField.TITLE, "TITLE UPPER CASED"); + + private static BibEntry mininimalSentenceCase = new BibEntry(StandardEntryType.Misc) + .withCitationKey("minimal-sentence-case") + .withFiles(Collections.singletonList(new LinkedFile("", "minimal-sentence-case.pdf", StandardFileType.PDF.getName()))); + private static BibEntry minimalAllUpperCase = new BibEntry(StandardEntryType.Misc) + .withCitationKey("minimal-all-upper-case") + .withFiles(Collections.singletonList(new LinkedFile("", "minimal-all-upper-case.pdf", StandardFileType.PDF.getName()))); + private static BibEntry minimalMixedCase = new BibEntry(StandardEntryType.Misc) + .withCitationKey("minimal-mixed-case") + .withFiles(Collections.singletonList(new LinkedFile("", "minimal-mixed-case.pdf", StandardFileType.PDF.getName()))); + private static BibEntry minimalNoteSentenceCase = new BibEntry(StandardEntryType.Misc) + .withCitationKey("minimal-note-sentence-case") + .withFiles(Collections.singletonList(new LinkedFile("", "minimal-note-sentence-case.pdf", StandardFileType.PDF.getName()))); + private static BibEntry minimalNoteAllUpperCase = new BibEntry(StandardEntryType.Misc) + .withCitationKey("minimal-note-all-upper-case") + .withFiles(Collections.singletonList(new LinkedFile("", "minimal-note-all-upper-case.pdf", StandardFileType.PDF.getName()))); + private static BibEntry minimalNoteMixedCase = new BibEntry(StandardEntryType.Misc) + .withCitationKey("minimal-note-mixed-case") + .withFiles(Collections.singletonList(new LinkedFile("", "minimal-note-mixed-case.pdf", StandardFileType.PDF.getName()))); + + FilePreferences filePreferences = mock(FilePreferences.class); + + @TempDir + private Path indexDir; + private PdfIndexer pdfIndexer; + + private BibDatabase initializeDatabaseFromPath(String testFile) throws Exception { + return initializeDatabaseFromPath(Path.of(Objects.requireNonNull(DatabaseSearcherWithBibFilesTest.class.getResource(testFile)).toURI())); + } + + private BibDatabase initializeDatabaseFromPath(Path testFile) throws Exception { + ParserResult result = new BibtexImporter(mock(ImportFormatPreferences.class, Answers.RETURNS_DEEP_STUBS), new DummyFileUpdateMonitor()).importDatabase(testFile); + BibDatabase database = result.getDatabase(); + + BibDatabaseContext context = mock(BibDatabaseContext.class); + when(context.getFileDirectories(Mockito.any())).thenReturn(List.of(testFile.getParent())); + when(context.getFulltextIndexPath()).thenReturn(indexDir); + when(context.getDatabase()).thenReturn(database); + when(context.getEntries()).thenReturn(database.getEntries()); + + // Required because of {@Link org.jabref.model.search.rules.FullTextSearchRule.FullTextSearchRule} + Globals.stateManager.setActiveDatabase(context); + PreferencesService preferencesService = mock(PreferencesService.class); + when(preferencesService.getFilePreferences()).thenReturn(filePreferences); + Globals.prefs = preferencesService; + + pdfIndexer = PdfIndexerManager.getIndexer(context, filePreferences); + // Alternative - For debugging with Luke (part of the Apache Lucene distribution) + // pdfIndexer = PdfIndexer.of(context, Path.of("C:\\temp\\index"), filePreferences); + + pdfIndexer.rebuildIndex(); + return database; + } + + @AfterEach + public void tearDown() throws Exception { + pdfIndexer.close(); + } + + private static Stream searchLibrary() { + return Stream.of( + // empty library + Arguments.of(List.of(), "empty.bib", "Test", EnumSet.noneOf(SearchRules.SearchFlags.class)), + + // test-library-title-casing + + Arguments.of(List.of(), "test-library-title-casing.bib", "NotExisting", EnumSet.noneOf(SearchRules.SearchFlags.class)), + Arguments.of(List.of(titleSentenceCased, titleMixedCased, titleUpperCased), "test-library-title-casing.bib", "Title", EnumSet.noneOf(SearchRules.SearchFlags.class)), + + Arguments.of(List.of(), "test-library-title-casing.bib", "title=NotExisting", EnumSet.noneOf(SearchRules.SearchFlags.class)), + Arguments.of(List.of(titleSentenceCased, titleMixedCased, titleUpperCased), "test-library-title-casing.bib", "title=Title", EnumSet.noneOf(SearchRules.SearchFlags.class)), + + Arguments.of(List.of(), "test-library-title-casing.bib", "title=TiTLE", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)), + Arguments.of(List.of(titleSentenceCased), "test-library-title-casing.bib", "title=Title", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)), + + Arguments.of(List.of(), "test-library-title-casing.bib", "TiTLE", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)), + Arguments.of(List.of(titleMixedCased), "test-library-title-casing.bib", "TiTle", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)), + + Arguments.of(List.of(), "test-library-title-casing.bib", "title=NotExisting", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)), + Arguments.of(List.of(titleMixedCased), "test-library-title-casing.bib", "title=TiTle", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)), + + Arguments.of(List.of(), "test-library-title-casing.bib", "[Y]", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)), + Arguments.of(List.of(titleUpperCased), "test-library-title-casing.bib", "[U]", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)), + + // Word boundaries + Arguments.of(List.of(), "test-library-title-casing.bib", "\\bTit\\b", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION, SearchRules.SearchFlags.CASE_SENSITIVE)), + Arguments.of(List.of(titleSentenceCased), "test-library-title-casing.bib", "\\bTitle\\b", EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION, SearchRules.SearchFlags.CASE_SENSITIVE)), + + // test-library-with-attached-files + + Arguments.of(List.of(), "test-library-with-attached-files.bib", "This is a test.", EnumSet.of(SearchRules.SearchFlags.FULLTEXT, SearchRules.SearchFlags.CASE_SENSITIVE)), + + Arguments.of(List.of(mininimalSentenceCase, minimalAllUpperCase, minimalMixedCase), "test-library-with-attached-files.bib", "This is a short sentence, comma included.", EnumSet.of(SearchRules.SearchFlags.FULLTEXT)), + Arguments.of(List.of(mininimalSentenceCase, minimalAllUpperCase, minimalMixedCase), "test-library-with-attached-files.bib", "comma", EnumSet.of(SearchRules.SearchFlags.FULLTEXT)), + // TODO: PDF search does not support case sensitive search (yet) + // Arguments.of(List.of(minimalAllUpperCase, minimalMixedCase), "test-library-with-attached-files.bib", "THIS", EnumSet.of(SearchRules.SearchFlags.FULLTEXT, SearchRules.SearchFlags.CASE_SENSITIVE)), + // Arguments.of(List.of(minimalAllUpperCase), "test-library-with-attached-files.bib", "THIS is a short sentence, comma included.", EnumSet.of(SearchRules.SearchFlags.FULLTEXT, SearchRules.SearchFlags.CASE_SENSITIVE)), + // Arguments.of(List.of(minimalSentenceCase, minimalAllUpperCase, minimalMixedCase), "test-library-with-attached-files.bib", "comma", EnumSet.of(SearchRules.SearchFlags.FULLTEXT, SearchRules.SearchFlags.CASE_SENSITIVE)), + // Arguments.of(List.of(minimalNoteAllUpperCase), "test-library-with-attached-files.bib", "THIS IS A SHORT SENTENCE, COMMA INCLUDED.", EnumSet.of(SearchRules.SearchFlags.FULLTEXT, SearchRules.SearchFlags.CASE_SENSITIVE)), + + Arguments.of(List.of(), "test-library-with-attached-files.bib", "NotExisting", EnumSet.of(SearchRules.SearchFlags.FULLTEXT)), + + Arguments.of(List.of(minimalNoteSentenceCase, minimalNoteAllUpperCase, minimalNoteMixedCase), "test-library-with-attached-files.bib", "world", EnumSet.of(SearchRules.SearchFlags.FULLTEXT)), + Arguments.of(List.of(minimalNoteSentenceCase, minimalNoteAllUpperCase, minimalNoteMixedCase), "test-library-with-attached-files.bib", "Hello World", EnumSet.of(SearchRules.SearchFlags.FULLTEXT)), + // TODO: PDF search does not support case sensitive search (yet) + // Arguments.of(List.of(minimalNoteAllUpperCase), "test-library-with-attached-files.bib", "HELLO WORLD", EnumSet.of(SearchRules.SearchFlags.FULLTEXT, SearchRules.SearchFlags.CASE_SENSITIVE)), + Arguments.of(List.of(), "test-library-with-attached-files.bib", "NotExisting", EnumSet.of(SearchRules.SearchFlags.FULLTEXT, SearchRules.SearchFlags.CASE_SENSITIVE)) + ); + } + + @ParameterizedTest(name = "{index} => query={2}, searchFlags={3}, testFile={1}, expected={0}") + @MethodSource + public void searchLibrary(List expected, String testFile, String query, EnumSet searchFlags) throws Exception { + BibDatabase database = initializeDatabaseFromPath(testFile); + List matches = new DatabaseSearcher(new SearchQuery(query, searchFlags), database).getMatches(); + assertEquals(expected, matches); + } +} + + diff --git a/src/test/java/org/jabref/logic/search/SearchQueryTest.java b/src/test/java/org/jabref/logic/search/SearchQueryTest.java index 4bf01fadb73..399c89089e6 100644 --- a/src/test/java/org/jabref/logic/search/SearchQueryTest.java +++ b/src/test/java/org/jabref/logic/search/SearchQueryTest.java @@ -20,8 +20,8 @@ public class SearchQueryTest { @Test public void testToString() { - 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()); + assertEquals("\"asdf\" (case sensitive, regular expression) [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 diff --git a/src/test/java/org/jabref/model/database/BibDatabaseContextTest.java b/src/test/java/org/jabref/model/database/BibDatabaseContextTest.java index 55e4fd2a744..dfb069680d0 100644 --- a/src/test/java/org/jabref/model/database/BibDatabaseContextTest.java +++ b/src/test/java/org/jabref/model/database/BibDatabaseContextTest.java @@ -16,6 +16,7 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -150,9 +151,11 @@ void testGetFullTextIndexPathWhenPathIsNotNull() { BibDatabaseContext bibDatabaseContext = new BibDatabaseContext(); bibDatabaseContext.setDatabasePath(existingPath); - Path expectedPath = OS.getNativeDesktop().getFulltextIndexBaseDirectory().resolve(existingPath.hashCode() + ""); Path actualPath = bibDatabaseContext.getFulltextIndexPath(); + assertNotNull(actualPath); - assertEquals(expectedPath, actualPath); + String fulltextIndexBaseDirectory = OS.getNativeDesktop().getFulltextIndexBaseDirectory().toString(); + String actualPathStart = actualPath.toString().substring(0, fulltextIndexBaseDirectory.length()); + assertEquals(fulltextIndexBaseDirectory, actualPathStart); } } diff --git a/src/test/java/org/jabref/model/groups/SearchGroupTest.java b/src/test/java/org/jabref/model/groups/SearchGroupTest.java index 5bdec41d87e..75829b5a9b5 100644 --- a/src/test/java/org/jabref/model/groups/SearchGroupTest.java +++ b/src/test/java/org/jabref/model/groups/SearchGroupTest.java @@ -1,9 +1,11 @@ package org.jabref.model.groups; import java.util.EnumSet; +import java.util.List; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.StandardEntryType; import org.jabref.model.search.rules.SearchRules; import org.junit.jupiter.api.Test; @@ -13,6 +15,32 @@ public class SearchGroupTest { + private static BibEntry entry1D = new BibEntry(StandardEntryType.Misc) + .withCitationKey("entry1") + .withField(StandardField.AUTHOR, "Test") + .withField(StandardField.TITLE, "Case") + .withField(StandardField.GROUPS, "A"); + + private static BibEntry entry2D = new BibEntry(StandardEntryType.Misc) + .withCitationKey("entry2") + .withField(StandardField.AUTHOR, "TEST") + .withField(StandardField.TITLE, "CASE") + .withField(StandardField.GROUPS, "A"); + + @Test + public void containsFindsWords() { + SearchGroup groupPositive = new SearchGroup("A", GroupHierarchyType.INDEPENDENT, "Test", EnumSet.noneOf(SearchRules.SearchFlags.class)); + List positiveResult = List.of(entry1D, entry2D); + assertTrue(groupPositive.containsAll(positiveResult)); + } + + @Test + public void containsDoesNotFindWords() { + SearchGroup groupNegative = new SearchGroup("A", GroupHierarchyType.INDEPENDENT, "Unknown", EnumSet.noneOf(SearchRules.SearchFlags.class)); + List positiveResult = List.of(entry1D, entry2D); + assertFalse(groupNegative.containsAny(positiveResult)); + } + @Test public void containsFindsWordWithRegularExpression() { SearchGroup group = new SearchGroup("myExplicitGroup", GroupHierarchyType.INDEPENDENT, "anyfield=rev*", EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); diff --git a/src/test/search/resources/.gitignore b/src/test/resources/org/jabref/logic/search/.gitignore similarity index 83% rename from src/test/search/resources/.gitignore rename to src/test/resources/org/jabref/logic/search/.gitignore index 30aaa5e7abe..a5db2fdd1bd 100644 --- a/src/test/search/resources/.gitignore +++ b/src/test/resources/org/jabref/logic/search/.gitignore @@ -1,6 +1,8 @@ # LaTeX temp files *.aux +*.out +*.log *.synctex *.synctex.gz diff --git a/src/test/resources/org/jabref/logic/search/README.md b/src/test/resources/org/jabref/logic/search/README.md new file mode 100644 index 00000000000..f607cfa269f --- /dev/null +++ b/src/test/resources/org/jabref/logic/search/README.md @@ -0,0 +1,11 @@ +# Libraries and PDFs for testing search functionality + +This folder contains manual use-cases for testing the search functionality. + +It is used at the test code `org.jabref.logic.search.DatabaseSearcherWithBibFilesTest`. + +The `.pdf` files are generated from the `.tex` files using `pdflatex`. + +Compile all files in cmd.exe: + + for %f in (*.tex) do pdflatex "%f" diff --git a/src/test/search/resources/empty.bib b/src/test/resources/org/jabref/logic/search/empty.bib similarity index 100% rename from src/test/search/resources/empty.bib rename to src/test/resources/org/jabref/logic/search/empty.bib diff --git a/src/test/search/resources/minimal-note1.pdf b/src/test/resources/org/jabref/logic/search/minimal-all-upper-case.pdf similarity index 89% rename from src/test/search/resources/minimal-note1.pdf rename to src/test/resources/org/jabref/logic/search/minimal-all-upper-case.pdf index d6497ba5bbb93c796917d5fe9639d9d2e54d3581..764b3807a3f79c3a95c5816dfc8cd86205b638ac 100644 GIT binary patch delta 836 zcmaiyOK1~89L7oGp^Wc{k7BW7N=Qm+c4i;h-53H*(uI@;LlZ>}QPO0qDRu`ot41rt zAfzqWgUm$`kzTwg)KWnNuLTbt0##`t7Y`y;!Gjb;q;>K@X+)gU%*XHl@!xO1&0@ve z=ZZ(P0qm84tNLbn6$p2OvC`xFTgRliXJd({pQA5lzixFp*Yn@+m0!&G&rep$l}o*^ z4zFGuzI^%BsS5%Q9b)kUz;whR2FLwvv2S8 zu0OBcy6q-iBQvw+rwcz9k_R{J*B6|5pc}b@W$Ky9`i+cDP+)VG1c=}$;70WVxngGZ z0>n)rrswjR5GleGVuGYfUS9S=MZ{i3O#?2TF?GX&QUeYKTaKVU%$z|W062{>AP|6@ z#w$>)gOGt-+$7nAZow4fSS0kE{Q$k$(Mhk`KiK4@^p1~SIwc>XDWSGsZZlVxhDB_r zefC{+S`OP3`daoleGQ09l2b-LOL96?+omMirKt3X;-H(#UdLYxV|rR0NQ7A|7BQhG zsh{ZsT;rK1MkHhD&=c|Vh!+t?GEzlRkoysKBjjdWjO6}_(4GE4n~%o)(8g24-`yh! zJVf;a4G0<+gm|7M*3@X(f3$=J`H#|~u%O5+z9#j1Qe-~unonSnZR0TS6WJ%XX)0oW zeY+;80^7vVE-IM5Kha&{SwRS~Ag*_TwIynd6Np<)iAG>5ps8Azd1xUl3sOYFAyFC% mg?TX|<3YB*6omgr()E56tc+2H1G=-SnlNj delta 1684 zcmah~U1%d!6sGNyD-^0J3kT%NhetsQ&ZBE-5Lv~?Zl=gGbJ-& zvn!alg2IAfeDgu}#Rpk%mlb?bQBYs~X^|BLA4L#RbbZ)|UC*7EHc2VYOYS}QoO{0S zobTN1H1D_PrWc*uEK`Lt{mtH&cN7K`{IxA5l>)K^i#9MhiXhXIs9i)pPY2b83xR{({Su57zNVrPXcwbsITUvk?anTN8x zi>(5gM=)W{_nb=TLc)2LQ?5}#ZQh}};9`byN|QhqhR%kL=F`Ab3Zw)Hbi_nk@#2L_ z1Jlp@jfTXI3opb)0Ey?a`F!Ces?&>wV&1`!!nW%{wNekzKL)1oT-^CS|N zx;SVq({FEbHW9h($>;-NOTS-5;wdIokNPS3%U*5W-*AEH(h|LFoQfPH(UJP{_1UGc(hQRzOHhQwcI6dUWl4~w#BG2rKv|rVCdssjI?UG^H7UHo=t0N9!m|CA zOr;d!xB&*Tp3(;xnNh=l3w>etz{QcsXAuD{ARV$CxP?|&uX%2u#9~Ru(4fbA;5I9I z*O=|3Tj8ePOe9IE7S>(-m6Aj@xQfM&XZcFWy#$wQ;U;Y2R5Y7zHA$X#uUz(<8$lwe z96c3%<)5YWEczk7(ZP9$_kE(%W_nCS6%!_Hh3bVSt8dL^W%wW8=f&`}j`AidM)4uj&J{qn)oQ!xH}ay6?mUsi0X5%zS%aHr`(l>u`zfh0BUwcE1l3Y^LpgwPx!ko!M4D zvkE1jzu$E`W~$=OXHuURF0}B!Y^P&dSEs2S;=ix)8{gcE7kjZz|_py%+=Y*z)r!2kdnzCtz-ba C;8J=3 delta 240 zcmeAz>n)oQ!?Jky&+Lf_QbChi9r@?ke2~8I%}T-1g0=TagRqTC|I>9=$vrx!mxb+% ziaGXk_dBz2SI2cbkJV&ooV&b*-@A8y{3*}X>h~GuUY#aj^}P9=v-QEXLgHfev)fdK zCZ3tWXtFt#ae}#_v4N?9rICS&iLr^cfswj_fx0G_zHfetOJYf?f`*Hgk%1AalF4e8 zj*O<8Gc4U0olPu_ot#ZfT-=;ZoGe{jOf8*UOkJGZj2ul&K_ZrR3O0n4O#Wyk0{}kv BQ3?P6 diff --git a/src/test/search/resources/minimal2.tex b/src/test/resources/org/jabref/logic/search/minimal-mixed-case.tex similarity index 80% rename from src/test/search/resources/minimal2.tex rename to src/test/resources/org/jabref/logic/search/minimal-mixed-case.tex index 42b3e319f8d..9c71a665bac 100644 --- a/src/test/search/resources/minimal2.tex +++ b/src/test/resources/org/jabref/logic/search/minimal-mixed-case.tex @@ -1,9 +1,5 @@ \documentclass{article} -\usepackage[utf8]{inputenc} \usepackage[english]{babel} - \begin{document} - ThiS is a short sentence, comma included. - \end{document} diff --git a/src/test/resources/org/jabref/logic/search/minimal-note-all-upper-case.pdf b/src/test/resources/org/jabref/logic/search/minimal-note-all-upper-case.pdf new file mode 100644 index 0000000000000000000000000000000000000000..6139da156a2dd1ae53ddaa711683984c93bb8fb6 GIT binary patch literal 15771 zcma*O1ytNi66lQt2u^Sf6C_w*W^gCCy9Rf6cOpPS@Zj$59^5UsySuyFHzap=@7+D` zdv6Z&pYH0a?yl@{wgn%Nu6_p`{Nn1yx%w6J=9c#8_fSr_D&jksM=3TibDEX8*oZ=*t# zV$Y`$ybUC+uD7wfn#U8d)_lqF_OX)Riy+e@{w$MmW#iq++1Y9T?)?ICsm@Us+R$8T+I1L;Kr!1P=PXgx>duc4nM{~Y?aG7?bH zN#F7JRFsTd9f5*>MgVI8fP#=nI-1#73qb-5pcdj}1Tiv$z#ujd2QxDh8y%zwq5%M< z|IbZ=`~WZm2mn+9P(u_U3j|HtxqvH!J3B9JWscs?m2jO+k*NQ1)j*#OGf+ZZSsL1F;eIzl3l*g_WVAL|VM z*D2FrVP|4s0|wWT0x-yd9$&A0L;vgIR9>8V`OB2 z?781I&)OUyMu;^S1ZHC3_)Gk!!#~ewl|RG$r}W48uVLBQA$Cl^pU)cqX!||M|Mc_R z{{Qm+`}lv#e|lwRd!G8A=O4+lYiuBPNGSeti}l$HR(2316s#=Ijxl@(eXHGX&er~J3o`z`%r@t5@XSbrpcEybUm@Ep!RrQaG%9AJnmOdQWE{d+;Q z{*^HxN#;M>@lOT@1HddC;D6;~MuMab5tu8aiVWHa%eFA<@V)vFQGG)tJpc9h>7PdiHS8$9UVCVx#Y(-V8W!=!xN}4 zpphTi-_PQhB6@}QstM@qE4|VE_@kcuV+2u4FI7h`u%i`vd371%KITn_@=Jih4sD1l@~cQzsNk#NsVfPAY(}FXa$QoGO*BXBHDhuF7{7Hc4Bi zvfA;5sbC@h``jxZKV9syuL4Pa(=$9gJTvc-=BEM0&iYdCpky(|U#G%3!q&M%SwZ<= zqsUb_!aOPy5D}8R?|!|08KX(0k9<+-2nVD6A_QOom_F!iC$WTegliy18&*+*&gFt_ z^&u#@H)?^ou4jXiVIB1OdN+3$`W|qcG8jEMi9ux|S<3d(5}_210|t86p4L(hNR8Zr zX?gZFEh~d)y>n@_9dq5(>_Y$8WHTCyLtOgfhU6!om@VT2Bzyn5vf59?Pt@TLp&+1E zG~Aq+U}EA&-CUws(wp9onx7_tR#(PP7Uedr7_y~DOD(uBs>>cSUJ|*OIBS7=I!SV= z&uV%DUmox=asjYg`dV9Cfjy{Dbx((hq})fGVYC+_ce3Yr;NeT)J)@^;7+y>4 znx97^R0yB;lQ*MRS`6xo{p|s+mxfs?5I)l(=#FpzTdl2*^S2W?eU)6i4H6JnS zw$?tqhtsZ`C#efQt;Z+)N72Y9qTVOAHykda-Fy>4KD$qR=8{xl8COg2z)YvFP47O^ zt{b7YJXz$U-&>d|e_{o=CG@6UW=sLBb3HBx>-`J4J@eAzWX<~Scs>v)AUbY zUO?|pK8g4kc3YpJ_aN#bJi^*TPg^~~>O@vQeI#yxavtVemNd-q!EY9Ve}Zjd7kor@ z)3($p9(vN{g{nV8wgWu%_(dSJ9NwZ|+sDuGEe9f;%U9F{i-J3nxAvHdM(#cFdr^j? z@2M_Nqu;G%b&ySD9O)U*+Z!sT8=o~UeYp`W3P>`pys>LrENc+#jem`#WUoRJpw%pj z$Je-Aa~xdC7r9-oH4g`3XX>xs}#B-%uF2E;-lM5aKms{ZBo*U zmj-~n&N-QT70pnaLx~I-xf#pXUl1O;9&p9VmW=7DM(wkv^!AS9Jmqc6Hwi$&dZs@3 z@;KD|`^V1)4gqv`o~ zktP_8kc%%vk1%R>u&u|8lhAA#zee~P4yBK`54u6Lsa`6%dfad_F}K~(JF`|!|`N~4Kdn1DvF$EN|LGK`?P%vKPeUQc?O z5KS1$pkAtCQUzmJVXhw7(#7^Ur>hpv5f3F4h&Os+w{w>3;LQVZ2p8#HjVhYml+|gC zL~Nl(>Urp{f_@OXHm9|dMxPZ+(@_^JNb?A5Gr|`yyIW))^CB6k zFABf>j3670DNMlWe-AhDdIL6GtQl_1E$*J-M?L_t%_Zi7Uvr{pmn{fXGFqm(IF@z9 z(AvyuLy|jncOgV8oL%H_c2PCNDK#~}-h+EF+<{wEVo)poM%D%PeJmR*+~Ns2&&9lo z+-_$)wSnb%gq34^esPZB0ohGW<&O69to#$`^SH&jiZz;N*$6q@du_1bZN3CA)8mzcB({iM<$KBSV+HvLXF;`D7J?J%g_PX2dUQ|m4-wj@9P!Id3gzRb zKdj^^7xr_cc54xtaTxH^ZMx#}*qdukVdHWQtlCipH{o6lQJTfx`o5^v{wl9-6!a}ssZAZu)a{}p@+pc9ys@wNk%cm; zH@?p-Ok8n{A(S%|p~^1)rb>ToDt|2g7sBU7tv!5&5SH(w4$4^?6Ym5=%2&`|H+fog zq!>$E*Fo79e_(*kW1go-pE^!qbbN~%H4_~b;DZKoD#hqUr@d1b5hoJwHK|UQS8xS_ z^+o8IgdFYi9He%k#>#nelD$`I@=B!cN%3JZ{$XI|p=gGTkgGy8B1}@bbtTYy^ zli}$L|K2z)+{#4l2G_j@wW}i!(2v116N^=AWUK1n_y9*i^~LS<+ll>Hp(frKyosid z$W`A53_EcN)IVLJJVf4Z*@P;+=ud`ni;F#F^=3hx7i;S9xraLYU17 z-m6TYF;8NCet+auEe+*c?Erq+wj^R!Ql7x5P*t^KAs*}|p7^(Uj&?Wi5Qw_n$ zN~VaYjzqdE7DHS?1ZoF3W*Z*PI{9bV`W?w}ajxxUyTJ;T5-Y%DBpd49EJeV}4j_%tS7fnSEOVMH@ zIt{iHU!vgG>L{f&Id~{G$;^~@DJUlq#Tw){1c6b=LIRym?`r9@c9bv+ zpve07#Jz@N$gY;g48Du1O&;pNgqN--!k6GCo3CK9VrwHcrTnlfmJ=B#TIgNQ@7eQNSrsghn9kv~U5vytxynIC%Wv|dgGjkZ&UUZ-OYu;po z82inBy#iwpp=NQf^v{wUD(;jqMobpJfKM7}gW1WoX!>TaFsPHVPKQ zf#i-Zjl^Qq;+zL!YOWAZ&cQ=0UdiT;pFt|I?Cd%9CWB^hi@vcpDEC~r=$hfjAkSK- z>vnGlz>*0xf_AA!cG-+5f{U+52Cpzhzfbuzi^$0==CGk&jpxbNjaP$_IOC0}=_SKa zQWmlEFk}H$Pn8pD=cMhv7~>Skjzz$nUE&4vjSr90Bhf|IP)^8Gsy6Q(SdUWtacdk3ek&K<$$aXO2yTS zd}%YU@az$vHI4XA??76zQp|>5W>f`T8($l-LBr!ys#wxJ0*cv(K-ne$aSRC3_?K zV67PAm9m>h5oRfAVDTa zq)0sONstn$Lgm+fc$tOwtYGts5&B|T8uD=S#vJ!e!F}wWTf10r{gP>heQ8~}PAEgN z{!n(L;G9V*wmw1rUWaZlGo?rwHr`~KW9{5f@6JJ&u+;s+xfS}%WyW%l>XH~53PESPDT&*FJ^xz za{lZCkMUX~S7|do{oRMnoy`{3%WGbt6?eiOQGWQ_P_EpKc;J%wsjJ7+cE$7%`bqN*Z{Lm3Y%n0a!uMME z#W6o9y%vZ{zvtVlys_;_*!cb18TyNP_;;cWs@}oSW>qkEE|#zhy-RUcqIaGRf!qRl zto;R9p0x1f6=dEH0UlB{sesmp2GiQaz@ zQi;9cPG~uzDt{C#I{v}E@s79p&1=+}ahpZIz^@AlsBl>Ec?t%ZM1$)ikp>ppRYa|r zCSV5dEaSZHKu$L2>xk_m=b+}m#+S?=Kxz72I0-Q10jJhHsI3A@>Xo0fJ+IH@U1Sz< zq*6J5n1+6W{W?hZkd7Z^O;5vwL{B5xP5M=vWpl>CpFm)uA>S|RrA0lJS27E|A#eCtC2hEO?R;T_j|5iwgzq}qhwEAtcz}I+lMbh~i(Fr=v zw~^F$?HzSNS7=~l7MbeM^~Z%JuIM~kKGxI#uUVJ-w4HnK=gM=7Wo$){j;o-#x_gI9 zc0EHYOn+75;2429xffYBXqbE_jS?0DMg$z|2!5goFQb{YmKX7^yxa@ghh3us=o4u$ zj|AY8mz>f4IrGIF-V!>(x{8YNWz>2m?m5TxqvI}x5L~T9P8wTN4NWZXBc;ATYcX>O zPIZW(r7U1T^QYuse2mY}5Kgm%K{Gup<>Dwfp*?Hg<%7w1h^>p>RHe_r`O!BOOqfIK zHoxZh;B)SHB%;Us))Qb)F{v7nywaT+LuiktKV2gDh5* zI=%`sCGx1qY6E;jQ>=MgSvN83WRP4L5Nx=Bh&b%*by~d+mursy3vVb0SrLh>BRqi& zTNZ<$4Wr9f@)(}7uuvNGBYh#$2U~Dx8>xsiOy2g}{rAQVk!1;t)vCmx>b^;uYIa_Y z4{=PFmkyoj1~nir`wffm#?JA42VL%ngO2J%*?3_uoyO}adSMHEL@T&k!*G+1XkV-s zsR;NYP(XG;2H$x@$s-L!mmxo+T8gR~m%ixX5*p)sDT`fA$iE5JF$%yFIWsOq`_+&0 z7QJR)1>w9jm`^)<**82;s`IU}dd8ICZ|g!I(N&!;Q2iI2?2Nbh#~d8d$i2pqta8A+ zNJG@H~MlM%eOX3JuId0-0blngkYQ#^-;0fS zaw|=oNRpK$vcs}Wlozso!{HNV9OmR)ZF!ZBu2JyI^%1%kC%ftR;z#E^`}FXXUhn6* zp;v=n(%aGl_(#BZVA}_x3z-AK!Hr0ft>y5eoD#9}C>>2ghis-kRW~-k;?4W_)eqMl@foz8Etd zeD=j2te6{buaCi}>;JUdihynJZY9gGCg!Veba=P7PwpXdkZs^`g9o+ZG zYTzoV^&~u%UVt%a<-v{QaVC5)CunQb7R(klX~~uledYgC~=ZHrp0WZ$qY^%0%2K8B=iFG>z$IC zQ+SNH4n#@gq`4Q$23?1Y+u1iKyBW#TM#dTs%Nmj$2T8Y_LOvn4IG6LPAzu+8vMccU zmXp(dp4rHKm>t)y5aun%x|BK!@IdaXAfEs19b~%pYsAztu(w)orhi^Wb`-6HTvDQ3 zm4mlF)d8muM5gKUwKcyHI^b@8o8&3^J+N>=@!{xO4-MC{Sk09Vdy5Uo3rt7S8T{Rg zLH`H5ok0iT<%^KK#1G1Ex9=Q|R%D~-JD_l@gdLM`dv#=0yitBeePeS&VY!Nr<8JVX zcn`YE<@hO|X>3F^*f2g?O&E=s#viOfJuYl@68E$8&WugJ_JMb?QH8;SOgIyHXnD{h zD=pKK`0!8~S+jXzf+_STbc~q^w`{n;ZqHO@?U_ArKqR*7L2VqI@j{-@vC5Dc|UqTzKd5RXImjMRPLz!w(w#!e5pUt7|)Vu#OXk z3_`E^U6JA7%SHWqrynf&*mCE(>rEG_>CMgzs)2+GD(PWIXS{o z4Zg9k5#O2`JFCqlBd5>ir#@|cL|wiuOAZ>Mjp147`KZ zDrcLr=`p$QM*P$kme#vqL%DxZx59&S1s8Q$Di`ntXYd453sz}|oUwjdH{|wAoYSvh z(cG3QAjV9YTp)3N+D_!A}qlZu$m|u0(8gDlJ_hc%rH2 zzJA57T>mpO&c+o^wR;Y=CaDPTH`7RRbQIvZZgp=w9f z4n!BShu$>PCb3I9u8Z>qqrf9Nd4=npVye|uOnZLceyc>I=h3ICk1hb~zIkN}tIq`Y z*#?{A9x-QE%G7fUQBJ1T3D3*j2lb~-U)Ee3I&j@j$Gi{+m9e)=6j9fYdwVly$&FDt zab`sr{nbw?+N>HTIwD+C)OsCUajS+|X&e7M- zLE|Ug{k-w}dcEab&_8azCcK3VHxu2@UWM%}VV~k1%QGgjwTnx6!)~EzpvaeUof>If zt|=(?LB?Cg)T>gA_}n&d<16zED~IL8ZRv=42e0pMb4^onUC9VkPPYx^vroSW^OT^H zN{(s-hsuiHorYx-s8}>K(5BDn0fa&@%70Q=4|odKJfdEx6}f$oNKgf>zOBrFXDz0P z$hN5FYV5Ms*-AAsn(NP3Yes-G3n;rqr3Xx6aY96Y%aJQRdY}`A~CE-KmPf&LA z&ut-QA{RDT=aqv2R_Yow|ImCGhdXWH<;^TMyN8Uge9Onf_k4K(>d(aiL4aaABk|}y|`GM-|`Y?8bwOy zF*^TlwBxSFxqW(U>B_{lTFmP3#k(X5Y!*B|my2GKbRIJu%xn%bb^f7H=uGVB%X>3s z&e_YTkemu%`%gjxo8LTiMiV?nXtwlz%s7tYiMhPHzbLMvU z^nIahBL3p)XfrL(N6ky;TB=i}S6jBnFc#?)9@{B7UQ&I;CdSJnh7d)<%ZV3cD#PX@ z5-U$EffB2Zi&fvE@q%w#Mp@q{Ml^&Timl;^fbP4ckav+h>uP@3$A)29u+7Fv(>VSr zWqQxk4ni?JQQH^XAMqFLs~ioOS+LAAOM^sJGjlqsA1QS*1NfI}UX;@D=J#i6%*Xid zyyMvlF}&K@BYP{o-PgQPw_Pxu^F~R=&TEF&c;iTI`q9I`k)B5Th{8I=pSZa1E#ir- z7dEo8!@CVsg%84gs$)$!FU5PFUKCo&@IHDfO^c!N3&Iejg?_U6+;Xg2(U746jK$B) zq(^xz%UMBZFIb5YGv4WiNceWO{#8W|@-2P+ZAM>}v{UJa8i|cEKnJg^?<0ct%XtP1 zH4K4JLWPHeWhZzz8nPp-m3+W185&-gENSzBo+(-1KA63OmN%9H)~R>Q^t4NNeDQVK z9$s(#yS4a@pGE7}b&f2AaUEx8iTQ{pQF~hRC^twiy3DcVcz->(#Anct3zEZ@X8qMq_|=eRKKF*Kw)36*%Fjn%+@&~Y zc!B_otjweJ83pJNbo#{4SRt276NS;h1Jrd%C25|N@7xYI>jr(|W#gE{vldof~=0quj>rZ2DP%MBIX zfqIKEXK=bBznt4=fq=Druu<6kxrERtOo|=ab%s1)H{FPb$PwpW@9@6FSG}67I2R89 zSuZ*YwLHbO^ujt zN^ZkqH4%>pOn3_Eq6dOu^!D zxNeJ|4D7Cm$KDvak0KZ|e>;gcNLljOakl!1h$e4guDJ@`2TJn^jwtveb)?tV@1habDj0j@g{2}xTSC9~Q|Odbo{{?-vN0<}nb^5I@}3^Gr1&*-&YNSk ztY6%Q-6VW5SHEgBx+Vg_^wQzqF=S_uUYT}U2o&~&!I9HAF*Uo z>UL=sZG%RTcRdK^k5}G-H^@FX)0V=MdV#R%Io_~lnlyEuQr8&FGzb5BO>LYt;DO9V z)Vn|3{M}eMzD3Uc9=pDx&TJ44TatvW&-l*s5h?XD&0%DovSkY$Z(q5b+PEl4L+MJS z)7m~(2;p6{ZcGac?tU`y8@{(PhFKZ>A2HyNoOA-1baCKn*IN?5-RW-%2^}q`63`t* zm)6`iAbt5^&e?dr@~TyTXt53a&ZPgnolhBkXQC~)>B^5mr54QW&t~qZVbTKmmNYc_qS(0W> zvMJYNg$AEVw3@J6y+WJbwF!sk{75$8>9va*JB1->kc7(hZ+;5vKQ&@?=qv_Ma$RZ7 z8xaHt{;I@mqmEFrcM>zJAUl$ZnMk6nyp0cf&^C`?FErf`>kr6V(vXv=eG9GEAQt9}v^qi-(uRQrD~0 zj8vF!P6L+b^Xt z&q1R?J2wxtdZ^=cM>Qwlsg{LylN>Wl=E-OU`invQLr-}3VE8ymk|+&p8|j3(kfLfL z!-mN{S_YTTWA-b7kNd-l^YXUivluZS^-T?7DG%y<vx?X-33S@$l#LAP8Q(R6`romV;%T3D-Pn++g%qaTt`S*o?4ozDj9Y z`9`^h=$?K8jr3MrTav8b!{FNU+TuIt=E zAl3VJx!kcbEZm~^=W^3(kHopmo`uB^a0Xf{TL~e@;`YIjZ*x&-xI){(PMl61#)~$X zp|W+Qcv*xqFRd`L%;#<$G_w0%-vHfqq(Oai+_-)s&^;zUZP8H|_?cI)tc%{!ye7uH zViJ3}dszdgxvjg>S#Cg^;4<#4M^&oO*7{c^QLSLK9~&smL&2Q# ziHr4)yGn+jGvu;{IxSo||<%EiyQ$SH0=B%)xQ zk*ulG>D1+&p3-^gCiz=P?*wWl5T1MkEzbFI8vYvb2ex1V*EZeoJSo15SedY)RHa+sfWCx@nA z=Tneggk@cpEj_ZAEO3Vk2S&;8D{iH6*(a1Ogk+oh_h{3rE566e<}&K$)|Xg`NjlL* z#q7*Dr*X^(vH!G0$}480UA{DMb5$edb;(jP$Irwpowv&K zViIn&CE~cHpH6Ek>n(;_S)WRY6y7v(zkVgTHXTLk*IIZr#NxqJ5!JIo95*cCE-)SJ zz&z=jlI6URW964Q3UkH0+g$J4Xf-O^E|naxo`U`DRoBl;q_-wnw9?;_mT<-=bR@gx zOZ@QYMb`xkeK9#h2`p4H$)+)nEz+2bp~cLAI3{1~vPgq(a2;ZxOFTgly|8$afI8wsk(7B2!{nRp?pxoZPb1TY`Ww{~r z@+n}zT%D7WEUSInOv3$4&JoPA<4~#C()BgBmo#PIr3FZ_r!kOkyyeDux;7VBBCJhz ziotn37Y-O7;IP({M~i(qHQUfsi4`5AH7TUTc>t2)z4CYD$)T(b8bhqeS&e~KD#A+^ zKauZ{&X$C=i`{>@B)2oW>S>tvyrCC#e?qoMLf*y0fe9AF6wA$K9dB+3p zX#GwupR0!oc!;1wfg@s?th*f2;^WCufcb&l)zlN8=&)}qtTQ#=GT`%hn`hJfSDQRZ zrQhIq)G_-dnycE(oNMw>Cttccx5mcZX6fpVf>RFFFQs7=T99-@!S?Y!? zUd)ScS#d@xZ0-1gMm)B!Eqe{rC_V>vw@zJ{T6U**Ulh5!vk}F`BX_lg_h;I)u$hs$ z~$ije3``MsFw1?HC38AV^M479&luR&tNgC@r!cWjb;uiZz z2K`!9HZ-MBk5K(?2xS$Gp5BTy91YL&ikFR%E&xf^{nL?tc*Tv~&#${7Q*6Gg3p^3* z=aAbUl&lJf4CAMq#F(8Lai$ruZ0y>5ulk#5e=+Y9-%|$;tzJuoarWyYt2FagJeD^#u>nO{zK*4GE})am|G*M=}|ro zwy*HY>eWF1k@ZEQKZE`K?m`(&nc~2g^1@G#Bz~=%o9r0_78u4!MwNM8{HV-t6gLN}SHNLA zMV+$8Nao7+J8f?|33LLF=1%!umwa08B%6{DMxYC3427w_jXR+fjF zL;-W_t2USZ79VSa$vv<$kX#}gcR!*B)p!HB$#z#kFLo4%40Ec!gL7a^f!F7*kuz4R zdL^_BkDS3#^wC@@1F7C8^5d>!4R4vNp95!@`GLS$L(JY!WtJ5lE{jRr4Wih6y)m^B zK|L3R7znuf=ae4S#GXTegkOlPs-V?FFfZjB2sY zBCRa85ah7mfL;(}Y7xiME_f~MQ$B^r;LzixnkV$0^aa7C0nZMA|s?pxV+o zQcn-)jh(|m_!aK50XDtn4n*UDfENd=*SQoqrRtmC9D%q2HWLLEr?~gk&A_@rTzaZPK&*y%-C|i@nHZ+xp!rc2v z9ZA=$@ZAbSyD~bfntLZlm=&8b*yf33(mHpp1bY!t)IOiP42*p}JprUT2|t;jE+$-` z>nJbroHbe7G%DFSrw`wE&68&Iw92-iMD1YqLTB|Hq8+5;4r|hrW_#dP;)-rRqjFSj zbJN~HW;*R6B4?LAiTzqQf5TllP&TILI9uu255t(#Uo`hq;0V{LZQptY_nt(``+J?g zda<_4nKeIc1!gF%4%s9Lm37pFVY=1~(M1u|6m;ai#kj`|;L4xFIutts8n&wOSBrA; zYD$UFZkzJLPO*F*F|mgCl+DeImqS^A%A9{|6jq6!AL;Rrk`)50PVLT#*>;hSRIU2O zOR)qP4=|d;1v@0W82YHn7RA%`q#qZ9${FNO!4aNqDy^T>4=C?9^~$0Z3kCPz`{$sL zO0*~lx*)gaJH%4jc-W$ph+@cCHw^hse8cIKffdI(AuGE;>V{YMmck3<(n=9;!^*v7 zRI)yD#pjgiy)ktcT@)yh+zWX;g|qDXC4Mt@C}gz~HMu3duCU{giIM+(Dwh zFHKx@wZx)^#_R3sb(TQ^{**UAe*|Ma)-J3Pjk}o1)bi#oOZ;#aoE$FmL$$>92?hu@t|cu1X8?*jY2@O3Z0|S_ij8u+!baDn&5W zdWLSiT^^tR=;M-Hb5?LCaTVk{UUPL8ijnvAs2J99+w-Pf@N6B)gT-m^_#H9q*}hjk zuR1?zh5lj_k9*J!Uu@?(&i^ts;Qu9Cp4kZ@BL@R}Gh0U+d&muRNc|6WqO2mUtRO%q zD6Ig7Tvms$LnaW!=8uBFZ@h+{83dqb{C!y;1R)z(KoH1=-@)LSRAJ?SXbS4tiW!-i zm_n-TOh~}zQJ&lA!OxB2j(V162K?40mPXHa;T0W?tW+RoNICn1w6Qh<+8WuL*%$)tfet`Nnm-eS5Kl&c-Jq{Fqr?or4L|Z;`o2&q^y*sZB{|p4O^#; zVW}8s4_A99~p zXV9g&R+gLC+86Dc5{#?6tj+uBZ;Wmg$Eswdxa|k6(oFXXxmwz3VznJhcXBPKDxG{@ zwO5DXzU=L8V~%alUvT1&FW*T>;zoVQQM|^G(z%^Im%eK~w27RF zySQ9)Msm=+pJ8B{!dtK$0Q1}a57>%g0=#j{mN?Kexe_9n z-4cIpx^f%!8;W4?N@hGfi$qMxk-FtJs@O}a)oHrRTC-~7a`(E(cUYQ37~!ctOl!xQ zs@%MNOTY4v>aFs!BG>)@IMh zBH;J&{l|HJM*03ZyJtqw;TaNig3JZdwWOILgz@`LP5zx&1OJ_vd`@=|C2AoX1A0YA zJ$pwQz%$XN0HC&o^u_?D0X&m&kg5ZOsDnU$w9leva!mn}@TAPF%$_mAzgz!Z@gJl9 zgRnDlfY|+>$Ul7gJ52W{;(~gRTxnzSOfW*6`!~S&XWHQ3bNkaK4T%y^*}+KG$-D8jlH1* zHKdRKDn!kNJxI*TO1R>8jkU;-_K^Qj{2sCL7__yqN!Jc0L z>wn7_*&w_6zhq1tkahVl8T<2E{g({H3R#!`lCd)XZ+~o%5&x+NW@cuG?3MqrWn+OX z>px^*W>#j%_ufBbAP_tI|H#11Z2vO`6Z7+l{f8|WLXiIN7;GTO8T+RmGdtt|^vBH3 z^4~E4Gjses29EZ6kne8$Kj5>OJLJ?ueiJCz*g#J6??Vas>w6a5hfvi0X8;X!2ez4H%{&FeENUy1279C6EhMe KrLe3B(*Fk_Z5@{wgn%Nu6_p`{Nn1yx%w6J=9c#8_fSr_D&jksM=3TibDEX8*oZ=*t# zV$Y`$ybUC+uD7wfn#U8d)_lqF_OX)Riy+e@{w$MmW#iq++1Y9T?)?ICsm@Us+R$8T+I1L;Kr!1P=PXgx>duc4nM{~Y?aG7?bH zN#F7JRFsTd9f5*>MgVI8fP#=nI-1#73qb-5pcdj}1Tiv$z#ujd2QxDhI~}A5q5%M< z|IbZ=`~WZm2mn+9P(u_U3j|HtxqvH!J3B9JWscs?m2jO+k*NQ1)j*#OGf+ZZSsL1F;eIzl3l*g_WVAL|VM z*D2FrVP|4s0|wWT0x-yd9$&A0L;vgIR9>8V`OB2 z?781I&)OUyMu;^S1ZHC3_)Gk!!#~ewl|RG$r}W48uVLBQA$Cl^pU)cqX!||M|Mc_R z{{Qm+`}lv#e|lwRd!G8A=O4+lYiuBPNGSeti}l$HR(2316s#=Ijxl@(eXHGX&er~J3o`z`%r@t5@XSbrpcEybUm@Ep!RrQaG%9AJnmOdQWE{d+;Q z{*^HxN#;M>@lOT@1HddC;D6;~MuMab5tu8aiVWHa%eFA<@V)vFQGG)tJpc9h>7PdiHS8$9UVCVx#Y(-V8W!=!xN}4 zpphTi-_PQhB6@}QstM@qE4|VE_@kcuV+2u4FI7h`u%i`vd371%KITn_@=Jih4sD1l@~cQzsNk#NsVfPAY(}FXa$QoGO*BXBHDhuF7{7Hc4Bi zvfA;5sbC@h``jxZKV9syuL4Pa(=$9gJTvc-=BEM0&iYdCpky(|U#G%3!q&M%SwZ<= zqsUb_!aOPy5D}8R?|!|08KX(0k9<+-2nVD6A_QOom_F!iC$WTegliy18&*+*&gFt_ z^&u#@H)?^ou4jXiVIB1OdN+3$`W|qcG8jEMi9ux|S<3d(5}_210|t86p4L(hNR8Zr zX?gZFEh~d)y>n@_9dq5(>_Y$8WHTCyLtOgfhU6!om@VT2Bzyn5vf59?Pt@TLp&+1E zG~Aq+U}EA&-CUws(wp9onx7_tR#(PP7Uedr7_y~DOD(uBs>>cSUJ|*OIBS7=I!SV= z&uV%DUmox=asjYg`dV9Cfjy{Dbx((hq})fGVYC+_ce3Yr;NeT)J)@^;7+y>4 znx97^R0yB;lQ*MRS`6xo{p|s+mxfs?5I)l(=#FpzTdl2*^S2W?eU)6i4H6JnS zw$?tqhtsZ`C#efQt;Z+)N72Y9qTVOAHykda-Fy>4KD$qR=8{xl8COg2z)YvFP47O^ zt{b7YJXz$U-&>d|e_{o=CG@6UW=sLBb3HBx>-`J4J@eAzWX<~Scs>v)AUbY zUO?|pK8g4kc3YpJ_aN#bJi^*TPg^~~>O@vQeI#yxavtVemNd-q!EY9Ve}Zjd7kor@ z)3($p9(vN{g{nV8wgWu%_(dSJ9NwZ|+sDuGEe9f;%U9F{i-J3nxAvHdM(#cFdr^j? z@2M_Nqu;G%b&ySD9O)U*+Z!sT8=o~UeYp`W3P>`pys>LrENc+#jem`#WUoRJpw%pj z$Je-Aa~xdC7r9-oH4g`3XX>xs}#B-%uF2E;-lM5aKms{ZBo*U zmj-~n&N-QT70pnaLx~I-xf#pXUl1O;9&p9VmW=7DM(wkv^!AS9Jmqc6Hwi$&dZs@3 z@;KD|`^V1)4gqv`o~ zktP_8kc%%vk1%R>u&u|8lhAA#zee~P4yBK`54u6Lsa`6%dfad_F}K~(JF`|!|`N~4Kdn1DvF$EN|LGK`?P%vKPeUQc?O z5KS1$pkAtCQUzmJVXhw7(#7^Ur>hpv5f3F4h&Os+w{w>3;LQVZ2p8#HjVhYml+|gC zL~Nl(>Urp{f_@OXHm9|dMxPZ+(@_^JNb?A5Gr|`yyIW))^CB6k zFABf>j3670DNMlWe-AhDdIL6GtQl_1E$*J-M?L_t%_Zi7Uvr{pmn{fXGFqm(IF@z9 z(AvyuLy|jncOgV8oL%H_c2PCNDK#~}-h+EF+<{wEVo)poM%D%PeJmR*+~Ns2&&9lo z+-_$)wSnb%gq34^esPZB0ohGW<&O69to#$`^SH&jiZz;N*$6q@du_1bZN3CA)8mzcB({iM<$KBSV+HvLXF;`D7J?J%g_PX2dUQ|m4-wj@9P!Id3gzRb zKdj^^7xr_cc54xtaTxH^ZMx#}*qdukVdHWQtlCipH{o6lQJTfx`o5^v{wl9-6!a}ssZAZu)a{}p@+pc9ys@wNk%cm; zH@?p-Ok8n{A(S%|p~^1)rb>ToDt|2g7sBU7tv!5&5SH(w4$4^?6Ym5=%2&`|H+fog zq!>$E*Fo79e_(*kW1go-pE^!qbbN~%H4_~b;DZKoD#hqUr@d1b5hoJwHK|UQS8xS_ z^+o8IgdFYi9He%k#>#nelD$`I@=B!cN%3JZ{$XI|p=gGTkgGy8B1}@bbtTYy^ zli}$L|K2z)+{#4l2G_j@wW}i!(2v116N^=AWUK1n_y9*i^~LS<+ll>Hp(frKyosid z$W`A53_EcN)IVLJJVf4Z*@P;+=ud`ni;F#F^=3hx7i;S9xraLYU17 z-m6TYF;8NCet+auEe+*c?Erq+wj^R!Ql7x5P*t^KAs*}|p7^(Uj&?Wi5Qw_n$ zN~VaYjzqdE7DHS?1ZoF3W*Z*PI{9bV`W?w}ajxxUyTJ;T5-Y%DBpd49EJeV}4j_%tS7fnSEOVMH@ zIt{iHU!vgG>L{f&Id~{G$;^~@DJUlq#Tw){1c6b=LIRym?`r9@c9bv+ zpve07#Jz@N$gY;g48Du1O&;pNgqN--!k6GCo3CK9VrwHcrTnlfmJ=B#TIgNQ@7eQNSrsghn9kv~U5vytxynIC%Wv|dgGjkZ&UUZ-OYu;po z82inBy#iwpp=NQf^v{wUD(;jqMobpJfKM7}gW1WoX!>TaFsPHVPKQ zf#i-Zjl^Qq;+zL!YOWAZ&cQ=0UdiT;pFt|I?Cd%9CWB^hi@vcpDEC~r=$hfjAkSK- z>vnGlz>*0xf_AA!cG-+5f{U+52Cpzhzfbuzi^$0==CGk&jpxbNjaP$_IOC0}=_SKa zQWmlEFk}H$Pn8pD=cMhv7~>Skjzz$nUE&4vjSr90Bhf|IP)^8Gsy6Q(SdUWtacdk3ek&K<$$aXO2yTS zd}%YU@az$vHI4XA??76zQp|>5W>f`T8($l-LBr!ys#wxJ0*cv(K-ne$aSRC3_?K zV67PAm9m>h5oRfAVDTa zq)0sONstn$Lgm+fc$tOwtYGts5&B|T8uD=S#vJ!e!F}wWTf10r{gP>heQ8~}PAEgN z{!n(L;G9V*wmw1rUWaZlGo?rwHr`~KW9{5f@6JJ&u+;s+xfS}%WyW%l>XH~53PESPDT&*FJ^xz za{lZCkMUX~S7|do{oRMnoy`{3%WGbt6?eiOQGWQ_P_EpKc;J%wsjJ7+cE$7%`bqN*Z{Lm3Y%n0a!uMME z#W6o9y%vZ{zvtVlys_;_*!cb18TyNP_;;cWs@}oSW>qkEE|#zhy-RUcqIaGRf!qRl zto;R9p0x1f6=dEH0UlB{sesmp2GiQaz@ zQi;9cPG~uzDt{C#I{v}E@s79p&1=+}ahpZIz^@AlsBl>Ec?t%ZM1$)ikp>ppRYa|r zCSV5dEaSZHKu$L2>xk_m=b+}m#+S?=Kxz72I0-Q10jJhHsI3A@>Xo0fJ+IH@U1Sz< zq*6J5n1+6W{W?hZkd7Z^O;5vwL{B5xP5M=vWpl>CpFm)uA>S|RrA0lJS27E|A#eCtC2hEO?R;T_j|5iwgzq}qhwEAtcz}I+lMbh~i(Fr=v zw~^F$?HzSNS7=~l7MbeM^~Z%JuIM~kKGxI#uUVJ-w4HnK=gM=7Wo$){j;o-#x_gI9 zc0EHYOn+75;2429xffYBXqbE_jS?0DMg$z|2!5goFQb{YmKX7^yxa@ghh3us=o4u$ zj|AY8mz>f4IrGIF-V!>(x{8YNWz>2m?m5TxqvI}x5L~T9P8wTN4NWZXBc;ATYcX>O zPIZW(r7U1T^QYuse2mY}5Kgm%K{Gup<>Dwfp*?Hg<%7w1h^>p>RHe_r`O!BOOqfIK zHoxZh;B)SHB%;Us))Qb)F{v7nywaT+LuiktKV2gDh5* zI=%`sCGx1qY6E;jQ>=MgSvN83WRP4L5Nx=Bh&b%*by~d+mursy3vVb0SrLh>BRqi& zTNZ<$4Wr9f@)(}7uuvNGBYh#$2U~Dx8>xsiOy2g}{rAQVk!1;t)vCmx>b^;uYIa_Y z4{=PFmkyoj1~nir`wffm#?JA42VL%ngO2J%*?3_uoyO}adSMHEL@T&k!*G+1XkV-s zsR;NYP(XG;2H$x@$s-L!mmxo+T8gR~m%ixX5*p)sDT`fA$iE5JF$%yFIWsOq`_+&0 z7QJR)1>w9jm`^)<**82;s`IU}dd8ICZ|g!I(N&!;Q2iI2?2Nbh#~d8d$i2pqta8A+ zNJG@H~MlM%eOX3JuId0-0blngkYQ#^-;0fS zaw|=oNRpK$vcs}Wlozso!{HNV9OmR)ZF!ZBu2JyI^%1%kC%ftR;z#E^`}FXXUhn6* zp;v=n(%aGl_(#BZVA}_x3z-AK!Hr0ft>y5eoD#9}C>>2ghis-kRW~-k;?4W_)eqMl@foz8Etd zeD=j2te6{buaCi}>;JUdihynJZY9gGCg!Veba=P7PwpXdkZs^`g9o+ZG zYTzoV^&~u%UVt%a<-v{QaVC5)CunQb7R(klX~~uledYgC~=ZHrp0WZ$qY^%0%2K8B=iFG>z$IC zQ+SNH4n#@gq`4Q$23?1Y+u1iKyBW#TM#dTs%Nmj$2T8Y_LOvn4IG6LPAzu+8vMccU zmXp(dp4rHKm>t)y5aun%x|BK!@IdaXAfEs19b~%pYsAztu(w)orhi^Wb`-6HTvDQ3 zm4mlF)d8muM5gKUwKcyHI^b@8o8&3^J+N>=@!{xO4-MC{Sk09Vdy5Uo3rt7S8T{Rg zLH`H5ok0iT<%^KK#1G1Ex9=Q|R%D~-JD_l@gdLM`dv#=0yitBeePeS&VY!Nr<8JVX zcn`YE<@hO|X>3F^*f2g?O&E=s#viOfJuYl@68E$8&WugJ_JMb?QH8;SOgIyHXnD{h zD=pKK`0!8~S+jXzf+_STbc~q^w`{n;ZqHO@?U_ArKqR*7L2VqI@j{-@vC5Dc|UqTzKd5RXImjMRPLz!w(w#!e5pUt7|)Vu#OXk z3_`E^U6JA7%SHWqrynf&*mCE(>rEG_>CMgzs)2+GD(PWIXS{o z4Zg9k5#O2`JFCqlBd5>ir#@|cL|wiuOAZ>Mjp147`KZ zDrcLr=`p$QM*P$kme#vqL%DxZx59&S1s8Q$Di`ntXYd453sz}|oUwjdH{|wAoYSvh z(cG3QAjV9YTp)3N+D_!A}qlZu$m|u0(8gDlJ_hc%rH2 zzJA57T>mpO&c+o^wR;Y=CaDPTH`7RRbQIvZZgp=w9f z4n!BShu$>PCb3I9u8Z>qqrf9Nd4=npVye|uOnZLceyc>I=h3ICk1hb~zIkN}tIq`Y z*#?{A9x-QE%G7fUQBJ1T3D3*j2lb~-U)Ee3I&j@j$Gi{+m9e)=6j9fYdwVly$&FDt zab`sr{nbw?+N>HTIwD+C)OsCUajS+|X&e7M- zLE|Ug{k-w}dcEab&_8azCcK3VHxu2@UWM%}VV~k1%QGgjwTnx6!)~EzpvaeUof>If zt|=(?LB?Cg)T>gA_}n&d<16zED~IL8ZRv=42e0pMb4^onUC9VkPPYx^vroSW^OT^H zN{(s-hsuiHorYx-s8}>K(5BDn0fa&@%70Q=4|odKJfdEx6}f$oNKgf>zOBrFXDz0P z$hN5FYV5Ms*-AAsn(NP3Yes-G3n;rqr3Xx6aY96Y%aJQRdY}`A~CE-KmPf&LA z&ut-QA{RDT=aqv2R_Yow|ImCGhdXWH<;^TMyN8Uge9Onf_k4K(>d(aiL4aaABk|}y|`GM-|`Y?8bwOy zF*^TlwBxSFxqW(U>B_{lTFmP3#k(X5Y!*B|my2GKbRIJu%xn%bb^f7H=uGVB%X>3s z&e_YTkemu%`%gjxo8LTiMiV?nXtwlz%s7tYiMhPHzbLMvU z^nIahBL3p)XfrL(N6ky;TB=i}S6jBnFc#?)9@{B7UQ&I;CdSJnh7d)<%ZV3cD#PX@ z5-U$EffB2Zi&fvE@q%w#Mp@q{Ml^&Timl;^fbP4ckav+h>uP@3$A)29u+7Fv(>VSr zWqQxk4ni?JQQH^XAMqFLs~ioOS+LAAOM^sJGjlqsA1QS*1NfI}UX;@D=J#i6%*Xid zyyMvlF}&K@BYP{o-PgQPw_Pxu^F~R=&TEF&c;iTI`q9I`k)B5Th{8I=pSZa1E#ir- z7dEo8!@CVsg%84gs$)$!FU5PFUKCo&@IHDfO^c!N3&Iejg?_U6+;Xg2(U746jK$B) zq(^xz%UMBZFIb5YGv4WiNceWO{#8W|@-2P+ZAM>}v{UJa8i|cEKnJg^?<0ct%XtP1 zH4K4JLWPHeWhZzz8nPp-m3+W185&-gENSzBo+(-1KA63OmN%9H)~R>Q^t4NNeDQVK z9$s(#yS4a@pGE7}b&f2AaUEx8iTQ{pQF~hRC^twiy3DcVcz->(#Anct3zEZ@X8qMq_|=eRKKF*Kw)36*%Fjn%+@&~Y zc!B_otjweJ83pJNbo#{4SRt276NS;h1Jrd%C25|N@7xYI>jr(|W#gE{vldof~=0quj>rZ2DP%MBIX zfqIKEXK=bBznt4=fq=Druu<6kxrERtOo|=ab%s1)H{FPb$PwpW@9@6FSG}67I2R89 zSuZ*YwLHbO^ujt zN^ZkqH4%>pOn3_Eq6dOu^!D zxNeJ|4D7Cm$KDvak0KZ|e>;gcNLljOakl!1h$e4guDJ@`2TJn^jwtveb)?tV@1habDj0j@g{2}xTSC9~Q|Odbo{{?-vN0<}nb^5I@}3^Gr1&*-&YNSk ztY6%Q-6VW5SHEgBx+Vg_^wQzqF=S_uUYT}U2o&~&!I9HAF*Uo z>UL=sZG%RTcRdK^k5}G-H^@FX)0V=MdV#R%Io_~lnlyEuQr8&FGzb5BO>LYt;DO9V z)Vn|3{M}eMzD3Uc9=pDx&TJ44TatvW&-l*s5h?XD&0%DovSkY$Z(q5b+PEl4L+MJS z)7m~(2;p6{ZcGac?tU`y8@{(PhFKZ>A2HyNoOA-1baCKn*IN?5-RW-%2^}q`63`t* zm)6`iAbt5^&e?dr@~TyTXt53a&ZPgnolhBkXQC~)>B^5mr54QW&t~qZVbTKmmNYc_qS(0W> zvMJYNg$AEVw3@J6y+WJbwF!sk{75$8>9va*JB1->kc7(hZ+;5vKQ&@?=qv_Ma$RZ7 z8xaHt{;I@mqmEFrcM>zJAUl$ZnMk6nyp0cf&^C`?FErf`>kr6V(vXv=eG9GEAQt9}v^qi-(uRQrD~0 zj8vF!P6L+b^Xt z&q1R?J2wxtdZ^=cM>Qwlsg{LylN>Wl=E-OU`invQLr-}3VE8ymk|+&p8|j3(kfLfL z!-mN{S_YTTWA-b7kNd-l^YXUivluZS^-T?7DG%y<vx?X-33S@$l#LAP8Q(R6`romV;%T3D-Pn++g%qaTt`S*o?4ozDj9Y z`9`^h=$?K8jr3MrTav8b!{FNU+TuIt=E zAl3VJx!kcbEZm~^=W^3(kHopmo`uB^a0Xf{TL~e@;`YIjZ*x&-xI){(PMl61#)~$X zp|W+Qcv*xqFRd`L%;#<$G_w0%-vHfqq(Oai+_-)s&^;zUZP8H|_?cI)tc%{!ye7uH zViJ3}dszdgxvjg>S#Cg^;4<#4M^&oO*7{c^QLSLK9~&smL&2Q# ziHr4)yGn+jGvu;{IxSo||<%EiyQ$SH0=B%)xQ zk*ulG>D1+&p3-^gCiz=P?*wWl5T1MkEzbFI8vYvb2ex1V*EZeoJSo15SedY)RHa+sfWCx@nA z=Tneggk@cpEj_ZAEO3Vk2S&;8D{iH6*(a1Ogk+oh_h{3rE566e<}&K$)|Xg`NjlL* z#q7*Dr*X^(vH!G0$}480UA{DMb5$edb;(jP$Irwpowv&K zViIn&CE~cHpH6Ek>n(;_S)WRY6y7v(zkVgTHXTLk*IIZr#NxqJ5!JIo95*cCE-)SJ zz&z=jlI6URW964Q3UkH0+g$J4Xf-O^E|naxo`U`DRoBl;q_-wnw9?;_mT<-=bR@gx zOZ@QYMb`xkeK9#h2`p4H$)+)nEz+2bp~cLAI3{1~vPgq(a2;ZxOFTgly|8$afI8wsk(7B2!{nRp?pxoZPb1TY`Ww{~r z@+n}zT%D7WEUSInOv3$4&JoPA<4~#C()BgBmo#PIr3FZ_r!kOkyyeDux;7VBBCJhz ziotn37Y-O7;IP({M~i(qHQUfsi4`5AH7TUTc>t2)z4CYD$)T(b8bhqeS&e~KD#A+^ zKauZ{&X$C=i`{>@B)2oW>S>tvyrCC#e?qoMLf*y0fe9AF6wA$K9dB+3p zX#GwupR0!oc!;1wfg@s?th*f2;^WCufcb&l)zlN8=&)}qtTQ#=GT`%hn`hJfSDQRZ zrQhIq)G_-dnycE(oNMw>Cttccx5mcZX6fpVf>RFFFQs7=T99-@!S?Y!? zUd)ScS#d@xZ0-1gMm)B!Eqe{rC_V>vw@zJ{T6U**Ulh5!vk}F`BX_lg_h;I)u$hs$ z~$ije3``MsFw1?HC38AV^M479&luR&tNgC@r!cWjb;uiZz z2K`!9HZ-MBk5K(?2xS$Gp5BTy91YL&ikFR%E&xf^{nL?tc*Tv~&#${7Q*6Gg3p^3* z=aAbUl&lJf4CAMq#F(8Lai$ruZ0y>5ulk#5e=+Y9-%|$;tzJuoarWyYt2FagJeD^#u>nO{zK*4GE})am|G*M=}|ro zwy*HY>eWF1k@ZEQKZE`K?m`(&nc~2g^1@G#Bz~=%o9r0_78u4!MwNM8{HV-t6gLN}SHNLA zMV+$8Nao7+J8f?|33LLF=1%!umwa08B%6{DMxYC3427w_jXR+fjF zL;-W_t2USZ79VSa$vv<$kX#}gcR!*B)p!HB$#z#kFLo4%40Ec!gL7a^f!F7*kuz4R zdL^_BkDS3#^wC@@1F7C8^5d>!4R4vNp95!@`GLS$L(JY!WtJ5lE{jRr4Wih6y)m^B zK|L3R7znuf=ae4S#GXTegkOlPs-V?FFfZjB2sY zBCRa85ah7mfL;(}Y7xiME_f~MQ$B^r;LzixnkV$0^aa7C0nZMA|s?pxV+o zQcn-)jh(|m_!aK50XDtn4n*UDfENd=*SQoqrRtmC9D%q2HWLLEr?~gk&A_@rTzaZPK&*y%-C|i@nHZ+xp!rc2v z9ZA=$@ZAbSyD~bfntLZlm=&8b*yf33(mHpp1bY!t)IOiP42*p}JprUT2|t;jE+$-` z>nJbroHbe7G%DFSrw`wE&68&Iw92-iMD1YqLTB|Hq8+5;4r|hrW_#dP;)-rRqjFSj zbJN~HW;*R6B4?LAiTzqQf5TllP&TILI9uu255t(#Uo`hq;0V{LZQptY_nt(``+J?g zda<_4nKeIc1!gF%4%s9Lm37pFVY=1~(M1u|6m;ai#kj`|;L4xFIutts8n&wOSBrA; zYD$UFZkzJLPO*F*F|mgCl+DeImqS^A%A9{|6jq6!AL;Rrk`)50PVLT#*>;hSRIU2O zOR)qP4=|d;1v@0W82YHn7RA%`q#qZ9${FNO!4aNqDy^T>4=C?9^~$0Z3kCPz`{$sL zO0*~lx*)gaJH%4jc-W$ph+@cCHw^hse8cIKffdI(AuGE;>V{YMmck3<(n=9;!^*v7 zRI)yD#pjgiy)ktcT@)yh+zWX;g|qDXC4Mt@C}gz~HMu3duCU{giIM+(Dwh zFHKx@wZx)^#_R3sb(TQ^{**UAe*|Ma)-J3Pjk}o1)bi#oOZ;#aoE$FmL$$>92?hu@t|cu1X8?*jY2@O3Z0|S_ij8u+!baDn&5W zdWLSiT^^tR=;M-Hb5?LCaTVk{UUPL8ijnvAs2J99+w-Pf@N6B)gT-m^_#H9q*}hjk zuR1?zh5lj_k9*J!Uu@?(&i^ts;Qu9Cp4kZ@BL@R}Gh0U+d&muRNc|6WqO2mUtRO%q zD6Ig7Tvms$LnaW!=8uBFZ@h+{83dqb{C!y;1R)z(KoH1=-@)LSRAJ?SXbS4tiW!-i zm_n-TOh~}zQJ&lA!OxB2j(V162K?40mPXHa;T0W?tW+RoNICn1w6Qh<+8WuL*%$)tfet`Nnm-eS5Kl&c-Jq{Fqr?or4L|Z;`o2&q^y*sZB{|p4O^#; zVW}8s4_A99~p zXV9g&R+gLC+86Dc5{#?6tj+uBZ;Wmg$Eswdxa|k6(oFXXxmwz3VznJhcXBPKDxG{@ zwO5DXzU=L8V~%alUvT1&FW*T>;zoVQQM|^G(z%^Im%eK~w27RF zySQ9)Msm=+pJ8B{!dtK$0Q1}a57>%g0=#j{mN?Kexe_9n z-4cIpx^f%!8;W4?N@hGfi$qMxk-FtJs@O}a)oHrRTC-~7a`(E(cUYQ37~!ctOl!xQ zs@%MNOTY4v>aFs!BG>)@IMh zBH;J&{l|HJM*03ZyJtqw;TaNig3JZdwWOILgz@`LP5zx&1OJ_vd`@=|C2AoX1A0YA zJ$pwQz%$XN0HC&o^u_?D0X&m&kg5ZOsDnU$w9leva!mn}@TAPF%$_mAzgz!Z@gJl9 zgRnDlfY|+>$Ul7gJ52W{;(~gRTxnzSOfW*6`!~S&XWHQ3bNkaK4T%y^*}+KG$-D8jlH1* zHKdRKDn!kNJxI*TO1R>8jkU;-_K^Qj{2sCL7__yqN!Jc0L z>wn7_*&w_6zhq1tkahVl8T<2E{g({H3R#!`lCd)XZ+~o%5&x+NW@cuG?3MqrWn+OX z>px^*W>#j%_ufBbAP_tI|H#11Z2vO`6Z7+l{f8|WLXiIN7;GTO8T+RmGdtt|^vBH3 z^4~E4Gjses29EZ6kne8$Kj5>OJLJ?ueiJCz*g#J6??Vas@{wgn%Nu6_p`{Nn1yx%w6J=9c#8_fSr_D&jksM=3TibDEX8*oZ=*t# zV$Y`$ybUC+uD7wfn#U8d)_lqF_OX)Riy+e@{w$MmW#iq++1Y9T?)?ICsm@Us+R$8T+I1L;Kr!1P=PXgx>duc4nM{~Y?aG7?bH zN#F7JRFsTd9f5*>MgVI8fP#=nI-1#73qb-5pcdj}1Tiv$z#ujd2QxDh2OXpcq5%M< z|IbZ=`~WZm2mn+9P(u_U3j|HtxqvH!J3B9JWscs?m2jO+k*NQ1)j*#OGf+ZZSsL1F;eIzl3l*g_WVAL|VM z*D2FrVP|4s0|wWT0x-yd9$&A0L;vgIR9>8V`OB2 z?781I&)OUyMu;^S1ZHC3_)Gk!!#~ewl|RG$r}W48uVLBQA$Cl^pU)cqX!||M|Mc_R z{{Qm+`}lv#e|lwRd!G8A=O4+lYiuBPNGSeti}l$HR(2316s#=Ijxl@(eXHGX&er~J3o`z`%r@t5@XSbrpcEybUm@Ep!RrQaG%9AJnmOdQWE{d+;Q z{*^HxN#;M>@lOT@1HddC;D6;~MuMab5tu8aiVWHa%eFA<@V)vFQGG)tJpc9h>7PdiHS8$9UVCVx#Y(-V8W!=!xN}4 zpphTi-_PQhB6@}QstM@qE4|VE_@kcuV+2u4FI7h`u%i`vd371%KITn_@=Jih4sD1l@~cQzsNk#NsVfPAY(}FXa$QoGO*BXBHDhuF7{7Hc4Bi zvfA;5sbC@h``jxZKV9syuL4Pa(=$9gJTvc-=BEM0&iYdCpky(|U#G%3!q&M%SwZ<= zqsUb_!aOPy5D}8R?|!|08KX(0k9<+-2nVD6A_QOom_F!iC$WTegliy18&*+*&gFt_ z^&u#@H)?^ou4jXiVIB1OdN+3$`W|qcG8jEMi9ux|S<3d(5}_210|t86p4L(hNR8Zr zX?gZFEh~d)y>n@_9dq5(>_Y$8WHTCyLtOgfhU6!om@VT2Bzyn5vf59?Pt@TLp&+1E zG~Aq+U}EA&-CUws(wp9onx7_tR#(PP7Uedr7_y~DOD(uBs>>cSUJ|*OIBS7=I!SV= z&uV%DUmox=asjYg`dV9Cfjy{Dbx((hq})fGVYC+_ce3Yr;NeT)J)@^;7+y>4 znx97^R0yB;lQ*MRS`6xo{p|s+mxfs?5I)l(=#FpzTdl2*^S2W?eU)6i4H6JnS zw$?tqhtsZ`C#efQt;Z+)N72Y9qTVOAHykda-Fy>4KD$qR=8{xl8COg2z)YvFP47O^ zt{b7YJXz$U-&>d|e_{o=CG@6UW=sLBb3HBx>-`J4J@eAzWX<~Scs>v)AUbY zUO?|pK8g4kc3YpJ_aN#bJi^*TPg^~~>O@vQeI#yxavtVemNd-q!EY9Ve}Zjd7kor@ z)3($p9(vN{g{nV8wgWu%_(dSJ9NwZ|+sDuGEe9f;%U9F{i-J3nxAvHdM(#cFdr^j? z@2M_Nqu;G%b&ySD9O)U*+Z!sT8=o~UeYp`W3P>`pys>LrENc+#jem`#WUoRJpw%pj z$Je-Aa~xdC7r9-oH4g`3XX>xs}#B-%uF2E;-lM5aKms{ZBo*U zmj-~n&N-QT70pnaLx~I-xf#pXUl1O;9&p9VmW=7DM(wkv^!AS9Jmqc6Hwi$&dZs@3 z@;KD|`^V1)4gqv`o~ zktP_8kc%%vk1%R>u&u|8lhAA#zee~P4yBK`54u6Lsa`6%dfad_F}K~(JF`|!|`N~4Kdn1DvF$EN|LGK`?P%vKPeUQc?O z5KS1$pkAtCQUzmJVXhw7(#7^Ur>hpv5f3F4h&Os+w{w>3;LQVZ2p8#HjVhYml+|gC zL~Nl(>Urp{f_@OXHm9|dMxPZ+(@_^JNb?A5Gr|`yyIW))^CB6k zFABf>j3670DNMlWe-AhDdIL6GtQl_1E$*J-M?L_t%_Zi7Uvr{pmn{fXGFqm(IF@z9 z(AvyuLy|jncOgV8oL%H_c2PCNDK#~}-h+EF+<{wEVo)poM%D%PeJmR*+~Ns2&&9lo z+-_$)wSnb%gq34^esPZB0ohGW<&O69to#$`^SH&jiZz;N*$6q@du_1bZN3CA)8mzcB({iM<$KBSV+HvLXF;`D7J?J%g_PX2dUQ|m4-wj@9P!Id3gzRb zKdj^^7xr_cc54xtaTxH^ZMx#}*qdukVdHWQtlCipH{o6lQJTfx`o5^v{wl9-6!a}ssZAZu)a{}p@+pc9ys@wNk%cm; zH@?p-Ok8n{A(S%|p~^1)rb>ToDt|2g7sBU7tv!5&5SH(w4$4^?6Ym5=%2&`|H+fog zq!>$E*Fo79e_(*kW1go-pE^!qbbN~%H4_~b;DZKoD#hqUr@d1b5hoJwHK|UQS8xS_ z^+o8IgdFYi9He%k#>#nelD$`I@=B!cN%3JZ{$XI|p=gGTkgGy8B1}@bbtTYy^ zli}$L|K2z)+{#4l2G_j@wW}i!(2v116N^=AWUK1n_y9*i^~LS<+ll>Hp(frKyosid z$W`A53_EcN)IVLJJVf4Z*@P;+=ud`ni;F#F^=3hx7i;S9xraLYU17 z-m6TYF;8NCet+auEe+*c?Erq+wj^R!Ql7x5P*t^KAs*}|p7^(Uj&?Wi5Qw_n$ zN~VaYjzqdE7DHS?1ZoF3W*Z*PI{9bV`W?w}ajxxUyTJ;T5-Y%DBpd49EJeV}4j_%tS7fnSEOVMH@ zIt{iHU!vgG>L{f&Id~{G$;^~@DJUlq#Tw){1c6b=LIRym?`r9@c9bv+ zpve07#Jz@N$gY;g48Du1O&;pNgqN--!k6GCo3CK9VrwHcrTnlfmJ=B#TIgNQ@7eQNSrsghn9kv~U5vytxynIC%Wv|dgGjkZ&UUZ-OYu;po z82inBy#iwpp=NQf^v{wUD(;jqMobpJfKM7}gW1WoX!>TaFsPHVPKQ zf#i-Zjl^Qq;+zL!YOWAZ&cQ=0UdiT;pFt|I?Cd%9CWB^hi@vcpDEC~r=$hfjAkSK- z>vnGlz>*0xf_AA!cG-+5f{U+52Cpzhzfbuzi^$0==CGk&jpxbNjaP$_IOC0}=_SKa zQWmlEFk}H$Pn8pD=cMhv7~>Skjzz$nUE&4vjSr90Bhf|IP)^8Gsy6Q(SdUWtacdk3ek&K<$$aXO2yTS zd}%YU@az$vHI4XA??76zQp|>5W>f`T8($l-LBr!ys#wxJ0*cv(K-ne$aSRC3_?K zV67PAm9m>h5oRfAVDTa zq)0sONstn$Lgm+fc$tOwtYGts5&B|T8uD=S#vJ!e!F}wWTf10r{gP>heQ8~}PAEgN z{!n(L;G9V*wmw1rUWaZlGo?rwHr`~KW9{5f@6JJ&u+;s+xfS}%WyW%l>XH~53PESPDT&*FJ^xz za{lZCkMUX~S7|do{oRMnoy`{3%WGbt6?eiOQGWQ_P_EpKc;J%wsjJ7+cE$7%`bqN*Z{Lm3Y%n0a!uMME z#W6o9y%vZ{zvtVlys_;_*!cb18TyNP_;;cWs@}oSW>qkEE|#zhy-RUcqIaGRf!qRl zto;R9p0x1f6=dEH0UlB{sesmp2GiQaz@ zQi;9cPG~uzDt{C#I{v}E@s79p&1=+}ahpZIz^@AlsBl>Ec?t%ZM1$)ikp>ppRYa|r zCSV5dEaSZHKu$L2>xk_m=b+}m#+S?=Kxz72I0-Q10jJhHsI3A@>Xo0fJ+IH@U1Sz< zq*6J5n1+6W{W?hZkd7Z^O;5vwL{B5xP5M=vWpl>CpFm)uA>S|RrA0lJS27E|A#eCtC2hEO?R;T_j|5iwgzq}qhwEAtcz}I+lMbh~i(Fr=v zw~^F$?HzSNS7=~l7MbeM^~Z%JuIM~kKGxI#uUVJ-w4HnK=gM=7Wo$){j;o-#x_gI9 zc0EHYOn+75;2429xffYBXqbE_jS?0DMg$z|2!5goFQb{YmKX7^yxa@ghh3us=o4u$ zj|AY8mz>f4IrGIF-V!>(x{8YNWz>2m?m5TxqvI}x5L~T9P8wTN4NWZXBc;ATYcX>O zPIZW(r7U1T^QYuse2mY}5Kgm%K{Gup<>Dwfp*?Hg<%7w1h^>p>RHe_r`O!BOOqfIK zHoxZh;B)SHB%;Us))Qb)F{v7nywaT+LuiktKV2gDh5* zI=%`sCGx1qY6E;jQ>=MgSvN83WRP4L5Nx=Bh&b%*by~d+mursy3vVb0SrLh>BRqi& zTNZ<$4Wr9f@)(}7uuvNGBYh#$2U~Dx8>xsiOy2g}{rAQVk!1;t)vCmx>b^;uYIa_Y z4{=PFmkyoj1~nir`wffm#?JA42VL%ngO2J%*?3_uoyO}adSMHEL@T&k!*G+1XkV-s zsR;NYP(XG;2H$x@$s-L!mmxo+T8gR~m%ixX5*p)sDT`fA$iE5JF$%yFIWsOq`_+&0 z7QJR)1>w9jm`^)<**82;s`IU}dd8ICZ|g!I(N&!;Q2iI2?2Nbh#~d8d$i2pqta8A+ zNJG@H~MlM%eOX3JuId0-0blngkYQ#^-;0fS zaw|=oNRpK$vcs}Wlozso!{HNV9OmR)ZF!ZBu2JyI^%1%kC%ftR;z#E^`}FXXUhn6* zp;v=n(%aGl_(#BZVA}_x3z-AK!Hr0ft>y5eoD#9}C>>2ghis-kRW~-k;?4W_)eqMl@foz8Etd zeD=j2te6{buaCi}>;JUdihynJZY9gGCg!Veba=P7PwpXdkZs^`g9o+ZG zYTzoV^&~u%UVt%a<-v{QaVC5)CunQb7R(klX~~uledYgC~=ZHrp0WZ$qY^%0%2K8B=iFG>z$IC zQ+SNH4n#@gq`4Q$23?1Y+u1iKyBW#TM#dTs%Nmj$2T8Y_LOvn4IG6LPAzu+8vMccU zmXp(dp4rHKm>t)y5aun%x|BK!@IdaXAfEs19b~%pYsAztu(w)orhi^Wb`-6HTvDQ3 zm4mlF)d8muM5gKUwKcyHI^b@8o8&3^J+N>=@!{xO4-MC{Sk09Vdy5Uo3rt7S8T{Rg zLH`H5ok0iT<%^KK#1G1Ex9=Q|R%D~-JD_l@gdLM`dv#=0yitBeePeS&VY!Nr<8JVX zcn`YE<@hO|X>3F^*f2g?O&E=s#viOfJuYl@68E$8&WugJ_JMb?QH8;SOgIyHXnD{h zD=pKK`0!8~S+jXzf+_STbc~q^w`{n;ZqHO@?U_ArKqR*7L2VqI@j{-@vC5Dc|UqTzKd5RXImjMRPLz!w(w#!e5pUt7|)Vu#OXk z3_`E^U6JA7%SHWqrynf&*mCE(>rEG_>CMgzs)2+GD(PWIXS{o z4Zg9k5#O2`JFCqlBd5>ir#@|cL|wiuOAZ>Mjp147`KZ zDrcLr=`p$QM*P$kme#vqL%DxZx59&S1s8Q$Di`ntXYd453sz}|oUwjdH{|wAoYSvh z(cG3QAjV9YTp)3N+D_!A}qlZu$m|u0(8gDlJ_hc%rH2 zzJA57T>mpO&c+o^wR;Y=CaDPTH`7RRbQIvZZgp=w9f z4n!BShu$>PCb3I9u8Z>qqrf9Nd4=npVye|uOnZLceyc>I=h3ICk1hb~zIkN}tIq`Y z*#?{A9x-QE%G7fUQBJ1T3D3*j2lb~-U)Ee3I&j@j$Gi{+m9e)=6j9fYdwVly$&FDt zab`sr{nbw?+N>HTIwD+C)OsCUajS+|X&e7M- zLE|Ug{k-w}dcEab&_8azCcK3VHxu2@UWM%}VV~k1%QGgjwTnx6!)~EzpvaeUof>If zt|=(?LB?Cg)T>gA_}n&d<16zED~IL8ZRv=42e0pMb4^onUC9VkPPYx^vroSW^OT^H zN{(s-hsuiHorYx-s8}>K(5BDn0fa&@%70Q=4|odKJfdEx6}f$oNKgf>zOBrFXDz0P z$hN5FYV5Ms*-AAsn(NP3Yes-G3n;rqr3Xx6aY96Y%aJQRdY}`A~CE-KmPf&LA z&ut-QA{RDT=aqv2R_Yow|ImCGhdXWH<;^TMyN8Uge9Onf_k4K(>d(aiL4aaABk|}y|`GM-|`Y?8bwOy zF*^TlwBxSFxqW(U>B_{lTFmP3#k(X5Y!*B|my2GKbRIJu%xn%bb^f7H=uGVB%X>3s z&e_YTkemu%`%gjxo8LTiMiV?nXtwlz%s7tYiMhPHzbLMvU z^nIahBL3p)XfrL(N6ky;TB=i}S6jBnFc#?)9@{B7UQ&I;CdSJnh7d)<%ZV3cD#PX@ z5-U$EffB2Zi&fvE@q%w#Mp@q{Ml^&Timl;^fbP4ckav+h>uP@3$A)29u+7Fv(>VSr zWqQxk4ni?JQQH^XAMqFLs~ioOS+LAAOM^sJGjlqsA1QS*1NfI}UX;@D=J#i6%*Xid zyyMvlF}&K@BYP{o-PgQPw_Pxu^F~R=&TEF&c;iTI`q9I`k)B5Th{8I=pSZa1E#ir- z7dEo8!@CVsg%84gs$)$!FU5PFUKCo&@IHDfO^c!N3&Iejg?_U6+;Xg2(U746jK$B) zq(^xz%UMBZFIb5YGv4WiNceWO{#8W|@-2P+ZAM>}v{UJa8i|cEKnJg^?<0ct%XtP1 zH4K4JLWPHeWhZzz8nPp-m3+W185&-gENSzBo+(-1KA63OmN%9H)~R>Q^t4NNeDQVK z9$s(#yS4a@pGE7}b&f2AaUEx8iTQ{pQF~hRC^twiy3DcVcz->(#Anct3zEZ@X8qMq_|=eRKKF*Kw)36*%Fjn%+@&~Y zc!B_otjweJ83pJNbo#{4SRt276NS;h1Jrd%C25|N@7xYI>jr(|W#gE{vldof~=0quj>rZ2DP%MBIX zfqIKEXK=bBznt4=fq=Druu<6kxrERtOo|=ab%s1)H{FPb$PwpW@9@6FSG}67I2R89 zSuZ*YwLHbO^ujt zN^ZkqH4%>pOn3_Eq6dOu^!D zxNeJ|4D7Cm$KDvak0KZ|e>;gcNLljOakl!1h$e4guDJ@`2TJn^jwtveb)?tV@1habDj0j@g{2}xTSC9~Q|Odbo{{?-vN0<}nb^5I@}3^Gr1&*-&YNSk ztY6%Q-6VW5SHEgBx+Vg_^wQzqF=S_uUYT}U2o&~&!I9HAF*Uo z>UL=sZG%RTcRdK^k5}G-H^@FX)0V=MdV#R%Io_~lnlyEuQr8&FGzb5BO>LYt;DO9V z)Vn|3{M}eMzD3Uc9=pDx&TJ44TatvW&-l*s5h?XD&0%DovSkY$Z(q5b+PEl4L+MJS z)7m~(2;p6{ZcGac?tU`y8@{(PhFKZ>A2HyNoOA-1baCKn*IN?5-RW-%2^}q`63`t* zm)6`iAbt5^&e?dr@~TyTXt53a&ZPgnolhBkXQC~)>B^5mr54QW&t~qZVbTKmmNYc_qS(0W> zvMJYNg$AEVw3@J6y+WJbwF!sk{75$8>9va*JB1->kc7(hZ+;5vKQ&@?=qv_Ma$RZ7 z8xaHt{;I@mqmEFrcM>zJAUl$ZnMk6nyp0cf&^C`?FErf`>kr6V(vXv=eG9GEAQt9}v^qi-(uRQrD~0 zj8vF!P6L+b^Xt z&q1R?J2wxtdZ^=cM>Qwlsg{LylN>Wl=E-OU`invQLr-}3VE8ymk|+&p8|j3(kfLfL z!-mN{S_YTTWA-b7kNd-l^YXUivluZS^-T?7DG%y<vx?X-33S@$l#LAP8Q(R6`romV;%T3D-Pn++g%qaTt`S*o?4ozDj9Y z`9`^h=$?K8jr3MrTav8b!{FNU+TuIt=E zAl3VJx!kcbEZm~^=W^3(kHopmo`uB^a0Xf{TL~e@;`YIjZ*x&-xI){(PMl61#)~$X zp|W+Qcv*xqFRd`L%;#<$G_w0%-vHfqq(Oai+_-)s&^;zUZP8H|_?cI)tc%{!ye7uH zViJ3}dszdgxvjg>S#Cg^;4<#4M^&oO*7{c^QLSLK9~&smL&2Q# ziHr4)yGn+jGvu;{IxSo||<%EiyQ$SH0=B%)xQ zk*ulG>D1+&p3-^gCiz=P?*wWl5T1MkEzbFI8vYvb2ex1V*EZeoJSo15SedY)RHa+sfWCx@nA z=Tneggk@cpEj_ZAEO3Vk2S&;8D{iH6*(a1Ogk+oh_h{3rE566e<}&K$)|Xg`NjlL* z#q7*Dr*X^(vH!G0$}480UA{DMb5$edb;(jP$Irwpowv&K zViIn&CE~cHpH6Ek>n(;_S)WRY6y7v(zkVgTHXTLk*IIZr#NxqJ5!JIo95*cCE-)SJ zz&z=jlI6URW964Q3UkH0+g$J4Xf-O^E|naxo`U`DRoBl;q_-wnw9?;_mT<-=bR@gx zOZ@QYMb`xkeK9#h2`p4H$)+)nEz+2bp~cLAI3{1~vPgq(a2;ZxOFTgly|8$afI8wsk(7B2!{nRp?pxoZPb1TY`Ww{~r z@+n}zT%D7WEUSInOv3$4&JoPA<4~#C()BgBmo#PIr3FZ_r!kOkyyeDux;7VBBCJhz ziotn37Y-O7;IP({M~i(qHQUfsi4`5AH7TUTc>t2)z4CYD$)T(b8bhqeS&e~KD#A+^ zKauZ{&X$C=i`{>@B)2oW>S>tvyrCC#e?qoMLf*y0fe9AF6wA$K9dB+3p zX#GwupR0!oc!;1wfg@s?th*f2;^WCufcb&l)zlN8=&)}qtTQ#=GT`%hn`hJfSDQRZ zrQhIq)G_-dnycE(oNMw>Cttccx5mcZX6fpVf>RFFFQs7=T99-@!S?Y!? zUd)ScS#d@xZ0-1gMm)B!Eqe{rC_V>vw@zJ{T6U**Ulh5!vk}F`BX_lg_h;I)u$hs$ z~$ije3``MsFw1?HC38AV^M479&luR&tNgC@r!cWjb;uiZz z2K`!9HZ-MBk5K(?2xS$Gp5BTy91YL&ikFR%E&xf^{nL?tc*Tv~&#${7Q*6Gg3p^3* z=aAbUl&lJf4CAMq#F(8Lai$ruZ0y>5ulk#5e=+Y9-%|$;tzJuoarWyYt2FagJeD^#u>nO{zK*4GE})am|G*M=}|ro zwy*HY>eWF1k@ZEQKZE`K?m`(&nc~2g^1@G#Bz~=%o9r0_78u4!MwNM8{HV-t6gLN}SHNLA zMV+$8Nao7+J8f?|33LLF=1%!umwa08B%6{DMxYC3427w_jXR+fjF zL;-W_t2USZ79VSa$vv<$kX#}gcR!*B)p!HB$#z#kFLo4%40Ec!gL7a^f!F7*kuz4R zdL^_BkDS3#^wC@@1F7C8^5d>!4R4vNp95!@`GLS$L(JY!WtJ5lE{jRr4Wih6y)m^B zK|L3R7znuf=ae4S#GXTegkOlPs-V?FFfZjB2sY zBCRa85ah7mfL;(}Y7xiME_f~MQ$B^r;LzixnkV$0^aa7C0nZMA|s?pxV+o zQcn-)jh(|m_!aK50XDtn4n*UDfENd=*SQoqrRtmC9D%q2HWLLEr?~gk&A_@rTzaZPK&*y%-C|i@nHZ+xp!rc2v z9ZA=$@ZAbSyD~bfntLZlm=&8b*yf33(mHpp1bY!t)IOiP42*p}JprUT2|t;jE+$-` z>nJbroHbe7G%DFSrw`wE&68&Iw92-iMD1YqLTB|Hq8+5;4r|hrW_#dP;)-rRqjFSj zbJN~HW;*R6B4?LAiTzqQf5TllP&TILI9uu255t(#Uo`hq;0V{LZQptY_nt(``+J?g zda<_4nKeIc1!gF%4%s9Lm37pFVY=1~(M1u|6m;ai#kj`|;L4xFIutts8n&wOSBrA; zYD$UFZkzJLPO*F*F|mgCl+DeImqS^A%A9{|6jq6!AL;Rrk`)50PVLT#*>;hSRIU2O zOR)qP4=|d;1v@0W82YHn7RA%`q#qZ9${FNO!4aNqDy^T>4=C?9^~$0Z3kCPz`{$sL zO0*~lx*)gaJH%4jc-W$ph+@cCHw^hse8cIKffdI(AuGE;>V{YMmck3<(n=9;!^*v7 zRI)yD#pjgiy)ktcT@)yh+zWX;g|qDXC4Mt@C}gz~HMu3duCU{giIM+(Dwh zFHKx@wZx)^#_R3sb(TQ^{**UAe*|Ma)-J3Pjk}o1)bi#oOZ;#aoE$FmL$$>92?hu@t|cu1X8?*jY2@O3Z0|S_ij8u+!baDn&5W zdWLSiT^^tR=;M-Hb5?LCaTVk{UUPL8ijnvAs2J99+w-Pf@N6B)gT-m^_#H9q*}hjk zuR1?zh5lj_k9*J!Uu@?(&i^ts;Qu9Cp4kZ@BL@R}Gh0U+d&muRNc|6WqO2mUtRO%q zD6Ig7Tvms$LnaW!=8uBFZ@h+{83dqb{C!y;1R)z(KoH1=-@)LSRAJ?SXbS4tiW!-i zm_n-TOh~}zQJ&lA!OxB2j(V162K?40mPXHa;T0W?tW+RoNICn1w6Qh<+8WuL*%$)tfet`Nnm-eS5Kl&c-Jq{Fqr?or4L|Z;`o2&q^y*sZB{|p4O^#; zVW}8s4_A99~p zXV9g&R+gLC+86Dc5{#?6tj+uBZ;Wmg$Eswdxa|k6(oFXXxmwz3VznJhcXBPKDxG{@ zwO5DXzU=L8V~%alUvT1&FW*T>;zoVQQM|^G(z%^Im%eK~w27RF zySQ9)Msm=+pJ8B{!dtK$0Q1}a57>%g0=#j{mN?Kexe_9n z-4cIpx^f%!8;W4?N@hGfi$qMxk-FtJs@O}a)oHrRTC-~7a`(E(cUYQ37~!ctOl!xQ zs@%MNOTY4v>aFs!BG>)@IMh zBH;J&{l|HJM*03ZyJtqw;TaNig3JZdwWOILgz@`LP5zx&1OJ_vd`@=|C2AoX1A0YA zJ$pwQz%$XN0HC&o^u_?D0X&m&kg5ZOsDnU$w9leva!mn}@TAPF%$_mAzgz!Z@gJl9 zgRnDlfY|+>$Ul7gJ52W{;(~gRTxnzSOfW*6`!~S&XWHQ3bNkaK4T%y^*}+KG$-D8jlH1* zHKdRKDn!kNJxI*TO1R>8jkU;-_K^Qj{2sCL7__yqN!Jc0L z>wn7_*&w_6zhq1tkahVl8T<2E{g({H3R#!`lCd)XZ+~o%5&x+NW@cuG?3MqrWn+OX z>px^*W>#j%_ufBbAP_tI|H#11Z2vO`6Z7+l{f8|WLXiIN7;GTO8T+RmGdtt|^vBH3 z^4~E4Gjses29EZ6kne8$Kj5>OJLJ?ueiJCz*g#J6??Vas7Ezxe^>d9Q#(AL{@?om%)-dTj6_K( JEGvTa{{d$*9kBoa literal 0 HcmV?d00001 diff --git a/src/test/search/resources/minimal-note.tex b/src/test/resources/org/jabref/logic/search/minimal-note-sentence-case.tex similarity index 63% rename from src/test/search/resources/minimal-note.tex rename to src/test/resources/org/jabref/logic/search/minimal-note-sentence-case.tex index c47b1aa8b5d..9042f3b87ef 100644 --- a/src/test/search/resources/minimal-note.tex +++ b/src/test/resources/org/jabref/logic/search/minimal-note-sentence-case.tex @@ -1,13 +1,7 @@ \documentclass{article} -\usepackage[utf8]{inputenc} \usepackage[english]{babel} - \usepackage{pdfcomment} - \begin{document} - -This is a short sentence, comma included. - +Demonstration of different cases of a comment. \pdfcomment{Hello World} - \end{document} diff --git a/src/test/search/resources/minimal.pdf b/src/test/resources/org/jabref/logic/search/minimal-sentence-case.pdf similarity index 96% rename from src/test/search/resources/minimal.pdf rename to src/test/resources/org/jabref/logic/search/minimal-sentence-case.pdf index 3658e1118b01792c1f7d6e23b4f0641a5bd59ceb..0c9533132a8a728af2e1bcb7717425373bc545fa 100644 GIT binary patch delta 466 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

1409D}1DUHQ;8 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 QI9OTP;VCG@6(!*RAGC7CQ2+n{ diff --git a/src/test/search/resources/test-library-A.bib b/src/test/search/resources/test-library-A.bib deleted file mode 100644 index 267ae48d767..00000000000 --- a/src/test/search/resources/test-library-A.bib +++ /dev/null @@ -1,26 +0,0 @@ -@Misc{entry1, - author = {Test}, - title = {cASe}, -} - -@Misc{entry2, - author = {test}, - title = {casE}, -} - -@Misc{entry3, - author = {tESt}, - title = {Case}, -} - -@Misc{entry4, - author = {tesT}, - title = {CASE}, -} - -@Misc{entry5, - author = {TEST}, - title = {case}, -} - -@Comment{jabref-meta: databaseType:bibtex;} diff --git a/src/test/search/resources/test-library-B.bib b/src/test/search/resources/test-library-B.bib deleted file mode 100644 index ff076a5a52c..00000000000 --- a/src/test/search/resources/test-library-B.bib +++ /dev/null @@ -1,21 +0,0 @@ -@Misc{entry1, - author = {Test}, - title = {Case}, -} - -@Misc{entry2, - author = {User}, - title = {case}, -} - -@Misc{entry3, - author = {test}, - title = {text}, -} - -@Misc{entry4, - author = {Special}, - title = {192? title.}, -} - -@Comment{jabref-meta: databaseType:bibtex;} diff --git a/src/test/search/resources/test-library-C.bib b/src/test/search/resources/test-library-C.bib deleted file mode 100644 index 02f08f7b632..00000000000 --- a/src/test/search/resources/test-library-C.bib +++ /dev/null @@ -1,34 +0,0 @@ -@Misc{minimal1, - file = {:minimal.pdf:PDF}, -} - -@Misc{minimal2, - file = {:minimal1.pdf:PDF}, -} - -@Misc{minimal3, - file = {:minimal2.pdf:PDF}, -} - -@Misc{minimal-note1, - file = {:minimal-note.pdf:PDF}, -} - -@Misc{minimal-note2, - file = {:minimal-note1.pdf:PDF}, -} - -@Misc{minimal-note3, - file = {:minimal-note2.pdf:PDF}, -} - -@Comment{jabref-meta: databaseType:bibtex;} - -@Comment{jabref-meta: fileDirectory:.;} - -@Comment{jabref-meta: saveActions:disabled; -all-text-fields[identity] -date[normalize_date] -month[normalize_month] -pages[normalize_page_numbers] -;} diff --git a/src/test/search/resources/test-library-D.bib b/src/test/search/resources/test-library-D.bib deleted file mode 100644 index 6d78ef8dd5d..00000000000 --- a/src/test/search/resources/test-library-D.bib +++ /dev/null @@ -1,55 +0,0 @@ -@Misc{entry1, - author = {Test}, - title = {Case}, - groups = {A}, -} - -@Misc{entry2, - author = {TEST}, - title = {CASE}, - groups = {A}, -} - -@Misc{entry3, - author = {Hello}, - title = {World}, - groups = {A}, -} - -@Misc{entry4, - author = {HELLO}, - title = {WORLD}, - groups = {A}, -} - -@Misc{entry5, - author = {tesT}, - title = {casE}, - groups = {B}, -} - -@Misc{entry6, - author = {test}, - title = {case}, - groups = {B}, -} - -@Misc{entry7, - author = {hellO}, - title = {worlD}, - groups = {B}, -} - -@Misc{entry8, - author = {hello}, - title = {world}, - groups = {B}, -} - -@Comment{jabref-meta: databaseType:bibtex;} - -@Comment{jabref-meta: grouping: -0 AllEntriesGroup:; -1 StaticGroup:A\;0\;0\;0x8a8a8aff\;\;\;; -1 StaticGroup:B\;0\;1\;0x8a8a8aff\;\;\;; -} From b45cc010bc18356243071fc83263c6d94fec5b2d Mon Sep 17 00:00:00 2001 From: Dumitru <150516959+DumitruDruta@users.noreply.github.com> Date: Tue, 9 Jan 2024 11:18:41 +0100 Subject: [PATCH 18/25] Changed default status of 'Automatically open folder of attached files' (#10748) * Changed default status of 'Automatically open folder of attached files' * Changed the original default value * Updated CHANGELOG.md * Update CHANGELOG.md * Made changes OS specific * Whitespace issues fixed * Update CHANGELOG.md --------- Co-authored-by: Christoph Co-authored-by: Oliver Kopp --- CHANGELOG.md | 1 + .../java/org/jabref/preferences/JabRefPreferences.java | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddbd81a149c..744c6d801b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Changed +- The "Automatically open folders of attached files" preference default status has been changed to enabled on Windows. [koppor#56](https://github.com/koppor/jabref/issues/56) - The Custom export format now uses the custom DOI base URI in the preferences for the `DOICheck`, if activated [forum#4084](https://discourse.jabref.org/t/export-html-disregards-custom-doi-base-uri/4084) - The index directories for full text search have now more readable names to increase debugging possibilities using Apache Lucense's Lurk. [#10193](https://github.com/JabRef/jabref/issues/10193) - The fulltext search also indexes files ending with .pdf (but do not having an explicit file type set). [#10193](https://github.com/JabRef/jabref/issues/10193) diff --git a/src/main/java/org/jabref/preferences/JabRefPreferences.java b/src/main/java/org/jabref/preferences/JabRefPreferences.java index 65b320e5971..30670250f62 100644 --- a/src/main/java/org/jabref/preferences/JabRefPreferences.java +++ b/src/main/java/org/jabref/preferences/JabRefPreferences.java @@ -737,7 +737,13 @@ private JabRefPreferences() { defaults.put(KEY_GEN_ALWAYS_ADD_LETTER, Boolean.FALSE); defaults.put(EMAIL_SUBJECT, Localization.lang("References")); defaults.put(KINDLE_EMAIL, ""); - defaults.put(OPEN_FOLDERS_OF_ATTACHED_FILES, Boolean.FALSE); + + if (OS.WINDOWS) { + defaults.put(OPEN_FOLDERS_OF_ATTACHED_FILES, Boolean.TRUE); + } else { + defaults.put(OPEN_FOLDERS_OF_ATTACHED_FILES, Boolean.FALSE); + } + defaults.put(WEB_SEARCH_VISIBLE, Boolean.TRUE); defaults.put(GROUP_SIDEPANE_VISIBLE, Boolean.TRUE); defaults.put(SELECTED_FETCHER_INDEX, 0); From 5636ad0f93164b431dbe5d60e337baba02ff107f Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Tue, 9 Jan 2024 16:25:22 +0100 Subject: [PATCH 19/25] Fix NPE when opening and re-opening a library (#10763) --- src/main/java/org/jabref/logic/pdf/search/PdfIndexer.java | 2 +- .../java/org/jabref/logic/pdf/search/PdfIndexerManager.java | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/jabref/logic/pdf/search/PdfIndexer.java b/src/main/java/org/jabref/logic/pdf/search/PdfIndexer.java index a973831e134..d28f33b6507 100644 --- a/src/main/java/org/jabref/logic/pdf/search/PdfIndexer.java +++ b/src/main/java/org/jabref/logic/pdf/search/PdfIndexer.java @@ -280,7 +280,7 @@ private void addToIndex(BibEntry entry, LinkedFile linkedFile, boolean shouldCom */ public Set getListOfFilePaths() { Set paths = new HashSet<>(); - try (IndexReader reader = DirectoryReader.open(indexWriter)) { + try (IndexReader reader = DirectoryReader.open(getIndexWriter())) { IndexSearcher searcher = new IndexSearcher(reader); MatchAllDocsQuery query = new MatchAllDocsQuery(); TopDocs allDocs = searcher.search(query, Integer.MAX_VALUE); diff --git a/src/main/java/org/jabref/logic/pdf/search/PdfIndexerManager.java b/src/main/java/org/jabref/logic/pdf/search/PdfIndexerManager.java index 3f4bca38c49..8b620c015c9 100644 --- a/src/main/java/org/jabref/logic/pdf/search/PdfIndexerManager.java +++ b/src/main/java/org/jabref/logic/pdf/search/PdfIndexerManager.java @@ -62,16 +62,20 @@ public static void shutdownAllIndexers() { LOGGER.debug("Problem closing PDF indexer", e); } }); + indexerMap.clear(); + pathFilePreferencesMap.clear(); } public static void shutdownIndexer(BibDatabaseContext context) { - PdfIndexer indexer = indexerMap.get(context.getFulltextIndexPath()); + Path fulltextIndexPath = context.getFulltextIndexPath(); + PdfIndexer indexer = indexerMap.remove(fulltextIndexPath); if (indexer != null) { try { indexer.close(); } catch (IOException e) { LOGGER.debug("Could not close indexer", e); } + pathFilePreferencesMap.remove(fulltextIndexPath); } else { LOGGER.debug("No indexer found for context {}", context); } From 01c3e1583f0f0112ed52c2925f8a9c9213af6d53 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Tue, 9 Jan 2024 16:48:31 +0100 Subject: [PATCH 20/25] Streamline test output (#10762) * Try to make test outputs more readable * Fix verbose test output * Revert gradle optimization --- build.gradle | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/build.gradle b/build.gradle index 256b3d85bee..a0d3bb83bce 100644 --- a/build.gradle +++ b/build.gradle @@ -443,33 +443,27 @@ test { } testlogger { - theme 'standard-parallel' + // See https://github.com/radarsh/gradle-test-logger-plugin#configuration for configuration options + + theme 'standard' + showPassed false showSkipped false + + showCauses false + showStackTraces false } tasks.register('databaseTest', Test) { useJUnitPlatform { includeTags 'DatabaseTest' } - - testLogging { - // set options for log level LIFECYCLE - events = ["FAILED"] - exceptionFormat "full" - } } tasks.register('fetcherTest', Test) { useJUnitPlatform { includeTags 'FetcherTest' } - - testLogging { - // set options for log level LIFECYCLE - events = ["FAILED"] - exceptionFormat "full" - } } tasks.register('guiTest', Test) { From 1b828ebdfc316f193b1d07c4f060a8decae2ade8 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Thu, 11 Jan 2024 10:39:43 +0100 Subject: [PATCH 21/25] Add initial .git-blame-ignore-revs (#10765) --- .git-blame-ignore-revs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000000..efa199bdab1 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,28 @@ +# Automatic code reformatting +c5bcad738fe6e8dfcb62442a426c2778241515a1 + +# net.sf.jabref -> org.jabref +b2ad6eb279f5def38fa21be12cb5dd4545c1ba1a +dd385f4ad6dc9251bfdb1c0b75ded402f680b32a +a6e80311c6376c973b5860658f6e4ace7a3fd1f4 +648a4703e80ac0dbe1ff044f354bc0e332e4064d +ac08c6a2c55e1600bf5072c0ea2e6aac920b915d +ec863166cf4f38a1a6bcc80a607fd33299b815aa +8c61577e4377746fc064db2a136bb68968cd4055 +0ef3228f0c1334ff203d4ea5e698e3ad88cb7089 +ce939b4ef19ca856e7ecac08275cf1ca764207b6 +7f1a8069d03737c202bcb7ce352b3755c6a36f5f +dbd0cfbc8177ada414e89da038cd014657d48ed3 +bc7ea00c8cc3ec9ec8b1a799b0dd96513bc51404 +9a5cff44aa1e0d4b737296a30b97f7f384c8b885 +bf81b595a77f0f7f254872be6f05a063c44528d8 +277b40c9e79e0158d272de33e24fa7fc06af91bf +662dd326d212ecfd336a00214e969145ec501c5a +33f040cfbb16111ada117f858e98d606a6bee4fd +29fe730f64eeb62ff9de10fcb460a63297e24be6 + +# This commit should not exist +185d7345946c29a2a4e2726c912be0c4db4810b9 +# Resulted in this problematic merge commits +7e1645978b3028df5e65af19f0f819ddfd0f24aa +a31f396765492ac12eaab228e33eb9d22487403b From 6946012bfc235eeb0928b26d680383093fe2b7ba Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Thu, 11 Jan 2024 11:09:34 +0100 Subject: [PATCH 22/25] Fix annotation (#10766) --- src/test/java/org/jabref/architecture/MainArchitectureTest.java | 1 + .../jabref/logic/search/DatabaseSearcherWithBibFilesTest.java | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/java/org/jabref/architecture/MainArchitectureTest.java b/src/test/java/org/jabref/architecture/MainArchitectureTest.java index b7aff2a46f9..4ebb4c4fbde 100644 --- a/src/test/java/org/jabref/architecture/MainArchitectureTest.java +++ b/src/test/java/org/jabref/architecture/MainArchitectureTest.java @@ -119,6 +119,7 @@ public static void restrictUsagesInModel(JavaClasses classes) { public static void restrictUsagesInLogic(JavaClasses classes) { noClasses().that().resideInAPackage(PACKAGE_ORG_JABREF_LOGIC) .and().areNotAnnotatedWith(AllowedToUseSwing.class) + .and().areNotAssignableFrom("org.jabref.logic.search.DatabaseSearcherWithBibFilesTest") .should().dependOnClassesThat().resideInAPackage(PACKAGE_JAVAX_SWING) .orShould().dependOnClassesThat().haveFullyQualifiedName(CLASS_ORG_JABREF_GLOBALS) .check(classes); diff --git a/src/test/java/org/jabref/logic/search/DatabaseSearcherWithBibFilesTest.java b/src/test/java/org/jabref/logic/search/DatabaseSearcherWithBibFilesTest.java index 84a54d6b3c1..269699c49b0 100644 --- a/src/test/java/org/jabref/logic/search/DatabaseSearcherWithBibFilesTest.java +++ b/src/test/java/org/jabref/logic/search/DatabaseSearcherWithBibFilesTest.java @@ -7,7 +7,6 @@ import java.util.Objects; import java.util.stream.Stream; -import org.jabref.architecture.AllowedToUseSwing; import org.jabref.gui.Globals; import org.jabref.logic.importer.ImportFormatPreferences; import org.jabref.logic.importer.ParserResult; @@ -38,7 +37,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -@AllowedToUseSwing("Makes use of Globals because of FullTextSearchRule") public class DatabaseSearcherWithBibFilesTest { private static BibEntry titleSentenceCased = new BibEntry(StandardEntryType.Misc) From b521028419d4f9bb271bf259f9100388960e4398 Mon Sep 17 00:00:00 2001 From: Carl Christian Snethlage <50491877+calixtus@users.noreply.github.com> Date: Thu, 11 Jan 2024 05:28:26 -0600 Subject: [PATCH 23/25] Refactored LibraryTab (#10732) * Removed code duplication * Moved LibraryTab to its own package * Moved call to LibraryTab::cleanup to libraryTab itself * Refactored BasePanelMode to LibraryTab private * Refactored closing library * Extracted dnd methods * Fixed merge errors * Fixed closing issues * Fixed entry editor showing * Reverted c6e53731 * Fixed quitting, removed unnecessary conversion between string and path * Reduced scope of UpdateTimestampListener again * Reduced scope of some methods and fixed concurrency issue after prefs changes * Fixed test * Fixed localization issue * Fixed merge errors * Fixed review comments * Fixed small concurrency errors and nullptr --- .../java/org/jabref/gui/BasePanelMode.java | 11 - src/main/java/org/jabref/gui/JabRefFrame.java | 386 +++++++----------- src/main/java/org/jabref/gui/JabRefGUI.java | 6 +- src/main/java/org/jabref/gui/LibraryTab.java | 211 ++++++---- .../org/jabref/gui/LibraryTabContainer.java | 15 +- .../java/org/jabref/gui/edit/EditAction.java | 2 +- .../jabref/gui/importer/ImportCommand.java | 5 +- .../org/jabref/gui/maintable/MainTable.java | 6 +- .../gui/maintable/MainTableDataModel.java | 8 +- .../preferences/ShowPreferencesAction.java | 2 +- .../jabref/http/server/LibrariesResource.java | 1 - .../jabref/http/server/LibraryResource.java | 1 - .../java/org/jabref/http/server/Server.java | 21 +- .../jabref/preferences/GuiPreferences.java | 8 +- .../jabref/preferences/JabRefPreferences.java | 11 +- src/main/resources/csl-styles | 2 +- src/main/resources/l10n/JabRef_en.properties | 8 +- .../autosaveandbackup/BackupManagerTest.java | 3 +- .../org/jabref/http/server/ServerTest.java | 4 +- 19 files changed, 337 insertions(+), 374 deletions(-) delete mode 100644 src/main/java/org/jabref/gui/BasePanelMode.java diff --git a/src/main/java/org/jabref/gui/BasePanelMode.java b/src/main/java/org/jabref/gui/BasePanelMode.java deleted file mode 100644 index 9305439ad2b..00000000000 --- a/src/main/java/org/jabref/gui/BasePanelMode.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.jabref.gui; - -/** - * Defines the different modes that the BasePanel can operate in - */ - -public enum BasePanelMode { - SHOWING_NOTHING, - SHOWING_EDITOR, - WILL_SHOW_EDITOR -} diff --git a/src/main/java/org/jabref/gui/JabRefFrame.java b/src/main/java/org/jabref/gui/JabRefFrame.java index 0631c7fedc5..b67be5f66da 100644 --- a/src/main/java/org/jabref/gui/JabRefFrame.java +++ b/src/main/java/org/jabref/gui/JabRefFrame.java @@ -2,7 +2,6 @@ import java.io.IOException; import java.nio.file.Path; -import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -17,8 +16,8 @@ import javafx.beans.binding.Bindings; import javafx.beans.binding.StringBinding; import javafx.collections.transformation.FilteredList; -import javafx.scene.control.Alert; -import javafx.scene.control.ButtonBar; +import javafx.event.Event; +import javafx.scene.Node; import javafx.scene.control.ButtonType; import javafx.scene.control.ContextMenu; import javafx.scene.control.SeparatorMenuItem; @@ -26,6 +25,7 @@ import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import javafx.scene.control.skin.TabPaneSkin; +import javafx.scene.input.DragEvent; import javafx.scene.input.Dragboard; import javafx.scene.input.KeyEvent; import javafx.scene.input.TransferMode; @@ -37,10 +37,7 @@ import org.jabref.gui.actions.ActionHelper; import org.jabref.gui.actions.SimpleCommand; import org.jabref.gui.actions.StandardActions; -import org.jabref.gui.autosaveandbackup.AutosaveManager; -import org.jabref.gui.autosaveandbackup.BackupManager; import org.jabref.gui.desktop.JabRefDesktop; -import org.jabref.gui.exporter.SaveDatabaseAction; import org.jabref.gui.help.HelpAction; import org.jabref.gui.importer.ImportEntriesDialog; import org.jabref.gui.importer.NewEntryAction; @@ -61,8 +58,6 @@ import org.jabref.logic.importer.ImportCleanup; import org.jabref.logic.importer.ParserResult; import org.jabref.logic.l10n.Localization; -import org.jabref.logic.pdf.search.PdfIndexerManager; -import org.jabref.logic.shared.DatabaseLocation; import org.jabref.logic.undo.AddUndoableActionEvent; import org.jabref.logic.undo.UndoChangeEvent; import org.jabref.logic.undo.UndoRedoEvent; @@ -145,104 +140,109 @@ private void initDragAndDrop() { return; } // Add drag and drop listeners to JabRefFrame - this.getScene().setOnDragOver(event -> { - if (DragAndDropHelper.hasBibFiles(event.getDragboard())) { - event.acceptTransferModes(TransferMode.ANY); - if (!tabbedPane.getTabs().contains(dndIndicator)) { - tabbedPane.getTabs().add(dndIndicator); - } - event.consume(); - } else { - tabbedPane.getTabs().remove(dndIndicator); - } - // Accept drag entries from MainTable - if (event.getDragboard().hasContent(DragAndDropDataFormats.ENTRIES)) { - event.acceptTransferModes(TransferMode.COPY); - event.consume(); - } - }); - + this.getScene().setOnDragOver(event -> onSceneDragOver(event, dndIndicator)); this.getScene().setOnDragEntered(event -> { // It is necessary to setOnDragOver for newly opened tabs // drag'n'drop on tabs covered dnd on tabbedPane, so dnd on tabs should contain all dnds on tabbedPane - tabbedPane.lookupAll(".tab").forEach(destinationTabNode -> { - destinationTabNode.setOnDragOver(tabDragEvent -> { - if (DragAndDropHelper.hasBibFiles(tabDragEvent.getDragboard()) || DragAndDropHelper.hasGroups(tabDragEvent.getDragboard())) { - tabDragEvent.acceptTransferModes(TransferMode.ANY); - if (!tabbedPane.getTabs().contains(dndIndicator)) { - tabbedPane.getTabs().add(dndIndicator); - } - event.consume(); - } else { - tabbedPane.getTabs().remove(dndIndicator); - } - - if (tabDragEvent.getDragboard().hasContent(DragAndDropDataFormats.ENTRIES)) { - tabDragEvent.acceptTransferModes(TransferMode.COPY); - tabDragEvent.consume(); - } - }); + for (Node destinationTabNode : tabbedPane.lookupAll(".tab")) { + destinationTabNode.setOnDragOver(tabDragEvent -> onTabDragOver(event, tabDragEvent, dndIndicator)); destinationTabNode.setOnDragExited(event1 -> tabbedPane.getTabs().remove(dndIndicator)); - destinationTabNode.setOnDragDropped(tabDragEvent -> { - - Dragboard dragboard = tabDragEvent.getDragboard(); - - if (DragAndDropHelper.hasBibFiles(dragboard)) { - tabbedPane.getTabs().remove(dndIndicator); - List bibFiles = DragAndDropHelper.getBibFiles(dragboard); - OpenDatabaseAction openDatabaseAction = this.getOpenDatabaseAction(); - openDatabaseAction.openFiles(bibFiles); - tabDragEvent.setDropCompleted(true); - tabDragEvent.consume(); - } else { - for (Tab libraryTab : tabbedPane.getTabs()) { - if (libraryTab.getId().equals(destinationTabNode.getId()) && - !tabbedPane.getSelectionModel().getSelectedItem().equals(libraryTab)) { - LibraryTab destinationLibraryTab = (LibraryTab) libraryTab; - if (DragAndDropHelper.hasGroups(dragboard)) { - List groupPathToSources = DragAndDropHelper.getGroups(dragboard); - - copyRootNode(destinationLibraryTab); - - GroupTreeNode destinationLibraryGroupRoot = destinationLibraryTab - .getBibDatabaseContext() - .getMetaData() - .getGroups().get(); - - for (String pathToSource : groupPathToSources) { - GroupTreeNode groupTreeNodeToCopy = getCurrentLibraryTab() - .getBibDatabaseContext() - .getMetaData() - .getGroups() - .get() - .getChildByPath(pathToSource) - .get(); - copyGroupTreeNode((LibraryTab) libraryTab, destinationLibraryGroupRoot, groupTreeNodeToCopy); - } - return; - } - destinationLibraryTab.dropEntry(stateManager.getLocalDragboard().getBibEntries()); - } - } - tabDragEvent.consume(); - } - }); - }); + destinationTabNode.setOnDragDropped(tabDragEvent -> onTabDragDropped(destinationTabNode, tabDragEvent, dndIndicator)); + } event.consume(); }); - this.getScene().setOnDragExited(event -> tabbedPane.getTabs().remove(dndIndicator)); - this.getScene().setOnDragDropped(event -> { - tabbedPane.getTabs().remove(dndIndicator); - List bibFiles = DragAndDropHelper.getBibFiles(event.getDragboard()); - OpenDatabaseAction openDatabaseAction = this.getOpenDatabaseAction(); - openDatabaseAction.openFiles(bibFiles); - event.setDropCompleted(true); - event.consume(); - }); + this.getScene().setOnDragDropped(event -> onSceneDragDropped(event, dndIndicator)); }); } + private void onTabDragDropped(Node destinationTabNode, DragEvent tabDragEvent, Tab dndIndicator) { + Dragboard dragboard = tabDragEvent.getDragboard(); + + if (DragAndDropHelper.hasBibFiles(dragboard)) { + tabbedPane.getTabs().remove(dndIndicator); + List bibFiles = DragAndDropHelper.getBibFiles(dragboard); + OpenDatabaseAction openDatabaseAction = this.getOpenDatabaseAction(); + openDatabaseAction.openFiles(bibFiles); + tabDragEvent.setDropCompleted(true); + tabDragEvent.consume(); + } else { + for (Tab libraryTab : tabbedPane.getTabs()) { + if (libraryTab.getId().equals(destinationTabNode.getId()) && + !tabbedPane.getSelectionModel().getSelectedItem().equals(libraryTab)) { + LibraryTab destinationLibraryTab = (LibraryTab) libraryTab; + if (DragAndDropHelper.hasGroups(dragboard)) { + List groupPathToSources = DragAndDropHelper.getGroups(dragboard); + + copyRootNode(destinationLibraryTab); + + GroupTreeNode destinationLibraryGroupRoot = destinationLibraryTab + .getBibDatabaseContext() + .getMetaData() + .getGroups().get(); + + for (String pathToSource : groupPathToSources) { + GroupTreeNode groupTreeNodeToCopy = getCurrentLibraryTab() + .getBibDatabaseContext() + .getMetaData() + .getGroups() + .get() + .getChildByPath(pathToSource) + .get(); + copyGroupTreeNode((LibraryTab) libraryTab, destinationLibraryGroupRoot, groupTreeNodeToCopy); + } + return; + } + destinationLibraryTab.dropEntry(stateManager.getLocalDragboard().getBibEntries()); + } + } + tabDragEvent.consume(); + } + } + + private void onTabDragOver(DragEvent event, DragEvent tabDragEvent, Tab dndIndicator) { + if (DragAndDropHelper.hasBibFiles(tabDragEvent.getDragboard()) || DragAndDropHelper.hasGroups(tabDragEvent.getDragboard())) { + tabDragEvent.acceptTransferModes(TransferMode.ANY); + if (!tabbedPane.getTabs().contains(dndIndicator)) { + tabbedPane.getTabs().add(dndIndicator); + } + event.consume(); + } else { + tabbedPane.getTabs().remove(dndIndicator); + } + + if (tabDragEvent.getDragboard().hasContent(DragAndDropDataFormats.ENTRIES)) { + tabDragEvent.acceptTransferModes(TransferMode.COPY); + tabDragEvent.consume(); + } + } + + private void onSceneDragOver(DragEvent event, Tab dndIndicator) { + if (DragAndDropHelper.hasBibFiles(event.getDragboard())) { + event.acceptTransferModes(TransferMode.ANY); + if (!tabbedPane.getTabs().contains(dndIndicator)) { + tabbedPane.getTabs().add(dndIndicator); + } + event.consume(); + } else { + tabbedPane.getTabs().remove(dndIndicator); + } + // Accept drag entries from MainTable + if (event.getDragboard().hasContent(DragAndDropDataFormats.ENTRIES)) { + event.acceptTransferModes(TransferMode.COPY); + event.consume(); + } + } + + private void onSceneDragDropped(DragEvent event, Tab dndIndicator) { + tabbedPane.getTabs().remove(dndIndicator); + List bibFiles = DragAndDropHelper.getBibFiles(event.getDragboard()); + OpenDatabaseAction openDatabaseAction = this.getOpenDatabaseAction(); + openDatabaseAction.openFiles(bibFiles); + event.setDropCompleted(true); + event.consume(); + } + private void initKeyBindings() { addEventFilter(KeyEvent.KEY_PRESSED, event -> { Optional keyBinding = Globals.getKeyPrefs().mapToKeyBinding(event); @@ -352,30 +352,17 @@ public void about() { new HelpAction(HelpFile.CONTENTS, dialogService, prefs.getFilePreferences()).execute(); } - /** - * Tears down all things started by JabRef - *

- * FIXME: Currently some threads remain and therefore hinder JabRef to be closed properly - * - * @param filenames the filenames of all currently opened files - used for storing them if prefs openLastEdited is - * set to true - */ - private void tearDownJabRef(List filenames) { + private void storeLastOpenedFiles(List filenames, Path focusedDatabase) { if (prefs.getWorkspacePreferences().shouldOpenLastEdited()) { // Here we store the names of all current files. If there is no current file, we remove any // previously stored filename. if (filenames.isEmpty()) { prefs.getGuiPreferences().getLastFilesOpened().clear(); } else { - Path focusedDatabase = getCurrentLibraryTab().getBibDatabaseContext() - .getDatabasePath() - .orElse(null); prefs.getGuiPreferences().setLastFilesOpened(filenames); prefs.getGuiPreferences().setLastFocusedFile(focusedDatabase); } } - - prefs.flush(); } /** @@ -385,7 +372,6 @@ private void tearDownJabRef(List filenames) { *

* Non-OSX JabRef calls this when choosing "Quit" from the menu *

- * SIDE EFFECT: tears down JabRef * * @return true if the user chose to quit; false otherwise */ @@ -405,25 +391,20 @@ public boolean quit() { } } - // Then ask if the user really wants to close, if the library has not been saved since last save. - List filenames = new ArrayList<>(); - for (LibraryTab libraryTab : getLibraryTabs()) { - final BibDatabaseContext context = libraryTab.getBibDatabaseContext(); + // Read the opened and focused databases before closing them + List openedLibraries = getLibraryTabs().stream() + .map(LibraryTab::getBibDatabaseContext) + .map(BibDatabaseContext::getDatabasePath) + .flatMap(Optional::stream) + .toList(); + Path focusedLibraries = Optional.ofNullable(getCurrentLibraryTab()) + .map(LibraryTab::getBibDatabaseContext) + .flatMap(BibDatabaseContext::getDatabasePath) + .orElse(null); - if (libraryTab.isModified() && (context.getLocation() == DatabaseLocation.LOCAL)) { - showLibraryTab(libraryTab); - if (!confirmClose(libraryTab)) { - return false; - } - } else if (context.getLocation() == DatabaseLocation.SHARED) { - context.convertToLocalDatabase(); - context.getDBMSSynchronizer().closeSharedDatabase(); - context.clearDBMSSynchronizer(); - } - - AutosaveManager.shutdown(context); - BackupManager.shutdown(context, prefs.getFilePreferences().getBackupDirectory(), prefs.getFilePreferences().shouldCreateBackup()); - context.getDatabasePath().map(Path::toAbsolutePath).map(Path::toString).ifPresent(filenames::add); + // Then ask if the user really wants to close, if the library has not been saved since last save. + if (!closeTabs()) { + return false; } WaitForSaveFinishedDialog waitForSaveFinishedDialog = new WaitForSaveFinishedDialog(dialogService); @@ -431,8 +412,11 @@ public boolean quit() { // We call saveWindow state here again because under Mac the windowClose listener on the stage isn't triggered when using cmd + q saveWindowState(); - // Good bye! - tearDownJabRef(filenames); + storeLastOpenedFiles(openedLibraries, focusedLibraries); // store only if successfully having closed the libraries + + prefs.flush(); + + // Goodbye! Platform.exit(); return true; } @@ -529,7 +513,7 @@ private void updateSidePane() { splitPane.getItems().remove(sidePane); } else { if (!splitPane.getItems().contains(sidePane)) { - splitPane.getItems().add(0, sidePane); + splitPane.getItems().addFirst(sidePane); setDividerPosition(); } } @@ -665,19 +649,6 @@ public LibraryTab getCurrentLibraryTab() { return (LibraryTab) tabbedPane.getSelectionModel().getSelectedItem(); } - /** - * This method causes all open LibraryTabs to set up their tables anew. When called from PreferencesDialogViewModel, - * this updates to the new settings. We need to notify all tabs about the changes to avoid problems when changing - * the column set. - */ - public void setupAllTables() { - tabbedPane.getTabs().forEach(tab -> { - if (tab instanceof LibraryTab libraryTab && (libraryTab.getDatabase() != null)) { - DefaultTaskExecutor.runInJavaFXThread(libraryTab::setupMainPanel); - } - }); - } - private ContextMenu createTabContextMenuFor(LibraryTab tab, KeyBindingRepository keyBindingRepository) { ContextMenu contextMenu = new ContextMenu(); ActionFactory factory = new ActionFactory(keyBindingRepository); @@ -701,12 +672,6 @@ public void addTab(LibraryTab libraryTab, boolean raisePanel) { tabbedPane.requestFocus(); } - libraryTab.setOnCloseRequest(event -> { - libraryTab.cancelLoading(); - closeTab(libraryTab); - event.consume(); - }); - libraryTab.setContextMenu(createTabContextMenuFor(libraryTab, Globals.getKeyPrefs())); libraryTab.getUndoManager().registerListener(new UndoRedoEventManager()); @@ -714,7 +679,7 @@ public void addTab(LibraryTab libraryTab, boolean raisePanel) { /** * Opens a new tab with existing data. - * Asynchronous loading is done at {@link org.jabref.gui.LibraryTab#createLibraryTab(BackgroundTask, Path, DialogService, PreferencesService, StateManager, JabRefFrame, FileUpdateMonitor, BibEntryTypesManager, CountingUndoManager)}. + * Asynchronous loading is done at {@link LibraryTab#createLibraryTab}. */ public void addTab(BibDatabaseContext databaseContext, boolean raisePanel) { Objects.requireNonNull(databaseContext); @@ -783,98 +748,33 @@ public FileHistoryMenu getFileHistory() { return fileHistory; } - /** - * Ask if the user really wants to close the given database. - * Offers to save or discard the changes -- or return to the library - * - * @return true if the user choose to close the database - */ - private boolean confirmClose(LibraryTab libraryTab) { - String filename = libraryTab.getBibDatabaseContext() - .getDatabasePath() - .map(Path::toAbsolutePath) - .map(Path::toString) - .orElse(Localization.lang("untitled")); - - ButtonType saveChanges = new ButtonType(Localization.lang("Save changes"), ButtonBar.ButtonData.YES); - ButtonType discardChanges = new ButtonType(Localization.lang("Discard changes"), ButtonBar.ButtonData.NO); - ButtonType returnToLibrary = new ButtonType(Localization.lang("Return to library"), ButtonBar.ButtonData.CANCEL_CLOSE); - - Optional response = dialogService.showCustomButtonDialogAndWait(Alert.AlertType.CONFIRMATION, - Localization.lang("Save before closing"), - Localization.lang("Library '%0' has changed.", filename), - saveChanges, discardChanges, returnToLibrary); - - if (response.isEmpty()) { - return true; - } - - ButtonType buttonType = response.get(); - - if (buttonType.equals(returnToLibrary)) { - return false; - } - - if (buttonType.equals(saveChanges)) { - try { - SaveDatabaseAction saveAction = new SaveDatabaseAction(libraryTab, dialogService, prefs, Globals.entryTypesManager); - if (saveAction.save()) { - return true; - } - // The action was either canceled or unsuccessful. - dialogService.notify(Localization.lang("Unable to save library")); - } catch (Throwable ex) { - LOGGER.error("A problem occurred when trying to save the file", ex); - dialogService.showErrorDialogAndWait(Localization.lang("Save library"), Localization.lang("Could not save file."), ex); - } - // Save was cancelled or an error occurred. - return false; - } - - if (buttonType.equals(discardChanges)) { - BackupManager.discardBackup(libraryTab.getBibDatabaseContext(), prefs.getFilePreferences().getBackupDirectory()); + public boolean closeTab(LibraryTab libraryTab) { + if (libraryTab.requestClose()) { + tabbedPane.getTabs().remove(libraryTab); + Event.fireEvent(libraryTab, new Event(this, libraryTab, Tab.CLOSED_EVENT)); return true; } - return false; } - public void closeTab(LibraryTab libraryTab) { - // empty tab without database - if (libraryTab == null) { - libraryTab = getCurrentLibraryTab(); - } - - final BibDatabaseContext context = libraryTab.getBibDatabaseContext(); - - if (libraryTab.isModified() && (context.getLocation() == DatabaseLocation.LOCAL)) { - if (confirmClose(libraryTab)) { - removeTab(libraryTab); - } else { - return; + private boolean closeTabs() { + // Ask before closing any tab, if any tab has changes + for (LibraryTab libraryTab : getLibraryTabs()) { + if (!libraryTab.requestClose()) { + return false; } - } else if (context.getLocation() == DatabaseLocation.SHARED) { - context.convertToLocalDatabase(); - context.getDBMSSynchronizer().closeSharedDatabase(); - context.clearDBMSSynchronizer(); - removeTab(libraryTab); - } else { - removeTab(libraryTab); } - AutosaveManager.shutdown(context); - BackupManager.shutdown(context, prefs.getFilePreferences().getBackupDirectory(), prefs.getFilePreferences().shouldCreateBackup()); - PdfIndexerManager.shutdownAllIndexers(); - } - private void removeTab(LibraryTab libraryTab) { - DefaultTaskExecutor.runInJavaFXThread(() -> { - libraryTab.cleanUp(); + // Close after checking for changes and saving all databases + for (LibraryTab libraryTab : getLibraryTabs()) { tabbedPane.getTabs().remove(libraryTab); - }); + Event.fireEvent(libraryTab, new Event(this, libraryTab, Tab.CLOSED_EVENT)); + } + return true; } - public void closeCurrentTab() { - removeTab(getCurrentLibraryTab()); + public boolean closeCurrentTab() { + return closeTab(getCurrentLibraryTab()); } public OpenDatabaseAction getOpenDatabaseAction() { @@ -921,7 +821,7 @@ private void copyGroupTreeNode(LibraryTab destinationLibraryTab, GroupTreeNode p } private void copyRootNode(LibraryTab destinationLibraryTab) { - if (!destinationLibraryTab.getBibDatabaseContext().getMetaData().getGroups().isEmpty()) { + if (destinationLibraryTab.getBibDatabaseContext().getMetaData().getGroups().isPresent()) { return; } // a root (all entries) GroupTreeNode @@ -944,10 +844,10 @@ public Stage getMainStage() { /** * Refreshes the ui after preferences changes */ - public void refresh() { + public void refresh() { globalSearchBar.updateHintVisibility(); - setupAllTables(); - getLibraryTabs().forEach(panel -> panel.getMainTable().getTableModel().refresh()); + getLibraryTabs().forEach(LibraryTab::setupMainPanel); + getLibraryTabs().forEach(tab -> tab.getMainTable().getTableModel().resetFieldFormatter()); } /** @@ -986,7 +886,13 @@ public CloseDatabaseAction(LibraryTabContainer tabContainer) { @Override public void execute() { - tabContainer.closeTab(libraryTab); + Platform.runLater(() -> { + if (libraryTab == null) { + tabContainer.closeCurrentTab(); + } else { + tabContainer.closeTab(libraryTab); + } + }); } } @@ -1005,7 +911,7 @@ public void execute() { for (Tab tab : tabbedPane.getTabs()) { LibraryTab libraryTab = (LibraryTab) tab; if (libraryTab != toKeepLibraryTab) { - closeTab(libraryTab); + Platform.runLater(() -> closeTab(libraryTab)); } } } @@ -1016,7 +922,7 @@ private class CloseAllDatabaseAction extends SimpleCommand { @Override public void execute() { for (Tab tab : tabbedPane.getTabs()) { - closeTab((LibraryTab) tab); + Platform.runLater(() -> closeTab((LibraryTab) tab)); } } } diff --git a/src/main/java/org/jabref/gui/JabRefGUI.java b/src/main/java/org/jabref/gui/JabRefGUI.java index 657429b2e3c..bf95eeebb16 100644 --- a/src/main/java/org/jabref/gui/JabRefGUI.java +++ b/src/main/java/org/jabref/gui/JabRefGUI.java @@ -5,7 +5,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; import javafx.application.Platform; import javafx.scene.Scene; @@ -320,13 +319,12 @@ private boolean isWindowPositionOutOfBounds() { } private void openLastEditedDatabases() { - List lastFiles = preferencesService.getGuiPreferences().getLastFilesOpened(); + List lastFiles = preferencesService.getGuiPreferences().getLastFilesOpened(); if (lastFiles.isEmpty()) { return; } - List filesToOpen = lastFiles.stream().map(Path::of).collect(Collectors.toList()); - getMainFrame().getOpenDatabaseAction().openFiles(filesToOpen); + getMainFrame().getOpenDatabaseAction().openFiles(lastFiles); } public static JabRefFrame getMainFrame() { diff --git a/src/main/java/org/jabref/gui/LibraryTab.java b/src/main/java/org/jabref/gui/LibraryTab.java index 658117d8483..fbd701e5057 100644 --- a/src/main/java/org/jabref/gui/LibraryTab.java +++ b/src/main/java/org/jabref/gui/LibraryTab.java @@ -16,8 +16,12 @@ import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.collections.ListChangeListener; +import javafx.event.Event; import javafx.geometry.Orientation; import javafx.scene.Node; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ButtonType; import javafx.scene.control.ProgressIndicator; import javafx.scene.control.SplitPane; import javafx.scene.control.Tab; @@ -25,6 +29,7 @@ import javafx.scene.layout.BorderPane; import javafx.util.Duration; +import org.jabref.gui.actions.StandardActions; import org.jabref.gui.autocompleter.AutoCompletePreferences; import org.jabref.gui.autocompleter.PersonNameSuggestionProvider; import org.jabref.gui.autocompleter.SuggestionProvider; @@ -34,6 +39,7 @@ import org.jabref.gui.collab.DatabaseChangeMonitor; import org.jabref.gui.dialogs.AutosaveUiManager; import org.jabref.gui.entryeditor.EntryEditor; +import org.jabref.gui.exporter.SaveDatabaseAction; import org.jabref.gui.importer.actions.OpenDatabaseAction; import org.jabref.gui.maintable.BibEntryTableViewModel; import org.jabref.gui.maintable.MainTable; @@ -84,8 +90,16 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * Represents the ui area where the notifier pane, the library table and the entry editor are shown. + */ public class LibraryTab extends Tab { + /** + * Defines the different modes that the tab can operate in + */ + private enum PanelMode { MAIN_TABLE, MAIN_TABLE_AND_ENTRY_EDITOR } + private static final Logger LOGGER = LoggerFactory.getLogger(LibraryTab.class); private final LibraryTabContainer tabContainer; private final CountingUndoManager undoManager; @@ -103,7 +117,7 @@ public class LibraryTab extends Tab { private FileAnnotationCache annotationCache; private EntryEditor entryEditor; private MainTable mainTable; - private BasePanelMode mode = BasePanelMode.SHOWING_NOTHING; + private PanelMode mode = PanelMode.MAIN_TABLE; private SplitPane splitPane; private DatabaseNotification databaseNotificationPane; private boolean saving; @@ -127,7 +141,7 @@ public class LibraryTab extends Tab { private final IndexingTaskManager indexingTaskManager; private final TaskExecutor taskExecutor; - public LibraryTab(BibDatabaseContext bibDatabaseContext, + private LibraryTab(BibDatabaseContext bibDatabaseContext, LibraryTabContainer tabContainer, DialogService dialogService, PreferencesService preferencesService, @@ -179,6 +193,9 @@ public LibraryTab(BibDatabaseContext bibDatabaseContext, stateManager.getOpenDatabases().addListener((ListChangeListener) c -> updateTabTitle(changedProperty.getValue())); }); + + setOnCloseRequest(this::onCloseRequest); + setOnClosed(this::onClosed); } private static void addChangedInformation(StringBuilder text, String fileName) { @@ -199,20 +216,14 @@ private static void addSharedDbInformation(StringBuilder text, BibDatabaseContex text.append("]"); } - public void setDataLoadingTask(BackgroundTask dataLoadingTask) { + private void setDataLoadingTask(BackgroundTask dataLoadingTask) { this.dataLoadingTask = dataLoadingTask; } - public void cancelLoading() { - if (dataLoadingTask != null) { - dataLoadingTask.cancel(); - } - } - /** * The layout to display in the tab when it's loading */ - public Node createLoadingAnimationLayout() { + private Node createLoadingAnimationLayout() { ProgressIndicator progressIndicator = new ProgressIndicator(ProgressIndicator.INDETERMINATE_PROGRESS); BorderPane pane = new BorderPane(); pane.setCenter(progressIndicator); @@ -220,13 +231,13 @@ public Node createLoadingAnimationLayout() { return pane; } - public void onDatabaseLoadingStarted() { + private void onDatabaseLoadingStarted() { Node loadingLayout = createLoadingAnimationLayout(); getMainTable().placeholderProperty().setValue(loadingLayout); tabContainer.addTab(this, true); } - public void onDatabaseLoadingSucceed(ParserResult result) { + private void onDatabaseLoadingSucceed(ParserResult result) { BibDatabaseContext context = result.getDatabaseContext(); OpenDatabaseAction.performPostOpenActions(result, dialogService); @@ -239,18 +250,18 @@ public void onDatabaseLoadingSucceed(ParserResult result) { LOGGER.error("Cannot access lucene index", e); } } + + dataLoadingTask = null; } - public void onDatabaseLoadingFailed(Exception ex) { + private void onDatabaseLoadingFailed(Exception ex) { String title = Localization.lang("Connection error"); String content = String.format("%s\n\n%s", ex.getMessage(), Localization.lang("A local copy will be opened.")); dialogService.showErrorDialogAndWait(title, content, ex); } - public void feedData(BibDatabaseContext bibDatabaseContextFromParserResult) { - cleanUp(); - + private void feedData(BibDatabaseContext bibDatabaseContextFromParserResult) { if (this.getTabPane().getSelectionModel().selectedItemProperty().get().equals(this)) { // If you open an existing library, a library tab with a loading animation is added immediately. // At that point, the library tab is given a temporary bibDatabaseContext with no entries. @@ -406,49 +417,46 @@ public SuggestionProviders getSuggestionProviders() { return suggestionProviders; } - public BasePanelMode getMode() { - return mode; - } - - public void setMode(BasePanelMode mode) { - this.mode = mode; - } - /** * Removes the selected entries from the database * - * @param cut If false the user will get asked if he really wants to delete the entries, and it will be localized as "deleted". If true the action will be localized as "cut" + * @param mode If DELETE_ENTRY the user will get asked if he really wants to delete the entries, and it will be localized as "deleted". If true the action will be localized as "cut" */ - public void delete(boolean cut) { - delete(cut, mainTable.getSelectedEntries()); + public void delete(StandardActions mode) { + delete(mode, mainTable.getSelectedEntries()); } /** * Removes the selected entries from the database * - * @param cut If false the user will get asked if he really wants to delete the entries, and it will be localized as "deleted". If true the action will be localized as "cut" + * @param mode If DELETE_ENTRY the user will get asked if he really wants to delete the entries, and it will be localized as "deleted". If true the action will be localized as "cut" */ - private void delete(boolean cut, List entries) { + private void delete(StandardActions mode, List entries) { if (entries.isEmpty()) { return; } - if (!cut && !showDeleteConfirmationDialog(entries.size())) { + if (mode == StandardActions.DELETE_ENTRY && !showDeleteConfirmationDialog(entries.size())) { return; } - getUndoManager().addEdit(new UndoableRemoveEntries(bibDatabaseContext.getDatabase(), entries, cut)); + getUndoManager().addEdit(new UndoableRemoveEntries(bibDatabaseContext.getDatabase(), entries, mode == StandardActions.CUT)); bibDatabaseContext.getDatabase().removeEntries(entries); ensureNotShowingBottomPanel(entries); this.changedProperty.setValue(true); - dialogService.notify(formatOutputMessage(cut ? Localization.lang("Cut") : Localization.lang("Deleted"), entries.size())); + switch (mode) { + case StandardActions.CUT -> + dialogService.notify(Localization.lang("Cut %0 entry(ies)", entries.size())); + case StandardActions.DELETE_ENTRY -> + dialogService.notify(Localization.lang("Deleted %0 entry(ies)", entries.size())); + } // prevent the main table from loosing focus mainTable.requestFocus(); } public void delete(BibEntry entry) { - delete(false, Collections.singletonList(entry)); + delete(StandardActions.DELETE_ENTRY, Collections.singletonList(entry)); } public void registerUndoableChanges(List changes) { @@ -579,37 +587,25 @@ public EntryEditor getEntryEditor() { * @param entry The entry to edit. */ public void showAndEdit(BibEntry entry) { - showBottomPane(BasePanelMode.SHOWING_EDITOR); + if (!splitPane.getItems().contains(entryEditor)) { + splitPane.getItems().addLast(entryEditor); + mode = PanelMode.MAIN_TABLE_AND_ENTRY_EDITOR; + splitPane.setDividerPositions(preferencesService.getEntryEditorPreferences().getDividerPosition()); + } // We use != instead of equals because of performance reasons - if (entry != getShowing()) { + if (entry != showing) { entryEditor.setEntry(entry); showing = entry; } entryEditor.requestFocus(); } - private void showBottomPane(BasePanelMode newMode) { - if (newMode != BasePanelMode.SHOWING_EDITOR) { - throw new UnsupportedOperationException("new mode not recognized: " + newMode.name()); - } - Node pane = entryEditor; - - if (splitPane.getItems().size() == 2) { - splitPane.getItems().set(1, pane); - } else { - splitPane.getItems().add(1, pane); - } - mode = newMode; - - splitPane.setDividerPositions(preferencesService.getEntryEditorPreferences().getDividerPosition()); - } - /** * Removes the bottom component. */ public void closeBottomPane() { - mode = BasePanelMode.SHOWING_NOTHING; + mode = PanelMode.MAIN_TABLE; splitPane.getItems().remove(entryEditor); } @@ -642,13 +638,13 @@ public void entryEditorClosing() { private void ensureNotShowingBottomPanel(List entriesToCheck) { // This method is not able to close the bottom pane currently - if ((mode == BasePanelMode.SHOWING_EDITOR) && (entriesToCheck.contains(entryEditor.getEntry()))) { + if ((mode == PanelMode.MAIN_TABLE_AND_ENTRY_EDITOR) && (entriesToCheck.contains(entryEditor.getEntry()))) { closeBottomPane(); } } public void updateEntryEditorIfShowing() { - if (mode == BasePanelMode.SHOWING_EDITOR) { + if (mode == PanelMode.MAIN_TABLE_AND_ENTRY_EDITOR) { BibEntry currentEntry = entryEditor.getEntry(); showAndEdit(currentEntry); } @@ -698,18 +694,101 @@ private boolean showDeleteConfirmationDialog(int numberOfEntries) { * Depending on whether a preview or an entry editor is showing, save the current divider location in the correct preference setting. */ private void saveDividerLocation(Number position) { - if (mode == BasePanelMode.SHOWING_EDITOR) { + if (mode == PanelMode.MAIN_TABLE_AND_ENTRY_EDITOR) { preferencesService.getEntryEditorPreferences().setDividerPosition(position.doubleValue()); } } + public boolean requestClose() { + if (isModified() && (bibDatabaseContext.getLocation() == DatabaseLocation.LOCAL)) { + return confirmClose(); + } else if (bibDatabaseContext.getLocation() == DatabaseLocation.SHARED) { + bibDatabaseContext.convertToLocalDatabase(); + bibDatabaseContext.getDBMSSynchronizer().closeSharedDatabase(); + bibDatabaseContext.clearDBMSSynchronizer(); + } + + return true; + } + + /** + * Ask if the user really wants to close the given database. + * Offers to save or discard the changes -- or return to the library + * + * @return true if the user choose to close the database + */ + private boolean confirmClose() { + // Database could not have been changed, since it is still loading + if (dataLoadingTask != null) { + dataLoadingTask.cancel(); + return true; + } + + String filename = getBibDatabaseContext() + .getDatabasePath() + .map(Path::toAbsolutePath) + .map(Path::toString) + .orElse(Localization.lang("untitled")); + + ButtonType saveChanges = new ButtonType(Localization.lang("Save changes"), ButtonBar.ButtonData.YES); + ButtonType discardChanges = new ButtonType(Localization.lang("Discard changes"), ButtonBar.ButtonData.NO); + ButtonType returnToLibrary = new ButtonType(Localization.lang("Return to library"), ButtonBar.ButtonData.CANCEL_CLOSE); + + Optional response = dialogService.showCustomButtonDialogAndWait(Alert.AlertType.CONFIRMATION, + Localization.lang("Save before closing"), + Localization.lang("Library '%0' has changed.", filename), + saveChanges, discardChanges, returnToLibrary); + + if (response.isEmpty()) { + return true; + } + + ButtonType buttonType = response.get(); + + if (buttonType.equals(returnToLibrary)) { + return false; + } + + if (buttonType.equals(saveChanges)) { + try { + SaveDatabaseAction saveAction = new SaveDatabaseAction(this, dialogService, preferencesService, Globals.entryTypesManager); + if (saveAction.save()) { + return true; + } + // The action was either canceled or unsuccessful. + dialogService.notify(Localization.lang("Unable to save library")); + } catch (Throwable ex) { + LOGGER.error("A problem occurred when trying to save the file", ex); + dialogService.showErrorDialogAndWait(Localization.lang("Save library"), Localization.lang("Could not save file."), ex); + } + // Save was cancelled or an error occurred. + return false; + } + + if (buttonType.equals(discardChanges)) { + BackupManager.discardBackup(bibDatabaseContext, preferencesService.getFilePreferences().getBackupDirectory()); + return true; + } + + return false; + } + + private void onCloseRequest(Event event) { + if (!requestClose()) { + event.consume(); + } + } + /** - * Perform necessary cleanup when this BasePanel is closed. + * Perform necessary cleanup when this Library is closed. */ - public void cleanUp() { + private void onClosed(Event event) { changeMonitor.ifPresent(DatabaseChangeMonitor::unregister); + PdfIndexerManager.shutdownIndexer(bibDatabaseContext); AutosaveManager.shutdown(bibDatabaseContext); - BackupManager.shutdown(bibDatabaseContext, preferencesService.getFilePreferences().getBackupDirectory(), preferencesService.getFilePreferences().shouldCreateBackup()); + BackupManager.shutdown(bibDatabaseContext, + preferencesService.getFilePreferences().getBackupDirectory(), + preferencesService.getFilePreferences().shouldCreateBackup()); } /** @@ -733,14 +812,6 @@ public void setSaving(boolean saving) { this.saving = saving; } - private BibEntry getShowing() { - return showing; - } - - public String formatOutputMessage(String start, int count) { - return String.format("%s %d %s.", start, count, (count > 1 ? Localization.lang("entries") : Localization.lang("entry"))); - } - public CountingUndoManager getUndoManager() { return undoManager; } @@ -794,10 +865,6 @@ public void cut() { mainTable.cut(); } - public BooleanProperty changedProperty() { - return changedProperty; - } - public boolean isModified() { return changedProperty.getValue(); } @@ -806,10 +873,6 @@ public void markBaseChanged() { this.changedProperty.setValue(true); } - public BooleanProperty nonUndoableChangeProperty() { - return nonUndoableChangeProperty; - } - public void markNonUndoableBaseChanged() { this.nonUndoableChangeProperty.setValue(true); this.changedProperty.setValue(true); diff --git a/src/main/java/org/jabref/gui/LibraryTabContainer.java b/src/main/java/org/jabref/gui/LibraryTabContainer.java index c823e5c8892..a290676bd3a 100644 --- a/src/main/java/org/jabref/gui/LibraryTabContainer.java +++ b/src/main/java/org/jabref/gui/LibraryTabContainer.java @@ -15,9 +15,20 @@ public interface LibraryTabContainer { void addTab(BibDatabaseContext bibDatabaseContext, boolean raisePanel); - void closeTab(LibraryTab libraryTab); + /** + * Closes a designated libraryTab + * + * @param libraryTab to be closed. + * @return true if closing the tab was successful + */ + boolean closeTab(LibraryTab libraryTab); - void closeCurrentTab(); + /** + * Closes the currently viewed libraryTab + * + * @return true if closing the tab was successful + */ + boolean closeCurrentTab(); /** * Refreshes the ui after changes to the preferences diff --git a/src/main/java/org/jabref/gui/edit/EditAction.java b/src/main/java/org/jabref/gui/edit/EditAction.java index be8472a1585..2c870a821cb 100644 --- a/src/main/java/org/jabref/gui/edit/EditAction.java +++ b/src/main/java/org/jabref/gui/edit/EditAction.java @@ -81,7 +81,7 @@ public void execute() { case COPY -> tabSupplier.get().copy(); case CUT -> tabSupplier.get().cut(); case PASTE -> tabSupplier.get().paste(); - case DELETE_ENTRY -> tabSupplier.get().delete(false); + case DELETE_ENTRY -> tabSupplier.get().delete(StandardActions.DELETE_ENTRY); case UNDO -> { if (undoManager.canUndo()) { undoManager.undo(); diff --git a/src/main/java/org/jabref/gui/importer/ImportCommand.java b/src/main/java/org/jabref/gui/importer/ImportCommand.java index 14159adb9e5..104d4dedec0 100644 --- a/src/main/java/org/jabref/gui/importer/ImportCommand.java +++ b/src/main/java/org/jabref/gui/importer/ImportCommand.java @@ -12,7 +12,6 @@ import javafx.stage.FileChooser; import org.jabref.gui.DialogService; -import org.jabref.gui.LibraryTab; import org.jabref.gui.LibraryTabContainer; import org.jabref.gui.StateManager; import org.jabref.gui.actions.SimpleCommand; @@ -116,9 +115,7 @@ private void importSingleFile(Path file, SortedSet importers, FileChoo }) .executeWith(taskExecutor); } else { - final LibraryTab libraryTab = tabContainer.getCurrentLibraryTab(); - - ImportEntriesDialog dialog = new ImportEntriesDialog(libraryTab.getBibDatabaseContext(), task); + ImportEntriesDialog dialog = new ImportEntriesDialog(tabContainer.getCurrentLibraryTab().getBibDatabaseContext(), task); dialog.setTitle(Localization.lang("Import")); dialogService.showCustomDialogAndWait(dialog); } diff --git a/src/main/java/org/jabref/gui/maintable/MainTable.java b/src/main/java/org/jabref/gui/maintable/MainTable.java index 439146b78a6..21fac134607 100644 --- a/src/main/java/org/jabref/gui/maintable/MainTable.java +++ b/src/main/java/org/jabref/gui/maintable/MainTable.java @@ -255,16 +255,16 @@ public void copy() { if (!selectedEntries.isEmpty()) { try { clipBoardManager.setContent(selectedEntries, entryTypesManager); - dialogService.notify(libraryTab.formatOutputMessage(Localization.lang("Copied"), selectedEntries.size())); + dialogService.notify(Localization.lang("Copied %0 entry(ies)", selectedEntries.size())); } catch (IOException e) { - LOGGER.error("Error while copying selected entries to clipboard", e); + LOGGER.error("Error while copying selected entries to clipboard.", e); } } } public void cut() { copy(); - libraryTab.delete(true); + libraryTab.delete(StandardActions.CUT); } private void setupKeyBindings(KeyBindingRepository keyBindings) { diff --git a/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java b/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java index bc91167bb88..f80a1d84baa 100644 --- a/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java +++ b/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java @@ -29,7 +29,7 @@ public class MainTableDataModel { private final FilteredList entriesFiltered; private final SortedList entriesFilteredAndSorted; - private final ObjectProperty fieldValueFormatter; + private final ObjectProperty fieldValueFormatter = new SimpleObjectProperty<>(); private final GroupsPreferences groupsPreferences; private final NameDisplayPreferences nameDisplayPreferences; private final BibDatabaseContext bibDatabaseContext; @@ -38,8 +38,8 @@ public MainTableDataModel(BibDatabaseContext context, PreferencesService prefere this.groupsPreferences = preferencesService.getGroupsPreferences(); this.nameDisplayPreferences = preferencesService.getNameDisplayPreferences(); this.bibDatabaseContext = context; - this.fieldValueFormatter = new SimpleObjectProperty<>( - new MainTableFieldValueFormatter(nameDisplayPreferences, bibDatabaseContext)); + + resetFieldFormatter(); ObservableList allEntries = BindingsHelper.forUI(context.getDatabase().getEntries()); ObservableList entriesViewModel = EasyBind.mapBacked(allEntries, entry -> @@ -96,7 +96,7 @@ public SortedList getEntriesFilteredAndSorted() { return entriesFilteredAndSorted; } - public void refresh() { + public void resetFieldFormatter() { this.fieldValueFormatter.setValue(new MainTableFieldValueFormatter(nameDisplayPreferences, bibDatabaseContext)); } } diff --git a/src/main/java/org/jabref/gui/preferences/ShowPreferencesAction.java b/src/main/java/org/jabref/gui/preferences/ShowPreferencesAction.java index ca1d59c6f8a..0ba021be858 100644 --- a/src/main/java/org/jabref/gui/preferences/ShowPreferencesAction.java +++ b/src/main/java/org/jabref/gui/preferences/ShowPreferencesAction.java @@ -23,7 +23,7 @@ public ShowPreferencesAction(LibraryTabContainer tabContainer, Class fileNamesWithUniqueSuffix = preferences.getGuiPreferences().getLastFilesOpened().stream() - .map(java.nio.file.Path::of) .map(p -> p.getFileName() + "-" + BackupFileUtil.getUniqueFilePrefix(p)) .toList(); return new Gson().toJson(fileNamesWithUniqueSuffix); diff --git a/src/main/java/org/jabref/http/server/LibraryResource.java b/src/main/java/org/jabref/http/server/LibraryResource.java index 2ff4b970723..d8f80e6e253 100644 --- a/src/main/java/org/jabref/http/server/LibraryResource.java +++ b/src/main/java/org/jabref/http/server/LibraryResource.java @@ -90,7 +90,6 @@ public Response getBibtex(@PathParam("id") String id) { private java.nio.file.Path getLibraryPath(String id) { return preferences.getGuiPreferences().getLastFilesOpened() .stream() - .map(java.nio.file.Path::of) .filter(p -> (p.getFileName() + "-" + BackupFileUtil.getUniqueFilePrefix(p)).equals(id)) .findAny() .orElseThrow(NotFoundException::new); diff --git a/src/main/java/org/jabref/http/server/Server.java b/src/main/java/org/jabref/http/server/Server.java index e238dceb577..a734031c585 100644 --- a/src/main/java/org/jabref/http/server/Server.java +++ b/src/main/java/org/jabref/http/server/Server.java @@ -5,7 +5,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; -import java.util.Collections; import java.util.List; import javax.net.ssl.SSLContext; @@ -37,26 +36,24 @@ public static void main(final String[] args) throws InterruptedException, URISyn SLF4JBridgeHandler.removeHandlersForRootLogger(); SLF4JBridgeHandler.install(); - final ObservableList lastFilesOpened = JabRefPreferences.getInstance().getGuiPreferences().getLastFilesOpened(); + final ObservableList lastFilesOpened = JabRefPreferences.getInstance().getGuiPreferences().getLastFilesOpened(); // The server serves the last opened files (see org.jabref.http.server.LibraryResource.getLibraryPath) // In a testing environment, this might be difficult to handle // This is a quick solution. The architectural fine solution would use some http context or other @Inject_ed variables in org.jabref.http.server.LibraryResource if (args.length > 0) { LOGGER.debug("Command line parameters passed"); - List filesToAdd = Arrays.stream(args) - .map(Path::of) - .filter(Files::exists) - .map(Path::toString) - .filter(path -> !lastFilesOpened.contains(path)) - .toList(); + List filesToAdd = Arrays.stream(args) + .map(Path::of) + .filter(Files::exists) + .filter(path -> !lastFilesOpened.contains(path)) + .toList(); LOGGER.debug("Adding following files to the list of opened libraries: {}", filesToAdd); // add the files in the front of the last opened libraries - Collections.reverse(filesToAdd); - for (String path : filesToAdd) { - lastFilesOpened.add(0, path); + for (Path path : filesToAdd.reversed()) { + lastFilesOpened.addFirst(path); } } @@ -66,7 +63,7 @@ public static void main(final String[] args) throws InterruptedException, URISyn // Path bibPath = Path.of(Server.class.getResource("http-server-demo.bib").toURI()); Path bibPath = Path.of("src/main/resources/org/jabref/http/server/http-server-demo.bib").toAbsolutePath(); LOGGER.debug("Location of demo library: {}", bibPath); - lastFilesOpened.add(bibPath.toString()); + lastFilesOpened.add(bibPath); } LOGGER.debug("Libraries served: {}", lastFilesOpened); diff --git a/src/main/java/org/jabref/preferences/GuiPreferences.java b/src/main/java/org/jabref/preferences/GuiPreferences.java index 479889efdc0..55c7ad95732 100644 --- a/src/main/java/org/jabref/preferences/GuiPreferences.java +++ b/src/main/java/org/jabref/preferences/GuiPreferences.java @@ -27,7 +27,7 @@ public class GuiPreferences { private final BooleanProperty windowFullScreen; // the last libraries that were open when jabref closes and should be reopened on startup - private final ObservableList lastFilesOpened; + private final ObservableList lastFilesOpened; private final ObjectProperty lastFocusedFile; // observable list last files opened in the file menu private final FileHistory fileHistory; @@ -46,7 +46,7 @@ public GuiPreferences(double positionX, double sizeY, boolean windowMaximised, boolean windowFullScreen, - List lastFilesOpened, + List lastFilesOpened, Path lastFocusedFile, FileHistory fileHistory, String lastSelectedIdBasedFetcher, @@ -147,11 +147,11 @@ public boolean isWindowFullscreen() { return windowFullScreen.get(); } - public ObservableList getLastFilesOpened() { + public ObservableList getLastFilesOpened() { return lastFilesOpened; } - public void setLastFilesOpened(List files) { + public void setLastFilesOpened(List files) { lastFilesOpened.setAll(files); } diff --git a/src/main/java/org/jabref/preferences/JabRefPreferences.java b/src/main/java/org/jabref/preferences/JabRefPreferences.java index 30670250f62..35fff0a5713 100644 --- a/src/main/java/org/jabref/preferences/JabRefPreferences.java +++ b/src/main/java/org/jabref/preferences/JabRefPreferences.java @@ -2588,7 +2588,9 @@ public GuiPreferences getGuiPreferences() { getDouble(SIZE_Y), getBoolean(WINDOW_MAXIMISED), getBoolean(WINDOW_FULLSCREEN), - getStringList(LAST_EDITED), + getStringList(LAST_EDITED).stream() + .map(Path::of) + .collect(Collectors.toList()), Path.of(get(LAST_FOCUSED)), getFileHistory(), get(ID_ENTRY_GENERATOR), @@ -2605,11 +2607,14 @@ public GuiPreferences getGuiPreferences() { EasyBind.listen(guiPreferences.sizeYProperty(), (obs, oldValue, newValue) -> putDouble(SIZE_Y, newValue.doubleValue())); EasyBind.listen(guiPreferences.windowMaximisedProperty(), (obs, oldValue, newValue) -> putBoolean(WINDOW_MAXIMISED, newValue)); EasyBind.listen(guiPreferences.windowFullScreenProperty(), (obs, oldValue, newValue) -> putBoolean(WINDOW_FULLSCREEN, newValue)); - guiPreferences.getLastFilesOpened().addListener((ListChangeListener) change -> { + guiPreferences.getLastFilesOpened().addListener((ListChangeListener) change -> { if (change.getList().isEmpty()) { prefs.remove(LAST_EDITED); } else { - putStringList(LAST_EDITED, guiPreferences.getLastFilesOpened()); + putStringList(LAST_EDITED, guiPreferences.getLastFilesOpened().stream() + .map(Path::toAbsolutePath) + .map(Path::toString) + .collect(Collectors.toList())); } }); EasyBind.listen(guiPreferences.lastFocusedFileProperty(), (obs, oldValue, newValue) -> { diff --git a/src/main/resources/csl-styles b/src/main/resources/csl-styles index eedecef83f6..9c43a7dacc1 160000 --- a/src/main/resources/csl-styles +++ b/src/main/resources/csl-styles @@ -1 +1 @@ -Subproject commit eedecef83f6a02dfc2068fc382b561045653b4b2 +Subproject commit 9c43a7dacc170d7f0225b53603e5dbc1666aaeb0 diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties index 3e766da2048..d1d613fd39c 100644 --- a/src/main/resources/l10n/JabRef_en.properties +++ b/src/main/resources/l10n/JabRef_en.properties @@ -164,9 +164,6 @@ Contained\ in=Contained in Content=Content -Copied=Copied - - Copy=Copy Copy\ title=Copy title @@ -235,8 +232,6 @@ Delete\ entry=Delete entry Delete\ multiple\ entries=Delete multiple entries -Deleted=Deleted - Permanently\ delete\ local\ file=Permanently delete local file Descending=Descending @@ -2623,6 +2618,9 @@ Pushing\ citations\ to\ TeXShop\ is\ only\ possible\ on\ macOS\!=Pushing citatio Single\ instance=Single instance Enforce\ single\ JabRef\ instance\ (and\ allow\ remote\ operations)\ using\ port=Enforce single JabRef instance (and allow remote operations) using port +Copied\ %0\ entry(ies)=Copied %0 entry(ies) +Cut\ %0\ entry(ies)=Cut %0 entry(ies) +Deleted\ %0\ entry(ies)=Deleted %0 entry(ies) Enable\ Journal\ Information\ Fetching?=Enable Journal Information Fetching? Would\ you\ like\ to\ enable\ fetching\ of\ journal\ information?\ This\ can\ be\ changed\ later\ in\ %0\ >\ %1.=Would you like to enable fetching of journal information? This can be changed later in %0 > %1. diff --git a/src/test/java/org/jabref/gui/autosaveandbackup/BackupManagerTest.java b/src/test/java/org/jabref/gui/autosaveandbackup/BackupManagerTest.java index c1a0caf267d..349bfd2f5c3 100644 --- a/src/test/java/org/jabref/gui/autosaveandbackup/BackupManagerTest.java +++ b/src/test/java/org/jabref/gui/autosaveandbackup/BackupManagerTest.java @@ -141,6 +141,7 @@ public void shouldNotCreateABackup(@TempDir Path customDir) throws Exception { var filePreferences = mock(FilePreferences.class); when(preferences.getFilePreferences()).thenReturn(filePreferences); when(filePreferences.getBackupDirectory()).thenReturn(backupDir); + when(filePreferences.shouldCreateBackup()).thenReturn(false); BackupManager manager = BackupManager.start( mock(LibraryTab.class), @@ -149,7 +150,7 @@ public void shouldNotCreateABackup(@TempDir Path customDir) throws Exception { preferences); manager.listen(new MetaDataChangedEvent(new MetaData())); - BackupManager.shutdown(database, backupDir, false); + BackupManager.shutdown(database, filePreferences.getBackupDirectory(), filePreferences.shouldCreateBackup()); List files = Files.list(backupDir).toList(); assertEquals(Collections.emptyList(), files); diff --git a/src/test/java/org/jabref/http/server/ServerTest.java b/src/test/java/org/jabref/http/server/ServerTest.java index 657a340e7c3..099c9fd88b0 100644 --- a/src/test/java/org/jabref/http/server/ServerTest.java +++ b/src/test/java/org/jabref/http/server/ServerTest.java @@ -67,7 +67,7 @@ protected void setAvailableLibraries(EnumSet files) { when(guiPreferences.getLastFilesOpened()).thenReturn( FXCollections.observableArrayList( files.stream() - .map(file -> file.path.toString()) + .map(file -> file.path) .collect(Collectors.toList()))); } @@ -93,6 +93,6 @@ private static void initializePreferencesService() { guiPreferences = mock(GuiPreferences.class); when(preferencesService.getGuiPreferences()).thenReturn(guiPreferences); - when(guiPreferences.getLastFilesOpened()).thenReturn(FXCollections.observableArrayList(TestBibFile.GENERAL_SERVER_TEST.path.toString())); + when(guiPreferences.getLastFilesOpened()).thenReturn(FXCollections.observableArrayList(TestBibFile.GENERAL_SERVER_TEST.path)); } } From e933df2513a81810c8cae36dab2090b0bebce1ca Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Thu, 11 Jan 2024 13:20:42 +0100 Subject: [PATCH 24/25] Reuse JUnit 5.7's @DisabledIfEnvironmentVariable (#10768) --- .../org/jabref/support/CIServerCondition.java | 44 ------------------- .../jabref/support/DisabledOnCIServer.java | 4 +- 2 files changed, 2 insertions(+), 46 deletions(-) delete mode 100644 src/test/java/org/jabref/support/CIServerCondition.java diff --git a/src/test/java/org/jabref/support/CIServerCondition.java b/src/test/java/org/jabref/support/CIServerCondition.java deleted file mode 100644 index f4829374c25..00000000000 --- a/src/test/java/org/jabref/support/CIServerCondition.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.jabref.support; - -import java.lang.reflect.AnnotatedElement; -import java.util.Optional; - -import org.jabref.model.strings.StringUtil; - -import org.junit.jupiter.api.extension.ConditionEvaluationResult; -import org.junit.jupiter.api.extension.ExecutionCondition; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.platform.commons.support.AnnotationSupport; - -public class CIServerCondition implements ExecutionCondition { - - private static final ConditionEvaluationResult ENABLED = ConditionEvaluationResult.enabled("not on CI server"); - - private static boolean isCIServer() { - // See http://docs.travis-ci.com/user/environment-variables/#Default-Environment-Variables - // See https://circleci.com/docs/environment-variables - return Boolean.valueOf(System.getenv("CI")); - } - - /** - * Containers and tests are disabled if they are annotated with {@link DisabledOnCIServer} and the tests are run on - * the CI server. - */ - @Override - public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { - if (!isCIServer()) { - return ENABLED; - } - - Optional element = context.getElement(); - Optional disabled = AnnotationSupport.findAnnotation(element, DisabledOnCIServer.class); - if (disabled.isPresent()) { - String reason = disabled.map(DisabledOnCIServer::value) - .filter(StringUtil::isNotBlank) - .orElseGet(() -> element.get() + " is disabled on CI server"); - return ConditionEvaluationResult.disabled(reason); - } - - return ENABLED; - } -} diff --git a/src/test/java/org/jabref/support/DisabledOnCIServer.java b/src/test/java/org/jabref/support/DisabledOnCIServer.java index f70e346d998..63cbef5c130 100644 --- a/src/test/java/org/jabref/support/DisabledOnCIServer.java +++ b/src/test/java/org/jabref/support/DisabledOnCIServer.java @@ -5,11 +5,11 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; @Target(value = {ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) -@ExtendWith(CIServerCondition.class) +@DisabledIfEnvironmentVariable(named = "CI", matches = "true") public @interface DisabledOnCIServer { String value(); } From 5dbc76ec6c09106c408df1187d39467ad2dc5700 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Thu, 11 Jan 2024 22:25:56 +0100 Subject: [PATCH 25/25] Remove remainging "testLogging" (#10769) --- build.gradle | 7 ------- 1 file changed, 7 deletions(-) diff --git a/build.gradle b/build.gradle index a0d3bb83bce..0ccbd954707 100644 --- a/build.gradle +++ b/build.gradle @@ -433,13 +433,6 @@ test { // TODO: Remove this as soon as ArchUnit is modularized runOnClasspath = true } - - testLogging { - // set options for log level LIFECYCLE - // for debugging tests: add "STANDARD_OUT", "STANDARD_ERROR" - events = ["FAILED"] - exceptionFormat "full" - } } testlogger {