From 534c79dd1c6fbae63bfb9652b82a34b2e1089ea1 Mon Sep 17 00:00:00 2001 From: Tobias Diez Date: Thu, 26 Apr 2018 11:19:39 +0200 Subject: [PATCH] Implement native messaging (#3246) --- build.gradle | 14 +- buildres/JabRef.bat | 3 + buildres/JabRef.ps1 | 48 +++++++ buildres/jabref.json | 9 ++ jabref.install4j | 51 ++++++- src/main/java/org/jabref/Globals.java | 1 - src/main/java/org/jabref/JabRefGUI.java | 2 +- src/main/java/org/jabref/JabRefMain.java | 54 ++++--- .../org/jabref/cli/ArgumentProcessor.java | 19 +++ src/main/java/org/jabref/cli/JabRefCLI.java | 20 ++- .../gui/remote/JabRefMessageHandler.java | 8 +- .../logic/remote/RemotePreferences.java | 10 ++ .../logic/remote/client/RemoteClient.java | 68 +++++++++ .../remote/client/RemoteListenerClient.java | 53 ------- .../logic/remote/server/MessageHandler.java | 2 +- .../remote/server/RemoteListenerServer.java | 36 +++-- .../jabref/logic/remote/shared/Protocol.java | 50 ++++--- .../logic/remote/shared/RemoteMessage.java | 20 +++ .../jabref/preferences/JabRefPreferences.java | 2 + .../architecture/MainArchitectureTests.java | 2 + .../java/org/jabref/cli/JabRefCLITest.java | 8 ++ .../logic/remote/RemoteCommunicationTest.java | 76 ++++++++++ .../jabref/logic/remote/RemoteSetupTest.java | 132 ++++++++++++++++++ .../org/jabref/logic/remote/RemoteTest.java | 112 --------------- 24 files changed, 563 insertions(+), 237 deletions(-) create mode 100644 buildres/JabRef.bat create mode 100644 buildres/JabRef.ps1 create mode 100644 buildres/jabref.json create mode 100644 src/main/java/org/jabref/logic/remote/client/RemoteClient.java delete mode 100644 src/main/java/org/jabref/logic/remote/client/RemoteListenerClient.java create mode 100644 src/main/java/org/jabref/logic/remote/shared/RemoteMessage.java create mode 100644 src/test/java/org/jabref/logic/remote/RemoteCommunicationTest.java create mode 100644 src/test/java/org/jabref/logic/remote/RemoteSetupTest.java delete mode 100644 src/test/java/org/jabref/logic/remote/RemoteTest.java diff --git a/build.gradle b/build.gradle index 8a20e2c9ceb..76a505c5b3b 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ buildscript { plugins { id 'com.gradle.build-scan' version '1.11' - id 'com.install4j.gradle' version '7.0.3' + id 'com.install4j.gradle' version '7.0.4' id 'com.github.johnrengelman.shadow' version '2.0.2' id "de.sebastianboegl.shadow.transformer.log4j" version "2.1.1" id "com.simonharrer.modernizer" version '1.6.0-1' @@ -50,7 +50,7 @@ apply from: 'xjc.gradle' group = "org.jabref" version = "4.2-dev" project.ext.threeDotVersion = "4.1.0.1" -project.ext.install4jDir = hasProperty("install4jDir") ? getProperty("install4jDir") : (OperatingSystem.current().isWindows() ? 'C:/Program Files/install4j6' : 'install4j6') +project.ext.install4jDir = hasProperty("install4jDir") ? getProperty("install4jDir") : (OperatingSystem.current().isWindows() ? 'C:/Program Files/install4j7' : 'install4j7') sourceCompatibility = 1.8 targetCompatibility = 1.8 mainClassName = "org.jabref.JabRefMain" @@ -448,8 +448,16 @@ install4j { installDir = file(project.ext.install4jDir) } +task generateFinalJabRefPS1File(type: Copy) { + from('buildres') { + include 'JabRef.ps1' + } + into 'build' + filter(org.apache.tools.ant.filters.ReplaceTokens, tokens: [jabRefJarFileName: jar.archiveName]) +} + // has to be defined AFTER 'dev' things to have the correct project.version -task media(type: com.install4j.gradle.Install4jTask, dependsOn: "releaseJar") { +task media(type: com.install4j.gradle.Install4jTask, dependsOn: ["releaseJar", "generateFinalJabRefPS1File"]) { projectFile = file('jabref.install4j') release = project.version winKeystorePassword = System.getenv('CERTIFICATE_PW') diff --git a/buildres/JabRef.bat b/buildres/JabRef.bat new file mode 100644 index 00000000000..7a2cee363b0 --- /dev/null +++ b/buildres/JabRef.bat @@ -0,0 +1,3 @@ +@echo off +pushd %~dp0 +@powershell.exe -ExecutionPolicy Bypass -NoLogo -NonInteractive -NoProfile -WindowStyle Hidden -File ".\JabRef.ps1" diff --git a/buildres/JabRef.ps1 b/buildres/JabRef.ps1 new file mode 100644 index 00000000000..fe814e6013e --- /dev/null +++ b/buildres/JabRef.ps1 @@ -0,0 +1,48 @@ +function Respond($response) { + $jsonResponse = $response | ConvertTo-Json + + try { + $writer = New-Object System.IO.BinaryWriter([System.Console]::OpenStandardOutput()) + $writer.Write([int]$jsonResponse.Length) + $writer.Write([System.Text.Encoding]::UTF8.GetBytes($jsonResponse)) + $writer.Close() + } finally { + $writer.Dispose() + } +} + +$jabRefJarFileName = "@jabRefJarFileName@" +$jabRefJar = [System.IO.Path]::Combine($PSScriptRoot, $jabRefJarFileName) + +try { + $reader = New-Object System.IO.BinaryReader([System.Console]::OpenStandardInput()) + $length = $reader.ReadInt32() + $messageRaw = [System.Text.Encoding]::UTF8.GetString($reader.ReadBytes($length)) + $message = $messageRaw | ConvertFrom-Json + + if ($message.Status -eq "validate") { + if (-not (Test-Path $jabRefJar)) { + return Respond @{message="jarNotFound";path=$jabRefJar} + } else { + return Respond @{message="jarFound"} + } + } + + if (-not (Test-Path $jabRefJar)) { + $wshell = New-Object -ComObject Wscript.Shell + $popup = "Unable to locate '$jabRefJarFileName' in '$([System.IO.Path]::GetDirectoryName($jabRefJar))'." + $wshell.Popup($popup,0,"JabRef", 0x0 + 0x30) + return + } + + #$wshell = New-Object -ComObject Wscript.Shell + #$wshell.Popup($message.Text,0,"JabRef", 0x0 + 0x30) + + $messageText = $message.Text + $output = & java -jar $jabRefJar -importBibtex "$messageText" 2>&1 + #$output = & echoargs -importBibtex $messageText 2>&1 + #$wshell.Popup($output,0,"JabRef", 0x0 + 0x30) + return Respond @{message="ok";output="$output"} +} finally { + $reader.Dispose() +} diff --git a/buildres/jabref.json b/buildres/jabref.json new file mode 100644 index 00000000000..dee50f008fe --- /dev/null +++ b/buildres/jabref.json @@ -0,0 +1,9 @@ +{ + "name": "org.jabref.jabref", + "description": "JabRef", + "path": "JabRef.bat", + "type": "stdio", + "allowed_extensions": [ + "@jabfox" + ] +} diff --git a/jabref.install4j b/jabref.install4j index 28d3fad052e..7992a0c8036 100644 --- a/jabref.install4j +++ b/jabref.install4j @@ -1,6 +1,6 @@ - - + + @@ -51,10 +51,13 @@ - + + + + @@ -561,6 +564,27 @@ return console.askOkCancel(message, true); !(Util.hasFullAdminRights() || Util.isAdminGroup()) + + + + + + SOFTWARE\Mozilla\NativeMessagingHosts\org.jabref.jabref + + + + com.install4j.api.windows.RegistryRoot + HKEY_LOCAL_MACHINE + + + + ${installer:sys.installationDir}\jabref.json + + + + + + @@ -896,6 +920,27 @@ return console.askYesNo(message, true); !(Util.hasFullAdminRights() || Util.isAdminGroup()) + + + + + + SOFTWARE\Mozilla\NativeMessagingHosts\org.jabref.jabref + + + false + + + + com.install4j.api.windows.RegistryRoot + HKEY_LOCAL_MACHINE + + + + + + + diff --git a/src/main/java/org/jabref/Globals.java b/src/main/java/org/jabref/Globals.java index 86bda457fa5..b00066a86a4 100644 --- a/src/main/java/org/jabref/Globals.java +++ b/src/main/java/org/jabref/Globals.java @@ -31,7 +31,6 @@ public class Globals { public static final BuildInfo BUILD_INFO = new BuildInfo(); // Remote listener public static final RemoteListenerServerLifecycle REMOTE_LISTENER = new RemoteListenerServerLifecycle(); - public static final ImportFormatReader IMPORT_FORMAT_READER = new ImportFormatReader(); public static final TaskExecutor TASK_EXECUTOR = new DefaultTaskExecutor(); // In the main program, this field is initialized in JabRef.java diff --git a/src/main/java/org/jabref/JabRefGUI.java b/src/main/java/org/jabref/JabRefGUI.java index e51014e4114..c20a6f6ea3c 100644 --- a/src/main/java/org/jabref/JabRefGUI.java +++ b/src/main/java/org/jabref/JabRefGUI.java @@ -102,7 +102,7 @@ private void openWindow() { for (Iterator parserResultIterator = bibDatabases.iterator(); parserResultIterator.hasNext();) { ParserResult pr = parserResultIterator.next(); // Define focused tab - if (pr.getFile().get().getAbsolutePath().equals(focusedFile)) { + if (pr.getFile().filter(path -> path.getAbsolutePath().equals(focusedFile)).isPresent()) { first = true; } diff --git a/src/main/java/org/jabref/JabRefMain.java b/src/main/java/org/jabref/JabRefMain.java index dda4032a76b..bd370dda02a 100644 --- a/src/main/java/org/jabref/JabRefMain.java +++ b/src/main/java/org/jabref/JabRefMain.java @@ -19,7 +19,7 @@ import org.jabref.logic.net.ProxyRegisterer; import org.jabref.logic.protectedterms.ProtectedTermsLoader; import org.jabref.logic.remote.RemotePreferences; -import org.jabref.logic.remote.client.RemoteListenerClient; +import org.jabref.logic.remote.client.RemoteClient; import org.jabref.logic.util.BuildInfo; import org.jabref.logic.util.JavaVersion; import org.jabref.logic.util.OS; @@ -42,13 +42,8 @@ public class JabRefMain extends Application { public static void main(String[] args) { arguments = args; - launch(arguments); - } - @Override - public void start(Stage mainStage) throws Exception { - Platform.setImplicitExit(false); - SwingUtilities.invokeLater(() -> start(arguments)); + launch(arguments); } /** @@ -98,25 +93,9 @@ private static void ensureCorrectJavaVersion() { } private static void start(String[] args) { - FallbackExceptionHandler.installExceptionHandler(); - + // Init preferences JabRefPreferences preferences = JabRefPreferences.getInstance(); - - ensureCorrectJavaVersion(); - - ProxyPreferences proxyPreferences = preferences.getProxyPreferences(); - ProxyRegisterer.register(proxyPreferences); - if (proxyPreferences.isUseProxy() && proxyPreferences.isUseAuthentication()) { - Authenticator.setDefault(new ProxyAuthenticator()); - } - Globals.prefs = preferences; - Globals.startBackgroundTasks(); - - // Note that the language was already set during the initialization of the preferences and it is safe to - // call the next function. - Globals.prefs.setLanguageDependentDefaultValues(); - // Perform Migrations // Perform checks and changes for users with a preference set from an older JabRef version. PreferencesMigrations.upgradePrefsToOrgJabRef(); @@ -129,6 +108,21 @@ private static void start(String[] args) { PreferencesMigrations.addCrossRefRelatedFieldsForAutoComplete(); PreferencesMigrations.upgradeObsoleteLookAndFeels(); + // Process arguments + ArgumentProcessor argumentProcessor = new ArgumentProcessor(args, ArgumentProcessor.Mode.INITIAL_START); + + FallbackExceptionHandler.installExceptionHandler(); + + ensureCorrectJavaVersion(); + + ProxyPreferences proxyPreferences = preferences.getProxyPreferences(); + ProxyRegisterer.register(proxyPreferences); + if (proxyPreferences.isUseProxy() && proxyPreferences.isUseAuthentication()) { + Authenticator.setDefault(new ProxyAuthenticator()); + } + + Globals.startBackgroundTasks(); + // Update handling of special fields based on preferences InternalBibtexFields .updateSpecialFields(Globals.prefs.getBoolean(JabRefPreferences.SERIALIZESPECIALFIELDS)); @@ -157,7 +151,7 @@ private static void start(String[] args) { if (!Globals.REMOTE_LISTENER.isOpen()) { // we are not alone, there is already a server out there, try to contact already running JabRef: - if (RemoteListenerClient.sendToActiveJabRefInstance(args, remotePreferences.getPort())) { + if (new RemoteClient(remotePreferences.getPort()).sendCommandLineArguments(args)) { // We have successfully sent our command line options through the socket to another JabRef instance. // So we assume it's all taken care of, and quit. LOGGER.info(Localization.lang("Arguments passed on to running JabRef instance. Shutting down.")); @@ -175,9 +169,6 @@ private static void start(String[] args) { // The preferences return the system newline character sequence as default OS.NEWLINE = Globals.prefs.get(JabRefPreferences.NEWLINE); - // Process arguments - ArgumentProcessor argumentProcessor = new ArgumentProcessor(args, ArgumentProcessor.Mode.INITIAL_START); - // See if we should shut down now if (argumentProcessor.shouldShutDown()) { Globals.shutdownThreadPools(); @@ -190,4 +181,11 @@ private static void start(String[] args) { .invokeLater(() -> new JabRefGUI(argumentProcessor.getParserResults(), argumentProcessor.isBlank())); } + + @Override + public void start(Stage mainStage) throws Exception { + Platform.setImplicitExit(false); + SwingUtilities.invokeLater(() -> start(arguments) + ); + } } diff --git a/src/main/java/org/jabref/cli/ArgumentProcessor.java b/src/main/java/org/jabref/cli/ArgumentProcessor.java index 643ca3bf713..7df241a209d 100644 --- a/src/main/java/org/jabref/cli/ArgumentProcessor.java +++ b/src/main/java/org/jabref/cli/ArgumentProcessor.java @@ -32,7 +32,9 @@ import org.jabref.logic.importer.ImportFormatReader; import org.jabref.logic.importer.OpenDatabase; import org.jabref.logic.importer.OutputPrinter; +import org.jabref.logic.importer.ParseException; import org.jabref.logic.importer.ParserResult; +import org.jabref.logic.importer.fileformat.BibtexParser; import org.jabref.logic.l10n.Localization; import org.jabref.logic.layout.LayoutFormatterPreferences; import org.jabref.logic.logging.JabRefLogger; @@ -83,6 +85,19 @@ private static Optional importToOpenBase(String argument) { return result; } + private static Optional importBibtexToOpenBase(String argument) { + BibtexParser parser = new BibtexParser(Globals.prefs.getImportFormatPreferences(), Globals.getFileUpdateMonitor()); + try { + List entries = parser.parseEntries(argument); + ParserResult result = new ParserResult(entries); + result.setToOpenTab(); + return Optional.of(result); + } catch (ParseException e) { + System.err.println(Localization.lang("Error occurred when parsing entry") + ": " + e.getLocalizedMessage()); + return Optional.empty(); + } + } + private static Optional importFile(String argument) { String[] data = argument.split(","); @@ -342,6 +357,10 @@ private List importAndOpenFiles() { importToOpenBase(cli.getImportToOpenBase()).ifPresent(loaded::add); } + if (!cli.isBlank() && cli.isBibtexImport()) { + importBibtexToOpenBase(cli.getBibtexImport()).ifPresent(loaded::add); + } + return loaded; } diff --git a/src/main/java/org/jabref/cli/JabRefCLI.java b/src/main/java/org/jabref/cli/JabRefCLI.java index f427c8e0dbb..ee79dee219d 100644 --- a/src/main/java/org/jabref/cli/JabRefCLI.java +++ b/src/main/java/org/jabref/cli/JabRefCLI.java @@ -1,6 +1,5 @@ package org.jabref.cli; -import java.util.Arrays; import java.util.List; import org.jabref.Globals; @@ -21,14 +20,13 @@ public class JabRefCLI { private final CommandLine cl; private List leftOver; - public JabRefCLI(String[] args) { Options options = getOptions(); try { this.cl = new DefaultParser().parse(options, args); - this.leftOver = Arrays.asList(cl.getArgs()); + this.leftOver = cl.getArgList(); } catch (ParseException e) { LOGGER.warn("Problem parsing arguments", e); @@ -96,6 +94,14 @@ public String getFileExport() { return cl.getOptionValue("output"); } + public boolean isBibtexImport() { + return cl.hasOption("importBibtex"); + } + + public String getBibtexImport() { + return cl.getOptionValue("importBibtex"); + } + public boolean isFileImport() { return cl.hasOption("import"); } @@ -164,6 +170,14 @@ private Options getOptions() { hasArg(). argName("FILE").build()); + options.addOption( + Option.builder("ib") + .longOpt("importBibtex") + .desc(String.format("%s: %s[,importBibtex bibtexString]", Localization.lang("Import") + " " + Localization.BIBTEX, Localization.lang("filename"))) + .hasArg() + .argName("FILE") + .build()); + options.addOption(Option.builder("o"). longOpt("output"). desc(String.format("%s: %s[,export format]", Localization.lang("Output or export file"), diff --git a/src/main/java/org/jabref/gui/remote/JabRefMessageHandler.java b/src/main/java/org/jabref/gui/remote/JabRefMessageHandler.java index b863c5b1e86..364052baa2d 100644 --- a/src/main/java/org/jabref/gui/remote/JabRefMessageHandler.java +++ b/src/main/java/org/jabref/gui/remote/JabRefMessageHandler.java @@ -1,5 +1,6 @@ package org.jabref.gui.remote; +import java.util.Arrays; import java.util.List; import org.jabref.JabRefGUI; @@ -10,11 +11,10 @@ public class JabRefMessageHandler implements MessageHandler { @Override - public void handleMessage(String message) { - ArgumentProcessor argumentProcessor = new ArgumentProcessor(message.split("\n"), - ArgumentProcessor.Mode.REMOTE_START); + public void handleCommandLineArguments(String[] message) { + ArgumentProcessor argumentProcessor = new ArgumentProcessor(message, ArgumentProcessor.Mode.REMOTE_START); if (!(argumentProcessor.hasParserResults())) { - throw new IllegalStateException("Could not start JabRef with arguments " + message); + throw new IllegalStateException("Could not start JabRef with arguments " + Arrays.toString(message)); } List loaded = argumentProcessor.getParserResults(); diff --git a/src/main/java/org/jabref/logic/remote/RemotePreferences.java b/src/main/java/org/jabref/logic/remote/RemotePreferences.java index aa85e8713ad..edda18d005c 100644 --- a/src/main/java/org/jabref/logic/remote/RemotePreferences.java +++ b/src/main/java/org/jabref/logic/remote/RemotePreferences.java @@ -1,5 +1,8 @@ package org.jabref.logic.remote; +import java.net.InetAddress; +import java.net.UnknownHostException; + /** * Place for handling the preferences for the remote communication */ @@ -34,4 +37,11 @@ public boolean isDifferentPort(int otherPort) { return getPort() != otherPort; } + /** + * Gets the IP address where the remote server is listening. + */ + public static InetAddress getIpAddress() throws UnknownHostException { + return InetAddress.getByName("localhost"); + } + } diff --git a/src/main/java/org/jabref/logic/remote/client/RemoteClient.java b/src/main/java/org/jabref/logic/remote/client/RemoteClient.java new file mode 100644 index 00000000000..5fc416da214 --- /dev/null +++ b/src/main/java/org/jabref/logic/remote/client/RemoteClient.java @@ -0,0 +1,68 @@ +package org.jabref.logic.remote.client; + +import java.io.IOException; +import java.net.Socket; + +import javafx.util.Pair; + +import org.jabref.logic.l10n.Localization; +import org.jabref.logic.remote.RemotePreferences; +import org.jabref.logic.remote.shared.Protocol; +import org.jabref.logic.remote.shared.RemoteMessage; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RemoteClient { + + private static final Logger LOGGER = LoggerFactory.getLogger(RemoteClient.class); + + private static final int TIMEOUT = 2000; + private int port; + + public RemoteClient(int port) { + this.port = port; + } + + public boolean ping() { + try (Protocol protocol = openNewConnection()) { + protocol.sendMessage(RemoteMessage.PING); + Pair response = protocol.receiveMessage(); + + if (response.getKey() == RemoteMessage.PONG && Protocol.IDENTIFIER.equals(response.getValue())) { + return true; + } else { + String port = String.valueOf(this.port); + String errorMessage = Localization.lang("Cannot use port %0 for remote operation; another application may be using it. Try specifying another port.", port); + LOGGER.error(errorMessage); + return false; + } + } catch (IOException e) { + LOGGER.debug("Could not ping server at port " + port, e); + return false; + } + } + + /** + * Attempt to send command line arguments to already running JabRef instance. + * + * @param args command line arguments. + * @return true if successful, false otherwise. + */ + public boolean sendCommandLineArguments(String[] args) { + try (Protocol protocol = openNewConnection()) { + protocol.sendMessage(RemoteMessage.SEND_COMMAND_LINE_ARGUMENTS, args); + Pair response = protocol.receiveMessage(); + return response.getKey() == RemoteMessage.OK; + } catch (IOException e) { + LOGGER.debug("Could not send args " + String.join(", ", args) + " to the server at port " + port, e); + return false; + } + } + + private Protocol openNewConnection() throws IOException { + Socket socket = new Socket(RemotePreferences.getIpAddress(), port); + socket.setSoTimeout(TIMEOUT); + return new Protocol(socket); + } +} diff --git a/src/main/java/org/jabref/logic/remote/client/RemoteListenerClient.java b/src/main/java/org/jabref/logic/remote/client/RemoteListenerClient.java deleted file mode 100644 index ce2b24998f3..00000000000 --- a/src/main/java/org/jabref/logic/remote/client/RemoteListenerClient.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.jabref.logic.remote.client; - -import java.net.InetAddress; -import java.net.Socket; - -import org.jabref.logic.l10n.Localization; -import org.jabref.logic.remote.shared.Protocol; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class RemoteListenerClient { - - private static final Logger LOGGER = LoggerFactory.getLogger(RemoteListenerClient.class); - - private static final int TIMEOUT = 2000; - - - private RemoteListenerClient() { - } - - /** - * Attempt to send command line arguments to already running JabRef instance. - * - * @param args Command line arguments. - * @return true if successful, false otherwise. - */ - public static boolean sendToActiveJabRefInstance(String[] args, int remoteServerPort) { - try (Socket socket = new Socket(InetAddress.getByName("localhost"), remoteServerPort)) { - socket.setSoTimeout(TIMEOUT); - - Protocol protocol = new Protocol(socket); - try { - String identifier = protocol.receiveMessage(); - - if (!Protocol.IDENTIFIER.equals(identifier)) { - String port = String.valueOf(remoteServerPort); - String errorMessage = Localization.lang("Cannot use port %0 for remote operation; another application may be using it. Try specifying another port.", port); - LOGGER.error(errorMessage); - return false; - } - protocol.sendMessage(String.join("\n", args)); - return true; - } finally { - protocol.close(); - } - } catch (Exception e) { - LOGGER.debug( - "Could not send args " + String.join(", ", args) + " to the server at port " + remoteServerPort, e); - return false; - } - } -} diff --git a/src/main/java/org/jabref/logic/remote/server/MessageHandler.java b/src/main/java/org/jabref/logic/remote/server/MessageHandler.java index 14648f8e992..f227bb175f6 100644 --- a/src/main/java/org/jabref/logic/remote/server/MessageHandler.java +++ b/src/main/java/org/jabref/logic/remote/server/MessageHandler.java @@ -3,6 +3,6 @@ @FunctionalInterface public interface MessageHandler { - void handleMessage(String message); + void handleCommandLineArguments(String[] message); } diff --git a/src/main/java/org/jabref/logic/remote/server/RemoteListenerServer.java b/src/main/java/org/jabref/logic/remote/server/RemoteListenerServer.java index 76757d46e2a..d87fa2d5b33 100644 --- a/src/main/java/org/jabref/logic/remote/server/RemoteListenerServer.java +++ b/src/main/java/org/jabref/logic/remote/server/RemoteListenerServer.java @@ -1,12 +1,15 @@ package org.jabref.logic.remote.server; import java.io.IOException; -import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; +import javafx.util.Pair; + +import org.jabref.logic.remote.RemotePreferences; import org.jabref.logic.remote.shared.Protocol; +import org.jabref.logic.remote.shared.RemoteMessage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,7 +26,7 @@ public class RemoteListenerServer implements Runnable { public RemoteListenerServer(MessageHandler messageHandler, int port) throws IOException { - this.serverSocket = new ServerSocket(port, BACKLOG, InetAddress.getByName("localhost")); + this.serverSocket = new ServerSocket(port, BACKLOG, RemotePreferences.getIpAddress()); this.messageHandler = messageHandler; } @@ -34,15 +37,10 @@ public void run() { try (Socket socket = serverSocket.accept()) { socket.setSoTimeout(ONE_SECOND_TIMEOUT); - Protocol protocol = new Protocol(socket); - protocol.sendMessage(Protocol.IDENTIFIER); - String message = protocol.receiveMessage(); - protocol.close(); - if (message.isEmpty()) { - continue; + try (Protocol protocol = new Protocol(socket)) { + Pair input = protocol.receiveMessage(); + handleMessage(protocol, input.getKey(), input.getValue()); } - messageHandler.handleMessage(message); - } catch (SocketException ex) { return; } catch (IOException e) { @@ -54,6 +52,24 @@ public void run() { } } + private void handleMessage(Protocol protocol, RemoteMessage type, Object argument) throws IOException { + switch (type) { + case PING: + protocol.sendMessage(RemoteMessage.PONG, Protocol.IDENTIFIER); + break; + case SEND_COMMAND_LINE_ARGUMENTS: + if (argument instanceof String[]) { + messageHandler.handleCommandLineArguments((String[]) argument); + protocol.sendMessage(RemoteMessage.OK); + } else { + throw new IOException("Argument for 'SEND_COMMAND_LINE_ARGUMENTS' is not of type String[]. Got " + argument); + } + break; + default: + throw new IOException("Unhandled message to server " + type); + } + } + public void closeServerSocket() { try { serverSocket.close(); diff --git a/src/main/java/org/jabref/logic/remote/shared/Protocol.java b/src/main/java/org/jabref/logic/remote/shared/Protocol.java index 25cfce268bf..446690059ad 100644 --- a/src/main/java/org/jabref/logic/remote/shared/Protocol.java +++ b/src/main/java/org/jabref/logic/remote/shared/Protocol.java @@ -1,53 +1,67 @@ package org.jabref.logic.remote.shared; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; import java.net.Socket; -import java.net.SocketTimeoutException; + +import javafx.util.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** + * @implNote The first byte of every message identifies its type as a {@link RemoteMessage}. * Every message is terminated with '\0'. */ -public class Protocol { +public class Protocol implements AutoCloseable { public static final String IDENTIFIER = "jabref"; private static final Logger LOGGER = LoggerFactory.getLogger(Protocol.class); private final Socket socket; - private final OutputStream out; - private final InputStream in; + private final ObjectOutputStream out; + private final ObjectInputStream in; public Protocol(Socket socket) throws IOException { this.socket = socket; - this.out = socket.getOutputStream(); - this.in = socket.getInputStream(); + this.out = new ObjectOutputStream(socket.getOutputStream()); + this.in = new ObjectInputStream(socket.getInputStream()); } - public void sendMessage(String message) throws IOException { - out.write(message.getBytes()); + public void sendMessage(RemoteMessage type) throws IOException { + out.writeObject(type); + out.writeObject(null); out.write('\0'); out.flush(); } - public String receiveMessage() throws IOException { - int c; - StringBuilder result = new StringBuilder(); + public void sendMessage(RemoteMessage type, Object argument) throws IOException { + out.writeObject(type); + out.writeObject(argument); + out.write('\0'); + out.flush(); + } + + public Pair receiveMessage() throws IOException { try { - while (((c = in.read()) != '\0') && (c >= 0)) { - result.append((char) c); + RemoteMessage type = (RemoteMessage) in.readObject(); + Object argument = in.readObject(); + int endOfMessage = in.read(); + + if (endOfMessage != '\0') { + throw new IOException("Message didn't end on correct end of message identifier. Got " + endOfMessage); } - } catch (SocketTimeoutException ex) { - LOGGER.info("Connection timed out.", ex); + + return new Pair<>(type, argument); + } catch (ClassNotFoundException e) { + throw new IOException("Could not deserialize message", e); } - return result.toString(); } + @Override public void close() { try { in.close(); diff --git a/src/main/java/org/jabref/logic/remote/shared/RemoteMessage.java b/src/main/java/org/jabref/logic/remote/shared/RemoteMessage.java new file mode 100644 index 00000000000..6f33cd2b3f6 --- /dev/null +++ b/src/main/java/org/jabref/logic/remote/shared/RemoteMessage.java @@ -0,0 +1,20 @@ +package org.jabref.logic.remote.shared; + +public enum RemoteMessage { + /** + * Send command line arguments. The message content is of type {@code String[]}. + */ + SEND_COMMAND_LINE_ARGUMENTS, + /** + * As a response to {@link #PING}. The message content is an identifier of type {@code String}. + */ + PONG, + /** + * Response signaling that the message was received successfully. No message content. + */ + OK, + /** + * Request server to identify itself. No message content. + */ + PING +} diff --git a/src/main/java/org/jabref/preferences/JabRefPreferences.java b/src/main/java/org/jabref/preferences/JabRefPreferences.java index 951ed50b612..83ebfd37fe7 100644 --- a/src/main/java/org/jabref/preferences/JabRefPreferences.java +++ b/src/main/java/org/jabref/preferences/JabRefPreferences.java @@ -833,6 +833,8 @@ private JabRefPreferences() { + "\\begin{abstract}

Abstract: \\format[HTMLChars]{\\abstract} \\end{abstract}__NEWLINE__" + "\\begin{review}

Review: \\format[HTMLChars]{\\review} \\end{review}" + "__NEWLINE__

"); + + setLanguageDependentDefaultValues(); } public static JabRefPreferences getInstance() { diff --git a/src/test/java/org/jabref/architecture/MainArchitectureTests.java b/src/test/java/org/jabref/architecture/MainArchitectureTests.java index 6d4dc2bcede..6c493a08d7b 100644 --- a/src/test/java/org/jabref/architecture/MainArchitectureTests.java +++ b/src/test/java/org/jabref/architecture/MainArchitectureTests.java @@ -34,6 +34,7 @@ public class MainArchitectureTests { private static final String EXCEPTION_PACKAGE_JAVA_FX_COLLECTIONS = "javafx.collections"; private static final String EXCEPTION_PACKAGE_JAVA_FX_BEANS = "javafx.beans"; private static final String EXCEPTION_CLASS_JAVA_FX_COLOR = "javafx.scene.paint.Color"; + private static final String EXCEPTION_CLASS_JAVA_FX_PAIR = "javafx.util.Pair"; private static Map> exceptions; @@ -48,6 +49,7 @@ public static void setUp() { logicExceptions.add(EXCEPTION_PACKAGE_JAVA_FX_COLLECTIONS); logicExceptions.add(EXCEPTION_PACKAGE_JAVA_FX_BEANS); logicExceptions.add(EXCEPTION_CLASS_JAVA_FX_COLOR); + logicExceptions.add(EXCEPTION_CLASS_JAVA_FX_PAIR); List modelExceptions = new ArrayList<>(4); modelExceptions.add(EXCEPTION_PACKAGE_JAVA_FX_COLLECTIONS); diff --git a/src/test/java/org/jabref/cli/JabRefCLITest.java b/src/test/java/org/jabref/cli/JabRefCLITest.java index bab13945e9e..12f12d1f751 100644 --- a/src/test/java/org/jabref/cli/JabRefCLITest.java +++ b/src/test/java/org/jabref/cli/JabRefCLITest.java @@ -38,4 +38,12 @@ public void testPreferencesExport() { assertTrue(cli.isDisableGui()); } + @Test + public void recognizesImportBibtex() { + String bibtex = "@article{test, title=\"test title\"}"; + JabRefCLI cli = new JabRefCLI(new String[]{"-ib", bibtex}); + assertEquals(Collections.emptyList(), cli.getLeftOver()); + assertTrue(cli.isBibtexImport()); + assertEquals(bibtex, cli.getBibtexImport()); + } } diff --git a/src/test/java/org/jabref/logic/remote/RemoteCommunicationTest.java b/src/test/java/org/jabref/logic/remote/RemoteCommunicationTest.java new file mode 100644 index 00000000000..a5bfe728722 --- /dev/null +++ b/src/test/java/org/jabref/logic/remote/RemoteCommunicationTest.java @@ -0,0 +1,76 @@ +package org.jabref.logic.remote; + +import java.io.IOException; + +import org.jabref.logic.remote.client.RemoteClient; +import org.jabref.logic.remote.server.MessageHandler; +import org.jabref.logic.remote.server.RemoteListenerServerLifecycle; +import org.jabref.support.DisabledOnCIServer; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for the case where the client and server are set-up correctly. + * Testing the exceptional cases happens in {@link RemoteSetupTest}. + */ +@DisabledOnCIServer("Tests fails sporadically on CI server") +class RemoteCommunicationTest { + + private RemoteClient client; + private RemoteListenerServerLifecycle serverLifeCycle; + private MessageHandler server; + + @BeforeEach + void setUp() { + final int port = 34567; + + server = mock(MessageHandler.class); + serverLifeCycle = new RemoteListenerServerLifecycle(); + serverLifeCycle.openAndStart(server, port); + + client = new RemoteClient(port); + } + + @AfterEach + void tearDown() { + serverLifeCycle.close(); + } + + @Test + void pingReturnsTrue() throws IOException, InterruptedException { + assertTrue(client.ping()); + } + + @Test + void commandLineArgumentSinglePassedToServer() { + final String[] message = new String[]{"my message"}; + + client.sendCommandLineArguments(message); + + verify(server).handleCommandLineArguments(message); + } + + @Test + void commandLineArgumentTwoPassedToServer() { + final String[] message = new String[]{"my message", "second"}; + + client.sendCommandLineArguments(message); + + verify(server).handleCommandLineArguments(message); + } + + @Test + void commandLineArgumentMultiLinePassedToServer() { + final String[] message = new String[]{"my message\n second line", "second \r and third"}; + + client.sendCommandLineArguments(message); + + verify(server).handleCommandLineArguments(message); + } +} diff --git a/src/test/java/org/jabref/logic/remote/RemoteSetupTest.java b/src/test/java/org/jabref/logic/remote/RemoteSetupTest.java new file mode 100644 index 00000000000..19c4e350403 --- /dev/null +++ b/src/test/java/org/jabref/logic/remote/RemoteSetupTest.java @@ -0,0 +1,132 @@ +package org.jabref.logic.remote; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.charset.StandardCharsets; + +import org.jabref.logic.remote.client.RemoteClient; +import org.jabref.logic.remote.server.MessageHandler; +import org.jabref.logic.remote.server.RemoteListenerServerLifecycle; +import org.jabref.logic.util.OS; +import org.jabref.support.DisabledOnCIServer; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +/** + * Tests where the remote client and server setup is wrong. + */ +@DisabledOnCIServer("Tests fails sporadically on CI server") +class RemoteSetupTest { + + private MessageHandler messageHandler; + + @BeforeEach + void setUp() { + messageHandler = mock(MessageHandler.class); + } + + @Test + void testGoodCase() { + final int port = 34567; + final String[] message = new String[]{"MYMESSAGE"}; + + try (RemoteListenerServerLifecycle server = new RemoteListenerServerLifecycle()) { + assertFalse(server.isOpen()); + server.openAndStart(messageHandler, port); + assertTrue(server.isOpen()); + assertTrue(new RemoteClient(port).sendCommandLineArguments(message)); + verify(messageHandler).handleCommandLineArguments(message); + server.stop(); + assertFalse(server.isOpen()); + } + } + + @Test + void testGoodCaseWithAllLifecycleMethods() { + final int port = 34567; + final String[] message = new String[]{"MYMESSAGE"}; + + try (RemoteListenerServerLifecycle server = new RemoteListenerServerLifecycle()) { + assertFalse(server.isOpen()); + assertTrue(server.isNotStartedBefore()); + server.stop(); + assertFalse(server.isOpen()); + assertTrue(server.isNotStartedBefore()); + server.open(messageHandler, port); + assertTrue(server.isOpen()); + assertTrue(server.isNotStartedBefore()); + server.start(); + assertTrue(server.isOpen()); + assertFalse(server.isNotStartedBefore()); + + assertTrue(new RemoteClient(port).sendCommandLineArguments(message)); + verify(messageHandler).handleCommandLineArguments(message); + server.stop(); + assertFalse(server.isOpen()); + assertTrue(server.isNotStartedBefore()); + } + } + + @Test + void testPortAlreadyInUse() throws IOException { + assumeFalse(OS.OS_X); + + final int port = 34567; + + try (ServerSocket socket = new ServerSocket(port)) { + assertTrue(socket.isBound()); + + try (RemoteListenerServerLifecycle server = new RemoteListenerServerLifecycle()) { + assertFalse(server.isOpen()); + server.openAndStart(messageHandler, port); + assertFalse(server.isOpen()); + verify(messageHandler, never()).handleCommandLineArguments(any()); + } + } + } + + @Test + void testClientTimeout() { + final int port = 34567; + final String message = "MYMESSAGE"; + + assertFalse(new RemoteClient(port).sendCommandLineArguments(new String[]{message})); + } + + @Test + void pingReturnsFalseForWrongServerListening() throws IOException, InterruptedException { + final int port = 34567; + + try (ServerSocket socket = new ServerSocket(port)) { + // Setup dummy server always answering "whatever" + new Thread(() -> { + try (Socket message = socket.accept(); OutputStream os = message.getOutputStream()) { + os.write("whatever".getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + // Ignored + } + }).start(); + Thread.sleep(100); + + assertFalse(new RemoteClient(port).ping()); + } + } + + @Test + void pingReturnsFalseForNoServerListening() throws IOException, InterruptedException { + final int port = 34567; + + assertFalse(new RemoteClient(port).ping()); + } +} diff --git a/src/test/java/org/jabref/logic/remote/RemoteTest.java b/src/test/java/org/jabref/logic/remote/RemoteTest.java deleted file mode 100644 index 9155fa2c6fb..00000000000 --- a/src/test/java/org/jabref/logic/remote/RemoteTest.java +++ /dev/null @@ -1,112 +0,0 @@ -package org.jabref.logic.remote; - -import java.io.IOException; -import java.io.OutputStream; -import java.net.ServerSocket; -import java.net.Socket; -import java.nio.charset.StandardCharsets; - -import org.jabref.logic.remote.client.RemoteListenerClient; -import org.jabref.logic.remote.server.RemoteListenerServerLifecycle; -import org.jabref.logic.util.OS; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; -import static org.junit.jupiter.api.Assumptions.assumeFalse; - -public class RemoteTest { - - @Test - public void testGoodCase() { - final int port = 34567; - final String message = "MYMESSAGE"; - - - try (RemoteListenerServerLifecycle server = new RemoteListenerServerLifecycle()) { - assertFalse(server.isOpen()); - server.openAndStart(msg -> assertEquals(message, msg), port); - assertTrue(server.isOpen()); - assertTrue(RemoteListenerClient.sendToActiveJabRefInstance(new String[]{message}, port)); - server.stop(); - assertFalse(server.isOpen()); - } - } - - @Test - public void testGoodCaseWithAllLifecycleMethods() { - final int port = 34567; - final String message = "MYMESSAGE"; - - try (RemoteListenerServerLifecycle server = new RemoteListenerServerLifecycle()) { - assertFalse(server.isOpen()); - assertTrue(server.isNotStartedBefore()); - server.stop(); - assertFalse(server.isOpen()); - assertTrue(server.isNotStartedBefore()); - server.open(msg -> assertEquals(message, msg), port); - assertTrue(server.isOpen()); - assertTrue(server.isNotStartedBefore()); - server.start(); - assertTrue(server.isOpen()); - assertFalse(server.isNotStartedBefore()); - - assertTrue(RemoteListenerClient.sendToActiveJabRefInstance(new String[]{message}, port)); - server.stop(); - assertFalse(server.isOpen()); - assertTrue(server.isNotStartedBefore()); - } - } - - @Test - public void testPortAlreadyInUse() throws IOException { - assumeFalse(OS.OS_X); - - final int port = 34567; - - try (ServerSocket socket = new ServerSocket(port)) { - assertTrue(socket.isBound()); - - try (RemoteListenerServerLifecycle server = new RemoteListenerServerLifecycle()) { - assertFalse(server.isOpen()); - server.openAndStart(msg -> fail("should not happen"), port); - assertFalse(server.isOpen()); - } catch (Exception e) { - fail("Exception: " + e.getMessage()); - } - } - } - - @Test - public void testClientTimeout() { - final int port = 34567; - final String message = "MYMESSAGE"; - - assertFalse(RemoteListenerClient.sendToActiveJabRefInstance(new String[]{message}, port)); - } - - @Test - public void testClientConnectingToWrongServer() throws IOException, InterruptedException { - final int port = 34567; - final String message = "MYMESSAGE"; - - try (ServerSocket socket = new ServerSocket(port)) { - new Thread() { - - @Override - public void run() { - try (Socket socket2 = socket.accept(); OutputStream os = socket2.getOutputStream()) { - os.write("whatever".getBytes(StandardCharsets.UTF_8)); - } catch (IOException e) { - // Ignored - } - } - }.start(); - Thread.sleep(100); - assertFalse(RemoteListenerClient.sendToActiveJabRefInstance(new String[]{message}, port)); - } - } -}