diff --git a/build.gradle b/build.gradle index c1d53cbba77..919118e8850 100644 --- a/build.gradle +++ b/build.gradle @@ -42,6 +42,7 @@ version = project.findProperty('projVersion') ?: '100.0.0' java { sourceCompatibility = JavaVersion.VERSION_19 targetCompatibility = JavaVersion.VERSION_19 + // Workaround needed for Eclipse, probably because of https://github.com/gradle/gradle/issues/16922 // Should be removed as soon as Gradle 7.0.1 is released ( https://github.com/gradle/gradle/issues/16922#issuecomment-828217060 ) modularity.inferModulePath.set(false) @@ -124,8 +125,6 @@ dependencies { implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0' implementation 'com.h2database:h2-mvstore:2.1.214' - implementation group: 'org.apache.tika', name: 'tika-core', version: '2.7.0' - // required for reading write-protected PDFs - see https://github.com/JabRef/jabref/pull/942#issuecomment-209252635 implementation 'org.bouncycastle:bcprov-jdk18on:1.71.1' @@ -142,9 +141,6 @@ dependencies { implementation group: 'org.eclipse.jgit', name: 'org.eclipse.jgit', version: '6.5.0.202303070854-r' - implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.14.2' - implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.14.2' - implementation group: 'org.mariadb.jdbc', name: 'mariadb-java-client', version: '2.7.7' implementation 'org.postgresql:postgresql:42.5.4' @@ -161,7 +157,7 @@ dependencies { implementation 'jakarta.annotation:jakarta.annotation-api:2.1.1' implementation 'jakarta.inject:jakarta.inject-api:2.0.1' - + implementation 'org.glassfish.jersey.inject:jersey-hk2:3.1.1' implementation 'com.github.JabRef:afterburner.fx:testmoduleinfo-SNAPSHOT' implementation 'org.kordamp.ikonli:ikonli-javafx:12.3.1' @@ -184,9 +180,27 @@ dependencies { implementation 'de.undercouch:citeproc-java:3.0.0-beta.2' - // jakarta.activation is already dependency of glassfish - implementation group: 'jakarta.xml.bind', name: 'jakarta.xml.bind-api', version: '3.0.1' - implementation group: 'org.glassfish.jaxb', name: 'jaxb-runtime', version: '3.0.2' + // Data mapping + implementation 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.0' + // Data mapping provider for JAXB annotated classes (e.g., MedlineImporter) + implementation group: 'org.glassfish.jaxb', name: 'jaxb-runtime', version: '4.0.2' + + // JSON mapping + implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.14.2' + // YML mapping + implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.14.2' + + // JAX-WS + implementation 'jakarta.ws.rs:jakarta.ws.rs-api:3.1.0' + + // http sever + // "Starting a Grizzly server to run a JAX-RS or Jersey application is one of the most lightweight and easy ways how to expose a functional RESTful services application." + implementation ('org.glassfish.jersey.containers:jersey-container-grizzly2-http:3.1.1') { + exclude module: "jakarta.activation" + } + + implementation group: 'org.glassfish.jersey.media', name: 'jersey-media-jaxb', version: '3.1.1' + implementation group: 'org.glassfish.jersey.media', name: 'jersey-media-json-jackson', version: '3.1.1' implementation ('com.github.tomtung:latex2unicode_2.13:0.3.2') { exclude module: 'fastparse_2.13' diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 86078c9afb0..cf1e2682e57 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -46,15 +46,32 @@ with org.jabref.gui.logging.GuiWriter, org.jabref.gui.logging.ApplicationInsightsWriter; - // Preferences and XML requires java.prefs; + + // XML, YAML, JSON + requires jdk.xml.dom; + // Enable JAXB annotations requires jakarta.xml.bind; - // needs to be loaded here as it's otherwise not found at runtime + + // Enable YAML and JSON parsing by Jackson + requires com.fasterxml.jackson.databind; + requires com.fasterxml.jackson.dataformat.yaml; + requires com.fasterxml.jackson.datatype.jsr310; + // Enable JSON mapping at the REST server using Jackson2 + // requires resteasy.jackson2.provider; + // Enable JAXB using the standard implementation by Glassfish requires org.glassfish.jaxb.runtime; - requires jdk.xml.dom; + + requires jersey.common; + requires jersey.server; + requires jersey.media.jaxb; + requires jersey.media.json.jackson; + requires jersey.container.grizzly2.http; + requires jersey.hk2; // Annotations (@PostConstruct) requires jakarta.annotation; + requires jakarta.validation; // Microsoft application insights requires applicationinsights.core; @@ -87,8 +104,7 @@ requires org.apache.commons.lang3; requires org.antlr.antlr4.runtime; requires org.fxmisc.flowless; - requires org.apache.tika.core; - uses org.apache.tika.detect.AutoDetectReader; + requires pdfbox; requires xmpbox; requires com.ibm.icu; @@ -96,6 +112,7 @@ requires flexmark; requires flexmark.util.ast; requires flexmark.util.data; + requires com.h2database.mvstore; // fulltext search @@ -108,14 +125,12 @@ requires org.apache.lucene.analysis.common; requires org.apache.lucene.highlighter; - requires com.fasterxml.jackson.databind; - requires com.fasterxml.jackson.dataformat.yaml; - requires com.fasterxml.jackson.datatype.jsr310; requires net.harawata.appdirs; requires com.sun.jna; requires com.sun.jna.platform; requires org.eclipse.jgit; + requires jakarta.ws.rs; uses org.eclipse.jgit.transport.SshSessionFactory; uses org.eclipse.jgit.lib.GpgSigner; } diff --git a/src/main/java/org/jabref/cli/Launcher.java b/src/main/java/org/jabref/cli/Launcher.java index bc9dd85d26e..81879e0ffbc 100644 --- a/src/main/java/org/jabref/cli/Launcher.java +++ b/src/main/java/org/jabref/cli/Launcher.java @@ -3,6 +3,7 @@ import java.io.File; import java.io.IOException; import java.net.Authenticator; +import java.net.URI; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; @@ -10,6 +11,7 @@ import java.util.Map; import org.jabref.gui.Globals; +import org.jabref.gui.JabRefExecutorService; import org.jabref.gui.MainApplication; import org.jabref.logic.journals.JournalAbbreviationLoader; import org.jabref.logic.l10n.Localization; @@ -21,6 +23,7 @@ import org.jabref.logic.protectedterms.ProtectedTermsLoader; import org.jabref.logic.remote.RemotePreferences; import org.jabref.logic.remote.client.RemoteClient; +import org.jabref.logic.shared.restserver.rest.Root; import org.jabref.logic.util.BuildInfo; import org.jabref.logic.util.OS; import org.jabref.migrations.PreferencesMigrations; @@ -29,6 +32,7 @@ import org.jabref.preferences.JabRefPreferences; import org.jabref.preferences.PreferencesService; +import jakarta.ws.rs.SeBootstrap; import net.harawata.appdirs.AppDirsFactory; import org.apache.commons.cli.ParseException; import org.slf4j.Logger; @@ -43,13 +47,15 @@ * - Start the JavaFX application (if not in cli mode) */ public class Launcher { - private static Logger LOGGER; - private static String[] ARGUMENTS; + // initialized after reading the preferences (which configure log directory, ...) + static Logger LOGGER; public static void main(String[] args) { - ARGUMENTS = args; addLogToDisk(); try { + // we need a copy of the original arguments + String[] arguments = args; + // Init preferences final JabRefPreferences preferences = JabRefPreferences.getInstance(); Globals.prefs = preferences; @@ -75,7 +81,9 @@ public static void main(String[] args) { return; } - MainApplication.main(argumentProcessor.getParserResults(), argumentProcessor.isBlank(), preferences, ARGUMENTS); + JabRefExecutorService.INSTANCE.execute(Launcher::startServer); + + MainApplication.main(argumentProcessor.getParserResults(), argumentProcessor.isBlank(), preferences, arguments); } catch (ParseException e) { LOGGER.error("Problem parsing arguments", e); JabRefCLI.printUsage(preferences); @@ -85,6 +93,28 @@ public static void main(String[] args) { } } + static void startServer() { + SeBootstrap.Configuration.Builder configBuilder = SeBootstrap.Configuration.builder(); + configBuilder.property(SeBootstrap.Configuration.PROTOCOL, "HTTP") + .property(SeBootstrap.Configuration.HOST, "localhost") + .property(SeBootstrap.Configuration.PORT, 2005); + SeBootstrap.start(new Root(), configBuilder.build()).thenAccept(instance -> { + instance.stopOnShutdown(stopResult -> + System.out.printf("JabRef REST server stop result: %s [Native stop result: %s].%n", stopResult, stopResult.unwrap(Object.class))); + URI uri = instance.configuration().baseUri(); + System.out.printf("JabRef REST server %s running at %s [Native handle: %s].%n", instance, uri, instance.unwrap(Object.class)); + }).exceptionally(ex -> { + LOGGER.error("Error starting server", ex); + return null; + }); + + try { + Thread.currentThread().join(); + } catch (InterruptedException e) { + LOGGER.error("could not join on current thread", e); + } + } + /** * This needs to be called as early as possible. After the first log write, it * is not possible to alter diff --git a/src/main/java/org/jabref/logic/layout/format/MarkdownFormatter.java b/src/main/java/org/jabref/logic/layout/format/MarkdownFormatter.java index 39accb95b8c..5aa733c880f 100644 --- a/src/main/java/org/jabref/logic/layout/format/MarkdownFormatter.java +++ b/src/main/java/org/jabref/logic/layout/format/MarkdownFormatter.java @@ -7,7 +7,6 @@ import com.vladsch.flexmark.html.HtmlRenderer; import com.vladsch.flexmark.parser.Parser; import com.vladsch.flexmark.util.ast.Node; -import com.vladsch.flexmark.util.data.MutableDataSet; public class MarkdownFormatter implements LayoutFormatter { @@ -15,9 +14,8 @@ public class MarkdownFormatter implements LayoutFormatter { private final HtmlRenderer renderer; public MarkdownFormatter() { - MutableDataSet options = new MutableDataSet(); - parser = Parser.builder(options).build(); - renderer = HtmlRenderer.builder(options).build(); + parser = Parser.builder().build(); + renderer = HtmlRenderer.builder().build(); } @Override diff --git a/src/main/java/org/jabref/logic/net/URLDownload.java b/src/main/java/org/jabref/logic/net/URLDownload.java index 08cd53b0c28..7a75abd398e 100644 --- a/src/main/java/org/jabref/logic/net/URLDownload.java +++ b/src/main/java/org/jabref/logic/net/URLDownload.java @@ -204,11 +204,11 @@ public String getMimeType() { */ public boolean canBeReached() throws UnirestException { - // Set a custom Apache Client Builder to be able to allow circular redirects, otherwise downloads from springer might not work + // Set a custom Apache Client Builder to be able to allow circular redirects, otherwise downloads from springer might not work Unirest.config().httpClient(new ApacheClient.Builder() - .withRequestConfig((c, r) -> RequestConfig.custom() - .setCircularRedirectsAllowed(true) - .build())); + .withRequestConfig((c, r) -> RequestConfig.custom() + .setCircularRedirectsAllowed(true) + .build())); Unirest.config().setDefaultHeader("User-Agent", USER_AGENT); @@ -391,8 +391,8 @@ public URLConnection openConnection() throws IOException { int status = ((HttpURLConnection) connection).getResponseCode(); if ((status == HttpURLConnection.HTTP_MOVED_TEMP) - || (status == HttpURLConnection.HTTP_MOVED_PERM) - || (status == HttpURLConnection.HTTP_SEE_OTHER)) { + || (status == HttpURLConnection.HTTP_MOVED_PERM) + || (status == HttpURLConnection.HTTP_SEE_OTHER)) { // get redirect url from "location" header field String newUrl = connection.getHeaderField("location"); // open the new connection again diff --git a/src/main/java/org/jabref/logic/shared/restserver/core/properties/ServerPropertyService.java b/src/main/java/org/jabref/logic/shared/restserver/core/properties/ServerPropertyService.java new file mode 100644 index 00000000000..1bb83195a8f --- /dev/null +++ b/src/main/java/org/jabref/logic/shared/restserver/core/properties/ServerPropertyService.java @@ -0,0 +1,49 @@ +package org.jabref.logic.shared.restserver.core.properties; + +import java.nio.file.Path; + +import org.jabref.gui.desktop.JabRefDesktop; +import org.jabref.gui.desktop.os.NativeDesktop; +import org.jabref.model.strings.StringUtil; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ServerPropertyService { + private static final Logger LOGGER = LoggerFactory.getLogger(ServerPropertyService.class); + private static ServerPropertyService instance; + private Path workingDirectory; + + private ServerPropertyService() { + workingDirectory = determineWorkingDirectory(); + } + + public static ServerPropertyService getInstance() { + if (instance == null) { + instance = new ServerPropertyService(); + } + return instance; + } + + /** + * Tries to determine the working directory of the library. + * Uses the first path it finds when resolving in this order: + * 1. Environment variable LIBRARY_WORKSPACE + * 2. Default User home (via {@link NativeDesktop#getDefaultFileChooserDirectory()}) with a new directory for the library + */ + private Path determineWorkingDirectory() { + String libraryWorkspaceEnvironmentVariable = System.getenv("LIBRARY_WORKSPACE"); + if (!StringUtil.isNullOrEmpty(libraryWorkspaceEnvironmentVariable)) { + LOGGER.info("Environment Variable found, using defined directory: {}", libraryWorkspaceEnvironmentVariable); + return Path.of(libraryWorkspaceEnvironmentVariable); + } else { + Path fallbackDirectory = JabRefDesktop.getNativeDesktop().getDefaultFileChooserDirectory().resolve("planqk-library"); + LOGGER.info("Working directory was not found in either the properties or the environment variables, falling back to default location: {}", fallbackDirectory); + return fallbackDirectory; + } + } + + public Path getWorkingDirectory() { + return workingDirectory; + } +} diff --git a/src/main/java/org/jabref/logic/shared/restserver/core/repository/CrawlTask.java b/src/main/java/org/jabref/logic/shared/restserver/core/repository/CrawlTask.java new file mode 100644 index 00000000000..6aafd545b37 --- /dev/null +++ b/src/main/java/org/jabref/logic/shared/restserver/core/repository/CrawlTask.java @@ -0,0 +1,33 @@ +package org.jabref.logic.shared.restserver.core.repository; + +import java.io.IOException; + +import org.jabref.logic.crawler.Crawler; +import org.jabref.logic.exporter.SaveException; + +import org.eclipse.jgit.api.errors.GitAPIException; + +public class CrawlTask implements Runnable { + private final Crawler crawler; + private TaskStatus status; + + public CrawlTask(Crawler crawler) { + this.crawler = crawler; + } + + public TaskStatus getStatus() { + return status; + } + + @Override + public void run() { + status = TaskStatus.RUNNING; + try { + crawler.performCrawl(); + } catch (IOException | GitAPIException | SaveException e) { + status = TaskStatus.FAILED; + throw new RuntimeException(e); + } + status = TaskStatus.DONE; + } +} diff --git a/src/main/java/org/jabref/logic/shared/restserver/core/repository/LibraryService.java b/src/main/java/org/jabref/logic/shared/restserver/core/repository/LibraryService.java new file mode 100644 index 00000000000..f678cf19a1f --- /dev/null +++ b/src/main/java/org/jabref/logic/shared/restserver/core/repository/LibraryService.java @@ -0,0 +1,200 @@ +package org.jabref.logic.shared.restserver.core.repository; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.jabref.gui.Globals; +import org.jabref.logic.database.DatabaseMerger; +import org.jabref.logic.exporter.AtomicFileWriter; +import org.jabref.logic.exporter.BibWriter; +import org.jabref.logic.exporter.BibtexDatabaseWriter; +import org.jabref.logic.exporter.SavePreferences; +import org.jabref.logic.importer.OpenDatabase; +import org.jabref.logic.shared.restserver.rest.model.NewLibraryDTO; +import org.jabref.logic.util.OS; +import org.jabref.model.database.BibDatabase; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.BibEntryTypesManager; +import org.jabref.model.util.DummyFileUpdateMonitor; +import org.jabref.model.util.FileUpdateMonitor; +import org.jabref.preferences.GeneralPreferences; +import org.jabref.preferences.JabRefPreferences; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LibraryService { + + private static final Map INSTANCES = new HashMap<>(); + private static final Logger LOGGER = LoggerFactory.getLogger(LibraryService.class); + private final Path workingDirectory; + + private LibraryService(Path workingDirectory) { + this.workingDirectory = workingDirectory; + if (Files.notExists(workingDirectory)) { + try { + Files.createDirectories(workingDirectory); + } catch (IOException e) { + LOGGER.error("Could not create working directory.", e); + System.exit(1); + } + } + } + + public static LibraryService getInstance(Path workingDirectory) { + return INSTANCES.computeIfAbsent(workingDirectory, LibraryService::new); + } + + public List getLibraryNames() throws IOException { + return Files.list(workingDirectory) // Alternatively walk(Path start, int depth) for recursive aggregation + .filter(file -> !Files.isDirectory(file)) + .map(Path::getFileName) + .map(Path::toString) + .filter(file -> file.endsWith(".bib")) + .collect(Collectors.toList()); + } + + public void createLibrary(NewLibraryDTO newLibraryConfiguration) throws IOException { + Files.createFile(getLibraryPath(newLibraryConfiguration.getLibraryName())); + } + + public Boolean deleteLibrary(String libraryName) throws IOException { + return Files.deleteIfExists(getLibraryPath(libraryName)); + } + + public boolean libraryExists(String libraryName) { + return Files.exists(getLibraryPath(libraryName)); + } + + public List getLibraryEntries(String libraryName) throws IOException { + Path libraryPath = getLibraryPath(libraryName); + if (!Files.exists(libraryPath)) { + throw new FileNotFoundException(); + } + // We do not need any update monitoring + return new ArrayList<>(OpenDatabase.loadDatabase(libraryPath, Globals.prefs.getImportFormatPreferences(), Globals.getFileUpdateMonitor()) + .getDatabase() + .getEntries()); + } + + public Optional getLibraryEntryMatchingCiteKey(String libraryName, String citeKey) throws IOException { + Path libraryPath = getLibraryPath(libraryName); + if (!Files.exists(libraryPath)) { + throw new FileNotFoundException(); + } + + // Note that this might lead to issues if multiple entries have the same cite key! + return OpenDatabase.loadDatabase(libraryPath, Globals.prefs.getImportFormatPreferences(), Globals.getFileUpdateMonitor()) + .getDatabase() + .getEntryByCitationKey(citeKey); + } + + public synchronized void addEntryToLibrary(String libraryName, BibEntry newEntry) throws IOException { + // Enforce that a citation key is provided and that is is not part of the library already. + if (newEntry.getCitationKey().isEmpty()) { + throw new IllegalArgumentException("Entry does not contain a citation key"); + } + Path libraryPath = getLibraryPath(libraryName); + BibDatabaseContext context; + if (!Files.exists(libraryPath)) { + throw new FileNotFoundException(); + } else { + context = OpenDatabase.loadDatabase(libraryPath, Globals.prefs.getImportFormatPreferences(), Globals.getFileUpdateMonitor()) + .getDatabaseContext(); + } + // Required to get serialized + newEntry.setChanged(true); + if (this.citationKeyAlreadyExists(libraryName, newEntry.getCitationKey().get())) { + throw new IllegalArgumentException("Library already contains an entry with that citation key."); + } + context.getDatabase().insertEntry(newEntry); + GeneralPreferences generalPreferences = JabRefPreferences.getInstance().getGeneralPreferences(); + SavePreferences savePreferences = JabRefPreferences.getInstance().getSavePreferences(); + + try (AtomicFileWriter fileWriter = new AtomicFileWriter(libraryPath, context.getMetaData().getEncoding().orElse(StandardCharsets.UTF_8), savePreferences.shouldMakeBackup())) { + BibWriter writer = new BibWriter(fileWriter, OS.NEWLINE); + BibtexDatabaseWriter databaseWriter = new BibtexDatabaseWriter(writer, JabRefPreferences.getInstance().getGeneralPreferences(), savePreferences, new BibEntryTypesManager()); + databaseWriter.saveDatabase(context); + } + } + + public synchronized void updateEntry(String libraryName, String citeKey, BibEntry updatedEntry) throws IOException { + // Enforce that a citation key is provided and that is is not part of the library already. + if (updatedEntry.getCitationKey().isEmpty()) { + throw new IllegalArgumentException("Entry does not contain a citation key"); + } + this.deleteEntryByCiteKey(libraryName, citeKey); + updatedEntry.setChanged(true); + this.addEntryToLibrary(libraryName, updatedEntry); + } + + public synchronized boolean deleteEntryByCiteKey(String libraryName, String citeKey) throws IOException { + Path libraryPath = getLibraryPath(libraryName); + BibDatabaseContext context; + if (!Files.exists(libraryPath)) { + return false; + } else { + context = OpenDatabase.loadDatabase(libraryPath, Globals.prefs.getImportFormatPreferences(), Globals.getFileUpdateMonitor()) + .getDatabaseContext(); + } + Optional entry = context.getDatabase().getEntryByCitationKey(citeKey); + if (entry.isEmpty()) { + return false; + } + + context.getDatabase().removeEntry(entry.get()); + GeneralPreferences generalPreferences = JabRefPreferences.getInstance().getGeneralPreferences(); + SavePreferences savePreferences = JabRefPreferences.getInstance().getSavePreferences(); + + try (AtomicFileWriter fileWriter = new AtomicFileWriter(libraryPath, context.getMetaData().getEncoding().orElse(StandardCharsets.UTF_8), savePreferences.shouldMakeBackup())) { + BibWriter writer = new BibWriter(fileWriter, OS.NEWLINE); + BibtexDatabaseWriter databaseWriter = new BibtexDatabaseWriter(writer, generalPreferences, savePreferences, new BibEntryTypesManager()); + databaseWriter.saveDatabase(context); + return true; + } + } + + public List getAllEntries() throws IOException { + List libraryNames = getLibraryNames(); + BibDatabase result = new BibDatabase(); + DatabaseMerger merger = new DatabaseMerger(JabRefPreferences.getInstance().getBibEntryPreferences().getKeywordSeparator()); + FileUpdateMonitor dummy = new DummyFileUpdateMonitor(); + libraryNames.stream() + .map(this::getLibraryPath) + .map(path -> { + try { + return OpenDatabase.loadDatabase(path, Globals.prefs.getImportFormatPreferences(), Globals.getFileUpdateMonitor()).getDatabase(); + } catch (IOException e) { + // Just return an empty database, a.k.a if opening fails, ignore it + return new BibDatabase(); + } + }) + .forEach(database -> merger.merge(result, database)); + return new ArrayList<>(result.getEntries()); + } + + private boolean citationKeyAlreadyExists(String libraryName, String citationKey) throws IOException { + return this.getLibraryEntryMatchingCiteKey(libraryName, citationKey).isPresent(); + } + + private Path getLibraryPath(String libraryName) { + libraryName = addBibExtensionIfMissing(libraryName); + LOGGER.info("Resolved path: {}", workingDirectory.resolve(libraryName)); + // For now make assumption that the directory is flat: + return workingDirectory.resolve(libraryName); + } + + private String addBibExtensionIfMissing(String libraryName) { + return libraryName.endsWith(".bib") ? libraryName : libraryName + ".bib"; + } +} diff --git a/src/main/java/org/jabref/logic/shared/restserver/core/repository/StudyService.java b/src/main/java/org/jabref/logic/shared/restserver/core/repository/StudyService.java new file mode 100644 index 00000000000..36ec08019b3 --- /dev/null +++ b/src/main/java/org/jabref/logic/shared/restserver/core/repository/StudyService.java @@ -0,0 +1,153 @@ +package org.jabref.logic.shared.restserver.core.repository; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.FileSystemAlreadyExistsException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.jabref.gui.Globals; +import org.jabref.logic.crawler.Crawler; +import org.jabref.logic.crawler.StudyYamlParser; +import org.jabref.logic.git.GitHandler; +import org.jabref.logic.git.SlrGitHandler; +import org.jabref.logic.importer.ParseException; +import org.jabref.model.entry.BibEntryTypesManager; +import org.jabref.model.study.Study; +import org.jabref.model.util.DummyFileUpdateMonitor; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class StudyService { + private static final Map INSTANCES = new HashMap<>(); + private static final Logger LOGGER = LoggerFactory.getLogger(StudyService.class); + // Contains all running tasks, + private final Map runningCrawls = new HashMap<>(); + private final Path studiesDirectory; + + /** + * Returns a study service instance for the selected working directory + * + * @param workingDirectory the path under which the studies can be found in the studies directory + */ + private StudyService(Path workingDirectory) { + this.studiesDirectory = workingDirectory.resolve("studies"); + if (Files.notExists(studiesDirectory)) { + try { + LOGGER.info(studiesDirectory.toString()); + Files.createDirectories(studiesDirectory); + Map env = new HashMap<>(); + env.put("create", "true"); + try { + FileSystems.newFileSystem(GitHandler.class.getResource("git.gitignore").toURI(), env); + } catch (IOException | URISyntaxException e) { + LOGGER.error("Setting up filesystem failed", e); + } catch (FileSystemAlreadyExistsException e) { + LOGGER.info("Filesystem already exists"); + } + } catch (IOException e) { + LOGGER.error("Could not create working directory.", e); + System.exit(1); + } + } + } + + public static synchronized StudyService getInstance(Path workingDirectory) { + return INSTANCES.computeIfAbsent(workingDirectory, StudyService::new); + } + + /** + * Each study is managed as a directory within the study directory + * + * @return A list of the currently existing studies + */ + public List getStudyNames() throws IOException { + return Files.list(studiesDirectory) // Alternatively walk(Path start, int depth) for recursive aggregation + .filter(Files::isDirectory) + .map(Path::getFileName) + .map(Path::toString) + .collect(Collectors.toList()); + } + + public synchronized void createStudy(Study study) throws IOException { + Files.createDirectories(studiesDirectory.resolve(Paths.get(study.getTitle()))); + StudyYamlParser parser = new StudyYamlParser(); + parser.writeStudyYamlFile(study, studiesDirectory.resolve(Paths.get(study.getTitle(), "study.yml"))); + } + + public boolean deleteStudy(String studyName) throws IOException { + Path study = studiesDirectory.resolve(Paths.get(studyName)); + if (Files.notExists(study)) { + return false; + } + + Files.walk(study) + .sorted(Comparator.reverseOrder()) + .forEach(t -> { + try { + Files.delete(t); + } catch (IOException e) { + LOGGER.error("Error deleting dir", e); + } + }); + return true; + } + + public boolean studyExists(String studyName) { + return Files.exists(getStudyPath(studyName)); + } + + /** + * Starts a crawl for a specific study + * + * @throws ParseException Occurs if the study definition file is malformed + */ + public synchronized void startCrawl(String studyName) throws IOException, ParseException { + if (runningCrawls.containsKey(studyName)) { + return; + } + Path studyDirectory = studiesDirectory.resolve(Paths.get(studyName)); + CrawlTask crawl = new CrawlTask(new Crawler(studyDirectory, + new SlrGitHandler(studyDirectory), + Globals.prefs.getGeneralPreferences(), + Globals.prefs.getImportFormatPreferences(), + Globals.prefs.getImporterPreferences(), + Globals.prefs.getSavePreferences(), + new BibEntryTypesManager(), + new DummyFileUpdateMonitor())); + runningCrawls.put(studyName, crawl); + new Thread(crawl).start(); + } + + /** + * Checks whether there is a crawl running for the specified study. + * Removes crawls that are finished or failed. + */ + public Boolean isCrawlRunning(String studyName) { + if (!runningCrawls.containsKey(studyName)) { + return false; + } + if (runningCrawls.get(studyName).getStatus() == TaskStatus.RUNNING) { + return true; + } + runningCrawls.remove(studyName); + return false; + } + + public Path getStudyPath(String studyName) { + return studiesDirectory.resolve(Paths.get(studyName)); + } + + public Study getStudyDefinition(String studyName) throws IOException { + StudyYamlParser parser = new StudyYamlParser(); + return parser.parseStudyYamlFile(studiesDirectory.resolve(Paths.get(studyName, "study.yml"))); + } +} diff --git a/src/main/java/org/jabref/logic/shared/restserver/core/repository/TaskStatus.java b/src/main/java/org/jabref/logic/shared/restserver/core/repository/TaskStatus.java new file mode 100644 index 00000000000..e2fc52f94a1 --- /dev/null +++ b/src/main/java/org/jabref/logic/shared/restserver/core/repository/TaskStatus.java @@ -0,0 +1,7 @@ +package org.jabref.logic.shared.restserver.core.repository; + +public enum TaskStatus { + RUNNING, + DONE, + FAILED +} diff --git a/src/main/java/org/jabref/logic/shared/restserver/core/representation/CSLStyleAdapter.java b/src/main/java/org/jabref/logic/shared/restserver/core/representation/CSLStyleAdapter.java new file mode 100644 index 00000000000..09702de0a5c --- /dev/null +++ b/src/main/java/org/jabref/logic/shared/restserver/core/representation/CSLStyleAdapter.java @@ -0,0 +1,91 @@ +package org.jabref.logic.shared.restserver.core.representation; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.FileSystemAlreadyExistsException; +import java.nio.file.FileSystems; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.jabref.logic.citationstyle.CitationStyle; +import org.jabref.logic.citationstyle.CitationStyleGenerator; +import org.jabref.logic.citationstyle.CitationStyleOutputFormat; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.BibEntryTypesManager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CSLStyleAdapter { + private static final Logger LOGGER = LoggerFactory.getLogger(CSLStyleAdapter.class); + private static final String STYLES_ROOT = "/csl-styles"; + private static CSLStyleAdapter instance; + + private final Map namesToStyles = new HashMap<>(); + + static { + // Setup file system + Map env = new HashMap<>(); + env.put("create", "true"); + try { + FileSystems.newFileSystem(CitationStyle.class.getResource(STYLES_ROOT).toURI(), env); + } catch (IOException | URISyntaxException e) { + LOGGER.error("Setting up filesystem failed", e); + } catch (FileSystemAlreadyExistsException e) { + LOGGER.info("Filesystem already exists"); + } + } + + private CSLStyleAdapter() throws URISyntaxException, IOException { + registerAvailableCitationStyles(); + } + + public static CSLStyleAdapter getInstance() throws URISyntaxException, IOException { + if (instance == null) { + instance = new CSLStyleAdapter(); + } + return instance; + } + + protected static void resetStyles() { + instance = null; + } + + /** + * @param entry Entry to be returned in the expected style + * @param style Expects a style from the list of styles returned by getRegisteredStyles() + * @return A styled Entry in HTML, if the style is not available use default style + */ + public String generateCitation(BibEntry entry, String style) { + return CitationStyleGenerator.generateCitation(entry, namesToStyles.getOrDefault(style, CitationStyle.getDefault()).getSource(), CitationStyleOutputFormat.HTML, new BibDatabaseContext(), new BibEntryTypesManager()); + } + + /** + * @param entry Entry to be returned in the expected style + * @param style Expects a style from the list of styles returned by getRegisteredStyles() + * @return A styled Entry in HTML, if the style is not available use default style + */ + public String generatePlainCitation(BibEntry entry, String style) { + return CitationStyleGenerator.generateCitation(entry, namesToStyles.getOrDefault(style, CitationStyle.getDefault()).getSource(), CitationStyleOutputFormat.TEXT, new BibDatabaseContext(), new BibEntryTypesManager()); + } + + public List getRegisteredStyles() { + return new ArrayList<>(namesToStyles.keySet()); + } + + private void registerAvailableCitationStyles() { + List styles = CitationStyle.discoverCitationStyles(); + namesToStyles.putAll(styles.stream().collect(Collectors.toMap(CitationStyle::getTitle, citationStyle -> citationStyle))); + } + + public CitationStyle registerCitationStyleFromFile(String citationStyleFile) throws IOException { + CitationStyle style = CitationStyle.createCitationStyleFromFile(citationStyleFile).orElseThrow(FileNotFoundException::new); + namesToStyles.putIfAbsent(style.getTitle(), style); + return style; + } +} diff --git a/src/main/java/org/jabref/logic/shared/restserver/core/serialization/BibEntryAdapter.java b/src/main/java/org/jabref/logic/shared/restserver/core/serialization/BibEntryAdapter.java new file mode 100644 index 00000000000..c263bb56dea --- /dev/null +++ b/src/main/java/org/jabref/logic/shared/restserver/core/serialization/BibEntryAdapter.java @@ -0,0 +1,113 @@ +package org.jabref.logic.shared.restserver.core.serialization; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.jabref.logic.TypedBibEntry; +import org.jabref.model.database.BibDatabaseMode; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.IEEEField; +import org.jabref.model.entry.field.InternalField; +import org.jabref.model.entry.field.SpecialField; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.field.UnknownField; +import org.jabref.model.entry.types.StandardEntryType; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +/** + * Used to convert a bibentry object between POJO and JSON + */ +public class BibEntryAdapter extends TypeAdapter { + + private static final String JSON_TYPE = "entrytype"; + private static final String JSON_KEY = "citekey"; + + @Override + public void write(JsonWriter writer, BibEntry entry) throws IOException { + if (entry == null) { + writer.nullValue(); + return; + } + writer.beginObject(); + writer.name(JSON_TYPE).value(new TypedBibEntry(entry, BibDatabaseMode.BIBTEX).getTypeForDisplay()); + writer.name(JSON_KEY).value(entry.getCitationKey().orElse("")); + + // Grab field entries and place in map + Map mapFieldToValue = new HashMap<>(); + // determine sorted fields -- all fields lower case + SortedSet sortedFields = new TreeSet<>(); + for (Map.Entry field : entry.getFieldMap().entrySet()) { + Field fieldName = field.getKey(); + String fieldValue = field.getValue(); + // JabRef stores the key in the field KEY_FIELD, which must not be serialized + if (!fieldName.equals(InternalField.KEY_FIELD)) { + String lowerCaseFieldName = fieldName.getName().toLowerCase(Locale.US); + sortedFields.add(lowerCaseFieldName); + mapFieldToValue.put(lowerCaseFieldName, fieldValue); + } + } + + // Add to writer + for (String fieldName : sortedFields) { + writer.name(fieldName).value(String.valueOf(mapFieldToValue.get(fieldName)).replaceAll("\\r\\n", "\n")); + } + writer.endObject(); + } + + @Override + public BibEntry read(JsonReader in) throws IOException { + // Create new entry + BibEntry deserializedEntry = new BibEntry(); + in.beginObject(); + while (in.hasNext()) { + String field = in.nextName(); + switch (field) { + case "citekey": + deserializedEntry.withCitationKey(in.nextString()); + break; + case "entrytype": + deserializedEntry.setType(StandardEntryType.valueOf(in.nextString())); + break; + default: + // We cannot apply an optional of Field here as e.g. Optional cannot be assigned to Optional + Optional parsedField = Arrays.stream(StandardField.values()) + .filter(e -> e.name().equalsIgnoreCase(field)) + .findAny(); + if (parsedField.isPresent()) { + deserializedEntry.withField(parsedField.get(), in.nextString()); + break; + } + + Optional parsedIEEEField = Arrays.stream(IEEEField.values()) + .filter(e -> e.name().equalsIgnoreCase(field)) + .findAny(); + if (parsedIEEEField.isPresent()) { + deserializedEntry.withField(parsedIEEEField.get(), in.nextString()); + break; + } + Optional parsedSpecialField = Arrays.stream(SpecialField.values()) + .filter(e -> e.name().equalsIgnoreCase(field)) + .findAny(); + if (parsedSpecialField.isPresent()) { + deserializedEntry.withField(parsedSpecialField.get(), in.nextString()); + break; + } + UnknownField unknownField = new UnknownField(field); + deserializedEntry.withField(unknownField, in.nextString()); + } + } + deserializedEntry.setChanged(true); + in.endObject(); + return deserializedEntry; + } +} diff --git a/src/main/java/org/jabref/logic/shared/restserver/core/serialization/BibEntryJacksonDeserializer.java b/src/main/java/org/jabref/logic/shared/restserver/core/serialization/BibEntryJacksonDeserializer.java new file mode 100644 index 00000000000..f913c1343ed --- /dev/null +++ b/src/main/java/org/jabref/logic/shared/restserver/core/serialization/BibEntryJacksonDeserializer.java @@ -0,0 +1,77 @@ +package org.jabref.logic.shared.restserver.core.serialization; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Optional; + +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.IEEEField; +import org.jabref.model.entry.field.SpecialField; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.field.UnknownField; +import org.jabref.model.entry.types.StandardEntryType; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BibEntryJacksonDeserializer extends JsonDeserializer { + private final Logger LOGGER = LoggerFactory.getLogger(BibEntryJacksonDeserializer.class); + + @Override + public BibEntry deserialize(JsonParser in, DeserializationContext ctxt) throws IOException { + BibEntry deserializedEntry = new BibEntry(); + ObjectMapper mapper = new ObjectMapper(); + try { + JsonNode node = mapper.readTree(in); + assert node.isObject(); + for (Iterator it = node.fieldNames(); it.hasNext(); ) { + String field = it.next(); + switch (field) { + case "citekey": + deserializedEntry.withCitationKey(node.get(field).asText()); + break; + case "entrytype": + deserializedEntry.setType(StandardEntryType.valueOf(node.get(field).asText())); + break; + default: + // We cannot apply an optional of Field here as e.g. Optional cannot be assigned to Optional + Optional parsedField = Arrays.stream(StandardField.values()) + .filter(e -> e.name().equalsIgnoreCase(field)) + .findAny(); + if (parsedField.isPresent()) { + deserializedEntry.withField(parsedField.get(), node.get(field).asText()); + break; + } + + Optional parsedIEEEField = Arrays.stream(IEEEField.values()) + .filter(e -> e.name().equalsIgnoreCase(field)) + .findAny(); + if (parsedIEEEField.isPresent()) { + deserializedEntry.withField(parsedIEEEField.get(), node.get(field).asText()); + break; + } + Optional parsedSpecialField = Arrays.stream(SpecialField.values()) + .filter(e -> e.name().equalsIgnoreCase(field)) + .findAny(); + if (parsedSpecialField.isPresent()) { + deserializedEntry.withField(parsedSpecialField.get(), node.get(field).asText()); + break; + } + UnknownField unknownField = new UnknownField(field); + deserializedEntry.withField(unknownField, node.get(field).asText()); + } + } + deserializedEntry.setChanged(true); + return deserializedEntry; + } catch (IOException ex) { + LOGGER.error("Deserialization failed", ex); + throw ex; + } + } +} diff --git a/src/main/java/org/jabref/logic/shared/restserver/core/serialization/BibEntryJacksonSerializer.java b/src/main/java/org/jabref/logic/shared/restserver/core/serialization/BibEntryJacksonSerializer.java new file mode 100644 index 00000000000..642148d0fb7 --- /dev/null +++ b/src/main/java/org/jabref/logic/shared/restserver/core/serialization/BibEntryJacksonSerializer.java @@ -0,0 +1,63 @@ +package org.jabref.logic.shared.restserver.core.serialization; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.jabref.logic.TypedBibEntry; +import org.jabref.model.database.BibDatabaseMode; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.InternalField; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BibEntryJacksonSerializer extends StdSerializer { + + private static final String JSON_TYPE = "entrytype"; + private static final String JSON_KEY = "citekey"; + private final Logger LOGGER = LoggerFactory.getLogger(BibEntryJacksonSerializer.class); + + public BibEntryJacksonSerializer(Class t) { + super(t); + } + + @Override + public void serialize(BibEntry entry, JsonGenerator writer, SerializerProvider provider) throws IOException { + if (entry == null) { + writer.writeNull(); + return; + } + writer.writeStartObject(); + writer.writeStringField(JSON_TYPE, new TypedBibEntry(entry, BibDatabaseMode.BIBTEX).getTypeForDisplay()); + writer.writeStringField(JSON_KEY, entry.getCitationKey().orElse("")); + + // Grab field entries and place in map + Map mapFieldToValue = new HashMap<>(); + // determine sorted fields -- all fields lower case + SortedSet sortedFields = new TreeSet<>(); + for (Map.Entry field : entry.getFieldMap().entrySet()) { + Field fieldName = field.getKey(); + String fieldValue = field.getValue(); + // JabRef stores the key in the field KEY_FIELD, which must not be serialized + if (!fieldName.equals(InternalField.KEY_FIELD)) { + String lowerCaseFieldName = fieldName.getName().toLowerCase(Locale.US); + sortedFields.add(lowerCaseFieldName); + mapFieldToValue.put(lowerCaseFieldName, fieldValue); + } + } + + // Add to writer + for (String fieldName : sortedFields) { + writer.writeStringField(fieldName, String.valueOf(mapFieldToValue.get(fieldName)).replaceAll("\\r\\n", "\n")); + } + writer.writeEndObject(); + } +} diff --git a/src/main/java/org/jabref/logic/shared/restserver/core/serialization/BibEntryMapper.java b/src/main/java/org/jabref/logic/shared/restserver/core/serialization/BibEntryMapper.java new file mode 100644 index 00000000000..8478c2c5022 --- /dev/null +++ b/src/main/java/org/jabref/logic/shared/restserver/core/serialization/BibEntryMapper.java @@ -0,0 +1,139 @@ +package org.jabref.logic.shared.restserver.core.serialization; + +import java.util.Arrays; +import java.util.Optional; + +import org.jabref.logic.shared.restserver.rest.model.BibEntryDTO; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.EntryType; +import org.jabref.model.entry.types.IEEETranEntryType; +import org.jabref.model.entry.types.StandardEntryType; +import org.jabref.model.entry.types.UnknownEntryType; + +public class BibEntryMapper { + + /** + * Maps the relevant standard fields of the complete entry into a DTO + * Note: This step is lossy. The BibEntryDTO contains way less information than the BibEntry. + */ + public static BibEntryDTO map(BibEntry entry) { + BibEntryDTO mappedEntry = new BibEntryDTO(); + mappedEntry.entryType = entry.getType().getName(); + entry.getCitationKey().ifPresent(s -> mappedEntry.citationKey = s); + entry.getField(StandardField.ADDRESS).ifPresent(s -> mappedEntry.address = s); + entry.getField(StandardField.AUTHOR).ifPresent(s -> mappedEntry.author = s); + entry.getField(StandardField.BOOKTITLE).ifPresent(s -> mappedEntry.booktitle = s); + entry.getField(StandardField.CHAPTER).ifPresent(s -> mappedEntry.chapter = s); + entry.getField(StandardField.EDITION).ifPresent(s -> mappedEntry.edition = s); + entry.getField(StandardField.EDITOR).ifPresent(s -> mappedEntry.editor = s); + entry.getField(StandardField.HOWPUBLISHED).ifPresent(s -> mappedEntry.howpublished = s); + entry.getField(StandardField.INSTITUTION).ifPresent(s -> mappedEntry.institution = s); + entry.getField(StandardField.JOURNAL).ifPresent(s -> mappedEntry.journal = s); + entry.getField(StandardField.MONTH).ifPresent(s -> mappedEntry.month = s); + entry.getField(StandardField.NOTE).ifPresent(s -> mappedEntry.note = s); + entry.getField(StandardField.NUMBER).ifPresent(s -> mappedEntry.number = s); + entry.getField(StandardField.ORGANIZATION).ifPresent(s -> mappedEntry.organization = s); + entry.getField(StandardField.PAGES).ifPresent(s -> mappedEntry.pages = s); + entry.getField(StandardField.PUBLISHER).ifPresent(s -> mappedEntry.publisher = s); + entry.getField(StandardField.SCHOOL).ifPresent(s -> mappedEntry.school = s); + entry.getField(StandardField.SERIES).ifPresent(s -> mappedEntry.series = s); + entry.getField(StandardField.TITLE).ifPresent(s -> mappedEntry.title = s); + entry.getField(StandardField.VOLUME).ifPresent(s -> mappedEntry.volume = s); + entry.getField(StandardField.YEAR).ifPresent(s -> mappedEntry.year = s); + entry.getField(StandardField.DATE).ifPresent(s -> mappedEntry.date = s); + return mappedEntry; + } + + /** + * Maps the BibEntryDTO into a BibEntry with the fields provided by the DTO mapped into the BibEntry + * Note that the Information provided by the DTO cannot be used to reconstruct + * a BibEntry that was previously mapped using the map method above. + */ + public static BibEntry map(BibEntryDTO entry) { + if ((entry.entryType == null) || entry.entryType.isBlank()) { + throw new IllegalArgumentException("Entry has to have an entry type"); + } + if ((entry.citationKey == null) || entry.citationKey.isBlank()) { + throw new IllegalArgumentException("Entry has to have a citation key"); + } + BibEntry mappedEntry = new BibEntry(getEntryType(entry.entryType)); + mappedEntry.withCitationKey(entry.citationKey); + if ((entry.address != null) && !entry.address.isBlank()) { + mappedEntry.withField(StandardField.ADDRESS, entry.address); + } + if ((entry.author != null) && !entry.author.isBlank()) { + mappedEntry.withField(StandardField.AUTHOR, entry.author); + } + if ((entry.booktitle != null) && !entry.booktitle.isBlank()) { + mappedEntry.withField(StandardField.BOOKTITLE, entry.booktitle); + } + if ((entry.chapter != null) && !entry.chapter.isBlank()) { + mappedEntry.withField(StandardField.CHAPTER, entry.chapter); + } + if ((entry.edition != null) && !entry.edition.isBlank()) { + mappedEntry.withField(StandardField.EDITION, entry.edition); + } + if ((entry.editor != null) && !entry.editor.isBlank()) { + mappedEntry.withField(StandardField.EDITOR, entry.editor); + } + if ((entry.howpublished != null) && !entry.howpublished.isBlank()) { + mappedEntry.withField(StandardField.HOWPUBLISHED, entry.howpublished); + } + if ((entry.institution != null) && !entry.institution.isBlank()) { + mappedEntry.withField(StandardField.INSTITUTION, entry.institution); + } + if ((entry.journal != null) && !entry.journal.isBlank()) { + mappedEntry.withField(StandardField.JOURNAL, entry.journal); + } + if ((entry.month != null) && !entry.month.isBlank()) { + mappedEntry.withField(StandardField.MONTH, entry.month); + } + if ((entry.note != null) && !entry.note.isBlank()) { + mappedEntry.withField(StandardField.NOTE, entry.note); + } + if ((entry.number != null) && !entry.number.isBlank()) { + mappedEntry.withField(StandardField.NUMBER, entry.number); + } + if ((entry.organization != null) && !entry.organization.isBlank()) { + mappedEntry .withField(StandardField.ORGANIZATION, entry.organization); + } + if ((entry.pages != null) && !entry.pages.isBlank()) { + mappedEntry.withField(StandardField.PAGES, entry.pages); + } + if ((entry.publisher != null) && !entry.publisher.isBlank()) { + mappedEntry.withField(StandardField.PUBLISHER, entry.publisher); + } + if ((entry.school != null) && !entry.school.isBlank()) { + mappedEntry.withField(StandardField.SCHOOL, entry.school); + } + if ((entry.series != null) && !entry.series.isBlank()) { + mappedEntry.withField(StandardField.SERIES, entry.series); + } + if ((entry.title != null) && !entry.title.isBlank()) { + mappedEntry.withField(StandardField.TITLE, entry.title); + } + if ((entry.volume != null) && !entry.volume.isBlank()) { + mappedEntry.withField(StandardField.VOLUME, entry.volume); + } + if ((entry.year != null) && !entry.year.isBlank()) { + mappedEntry.withField(StandardField.YEAR, entry.year); + } + if ((entry.date != null) && !entry.date.isBlank()) { + mappedEntry.withField(StandardField.DATE, entry.date); + } + return mappedEntry; + } + + private static EntryType getEntryType(String entryTypeAsString) { + Optional standardEntryType = Arrays.stream(StandardEntryType.values()).filter(entryType -> entryType.getName().equals(entryTypeAsString)).findFirst(); + if (standardEntryType.isPresent()) { + return standardEntryType.get(); + } + Optional ieeeEntryType = Arrays.stream(IEEETranEntryType.values()).filter(entryType -> entryType.getName().equals(entryTypeAsString)).findFirst(); + if (ieeeEntryType.isPresent()) { + return ieeeEntryType.get(); + } + return new UnknownEntryType(entryTypeAsString); + } +} diff --git a/src/main/java/org/jabref/logic/shared/restserver/rest/Root.java b/src/main/java/org/jabref/logic/shared/restserver/rest/Root.java new file mode 100644 index 00000000000..90d23533bb5 --- /dev/null +++ b/src/main/java/org/jabref/logic/shared/restserver/rest/Root.java @@ -0,0 +1,44 @@ +package org.jabref.logic.shared.restserver.rest; + +import java.util.Set; + +import org.jabref.logic.shared.restserver.rest.base.Accumulation; +import org.jabref.logic.shared.restserver.rest.base.Libraries; +import org.jabref.logic.shared.restserver.rest.base.Library; +import org.jabref.logic.shared.restserver.rest.slr.Studies; + +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.MediaType; + +@ApplicationPath("/") +@Path("") +public class Root extends Application { + + @Override + public Set> getClasses() { + return Set.of(Root.class, Libraries.class, Library.class, Studies.class, Accumulation.class); + } + + @GET + @Produces(MediaType.TEXT_HTML) + public String getText() { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(""" + + + + + """); + stringBuilder.append("

JabRef REST-ful http API

"); + stringBuilder.append("Use a JSON client and navigate to libraries."); + stringBuilder.append(""" + + + """); + return stringBuilder.toString(); + } +} diff --git a/src/main/java/org/jabref/logic/shared/restserver/rest/base/Accumulation.java b/src/main/java/org/jabref/logic/shared/restserver/rest/base/Accumulation.java new file mode 100644 index 00000000000..9ef9821af2b --- /dev/null +++ b/src/main/java/org/jabref/logic/shared/restserver/rest/base/Accumulation.java @@ -0,0 +1,40 @@ +package org.jabref.logic.shared.restserver.rest.base; + +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; + +import org.jabref.logic.shared.restserver.core.properties.ServerPropertyService; +import org.jabref.logic.shared.restserver.core.repository.LibraryService; +import org.jabref.logic.shared.restserver.core.serialization.BibEntryMapper; +import org.jabref.logic.shared.restserver.rest.model.Library; +import org.jabref.model.entry.BibEntry; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Path("all") +public class Accumulation { + private static final Logger LOGGER = LoggerFactory.getLogger(Accumulation.class); + private final LibraryService libraryService; + + public Accumulation() { + libraryService = LibraryService.getInstance(ServerPropertyService.getInstance().getWorkingDirectory()); + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + public Library getAllEntries() throws IOException { + try { + List entries = libraryService.getAllEntries(); + return new Library(entries.parallelStream().map(BibEntryMapper::map).collect(Collectors.toList())); + } catch (IOException e) { + LOGGER.error("Error accumulating all entries.", e); + throw e; + } + } +} diff --git a/src/main/java/org/jabref/logic/shared/restserver/rest/base/Libraries.java b/src/main/java/org/jabref/logic/shared/restserver/rest/base/Libraries.java new file mode 100644 index 00000000000..e67e007cca9 --- /dev/null +++ b/src/main/java/org/jabref/logic/shared/restserver/rest/base/Libraries.java @@ -0,0 +1,58 @@ +package org.jabref.logic.shared.restserver.rest.base; + +import java.io.IOException; +import java.util.List; + +import org.jabref.logic.shared.restserver.core.properties.ServerPropertyService; +import org.jabref.logic.shared.restserver.core.repository.LibraryService; +import org.jabref.logic.shared.restserver.rest.model.NewLibraryDTO; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Path("libraries") +public class Libraries { + private final LibraryService libraryService; + private final Logger LOGGER = LoggerFactory.getLogger(Libraries.class); + + public Libraries() { + libraryService = LibraryService.getInstance(ServerPropertyService.getInstance().getWorkingDirectory()); + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + public List getLibraryNames() throws IOException { + try { + return libraryService.getLibraryNames(); + } catch (IOException e) { + LOGGER.error("Error retrieving library names.", e); + throw e; + } + } + + @POST + @Consumes({MediaType.APPLICATION_JSON}) + public Response createNewLibrary(NewLibraryDTO newLibraryConfiguration) throws IOException { + if (libraryService.libraryExists(newLibraryConfiguration.getLibraryName())) { + return Response.status(Response.Status.CONFLICT) + .entity("The given library name is taken.") + .build(); + } + libraryService.createLibrary(newLibraryConfiguration); + return Response.ok() + .build(); + } + + @Path("{libraryName}") + public Library getLibraryResource(@PathParam("libraryName") String libraryName) { + return new Library(libraryName); + } +} diff --git a/src/main/java/org/jabref/logic/shared/restserver/rest/base/Library.java b/src/main/java/org/jabref/logic/shared/restserver/rest/base/Library.java new file mode 100644 index 00000000000..feabca78770 --- /dev/null +++ b/src/main/java/org/jabref/logic/shared/restserver/rest/base/Library.java @@ -0,0 +1,121 @@ +package org.jabref.logic.shared.restserver.rest.base; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.jabref.logic.shared.restserver.core.properties.ServerPropertyService; +import org.jabref.logic.shared.restserver.core.repository.LibraryService; +import org.jabref.logic.shared.restserver.core.representation.CSLStyleAdapter; +import org.jabref.logic.shared.restserver.core.serialization.BibEntryMapper; +import org.jabref.logic.shared.restserver.rest.model.BibEntryDTO; +import org.jabref.model.entry.BibEntry; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Library { + + private static final Logger LOGGER = LoggerFactory.getLogger(Library.class); + private final LibraryService libraryService; + private final String libraryName; + + public Library(String libraryName) { + this.libraryName = libraryName; + libraryService = LibraryService.getInstance(ServerPropertyService.getInstance().getWorkingDirectory()); + } + + public Library(java.nio.file.Path directory, String libraryName) { + this.libraryName = libraryName; + libraryService = LibraryService.getInstance(directory); + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + public org.jabref.logic.shared.restserver.rest.model.Library getLibraryEntries() throws IOException { + List entries = libraryService.getLibraryEntries(libraryName); + return new org.jabref.logic.shared.restserver.rest.model.Library(entries.parallelStream().map(BibEntryMapper::map).collect(Collectors.toList())); + } + + @GET + @Path("styles") + @Produces(MediaType.APPLICATION_JSON) + public List getCSLStyles() throws IOException, URISyntaxException { + return CSLStyleAdapter.getInstance().getRegisteredStyles(); + } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + public void addEntryToLibrary(BibEntryDTO bibEntry) throws IOException { + libraryService.addEntryToLibrary(libraryName, BibEntryMapper.map(bibEntry)); + } + + @DELETE + public Response deleteLibrary() throws IOException { + if (libraryService.deleteLibrary(libraryName)) { + return Response.ok().build(); + } else { + return Response.status(Response.Status.NOT_FOUND) + .build(); + } + } + + @GET + @Path("{citeKey}") + @Produces(MediaType.APPLICATION_JSON) + public BibEntryDTO getBibEntryMatchingCiteKey(@PathParam("citeKey") String citeKey) throws IOException { + Optional entry = libraryService.getLibraryEntryMatchingCiteKey(libraryName, citeKey); + if (entry.isPresent()) { + return BibEntryMapper.map(entry.get()); + } else { + throw new NotFoundException(); + } + } + + @GET + @Path("{citeKey}/{cslStyle}") + @Produces(MediaType.TEXT_HTML) + public String getBibEntryMatchingCiteKey(@PathParam("citeKey") String citeKey, @PathParam("cslStyle") String cslStyle) throws IOException, URISyntaxException { + Optional entry = libraryService.getLibraryEntryMatchingCiteKey(libraryName, citeKey); + if (entry.isPresent()) { + return CSLStyleAdapter.getInstance().generateCitation(entry.get(), cslStyle); + } else { + throw new NotFoundException(); + } + } + + @PUT + @Path("{citeKey}") + @Consumes(MediaType.APPLICATION_JSON) + public void updateEntry(@PathParam("citeKey") String citeKey, BibEntryDTO bibEntry) throws IOException { + BibEntry updatedEntry = BibEntryMapper.map(bibEntry); + libraryService.updateEntry(libraryName, citeKey, updatedEntry); + } + + @DELETE + @Path("{citeKey}") + @Produces(MediaType.APPLICATION_JSON) + public Response deleteEntryFromLibrary(@PathParam("citeKey") String citeKey) throws IOException { + boolean foundAndDeleted = libraryService.deleteEntryByCiteKey(libraryName, citeKey); + if (foundAndDeleted) { + return Response.ok() + .build(); + } else { + return Response.status(Response.Status.NOT_FOUND) + .build(); + } + } +} diff --git a/src/main/java/org/jabref/logic/shared/restserver/rest/model/BibEntryDTO.java b/src/main/java/org/jabref/logic/shared/restserver/rest/model/BibEntryDTO.java new file mode 100644 index 00000000000..d24568a4461 --- /dev/null +++ b/src/main/java/org/jabref/logic/shared/restserver/rest/model/BibEntryDTO.java @@ -0,0 +1,120 @@ +package org.jabref.logic.shared.restserver.rest.model; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class BibEntryDTO { + public String entryType; + public String citationKey; + + public String address; + public String author; + public String booktitle; + public String chapter; + public String edition; + public String editor; + public String howpublished; + public String institution; + public String journal; + public String month; + public String note; + public String number; + public String organization; + public String pages; + public String publisher; + public String school; + public String series; + public String title; + public String volume; + public String year; + public String date; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if ((o == null) || (getClass() != o.getClass())) { + return false; + } + + BibEntryDTO that = (BibEntryDTO) o; + + if (!entryType.equals(that.entryType)) { + return false; + } + if (!citationKey.equals(that.citationKey)) { + return false; + } + if (address != null ? !address.equals(that.address) : that.address != null) { + return false; + } + if (author != null ? !author.equals(that.author) : that.author != null) { + return false; + } + if (booktitle != null ? !booktitle.equals(that.booktitle) : that.booktitle != null) { + return false; + } + if (chapter != null ? !chapter.equals(that.chapter) : that.chapter != null) { + return false; + } + if (edition != null ? !edition.equals(that.edition) : that.edition != null) { + return false; + } + if (editor != null ? !editor.equals(that.editor) : that.editor != null) { + return false; + } + if (howpublished != null ? !howpublished.equals(that.howpublished) : that.howpublished != null) { + return false; + } + if (institution != null ? !institution.equals(that.institution) : that.institution != null) { + return false; + } + if (journal != null ? !journal.equals(that.journal) : that.journal != null) { + return false; + } + if (month != null ? !month.equals(that.month) : that.month != null) { + return false; + } + if (note != null ? !note.equals(that.note) : that.note != null) { + return false; + } + if (number != null ? !number.equals(that.number) : that.number != null) { + return false; + } + if (organization != null ? !organization.equals(that.organization) : that.organization != null) { + return false; + } + if (pages != null ? !pages.equals(that.pages) : that.pages != null) { + return false; + } + if (publisher != null ? !publisher.equals(that.publisher) : that.publisher != null) { + return false; + } + if (school != null ? !school.equals(that.school) : that.school != null) { + return false; + } + if (series != null ? !series.equals(that.series) : that.series != null) { + return false; + } + if (title != null ? !title.equals(that.title) : that.title != null) { + return false; + } + if (volume != null ? !volume.equals(that.volume) : that.volume != null) { + return false; + } + if (year != null ? !year.equals(that.year) : that.year != null) { + return false; + } + return date != null ? date.equals(that.date) : that.date == null; + } + + @Override + public int hashCode() { + int result = entryType.hashCode(); + result = (31 * result) + citationKey.hashCode(); + return result; + } +} + + diff --git a/src/main/java/org/jabref/logic/shared/restserver/rest/model/CrawlStatus.java b/src/main/java/org/jabref/logic/shared/restserver/rest/model/CrawlStatus.java new file mode 100644 index 00000000000..bf94f39c139 --- /dev/null +++ b/src/main/java/org/jabref/logic/shared/restserver/rest/model/CrawlStatus.java @@ -0,0 +1,9 @@ +package org.jabref.logic.shared.restserver.rest.model; + +public class CrawlStatus { + public boolean currentlyCrawling; + + public CrawlStatus(boolean currentlyCrawling) { + this.currentlyCrawling = currentlyCrawling; + } +} diff --git a/src/main/java/org/jabref/logic/shared/restserver/rest/model/Library.java b/src/main/java/org/jabref/logic/shared/restserver/rest/model/Library.java new file mode 100644 index 00000000000..475c298c79f --- /dev/null +++ b/src/main/java/org/jabref/logic/shared/restserver/rest/model/Library.java @@ -0,0 +1,11 @@ +package org.jabref.logic.shared.restserver.rest.model; + +import java.util.List; + +public class Library { + public List bibEntries; + + public Library(List bibEntries) { + this.bibEntries = bibEntries; + } +} diff --git a/src/main/java/org/jabref/logic/shared/restserver/rest/model/NewLibraryDTO.java b/src/main/java/org/jabref/logic/shared/restserver/rest/model/NewLibraryDTO.java new file mode 100644 index 00000000000..b9265ed317c --- /dev/null +++ b/src/main/java/org/jabref/logic/shared/restserver/rest/model/NewLibraryDTO.java @@ -0,0 +1,13 @@ +package org.jabref.logic.shared.restserver.rest.model; + +public class NewLibraryDTO { + public String libraryName; + + public String getLibraryName() { + return libraryName; + } + + public void setLibraryName(String libraryName) { + this.libraryName = libraryName; + } +} diff --git a/src/main/java/org/jabref/logic/shared/restserver/rest/model/StudyDTO.java b/src/main/java/org/jabref/logic/shared/restserver/rest/model/StudyDTO.java new file mode 100644 index 00000000000..e70fe43ec93 --- /dev/null +++ b/src/main/java/org/jabref/logic/shared/restserver/rest/model/StudyDTO.java @@ -0,0 +1,14 @@ +package org.jabref.logic.shared.restserver.rest.model; + +import org.jabref.model.study.Study; + +public class StudyDTO { + public Study studyDefinition; + + public StudyDTO() { + } + + public StudyDTO(Study studyDefinition) { + this.studyDefinition = studyDefinition; + } +} diff --git a/src/main/java/org/jabref/logic/shared/restserver/rest/serialization/ObjectMapperContextResolver.java b/src/main/java/org/jabref/logic/shared/restserver/rest/serialization/ObjectMapperContextResolver.java new file mode 100644 index 00000000000..2c8a1cd4532 --- /dev/null +++ b/src/main/java/org/jabref/logic/shared/restserver/rest/serialization/ObjectMapperContextResolver.java @@ -0,0 +1,30 @@ +package org.jabref.logic.shared.restserver.rest.serialization; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import jakarta.ws.rs.ext.ContextResolver; +import jakarta.ws.rs.ext.Provider; + +@Provider +public class ObjectMapperContextResolver implements ContextResolver { + + private final ObjectMapper mapper; + + public ObjectMapperContextResolver() { + this.mapper = createObjectMapper(); + } + + @Override + public ObjectMapper getContext(Class type) { + return mapper; + } + + private ObjectMapper createObjectMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.enable(SerializationFeature.INDENT_OUTPUT); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + return mapper; + } +} diff --git a/src/main/java/org/jabref/logic/shared/restserver/rest/slr/Studies.java b/src/main/java/org/jabref/logic/shared/restserver/rest/slr/Studies.java new file mode 100644 index 00000000000..99c219fe6d8 --- /dev/null +++ b/src/main/java/org/jabref/logic/shared/restserver/rest/slr/Studies.java @@ -0,0 +1,115 @@ +package org.jabref.logic.shared.restserver.rest.slr; + +import java.io.IOException; +import java.util.List; + +import org.jabref.logic.importer.ParseException; +import org.jabref.logic.shared.restserver.core.properties.ServerPropertyService; +import org.jabref.logic.shared.restserver.core.repository.StudyService; +import org.jabref.logic.shared.restserver.rest.base.Library; +import org.jabref.logic.shared.restserver.rest.model.CrawlStatus; +import org.jabref.logic.shared.restserver.rest.model.StudyDTO; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Path("studies") +public class Studies { + private static final Logger LOGGER = LoggerFactory.getLogger(Studies.class); + private final StudyService studyService; + + public Studies() { + studyService = StudyService.getInstance(ServerPropertyService.getInstance().getWorkingDirectory()); + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + public List getStudyNames() throws IOException { + try { + return studyService.getStudyNames(); + } catch (IOException e) { + LOGGER.error("Error retrieving study names.", e); + throw e; + } + } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + public Response createStudy(StudyDTO study) throws IOException { + try { + if (studyService.studyExists(study.studyDefinition.getTitle())) { + return Response.status(Response.Status.CONFLICT).entity("The given study name is already in use.").build(); + } + studyService.createStudy(study.studyDefinition); + return Response.ok() + .build(); + } catch (IOException e) { + LOGGER.error("Error retrieving study names.", e); + throw e; + } + } + + // TODO: How can we remove the /results + @Path("{studyName}/results") + public Library getStudyResults(@PathParam("studyName") String studyName) { + return new Library(studyService.getStudyPath(studyName), "studyResult"); + } + + @DELETE + @Path("{studyName}") + public Response deleteStudy(@PathParam("studyName") String studyName) throws IOException { + try { + if (studyService.deleteStudy(studyName)) { + return Response.ok() + .build(); + } + return Response.status(Response.Status.NOT_FOUND) + .build(); + } catch (IOException e) { + LOGGER.error("Error deleting study.", e); + throw e; + } + } + + @POST + // Workaround to fix Tomcat CORS Filter issue with missing media type header: https://stackoverflow.com/questions/59204624/cors-failing-when-post-request-has-no-body-and-server-response-is-a-403-forbidde + @Consumes(MediaType.TEXT_PLAIN) + @Path("{studyName}/crawl") + public void crawlStudy(@PathParam("studyName") String studyName, String unused) throws IOException, ParseException { + try { + // Note: This only starts a new crawl if no other crawl is currently running for this study + studyService.startCrawl(studyName); + } catch (IOException | ParseException e) { + LOGGER.error("Error during crawling", e); + throw e; + } + } + + @GET + @Path("{studyName}/crawl") + @Produces(MediaType.APPLICATION_JSON) + public CrawlStatus getCrawlStatus(@PathParam("studyName") String studyName) { + return new CrawlStatus(studyService.isCrawlRunning(studyName)); + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("{studyName}/studyDefinition") + public StudyDTO getStudyDefinition(@PathParam("studyName") String studyName) throws IOException { + try { + return new StudyDTO(studyService.getStudyDefinition(studyName)); + } catch (IOException e) { + LOGGER.error("Error retrieving study definition.", e); + throw e; + } + } +} diff --git a/src/main/java/org/jabref/logic/util/io/FileUtil.java b/src/main/java/org/jabref/logic/util/io/FileUtil.java index d670db1dd54..bb804a8cb62 100644 --- a/src/main/java/org/jabref/logic/util/io/FileUtil.java +++ b/src/main/java/org/jabref/logic/util/io/FileUtil.java @@ -132,7 +132,7 @@ public static Optional getUniquePathDirectory(List paths, Path c List uniquePathParts = uniquePathSubstrings(paths); return uniquePathParts.stream() .filter(part -> comparePath.toString().contains(part) - && !part.equals(fileName) && part.contains(File.separator)) + && !part.equals(fileName) && part.contains(File.separator)) .findFirst() .map(part -> part.substring(0, part.lastIndexOf(File.separator))); } @@ -145,9 +145,9 @@ public static Optional getUniquePathDirectory(List paths, Path c */ public static Optional getUniquePathFragment(List paths, Path comparePath) { return uniquePathSubstrings(paths).stream() - .filter(part -> comparePath.toString().contains(part)) - .sorted(Comparator.comparingInt(String::length).reversed()) - .findFirst(); + .filter(part -> comparePath.toString().contains(part)) + .sorted(Comparator.comparingInt(String::length).reversed()) + .findFirst(); } /** @@ -212,7 +212,7 @@ public static boolean copyFile(Path pathToSourceFile, Path pathToDestinationFile try { // Preserve Hard Links with OpenOption defaults included for clarity Files.write(pathToDestinationFile, Files.readAllBytes(pathToSourceFile), - StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); + StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); return true; } catch (IOException e) { LOGGER.error("Copying Files failed.", e); @@ -311,9 +311,9 @@ public static String createDirNameFromPattern(BibDatabase database, BibEntry ent public static Optional find(String filename, Path rootDirectory) { try (Stream pathStream = Files.walk(rootDirectory)) { return pathStream - .filter(Files::isRegularFile) - .filter(f -> f.getFileName().toString().equals(filename)) - .findFirst(); + .filter(Files::isRegularFile) + .filter(f -> f.getFileName().toString().equals(filename)) + .findFirst(); } catch (UncheckedIOException | IOException ex) { LOGGER.error("Error trying to locate the file " + filename + " inside the directory " + rootDirectory); } diff --git a/src/main/java/org/jabref/model/util/FileHelper.java b/src/main/java/org/jabref/model/util/FileHelper.java index fcfe0ce9499..c1f94e361a9 100644 --- a/src/main/java/org/jabref/model/util/FileHelper.java +++ b/src/main/java/org/jabref/model/util/FileHelper.java @@ -1,8 +1,6 @@ package org.jabref.model.util; -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; +import java.net.MalformedURLException; import java.net.URL; import java.nio.file.Files; import java.nio.file.InvalidPathException; @@ -13,18 +11,11 @@ import java.util.Objects; import java.util.Optional; +import org.jabref.logic.net.URLDownload; import org.jabref.logic.util.io.FileUtil; import org.jabref.model.database.BibDatabaseContext; import org.jabref.preferences.FilePreferences; -import org.apache.tika.config.TikaConfig; -import org.apache.tika.detect.Detector; -import org.apache.tika.metadata.Metadata; -import org.apache.tika.metadata.TikaCoreProperties; -import org.apache.tika.mime.MediaType; -import org.apache.tika.mime.MimeType; -import org.apache.tika.mime.MimeTypeException; -import org.apache.tika.parser.AutoDetectParser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -68,13 +59,10 @@ public static Optional getFileExtension(Path file) { * @return The extension (without leading dot), trimmed and in lowercase. */ public static Optional getFileExtension(String fileName) { - Metadata metadata = new Metadata(); - metadata.add(TikaCoreProperties.RESOURCE_NAME_KEY, fileName); - if (isUrl(fileName)) { - try (InputStream is = new URL(fileName).openStream()) { - return detectExtension(is, metadata); - } catch (IOException | MimeTypeException e) { + try { + return Optional.of(new URLDownload(fileName).getMimeType()); + } catch (MalformedURLException e) { return Optional.empty(); } } @@ -86,21 +74,6 @@ public static Optional getFileExtension(String fileName) { return Optional.empty(); } - private static Optional detectExtension(InputStream is, Metadata metaData) throws IOException, MimeTypeException { - BufferedInputStream bis = new BufferedInputStream(is); - AutoDetectParser parser = new AutoDetectParser(); - Detector detector = parser.getDetector(); - MediaType mediaType = detector.detect(bis, metaData); - MimeType mimeType = TikaConfig.getDefaultConfig().getMimeRepository().forName(mediaType.toString()); - String extension = mimeType.getExtension(); - - if (extension.isEmpty()) { - return Optional.empty(); - } - - return Optional.of(extension.substring(1)); - } - /** * Converts a relative filename to an absolute one, if necessary. Returns an empty optional if the file does not * exist.
diff --git a/src/test/java/org/jabref/cli/LauncherTest.java b/src/test/java/org/jabref/cli/LauncherTest.java new file mode 100644 index 00000000000..af1c649a511 --- /dev/null +++ b/src/test/java/org/jabref/cli/LauncherTest.java @@ -0,0 +1,15 @@ +package org.jabref.cli; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +class LauncherTest { + + @Test + @Disabled + void startServer() { + Launcher.LOGGER = LoggerFactory.getLogger(Launcher.class); + Launcher.startServer(); + } +}