Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
37ab8bc
including SearchRxiv integration for SLR and enhancements to citatio…
LoayTarek5 Apr 11, 2026
9919a8a
Add share functionality for SearchRxiv
LoayTarek5 Apr 11, 2026
a27d4ac
introduce buildStudy method for creating Study instances and enhance …
LoayTarek5 Apr 11, 2026
9f47f53
adding a button for exporting search queries to SearchRxiv and improv…
LoayTarek5 Apr 11, 2026
0f32f15
Implement SearchRxivExporter for exporting study search queries in JS…
LoayTarek5 Apr 11, 2026
9a95c12
adding export options for search queries in JSON format
LoayTarek5 Apr 11, 2026
ec9f863
Add unit tests for SearchRxivExporter, verifying file creation and JS…
LoayTarek5 Apr 11, 2026
45303dd
Revert unrelated changes, keep only SearchRxiv integration files
LoayTarek5 Apr 13, 2026
eea0bb9
Sync remaining unrelated files with upstream main
LoayTarek5 Apr 13, 2026
673dbd6
Merge branch 'main' into integrate-with-SearchRxiv-12618
LoayTarek5 Apr 13, 2026
2b52e3b
Merge branch 'main' into integrate-with-SearchRxiv-12618
LoayTarek5 Apr 13, 2026
be5620e
Fix localization
LoayTarek5 Apr 13, 2026
21f89d9
Merge branch 'integrate-with-SearchRxiv-12618' of github.com:LoayTare…
LoayTarek5 Apr 13, 2026
dcf4774
Fix localization: match tooltip key with FXML
LoayTarek5 Apr 14, 2026
af6d2eb
Fix localization: add missing key and match tooltip with FXML
LoayTarek5 Apr 14, 2026
303fc5a
Merge branch 'main' into integrate-with-SearchRxiv-12618
LoayTarek5 Apr 17, 2026
8e1cf45
Fix CHANGELOG, restore some missing
LoayTarek5 Apr 17, 2026
b1da1e9
Extract noCatalogEnabled to ActionHelper
LoayTarek5 Apr 17, 2026
8f8625a
Merge branch 'integrate-with-SearchRxiv-12618' of github.com:LoayTare…
LoayTarek5 Apr 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
- We added "All" option to the citation fetcher combo box, which queries all providers (CrossRef, OpenAlex, OpenCitations, SemanticScholar) and merges the results into a single deduplicated list.
- We added a quick setting toggle to enable cover images download. [#15322](https://github.com/JabRef/jabref/pull/15322)
- We now support refreshing existing CSL citations with respect to their in-text nature in the LibreOffice integration. [#15369](https://github.com/JabRef/jabref/pull/15369)
- We added SearchRxiv integration to the SLR feature. [#12618](https://github.com/JabRef/jabref/issues/12618)
- Added context menu entry "Sort tabs alphabetically" to the library tabs. [#15425](https://github.com/JabRef/jabref/pull/15425)
- We added a "Merge" action in the File menu to compare the current library with a selected BibTeX file and review changes. [#15401](https://github.com/JabRef/jabref/issues/15401)
- We added integrity checks that warn when the `booktitle` field contains a year, a country/location, or page numbers that should live in dedicated fields. [#12271](https://github.com/JabRef/jabref/issues/12271)
Expand Down
8 changes: 8 additions & 0 deletions jabgui/src/main/java/org/jabref/gui/actions/ActionHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import javafx.collections.ObservableList;

import org.jabref.gui.StateManager;
import org.jabref.gui.slr.StudyCatalogItem;
import org.jabref.logic.git.util.GitHandlerRegistry;
import org.jabref.logic.preferences.CliPreferences;
import org.jabref.logic.shared.DatabaseLocation;
Expand Down Expand Up @@ -143,4 +144,11 @@ public static BooleanExpression hasLinkedFileForSelectedEntries(StateManager sta
return BooleanExpression.booleanExpression(EasyBind.reduce(stateManager.getSelectedEntries(),
entries -> entries.anyMatch(entry -> !entry.getFiles().isEmpty())));
}

public static BooleanExpression noCatalogEnabled(ObservableList<StudyCatalogItem> catalogs) {
return BooleanExpression.booleanExpression(
Bindings.createBooleanBinding(
() -> catalogs.stream().noneMatch(StudyCatalogItem::isEnabled),
catalogs));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import javafx.scene.input.MouseButton;

import org.jabref.gui.DialogService;
import org.jabref.gui.actions.ActionHelper;
import org.jabref.gui.icon.IconTheme;
import org.jabref.gui.preferences.GuiPreferences;
import org.jabref.gui.util.BaseDialog;
Expand All @@ -36,16 +37,21 @@

import com.airhacks.afterburner.views.ViewLoader;
import jakarta.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/// This class controls the user interface of the study definition management dialog. The UI elements and their layout
/// are defined in the FXML file.
public class ManageStudyDefinitionView extends BaseDialog<SlrStudyAndDirectory> {
private static final Logger LOGGER = LoggerFactory.getLogger(ManageStudyDefinitionView.class);

@FXML private TextField studyTitle;
@FXML private TextField addAuthor;
@FXML private TextField addResearchQuestion;
@FXML private TextField addQuery;
@FXML private TextField studyDirectory;
@FXML private Button selectStudyDirectory;
@FXML private Button shareOnSearchRxivButton;

@FXML private ButtonType saveSurveyButtonType;
@FXML private Label helpIcon;
Expand Down Expand Up @@ -177,6 +183,11 @@ private void initialize() {
initQueriesTab();
initCatalogsTab();
initValidationBindings();
shareOnSearchRxivButton.disableProperty().bind(
Bindings.or(
Bindings.isEmpty(viewModel.getQueries()),
ActionHelper.noCatalogEnabled(viewModel.getCatalogs())
));
Comment thread
InAnYan marked this conversation as resolved.
}

private void updateDirectoryWarning(Path directory) {
Expand Down Expand Up @@ -295,6 +306,11 @@ private void setupCommonPropertiesForTables(Node addControl,
actionColumn.setResizable(false);
}

@FXML
private void shareOnSearchRxiv() {
viewModel.shareOnSearchRxiv(pathToStudyDataDirectory);
}

private void setupCellFactories(TableColumn<String, String> contentColumn,
TableColumn<String, String> actionColumn,
Consumer<String> removeAction) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@

import org.jabref.gui.DialogService;
import org.jabref.gui.WorkspacePreferences;
import org.jabref.gui.util.DirectoryDialogConfiguration;
import org.jabref.logic.crawler.StudyRepository;
import org.jabref.logic.crawler.StudyYamlParser;
import org.jabref.logic.exporter.SearchRxivExporter;
import org.jabref.logic.git.GitHandler;
import org.jabref.logic.git.preferences.GitPreferences;
import org.jabref.logic.importer.ImportFormatPreferences;
Expand Down Expand Up @@ -55,8 +57,10 @@ public class ManageStudyDefinitionViewModel {
private final ObservableList<String> authors = FXCollections.observableArrayList();
private final ObservableList<String> researchQuestions = FXCollections.observableArrayList();
private final ObservableList<String> queries = FXCollections.observableArrayList();
private final ObservableList<StudyCatalogItem> databases = FXCollections.observableArrayList();

// Observe changes to each item's enabledProperty so bindings re-evaluate when catalogs are toggled
private final ObservableList<StudyCatalogItem> databases = FXCollections.observableArrayList(
Comment thread
InAnYan marked this conversation as resolved.
item -> new javafx.beans.Observable[] {item.enabledProperty()}
);
// Hold the complement of databases for the selector
private final SimpleStringProperty directory = new SimpleStringProperty();

Expand Down Expand Up @@ -218,12 +222,7 @@ public void addQuery(String query) {
}

public SlrStudyAndDirectory saveStudy() {
Study study = new Study(
authors,
title.getValueSafe(),
researchQuestions,
queries.stream().map(StudyQuery::new).collect(Collectors.toList()),
databases.stream().map(studyDatabaseItem -> new StudyDatabase(studyDatabaseItem.getName(), studyDatabaseItem.isEnabled())).filter(StudyDatabase::isEnabled).collect(Collectors.toList()));
Study study = buildStudy();
Path studyDirectory;
final String studyDirectoryAsString = directory.getValueSafe();
try {
Expand Down Expand Up @@ -257,6 +256,19 @@ public SlrStudyAndDirectory saveStudy() {
return new SlrStudyAndDirectory(study, studyDirectory);
}

/// Builds a {@link Study} from the current UI state without persisting it.
public Study buildStudy() {
return new Study(
authors,
title.getValueSafe(),
researchQuestions,
queries.stream().map(StudyQuery::new).collect(Collectors.toList()),
databases.stream()
.map(item -> new StudyDatabase(item.getName(), item.isEnabled()))
.filter(StudyDatabase::isEnabled)
.collect(Collectors.toList()));
}

public Property<String> titleProperty() {
return title;
}
Expand Down Expand Up @@ -316,4 +328,26 @@ public StringProperty queriesValidationMessageProperty() {
public StringProperty catalogsValidationMessageProperty() {
return catalogsValidationMessage;
}

public void shareOnSearchRxiv(Path initialDirectory) {
Study study = buildStudy();
if (study.getDatabases().isEmpty()) {
dialogService.notify(Localization.lang("Please select at least one catalog."));
return;
}

DirectoryDialogConfiguration config = new DirectoryDialogConfiguration.Builder()
.withInitialDirectory(initialDirectory)
.build();

dialogService.showDirectorySelectionDialog(config).ifPresent(exportDirectory -> {
try {
new SearchRxivExporter().export(study, exportDirectory);
dialogService.notify(Localization.lang("Exported search queries for SearchRxiv."));
} catch (IOException e) {
LOGGER.error("Could not export search queries for SearchRxiv", e);
dialogService.notify(Localization.lang("Could not export search queries."));
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -270,15 +270,40 @@
</graphic>
</Button>
</HBox>
<Label fx:id="directoryWarning" text="%Warning: The selected directory is not empty." visible="false" styleClass="warning-message" />
<VBox spacing="5.0" fx:id="validationContainer">
<Label fx:id="validationHeaderLabel" text="%In order to proceed:" style="-fx-text-fill: -jr-error; -fx-font-weight: bold" visible="false" managed="false" />
<Button fx:id="shareOnSearchRxivButton"
onAction="#shareOnSearchRxiv"
text="%Export search queries (search-query format)">
<tooltip>
<Tooltip text="%Export search queries as search-query JSON files"/>
</tooltip>
</Button>
<Label fx:id="directoryWarning"
text="%Warning: The selected directory is not empty."
visible="false"
styleClass="warning-message"/>
<VBox spacing="5.0"
fx:id="validationContainer">
<Label fx:id="validationHeaderLabel"
text="%In order to proceed:"
style="-fx-text-fill: -jr-error; -fx-font-weight: bold"
visible="false"
managed="false"/>
Comment thread
InAnYan marked this conversation as resolved.
<VBox spacing="3.0">
<Label fx:id="titleValidationLabel" visible="false" managed="false" />
<Label fx:id="authorsValidationLabel" visible="false" managed="false" />
<Label fx:id="questionsValidationLabel" visible="false" managed="false" />
<Label fx:id="queriesValidationLabel" visible="false" managed="false" />
<Label fx:id="catalogsValidationLabel" visible="false" managed="false" />
<Label fx:id="titleValidationLabel"
visible="false"
managed="false"/>
<Label fx:id="authorsValidationLabel"
visible="false"
managed="false"/>
<Label fx:id="questionsValidationLabel"
visible="false"
managed="false"/>
<Label fx:id="queriesValidationLabel"
visible="false"
managed="false"/>
<Label fx:id="catalogsValidationLabel"
visible="false"
managed="false"/>
</VBox>
</VBox>
</VBox>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package org.jabref.logic.exporter;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.jabref.model.study.Study;
import org.jabref.model.study.StudyDatabase;
import org.jabref.model.study.StudyQuery;

import tools.jackson.databind.ObjectMapper;

/// Exports study search queries in the SearchRxiv / search-query JSON format.
/// One file is created per query and database combination,
/// as the format uses a single platform string per record.
///
/// The JSON format follows the search-query library's SearchFile specification.
///
/// @see <a href="https://github.com/CoLRev-Environment/search-query">search-query library</a>
/// @see <a href="https://colrev-environment.github.io/search-query/">search-query format documentation</a>
/// @see <a href="https://www.cabidigitallibrary.org/journal/searchrxiv">SearchRxiv</a>
public class SearchRxivExporter {

public static final String SEARCHRXIV_URL = "https://www.cabidigitallibrary.org/journal/searchrxiv";

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

/// Exports the study's search queries as JSON files to the given directory.
/// One file per query and database combination is created.
///
/// @param study The study containing queries, databases, and authors
/// @param directory The target directory to write the JSON files into
/// @throws IOException if a file cannot be written
public void export(Study study, Path directory) throws IOException {
int index = 0;
for (StudyQuery studyQuery : study.getQueries()) {
for (StudyDatabase database : study.getDatabases()) {
Path file = directory.resolve(buildFileName(studyQuery.getQuery(), database.getName(), index));
Files.writeString(file, buildJson(study, studyQuery.getQuery(), database.getName()));
index++;
}
}
}

/// Builds a JSON string following the search-query format.
/// Format spec: <a href="https://github.com/CoLRev-Environment/search-query">search-query</a>
private String buildJson(Study study, String query, String platform) throws IOException {
Map<String, Object> data = new LinkedHashMap<>();
data.put("search_string", query);
data.put("platform", platform.toLowerCase());
data.put("authors", study.getAuthors().stream()
.map(author -> Map.of("name", author))
.toList());
// Placeholder fields — to be filled by the user on SearchRxiv
data.put("record_info", Map.of());
data.put("date", Map.of());
data.put("database", List.of());
return OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(data);
}

private String buildFileName(String query, String databaseName, int index) {
String cleanDb = databaseName.replaceAll("[^A-Za-z0-9]", "_");
String cleanQuery = query.replaceAll("[^A-Za-z0-9]", "_");
if (cleanQuery.isEmpty()) {
cleanQuery = "query";
}
if (cleanQuery.length() > 20) {
cleanQuery = cleanQuery.substring(0, 20);
}
return cleanDb + "-" + cleanQuery + "-" + index + ".json";
}
}
5 changes: 5 additions & 0 deletions jablib/src/main/resources/l10n/JabRef_en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,11 @@ Error\ during\ persistence\ of\ crawling\ results.=Error during persistence of c
'%0'\ exists.\ Overwrite\ file?='%0' exists. Overwrite file?
Export=Export
Export\ to\ clipboard=Export to clipboard
Export\ search\ queries\ (search-query\ format)=Export search queries (search-query format)
Export\ search\ queries\ as\ search-query\ JSON\ files=Export search queries as search-query JSON files
Exported\ search\ queries\ for\ SearchRxiv.=Exported search queries for SearchRxiv.
Could\ not\ export\ search\ queries.=Could not export search queries.
Please\ select\ at\ least\ one\ catalog.=Please select at least one catalog.

Extract\ related\ work\ comments=Extract related work comments
Extract\ references\ from\ file\ (offline)=Extract references from file (offline)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package org.jabref.logic.exporter;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

import org.jabref.model.study.Study;
import org.jabref.model.study.StudyDatabase;
import org.jabref.model.study.StudyQuery;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

class SearchRxivExporterTest {

private final SearchRxivExporter exporter = new SearchRxivExporter();

private Study buildStudy() {
return new Study(
List.of("John Doe"),
"Test Study",
List.of("What is AI?"),
List.of(new StudyQuery("artificial intelligence AND health")),
List.of(new StudyDatabase("IEEE", true)));
}

@Test
void exportCreatesOneFilePerQueryAndDatabase(@TempDir Path tempDir) throws Exception {
exporter.export(buildStudy(), tempDir);

try (var files = Files.list(tempDir)) {
assertEquals(1, files.count());
}
}

@Test
void exportedJsonContainsCorrectFields(@TempDir Path tempDir) throws Exception {
exporter.export(buildStudy(), tempDir);

Path file;
try (var files = Files.list(tempDir)) {
file = files.findFirst().orElseThrow();
}
String content = Files.readString(file);
Comment thread
InAnYan marked this conversation as resolved.
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(content);

assertEquals("artificial intelligence AND health", root.get("search_string").asText());
assertEquals("ieee", root.get("platform").asText());
assertEquals("John Doe", root.get("authors").get(0).get("name").asText());
assertTrue(root.has("record_info"));
assertTrue(root.has("date"));
assertFalse(root.has("title"));
assertTrue(root.has("database"));
assertEquals(0, root.get("database").size());
}

@Test
void exportCreatesMultipleFilesForMultipleDatabases(@TempDir Path tempDir) throws Exception {
Study study = new Study(
List.of("John Doe"),
"Test Study",
List.of("What is AI?"),
List.of(new StudyQuery("machine learning")),
List.of(new StudyDatabase("IEEE", true),
new StudyDatabase("ACM", true)));

exporter.export(study, tempDir);

try (var files = Files.list(tempDir)) {
assertEquals(2, files.count());
}
}
}
Loading