Skip to content

Commit db1e651

Browse files
turhantolgaunalcalixtusSiedlerchr
authored
New study.yml format (JabRef#13844)
* Reformatted database variable to catalogue * Renamed StudyDatabase.java to StudyCatalog and updated its usage in other classes * Created an abstract StudyYamlMigrator class as a template for study.yml migrations * Created an implementation of the abstract Migrator class for migrations from the current study.yml version to the new one * Created a StudyMetadata class to handle the metadata part of the study.yml file * Created a new class called StudyYamlService for handling the migration and parsing of study.yml if needed * StudyYamlMigrator no longer uses child classes in constructor in order to avoid deadlock * Ran openrewrite and changed the jetbrains annotations in StudyYamlV1Migrator to JSpecify annotations * Changed StudyYamlParserTest.java to use the new StudyYamlParserTest instead of StudyYamlParser * Changed the usage of the word catalogue to catalog to be consistent with the rest of the codebase * Added functions for adding a metadata field to the study.yml * Added unit tests for MigratorV1, removed the study type field as it was not needed * Changed database variable to catalog and an immutable map is returned from getCatalogSpecific form StudyQuery * Reformatted files * Removed studytype parameter in StudyMetadata * yaml mapper variable in StudyYamlMigrator is now a static field * Changed null handling on some variables to use Optional * Changed the use of StringBuilder to StringJoiner in StudyYamlV1Migrator * Added Optionals to various functions * Refactored the tests for StudyYamlV1Migrator to use Optionals correctly * Refactored the tests for StudyYamlV1Migrator to use Optionals correctly * Refactored the tests for StudyYamlV1Migrator to use Optionals correctly * Refactored StudyQuery to not use collections import * Disabled the requirement for handlers for java8 optionals in StudyYamlParser * Reverted the disabling of the requirement for handlers for optionals in StudyYamlParser and removed Optionals in StudyQuery * Changed the usage of Optionals in Study and readded Optionals on StudyQuery * Added new dependency jackson datatype jdk 8 for serializing Optionals * Refactored Study and StudyQuery to use more Optionals, StudyYamlParser should support proper deserialization of Optionals * Added annotations for Json deserialization to avoid conflicts * getCatalogSpecificOptional function is ignored when deserializing Json * updated StudyQuery equals method for compatibility with testing * Update jablib/src/main/java/org/jabref/logic/crawler/StudyYamlV1Migrator.java Co-authored-by: Carl Christian Snethlage <[email protected]> * Reverted the update to instanceof check because of unsafe type conversion * Added nonnull annotations and changed the metadata field to not use Optional * Added nonnull annotations to getVersion function from StudyYamlV1Migrator * Changed the suppress warning annotation to the whole function in StudyYamlV1Migrator * fix version and remove unused stuff * readd --------- Co-authored-by: Carl Christian Snethlage <[email protected]> Co-authored-by: Christoph <[email protected]>
1 parent 016ea53 commit db1e651

20 files changed

+983
-63
lines changed

jabgui/src/main/java/org/jabref/gui/slr/EditExistingStudyAction.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import org.jabref.gui.actions.ActionHelper;
99
import org.jabref.gui.actions.SimpleCommand;
1010
import org.jabref.logic.crawler.StudyRepository;
11-
import org.jabref.logic.crawler.StudyYamlParser;
11+
import org.jabref.logic.crawler.StudyYamlService;
1212
import org.jabref.logic.l10n.Localization;
1313
import org.jabref.model.database.BibDatabaseContext;
1414
import org.jabref.model.study.Study;
@@ -51,7 +51,7 @@ public void execute() {
5151

5252
Study study;
5353
try {
54-
study = new StudyYamlParser().parseStudyYamlFile(studyDirectory.resolve(StudyRepository.STUDY_DEFINITION_FILE_NAME));
54+
study = new StudyYamlService().parseStudyYamlFile(studyDirectory.resolve(StudyRepository.STUDY_DEFINITION_FILE_NAME));
5555
} catch (IOException e) {
5656
dialogService.showErrorDialogAndWait(Localization.lang("Error opening file"), e);
5757
return;

jabgui/src/main/java/org/jabref/gui/slr/ManageStudyDefinitionViewModel.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import org.jabref.gui.DialogService;
1818
import org.jabref.gui.WorkspacePreferences;
1919
import org.jabref.logic.crawler.StudyRepository;
20-
import org.jabref.logic.crawler.StudyYamlParser;
20+
import org.jabref.logic.crawler.StudyYamlService;
2121
import org.jabref.logic.git.GitHandler;
2222
import org.jabref.logic.importer.ImportFormatPreferences;
2323
import org.jabref.logic.importer.ImporterPreferences;
@@ -30,7 +30,7 @@
3030
import org.jabref.logic.importer.fetcher.SpringerNatureWebFetcher;
3131
import org.jabref.logic.l10n.Localization;
3232
import org.jabref.model.study.Study;
33-
import org.jabref.model.study.StudyDatabase;
33+
import org.jabref.model.study.StudyCatalog;
3434
import org.jabref.model.study.StudyQuery;
3535

3636
import org.eclipse.jgit.api.errors.GitAPIException;
@@ -103,15 +103,15 @@ public ManageStudyDefinitionViewModel(@NonNull Study study,
103103
title.setValue(study.getTitle());
104104
researchQuestions.addAll(study.getResearchQuestions());
105105
queries.addAll(study.getQueries().stream().map(StudyQuery::getQuery).toList());
106-
List<StudyDatabase> studyDatabases = study.getDatabases();
106+
List<StudyCatalog> studyCatalogs = study.getCatalogs();
107107
databases.addAll(WebFetchers.getSearchBasedFetchers(importFormatPreferences, importerPreferences)
108108
.stream()
109109
.map(SearchBasedFetcher::getName)
110110
// The user wants to select specific fetchers
111111
// The fetcher summarizing ALL fetchers can be emulated by selecting ALL fetchers (which happens rarely when doing an SLR)
112112
.filter(name -> !CompositeSearchBasedFetcher.FETCHER_NAME.equals(name))
113113
.map(name -> {
114-
boolean enabled = studyDatabases.contains(new StudyDatabase(name, true));
114+
boolean enabled = studyCatalogs.contains(new StudyCatalog(name, true));
115115
return new StudyCatalogItem(name, enabled);
116116
})
117117
.toList());
@@ -172,7 +172,7 @@ public SlrStudyAndDirectory saveStudy() {
172172
title.getValueSafe(),
173173
researchQuestions,
174174
queries.stream().map(StudyQuery::new).collect(Collectors.toList()),
175-
databases.stream().map(studyDatabaseItem -> new StudyDatabase(studyDatabaseItem.getName(), studyDatabaseItem.isEnabled())).filter(StudyDatabase::isEnabled).collect(Collectors.toList()));
175+
databases.stream().map(studyDatabaseItem -> new StudyCatalog(studyDatabaseItem.getName(), studyDatabaseItem.isEnabled())).filter(StudyCatalog::isEnabled).collect(Collectors.toList()));
176176
Path studyDirectory;
177177
final String studyDirectoryAsString = directory.getValueSafe();
178178
try {
@@ -185,7 +185,7 @@ public SlrStudyAndDirectory saveStudy() {
185185
}
186186
Path studyDefinitionFile = studyDirectory.resolve(StudyRepository.STUDY_DEFINITION_FILE_NAME);
187187
try {
188-
new StudyYamlParser().writeStudyYamlFile(study, studyDefinitionFile);
188+
new StudyYamlService().writeStudyYamlFile(study, studyDefinitionFile);
189189
} catch (IOException e) {
190190
LOGGER.error("Could not write study file {}", studyDefinitionFile, e);
191191
dialogService.notify(Localization.lang("Please enter a valid file path.") +

jabgui/src/main/java/org/jabref/gui/slr/StartNewStudyAction.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import org.jabref.gui.StateManager;
1111
import org.jabref.gui.importer.actions.OpenDatabaseAction;
1212
import org.jabref.logic.crawler.StudyRepository;
13-
import org.jabref.logic.crawler.StudyYamlParser;
13+
import org.jabref.logic.crawler.StudyYamlService;
1414
import org.jabref.logic.git.GitHandler;
1515
import org.jabref.logic.l10n.Localization;
1616
import org.jabref.logic.preferences.CliPreferences;
@@ -57,8 +57,8 @@ public StartNewStudyAction(LibraryTabContainer tabContainer,
5757

5858
@Override
5959
protected void crawlPreparation(Path studyRepositoryRoot) throws IOException, GitAPIException {
60-
StudyYamlParser studyYAMLParser = new StudyYamlParser();
61-
studyYAMLParser.writeStudyYamlFile(newStudy, studyRepositoryRoot.resolve(StudyRepository.STUDY_DEFINITION_FILE_NAME));
60+
StudyYamlService studyYamlService = new StudyYamlService();
61+
studyYamlService.writeStudyYamlFile(newStudy, studyRepositoryRoot.resolve(StudyRepository.STUDY_DEFINITION_FILE_NAME));
6262

6363
// When execution reaches this point, the user created a new study.
6464
// The GitHandler is already called to initialize the repository with one single commit "Initial commit".

jabgui/src/main/java/org/jabref/gui/slr/StudyCatalogItem.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
import javafx.beans.property.SimpleStringProperty;
88
import javafx.beans.property.StringProperty;
99

10-
import org.jabref.model.study.StudyDatabase;
10+
import org.jabref.model.study.StudyCatalog;
1111

1212
import org.jspecify.annotations.NonNull;
1313

1414
/**
15-
* View representation of {@link StudyDatabase}
15+
* View representation of {@link StudyCatalog}
1616
*/
1717
public class StudyCatalogItem {
1818
private final StringProperty name;

jabgui/src/test/java/org/jabref/gui/slr/ManageStudyDefinitionViewModelTest.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import org.jabref.logic.importer.ImportFormatPreferences;
99
import org.jabref.logic.importer.ImporterPreferences;
1010
import org.jabref.model.study.Study;
11-
import org.jabref.model.study.StudyDatabase;
11+
import org.jabref.model.study.StudyCatalog;
1212

1313
import org.junit.jupiter.api.BeforeEach;
1414
import org.junit.jupiter.api.Test;
@@ -34,7 +34,7 @@ void setUp() {
3434
}
3535

3636
@Test
37-
void emptyStudyConstructorFillsDatabasesCorrectly() {
37+
void emptyStudyConstructorFillsCatalogsCorrectly() {
3838
ManageStudyDefinitionViewModel manageStudyDefinitionViewModel = new ManageStudyDefinitionViewModel(importFormatPreferences, importerPreferences, workspacePreferences, dialogService);
3939
assertEquals(List.of(
4040
new StudyCatalogItem("ACM Portal", true),
@@ -65,7 +65,7 @@ void emptyStudyConstructorFillsDatabasesCorrectly() {
6565
}
6666

6767
@Test
68-
void studyConstructorFillsDatabasesCorrectly(@TempDir Path tempDir) {
68+
void studyConstructorFillsCatalogsCorrectly(@TempDir Path tempDir) {
6969
ManageStudyDefinitionViewModel manageStudyDefinitionViewModel = getManageStudyDefinitionViewModel(tempDir);
7070
assertEquals(List.of(
7171
new StudyCatalogItem("ACM Portal", true),
@@ -96,14 +96,14 @@ void studyConstructorFillsDatabasesCorrectly(@TempDir Path tempDir) {
9696
}
9797

9898
private ManageStudyDefinitionViewModel getManageStudyDefinitionViewModel(Path tempDir) {
99-
List<StudyDatabase> databases = List.of(
100-
new StudyDatabase("ACM Portal", true));
99+
List<StudyCatalog> catalogs = List.of(
100+
new StudyCatalog("ACM Portal", true));
101101
Study study = new Study(
102102
List.of("Name"),
103103
"title",
104104
List.of("Q1"),
105105
List.of(),
106-
databases
106+
catalogs
107107
);
108108
return new ManageStudyDefinitionViewModel(
109109
study,

jablib/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ dependencies {
107107
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
108108
// TODO: Somwewhere we get a warning: unknown enum constant Id.CLASS reason: class file for com.fasterxml.jackson.annotation.JsonTypeInfo$Id not found
109109
// implementation("com.fasterxml.jackson.core:jackson-annotations:2.19.1")
110+
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8")
110111

111112
implementation("com.fasterxml:aalto-xml")
112113

jablib/src/main/java/module-info.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,5 +260,6 @@
260260
requires org.jooq.jool;
261261
requires org.libreoffice.uno;
262262
requires transitive org.jspecify;
263+
requires com.fasterxml.jackson.datatype.jdk8;
263264
// endregion
264265
}

jablib/src/main/java/org/jabref/logic/crawler/StudyCatalogToFetcherConverter.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@
99
import org.jabref.logic.importer.ImporterPreferences;
1010
import org.jabref.logic.importer.SearchBasedFetcher;
1111
import org.jabref.logic.importer.WebFetchers;
12-
import org.jabref.model.study.StudyDatabase;
12+
import org.jabref.model.study.StudyCatalog;
1313

1414
/**
1515
* Converts library entries from the given study into their corresponding fetchers.
1616
*/
1717
class StudyCatalogToFetcherConverter {
18-
private final List<StudyDatabase> libraryEntries;
18+
private final List<StudyCatalog> libraryEntries;
1919
private final ImportFormatPreferences importFormatPreferences;
2020
private final ImporterPreferences importerPreferences;
2121

22-
public StudyCatalogToFetcherConverter(List<StudyDatabase> libraryEntries,
22+
public StudyCatalogToFetcherConverter(List<StudyCatalog> libraryEntries,
2323
ImportFormatPreferences importFormatPreferences,
2424
ImporterPreferences importerPreferences) {
2525
this.libraryEntries = libraryEntries;
@@ -44,7 +44,7 @@ public List<SearchBasedFetcher> getActiveFetchers() {
4444
* @param libraryEntries List of entries
4545
* @return List of fetcher instances
4646
*/
47-
private List<SearchBasedFetcher> getFetchersFromLibraryEntries(List<StudyDatabase> libraryEntries) {
47+
private List<SearchBasedFetcher> getFetchersFromLibraryEntries(List<StudyCatalog> libraryEntries) {
4848
return libraryEntries.parallelStream()
4949
.map(this::createFetcherFromLibraryEntry)
5050
.filter(Objects::nonNull)
@@ -54,12 +54,12 @@ private List<SearchBasedFetcher> getFetchersFromLibraryEntries(List<StudyDatabas
5454
/**
5555
* Transforms a library entry into a SearchBasedFetcher instance. This only works if the library entry specifies a supported fetcher.
5656
*
57-
* @param studyDatabase the entry that will be converted
57+
* @param studyCatalog the entry that will be converted
5858
* @return An instance of the fetcher defined by the library entry.
5959
*/
60-
private SearchBasedFetcher createFetcherFromLibraryEntry(StudyDatabase studyDatabase) {
60+
private SearchBasedFetcher createFetcherFromLibraryEntry(StudyCatalog studyCatalog) {
6161
Set<SearchBasedFetcher> searchBasedFetchers = WebFetchers.getSearchBasedFetchers(importFormatPreferences, importerPreferences);
62-
String libraryNameFromFetcher = studyDatabase.getName();
62+
String libraryNameFromFetcher = studyCatalog.getName();
6363
return searchBasedFetchers.stream()
6464
.filter(searchBasedFetcher -> searchBasedFetcher.getName().equalsIgnoreCase(libraryNameFromFetcher))
6565
.findAny()

jablib/src/main/java/org/jabref/logic/crawler/StudyRepository.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
import org.jabref.model.study.FetchResult;
3535
import org.jabref.model.study.QueryResult;
3636
import org.jabref.model.study.Study;
37-
import org.jabref.model.study.StudyDatabase;
37+
import org.jabref.model.study.StudyCatalog;
3838
import org.jabref.model.study.StudyQuery;
3939
import org.jabref.model.util.FileUpdateMonitor;
4040

@@ -116,9 +116,9 @@ public StudyRepository(Path pathToRepository,
116116

117117
gitHandler.checkoutBranch(SEARCH_BRANCH);
118118
// If study definition does not exist on this branch or was changed on work branch, copy it from work
119-
boolean studyDefinitionDoesNotExistOrChanged = !(Files.exists(studyDefinitionFile) && new StudyYamlParser().parseStudyYamlFile(studyDefinitionFile).equals(study));
119+
boolean studyDefinitionDoesNotExistOrChanged = !(Files.exists(studyDefinitionFile) && new StudyYamlService().parseStudyYamlFile(studyDefinitionFile).equals(study));
120120
if (studyDefinitionDoesNotExistOrChanged) {
121-
new StudyYamlParser().writeStudyYamlFile(study, studyDefinitionFile);
121+
new StudyYamlService().writeStudyYamlFile(study, studyDefinitionFile);
122122
}
123123
setUpRepositoryStructureForQueriesAndFetchers();
124124
gitHandler.createCommitOnCurrentBranch(updateRepositoryStructureMessage, false);
@@ -175,7 +175,7 @@ public BibDatabaseContext getStudyResultEntries() throws IOException {
175175
* @throws IOException Problem opening the input stream.
176176
*/
177177
private Study parseStudyFile() throws IOException {
178-
return new StudyYamlParser().parseStudyYamlFile(studyDefinitionFile);
178+
return new StudyYamlService().parseStudyYamlFile(studyDefinitionFile);
179179
}
180180

181181
/**
@@ -196,10 +196,10 @@ public List<String> getSearchQueryStrings() {
196196
* @return List of BibEntries of type Library
197197
* @throws IllegalArgumentException If a transformation from Library entry to LibraryDefinition fails
198198
*/
199-
public List<StudyDatabase> getActiveLibraryEntries() throws IllegalArgumentException {
200-
return study.getDatabases()
199+
public List<StudyCatalog> getActiveLibraryEntries() throws IllegalArgumentException {
200+
return study.getCatalogs()
201201
.parallelStream()
202-
.filter(StudyDatabase::isEnabled)
202+
.filter(StudyCatalog::isEnabled)
203203
.collect(Collectors.toList());
204204
}
205205

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package org.jabref.logic.crawler;
2+
3+
import java.io.IOException;
4+
import java.io.InputStream;
5+
import java.nio.file.Files;
6+
import java.nio.file.Path;
7+
8+
import org.jabref.model.study.Study;
9+
10+
import com.fasterxml.jackson.databind.JsonNode;
11+
import com.fasterxml.jackson.databind.ObjectMapper;
12+
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
13+
import org.slf4j.Logger;
14+
import org.slf4j.LoggerFactory;
15+
16+
/**
17+
* Abstract base class for migrating study.yml files between versions.
18+
* Provides common functionality for version detection
19+
* and handles the migration process
20+
*/
21+
22+
public abstract class StudyYamlMigrator {
23+
protected static final String CURRENT_VERSION = "2.0";
24+
private static final Logger LOGGER = LoggerFactory.getLogger(StudyYamlMigrator.class);
25+
private static final ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory());
26+
27+
public static StudyYamlMigrator getMigratorForVersion(String version) {
28+
return switch (version) {
29+
case "1.0",
30+
"unknown" ->
31+
new StudyYamlV1Migrator();
32+
default -> {
33+
LOGGER.warn("Unknown version {}, using V1 migrator", version);
34+
yield new StudyYamlV1Migrator();
35+
}
36+
};
37+
}
38+
39+
/**
40+
* Main entry point for migration. Detects version and delegates to appropriate migrator.
41+
*/
42+
public static Study migrateStudyYamlFile(Path studyYamlFile) throws IOException {
43+
String version = detectVersion(studyYamlFile);
44+
45+
if (CURRENT_VERSION.equals(version)) {
46+
// Already current version, read the file normally
47+
try (InputStream fileInputStream = Files.newInputStream(studyYamlFile)) {
48+
return YAML_MAPPER.readValue(fileInputStream, Study.class);
49+
}
50+
}
51+
52+
LOGGER.info("Migrating study.yml from version {} to {}", version, CURRENT_VERSION);
53+
54+
StudyYamlMigrator migrator = getMigratorForVersion(version);
55+
Study migratedStudy = migrator.migrate(studyYamlFile);
56+
57+
LOGGER.info("Successfully migrated study.yml to version {}", CURRENT_VERSION);
58+
return migratedStudy;
59+
}
60+
61+
/**
62+
* Template method that subclasses must implement for specific version migration
63+
*/
64+
protected abstract Study migrate(Path studyYamlFile) throws IOException;
65+
66+
/**
67+
* Returns the version this migrator handles
68+
*/
69+
protected abstract String getSourceVersion();
70+
71+
/**
72+
* Returns the target version this migrator produces
73+
*/
74+
protected String getTargetVersion() {
75+
return CURRENT_VERSION;
76+
}
77+
78+
/**
79+
* Detects the version of a study.yml file
80+
*/
81+
private static String detectVersion(Path studyYamlFile) throws IOException {
82+
ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
83+
84+
try (InputStream fileInputStream = Files.newInputStream(studyYamlFile)) {
85+
JsonNode rootNode = yamlMapper.readTree(fileInputStream);
86+
JsonNode versionNode = rootNode.get("version");
87+
88+
if (versionNode != null) {
89+
return versionNode.asText();
90+
}
91+
92+
// If no version field, check for old structure indicators
93+
if (rootNode.has("databases") && !rootNode.has("catalogues")) {
94+
return "1.0"; // Old format
95+
}
96+
97+
return "unknown";
98+
}
99+
}
100+
}

0 commit comments

Comments
 (0)