From 1b545e48c9b1d9ceb09ddae3bf459a7d1c4b0205 Mon Sep 17 00:00:00 2001 From: Siedlerchr Date: Sat, 12 Aug 2017 10:57:23 +0200 Subject: [PATCH 01/37] Sharelatex Integration --- build.gradle | 7 + external-libraries.txt | 10 + src/main/java/org/jabref/Globals.java | 3 + .../java/org/jabref/gui/DefaultInjector.java | 3 + src/main/java/org/jabref/gui/JabRefFrame.java | 35 +- .../DisconnectFromSharelatexAction.java | 23 ++ .../SendChangesToShareLatexAction.java | 29 ++ .../SynchronizeWithShareLatexAction.java | 28 ++ .../org/jabref/gui/entryeditor/SourceTab.java | 4 +- .../ShareLatexLoginDialogController.java | 54 +++ .../sharelatex/ShareLatexLoginDialogView.java | 19 + .../ShareLatexLoginDialogViewModel.java | 7 + .../ShareLatexProjectDialogController.java | 84 +++++ .../ShareLatexProjectDialogView.java | 19 + .../ShareLatexProjectDialogViewModel.java | 61 ++++ .../ShareLatexProjectViewModel.java | 63 ++++ .../MyCustomClientEndpointConfigurator.java | 43 +++ .../sharelatex/ShareLatexJsonMessage.java | 66 ++++ .../logic/sharelatex/ShareLatexManager.java | 76 ++++ .../logic/sharelatex/ShareLatexParser.java | 175 +++++++++ .../logic/sharelatex/SharelatexConnector.java | 171 +++++++++ .../logic/sharelatex/SharelatexDoc.java | 61 ++++ .../sharelatex/WebSocketClientWrapper.java | 337 ++++++++++++++++++ .../ShareLatexContinueMessageEvent.java | 5 + .../events/ShareLatexEntryMessageEvent.java | 27 ++ .../events/ShareLatexErrorMessageEvent.java | 10 + .../model/sharelatex/ShareLatexProject.java | 71 ++++ .../jabref/preferences/JabRefPreferences.java | 3 +- .../preferences/PreferencesService.java | 4 + src/main/resources/images/Icons.properties | 1 + src/main/resources/images/external/lion.png | Bin 0 -> 804 bytes .../gui/sharelatex/ShareLatexLoginDialog.fxml | 47 +++ .../sharelatex/ShareLatexProjectDialog.fxml | 32 ++ .../sharelatex/ShareLatexJsonMessageTest.java | 43 +++ .../sharelatex/ShareLatexManagerTest.java | 33 ++ .../sharelatex/ShareLatexParserTest.java | 233 ++++++++++++ .../sharelatex/SharelatexConnectorTest.java | 26 ++ 37 files changed, 1897 insertions(+), 16 deletions(-) create mode 100644 src/main/java/org/jabref/gui/actions/DisconnectFromSharelatexAction.java create mode 100644 src/main/java/org/jabref/gui/actions/SendChangesToShareLatexAction.java create mode 100644 src/main/java/org/jabref/gui/actions/SynchronizeWithShareLatexAction.java create mode 100644 src/main/java/org/jabref/gui/sharelatex/ShareLatexLoginDialogController.java create mode 100644 src/main/java/org/jabref/gui/sharelatex/ShareLatexLoginDialogView.java create mode 100644 src/main/java/org/jabref/gui/sharelatex/ShareLatexLoginDialogViewModel.java create mode 100644 src/main/java/org/jabref/gui/sharelatex/ShareLatexProjectDialogController.java create mode 100644 src/main/java/org/jabref/gui/sharelatex/ShareLatexProjectDialogView.java create mode 100644 src/main/java/org/jabref/gui/sharelatex/ShareLatexProjectDialogViewModel.java create mode 100644 src/main/java/org/jabref/gui/sharelatex/ShareLatexProjectViewModel.java create mode 100644 src/main/java/org/jabref/logic/sharelatex/MyCustomClientEndpointConfigurator.java create mode 100644 src/main/java/org/jabref/logic/sharelatex/ShareLatexJsonMessage.java create mode 100644 src/main/java/org/jabref/logic/sharelatex/ShareLatexManager.java create mode 100644 src/main/java/org/jabref/logic/sharelatex/ShareLatexParser.java create mode 100644 src/main/java/org/jabref/logic/sharelatex/SharelatexConnector.java create mode 100644 src/main/java/org/jabref/logic/sharelatex/SharelatexDoc.java create mode 100644 src/main/java/org/jabref/logic/sharelatex/WebSocketClientWrapper.java create mode 100644 src/main/java/org/jabref/logic/sharelatex/events/ShareLatexContinueMessageEvent.java create mode 100644 src/main/java/org/jabref/logic/sharelatex/events/ShareLatexEntryMessageEvent.java create mode 100644 src/main/java/org/jabref/logic/sharelatex/events/ShareLatexErrorMessageEvent.java create mode 100644 src/main/java/org/jabref/model/sharelatex/ShareLatexProject.java create mode 100644 src/main/resources/images/external/lion.png create mode 100644 src/main/resources/org/jabref/gui/sharelatex/ShareLatexLoginDialog.fxml create mode 100644 src/main/resources/org/jabref/gui/sharelatex/ShareLatexProjectDialog.fxml create mode 100644 src/test/java/org/jabref/logic/sharelatex/ShareLatexJsonMessageTest.java create mode 100644 src/test/java/org/jabref/logic/sharelatex/ShareLatexManagerTest.java create mode 100644 src/test/java/org/jabref/logic/sharelatex/ShareLatexParserTest.java create mode 100644 src/test/java/org/jabref/logic/sharelatex/SharelatexConnectorTest.java diff --git a/build.gradle b/build.gradle index f3f7b8c4126..41124e68d79 100644 --- a/build.gradle +++ b/build.gradle @@ -111,6 +111,7 @@ dependencies { compile 'commons-logging:commons-logging:1.2' compile 'org.jsoup:jsoup:1.10.3' + compile 'com.mashape.unirest:unirest-java:1.4.9' compile 'info.debatty:java-string-similarity:0.24' @@ -130,6 +131,12 @@ dependencies { compile group: 'com.microsoft.azure', name: 'applicationinsights-core', version: '1.0.+' compile group: 'com.microsoft.azure', name: 'applicationinsights-logging-log4j2', version: '1.0.+' + compile "org.glassfish.tyrus.bundles:tyrus-standalone-client:1.13.1" + compile "org.glassfish.tyrus.ext:tyrus-extension-deflate:1.13.1" + + compile "com.google.code.gson:gson:2.8.0" + compile "org.bitbucket.cowwoc:diff-match-patch:1.1" + testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:2.8.47' testCompile 'com.github.tomakehurst:wiremock:2.7.1' diff --git a/external-libraries.txt b/external-libraries.txt index 02e257a40bc..f4b109f0403 100644 --- a/external-libraries.txt +++ b/external-libraries.txt @@ -143,6 +143,16 @@ Project: Flowless URL: https://github.com/TomasMikula/Flowless License: BSD-2-Clause +Id: org.glassfish.tyrus.bundles:tyrus-standalone-client +Projekt: Tyrus +URL: https://tyrus.java.net/ +Licence: CDDL 1.1 and GPL 2 with CPE + +Id: com.google.code.gson:gson +Project: google-gson +URL: https://github.com/google/gson +License: Apache-2.0 + Id: org.fxmisc.richtext:richtextfx Project: RichTextFX URL: https://github.com/TomasMikula/RichTextFX diff --git a/src/main/java/org/jabref/Globals.java b/src/main/java/org/jabref/Globals.java index 27fa65c92a6..64dfcfd259c 100644 --- a/src/main/java/org/jabref/Globals.java +++ b/src/main/java/org/jabref/Globals.java @@ -14,6 +14,7 @@ import org.jabref.logic.journals.JournalAbbreviationLoader; import org.jabref.logic.protectedterms.ProtectedTermsLoader; import org.jabref.logic.remote.server.RemoteListenerServerLifecycle; +import org.jabref.logic.sharelatex.ShareLatexManager; import org.jabref.logic.util.BuildInfo; import org.jabref.preferences.JabRefPreferences; @@ -48,6 +49,8 @@ public class Globals { * Manager for the state of the GUI. */ public static StateManager stateManager = new StateManager(); + + public static ShareLatexManager shareLatexManager = new ShareLatexManager(); // Key binding preferences private static KeyBindingRepository keyBindingRepository; // Background tasks diff --git a/src/main/java/org/jabref/gui/DefaultInjector.java b/src/main/java/org/jabref/gui/DefaultInjector.java index 93845ce5dca..60011aee853 100644 --- a/src/main/java/org/jabref/gui/DefaultInjector.java +++ b/src/main/java/org/jabref/gui/DefaultInjector.java @@ -6,6 +6,7 @@ import org.jabref.gui.keyboard.KeyBindingRepository; import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.journals.JournalAbbreviationLoader; +import org.jabref.logic.sharelatex.ShareLatexManager; import org.jabref.preferences.PreferencesService; import com.airhacks.afterburner.injection.Injector; @@ -35,6 +36,8 @@ private static Object createDependency(Class clazz) { return Globals.journalAbbreviationLoader; } else if (clazz == StateManager.class) { return Globals.stateManager; + } else if (clazz == ShareLatexManager.class) { + return Globals.shareLatexManager; } else { try { return clazz.newInstance(); diff --git a/src/main/java/org/jabref/gui/JabRefFrame.java b/src/main/java/org/jabref/gui/JabRefFrame.java index 92095b78852..1e7c1dec788 100644 --- a/src/main/java/org/jabref/gui/JabRefFrame.java +++ b/src/main/java/org/jabref/gui/JabRefFrame.java @@ -67,6 +67,7 @@ import org.jabref.gui.actions.Actions; import org.jabref.gui.actions.AutoLinkFilesAction; import org.jabref.gui.actions.ConnectToSharedDatabaseAction; +import org.jabref.gui.actions.DisconnectFromSharelatexAction; import org.jabref.gui.actions.ErrorConsoleAction; import org.jabref.gui.actions.IntegrityCheckAction; import org.jabref.gui.actions.LookupIdentifierAction; @@ -78,7 +79,9 @@ import org.jabref.gui.actions.NewSubDatabaseAction; import org.jabref.gui.actions.OpenBrowserAction; import org.jabref.gui.actions.SearchForUpdateAction; +import org.jabref.gui.actions.SendChangesToShareLatexAction; import org.jabref.gui.actions.SortTabsAction; +import org.jabref.gui.actions.SynchronizeWithShareLatexAction; import org.jabref.gui.autosaveandbackup.AutosaveUIManager; import org.jabref.gui.bibtexkeypattern.BibtexKeyPatternDialog; import org.jabref.gui.customentrytypes.EntryCustomizationDialog; @@ -153,6 +156,7 @@ * The main window of the application. */ public class JabRefFrame extends JFrame implements OutputPrinter { + private static final Log LOGGER = LogFactory.getLog(JabRefFrame.class); // Frame titles. @@ -170,13 +174,13 @@ public class JabRefFrame extends JFrame implements OutputPrinter { private final JLabel statusLine = new JLabel("", SwingConstants.LEFT); private final JLabel statusLabel = new JLabel( Localization.lang("Status") - + ':', SwingConstants.LEFT); + + ':', + SwingConstants.LEFT); private final JProgressBar progressBar = new JProgressBar(); private final FileHistoryMenu fileHistory = new FileHistoryMenu(prefs, this); private final OpenDatabaseAction open = new OpenDatabaseAction(this, true); private final EditModeAction editModeAction = new EditModeAction(); - // Here we instantiate menu/toolbar actions. Actions regarding // the currently open database are defined as a GeneralAction // with a unique command string. This causes the appropriate @@ -391,6 +395,10 @@ public void actionPerformed(ActionEvent e) { Localization.lang("Unabbreviate journal names of the selected entries"), Globals.getKeyPrefs().getKey(KeyBinding.UNABBREVIATE)); private final AbstractAction manageJournals = new ManageJournalsAction(); + private final AbstractAction synchronizeWithSharelatex = new SynchronizeWithShareLatexAction(); + private final AbstractAction sendChangesToShareLatex = new SendChangesToShareLatexAction(); + private final AbstractAction disconnectFromSharelatex = new DisconnectFromSharelatexAction(); + private final AbstractAction databaseProperties = new DatabasePropertiesAction(); private final AbstractAction bibtexKeyPattern = new BibtexKeyPatternAction(); private final AbstractAction errorConsole = new ErrorConsoleAction(); @@ -421,8 +429,7 @@ public void actionPerformed(ActionEvent e) { private final GeneralAction findUnlinkedFiles = new GeneralAction( FindUnlinkedFilesDialog.ACTION_COMMAND, FindUnlinkedFilesDialog.ACTION_MENU_TITLE, FindUnlinkedFilesDialog.ACTION_SHORT_DESCRIPTION, - Globals.getKeyPrefs().getKey(KeyBinding.FIND_UNLINKED_FILES) - ); + Globals.getKeyPrefs().getKey(KeyBinding.FIND_UNLINKED_FILES)); private final AutoLinkFilesAction autoLinkFile = new AutoLinkFilesAction(); // The action for adding a new entry of unspecified type. private final NewEntryAction newEntryAction = new NewEntryAction(this, Globals.getKeyPrefs().getKey(KeyBinding.NEW_ENTRY)); @@ -613,9 +620,7 @@ public void windowClosing(WindowEvent e) { // Poor-mans binding to global state // We need to invoke this in the JavaFX thread as all the listeners sit there - Platform.runLater(() -> - Globals.stateManager.activeDatabaseProperty().setValue(Optional.of(currentBasePanel.getBibDatabaseContext())) - ); + Platform.runLater(() -> Globals.stateManager.activeDatabaseProperty().setValue(Optional.of(currentBasePanel.getBibDatabaseContext()))); if (new SearchPreferences(Globals.prefs).isGlobalSearch()) { globalSearchBar.performSearch(); } else { @@ -707,7 +712,7 @@ public void setWindowTitle() { String changeFlag = panel.isModified() && !isAutosaveEnabled ? "*" : ""; String databaseFile = panel.getBibDatabaseContext().getDatabaseFile().map(File::getPath) .orElse(GUIGlobals.UNTITLED_TITLE); - setTitle(FRAME_TITLE + " - " + databaseFile + changeFlag + modeInfo); + setTitle(FRAME_TITLE + " - " + databaseFile + changeFlag + modeInfo); } else if (panel.getBibDatabaseContext().getLocation() == DatabaseLocation.SHARED) { setTitle(FRAME_TITLE + " - " + panel.getBibDatabaseContext().getDBMSSynchronizer().getDBName() + " [" + Localization.lang("shared") + "]" + modeInfo); @@ -1056,6 +1061,10 @@ private void fillMenu() { file.addSeparator(); file.add(connectToSharedDatabaseAction); file.add(pullChangesFromSharedDatabase); + file.addSeparator(); + file.add(synchronizeWithSharelatex); + file.add(sendChangesToShareLatex); + file.add(disconnectFromSharelatex); file.addSeparator(); file.add(databaseProperties); @@ -1332,6 +1341,7 @@ private void createToolBar() { tlb.addAction(cleanupEntries); tlb.addAction(mergeEntries); tlb.addAction(pullChangesFromSharedDatabase); + tlb.addAction(synchronizeWithSharelatex); tlb.addAction(openConsole); tlb.addSeparator(); @@ -1582,7 +1592,7 @@ private void trackOpenNewDatabase(BasePanel basePanel) { Map properties = new HashMap<>(); Map measurements = new HashMap<>(); - measurements.put("NumberOfEntries", (double)basePanel.getDatabaseContext().getDatabase().getEntryCount()); + measurements.put("NumberOfEntries", (double) basePanel.getDatabaseContext().getDatabase().getEntryCount()); Globals.getTelemetryClient().ifPresent(client -> client.trackEvent("OpenNewDatabase", properties, measurements)); } @@ -1927,6 +1937,7 @@ public GlobalSearchBar getGlobalSearchBar() { } private static class MyGlassPane extends JPanel { + public MyGlassPane() { addKeyListener(new KeyAdapter() { // Nothing @@ -2087,8 +2098,7 @@ private class ChangeTabAction extends MnemonicAwareAction { private final boolean next; public ChangeTabAction(boolean next) { - putValue(Action.NAME, next ? Localization.menuTitle("Next tab") : - Localization.menuTitle("Previous tab")); + putValue(Action.NAME, next ? Localization.menuTitle("Next tab") : Localization.menuTitle("Previous tab")); this.next = next; putValue(Action.ACCELERATOR_KEY, next ? Globals.getKeyPrefs().getKey(KeyBinding.NEXT_TAB) : Globals.getKeyPrefs().getKey(KeyBinding.PREVIOUS_TAB)); @@ -2125,7 +2135,8 @@ public EditAction(String command, String menuTitle, String description, KeyStrok putValue(Action.SHORT_DESCRIPTION, description); } - @Override public void actionPerformed(ActionEvent e) { + @Override + public void actionPerformed(ActionEvent e) { LOGGER.debug(Globals.getFocusListener().getFocused().toString()); JComponent source = Globals.getFocusListener().getFocused(); diff --git a/src/main/java/org/jabref/gui/actions/DisconnectFromSharelatexAction.java b/src/main/java/org/jabref/gui/actions/DisconnectFromSharelatexAction.java new file mode 100644 index 00000000000..0e587ba6902 --- /dev/null +++ b/src/main/java/org/jabref/gui/actions/DisconnectFromSharelatexAction.java @@ -0,0 +1,23 @@ +package org.jabref.gui.actions; + +import java.awt.event.ActionEvent; + +import javax.swing.AbstractAction; +import javax.swing.Action; + +import org.jabref.Globals; + +public class DisconnectFromSharelatexAction extends AbstractAction { + + public DisconnectFromSharelatexAction() { + super(); + putValue(Action.NAME, "Disconnect from ShareLaTeX"); + + } + + @Override + public void actionPerformed(ActionEvent e) { + Globals.shareLatexManager.disconnectAndCloseConnection(); + } + +} diff --git a/src/main/java/org/jabref/gui/actions/SendChangesToShareLatexAction.java b/src/main/java/org/jabref/gui/actions/SendChangesToShareLatexAction.java new file mode 100644 index 00000000000..d919ccbaf91 --- /dev/null +++ b/src/main/java/org/jabref/gui/actions/SendChangesToShareLatexAction.java @@ -0,0 +1,29 @@ +package org.jabref.gui.actions; + +import java.awt.event.ActionEvent; + +import javax.swing.AbstractAction; +import javax.swing.Action; + +import org.jabref.Globals; +import org.jabref.gui.StateManager; +import org.jabref.logic.sharelatex.ShareLatexManager; + +public class SendChangesToShareLatexAction extends AbstractAction { + + public SendChangesToShareLatexAction() { + super(); + putValue(Action.NAME, "Send changes to ShareLaTeX Server"); + + } + + @Override + public void actionPerformed(ActionEvent e) { + + ShareLatexManager manager = Globals.shareLatexManager; + StateManager stateManager = Globals.stateManager; + manager.sendNewDatabaseContent(stateManager.getActiveDatabase().get()); + System.out.println("Send changes"); + } + +} diff --git a/src/main/java/org/jabref/gui/actions/SynchronizeWithShareLatexAction.java b/src/main/java/org/jabref/gui/actions/SynchronizeWithShareLatexAction.java new file mode 100644 index 00000000000..e56148cd422 --- /dev/null +++ b/src/main/java/org/jabref/gui/actions/SynchronizeWithShareLatexAction.java @@ -0,0 +1,28 @@ +package org.jabref.gui.actions; + +import java.awt.event.ActionEvent; + +import javax.swing.AbstractAction; +import javax.swing.Action; + +import javafx.application.Platform; + +import org.jabref.gui.IconTheme; +import org.jabref.gui.sharelatex.ShareLatexLoginDialogView; + +public class SynchronizeWithShareLatexAction extends AbstractAction { + + public SynchronizeWithShareLatexAction() { + super(); + putValue(Action.NAME, "Synchronize with ShareLaTeX"); + putValue(Action.SMALL_ICON, IconTheme.getImage("sharelatex")); + putValue(Action.SHORT_DESCRIPTION, "Synchronize with ShareLaTeX"); + + } + + @Override + public void actionPerformed(ActionEvent e) { + Platform.runLater(() -> new ShareLatexLoginDialogView().show()); + + } +} diff --git a/src/main/java/org/jabref/gui/entryeditor/SourceTab.java b/src/main/java/org/jabref/gui/entryeditor/SourceTab.java index b92975cb71d..cecca578889 100644 --- a/src/main/java/org/jabref/gui/entryeditor/SourceTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/SourceTab.java @@ -96,7 +96,6 @@ private Node createSourceEditor(BibEntry entry, BibDatabaseMode mode) { codeArea.setEditable(false); LOGGER.debug("Incorrect entry", ex); } - return new VirtualizedScrollPane<>(codeArea); } @@ -206,8 +205,7 @@ private void storeSource() { Localization.lang("Problem with parsing entry"), Localization.lang("Error") + ": " + ex.getMessage(), Localization.lang("Edit"), - Localization.lang("Revert to original source") - ); + Localization.lang("Revert to original source")); if (!keepEditing) { // Revert diff --git a/src/main/java/org/jabref/gui/sharelatex/ShareLatexLoginDialogController.java b/src/main/java/org/jabref/gui/sharelatex/ShareLatexLoginDialogController.java new file mode 100644 index 00000000000..e1d03ff7903 --- /dev/null +++ b/src/main/java/org/jabref/gui/sharelatex/ShareLatexLoginDialogController.java @@ -0,0 +1,54 @@ +package org.jabref.gui.sharelatex; + +import javax.inject.Inject; + +import javafx.fxml.FXML; +import javafx.scene.control.Alert.AlertType; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; + +import org.jabref.gui.AbstractController; +import org.jabref.gui.DialogService; +import org.jabref.gui.FXDialog; +import org.jabref.gui.FXDialogService; +import org.jabref.logic.sharelatex.ShareLatexManager; + +public class ShareLatexLoginDialogController extends AbstractController { + + @FXML private TextField tbAddress; + @FXML private TextField tbUsername; + @FXML private PasswordField pfPassword; + @Inject private ShareLatexManager manager; + + @FXML + private void initialize() { + viewModel = new ShareLatexLoginDialogViewModel(); + } + + @FXML + private void closeDialog() { + getStage().close(); + } + + @FXML + private void signIn() { + + try { + String result = manager.login(tbAddress.getText(), tbUsername.getText(), pfPassword.getText()); + if (result.contains("incorrect")) { + FXDialog dlg = new FXDialog(AlertType.ERROR); + dlg.setContentText("Your email or password is incorrect. Please try again"); + dlg.showAndWait(); + } else { + ShareLatexProjectDialogView dlgprojects = new ShareLatexProjectDialogView(); + dlgprojects.show(); + closeDialog(); + } + } catch (Exception e) { + DialogService dlg = new FXDialogService(); + dlg.showErrorDialogAndWait(e); + + } + + } +} diff --git a/src/main/java/org/jabref/gui/sharelatex/ShareLatexLoginDialogView.java b/src/main/java/org/jabref/gui/sharelatex/ShareLatexLoginDialogView.java new file mode 100644 index 00000000000..8053ac9c43d --- /dev/null +++ b/src/main/java/org/jabref/gui/sharelatex/ShareLatexLoginDialogView.java @@ -0,0 +1,19 @@ +package org.jabref.gui.sharelatex; + +import javafx.scene.control.Alert.AlertType; +import javafx.scene.control.DialogPane; + +import org.jabref.gui.AbstractDialogView; +import org.jabref.gui.FXDialog; + +public class ShareLatexLoginDialogView extends AbstractDialogView { + + @Override + public void show() { + FXDialog sharelatexProjectDialog = new FXDialog(AlertType.INFORMATION, "Sharelatex Project Dialog"); + sharelatexProjectDialog.setDialogPane((DialogPane) this.getView()); + sharelatexProjectDialog.setResizable(true); + sharelatexProjectDialog.show(); + } + +} diff --git a/src/main/java/org/jabref/gui/sharelatex/ShareLatexLoginDialogViewModel.java b/src/main/java/org/jabref/gui/sharelatex/ShareLatexLoginDialogViewModel.java new file mode 100644 index 00000000000..a1eba10e4b4 --- /dev/null +++ b/src/main/java/org/jabref/gui/sharelatex/ShareLatexLoginDialogViewModel.java @@ -0,0 +1,7 @@ +package org.jabref.gui.sharelatex; + +import org.jabref.gui.AbstractViewModel; + +public class ShareLatexLoginDialogViewModel extends AbstractViewModel { + //default construtor used +} diff --git a/src/main/java/org/jabref/gui/sharelatex/ShareLatexProjectDialogController.java b/src/main/java/org/jabref/gui/sharelatex/ShareLatexProjectDialogController.java new file mode 100644 index 00000000000..282cd8f93b2 --- /dev/null +++ b/src/main/java/org/jabref/gui/sharelatex/ShareLatexProjectDialogController.java @@ -0,0 +1,84 @@ +package org.jabref.gui.sharelatex; + +import java.io.IOException; +import java.util.Optional; + +import javax.inject.Inject; + +import javafx.fxml.FXML; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.cell.CheckBoxTableCell; + +import org.jabref.gui.AbstractController; +import org.jabref.gui.StateManager; +import org.jabref.logic.sharelatex.ShareLatexManager; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.preferences.PreferencesService; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +public class ShareLatexProjectDialogController extends AbstractController { + + private static final Log LOGGER = LogFactory.getLog(ShareLatexProjectDialogViewModel.class); + + @FXML private TableColumn colActive; + @FXML private TableColumn colTitle; + @FXML private TableColumn colFirstName; + @FXML private TableColumn colLastName; + @FXML private TableColumn colLastModified; + @FXML private TableView tblProjects; + @Inject private ShareLatexManager manager; + @Inject private StateManager stateManager; + @Inject private PreferencesService preferences; + + @FXML + private void initialize() { + viewModel = new ShareLatexProjectDialogViewModel(stateManager, manager); + try { + viewModel.addProjects(manager.getProjects()); + } catch (IOException e) { + LOGGER.error("Could not add projects", e); + } + + tblProjects.setEditable(true); + colActive.setEditable(true); + + colActive.setCellFactory(CheckBoxTableCell.forTableColumn(colActive)); + + colActive.setCellValueFactory(cellData -> cellData.getValue().isActiveProperty()); + colTitle.setCellValueFactory(cellData -> cellData.getValue().getProjectTitle()); + colFirstName.setCellValueFactory(cellData -> cellData.getValue().getFirstName()); + colLastName.setCellValueFactory(cellData -> cellData.getValue().getLastName()); + colLastModified.setCellValueFactory(cellData -> cellData.getValue().getLastUpdated()); + setBindings(); + + } + + private void setBindings() { + tblProjects.itemsProperty().bindBidirectional(viewModel.projectsProperty()); + } + + @FXML + private void synchronizeLibrary() { + + Optional projects = viewModel.projectsProperty().filtered(x -> x.isActive()) + .stream().findFirst(); + + if (projects.isPresent() && stateManager.getActiveDatabase().isPresent()) { + String projectID = projects.get().getProjectId(); + BibDatabaseContext database = stateManager.getActiveDatabase().get(); + + manager.startWebSocketHandler(projectID, database, preferences.getImportFormatPreferences()); + } + + cancelAndClose(); + + } + + @FXML + private void cancelAndClose() { + getStage().close(); + } +} diff --git a/src/main/java/org/jabref/gui/sharelatex/ShareLatexProjectDialogView.java b/src/main/java/org/jabref/gui/sharelatex/ShareLatexProjectDialogView.java new file mode 100644 index 00000000000..b7872d11479 --- /dev/null +++ b/src/main/java/org/jabref/gui/sharelatex/ShareLatexProjectDialogView.java @@ -0,0 +1,19 @@ +package org.jabref.gui.sharelatex; + +import javafx.scene.control.Alert.AlertType; +import javafx.scene.control.DialogPane; + +import org.jabref.gui.AbstractDialogView; +import org.jabref.gui.FXDialog; + +public class ShareLatexProjectDialogView extends AbstractDialogView { + + @Override + public void show() { + FXDialog sharelatexProjectDialog = new FXDialog(AlertType.INFORMATION, "Choose Project"); + sharelatexProjectDialog.setDialogPane((DialogPane) this.getView()); + sharelatexProjectDialog.setResizable(true); + sharelatexProjectDialog.show(); + } + +} diff --git a/src/main/java/org/jabref/gui/sharelatex/ShareLatexProjectDialogViewModel.java b/src/main/java/org/jabref/gui/sharelatex/ShareLatexProjectDialogViewModel.java new file mode 100644 index 00000000000..13226d22b0a --- /dev/null +++ b/src/main/java/org/jabref/gui/sharelatex/ShareLatexProjectDialogViewModel.java @@ -0,0 +1,61 @@ +package org.jabref.gui.sharelatex; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Collectors; + +import javafx.beans.property.SimpleListProperty; +import javafx.collections.FXCollections; + +import org.jabref.gui.AbstractViewModel; +import org.jabref.gui.StateManager; +import org.jabref.logic.sharelatex.ShareLatexManager; +import org.jabref.logic.sharelatex.events.ShareLatexEntryMessageEvent; +import org.jabref.model.sharelatex.ShareLatexProject; + +import com.google.common.eventbus.Subscribe; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +public class ShareLatexProjectDialogViewModel extends AbstractViewModel { + + private static final Log LOGGER = LogFactory.getLog(ShareLatexProjectDialogViewModel.class); + + private final StateManager stateManager; + private final SimpleListProperty projects = new SimpleListProperty<>( + FXCollections.observableArrayList()); + + public ShareLatexProjectDialogViewModel(StateManager stateManager, ShareLatexManager manager) { + this.stateManager = stateManager; + manager.registerListener(this); + } + + public void addProjects(List projectsToAdd) { + this.projects.clear(); + this.projects.addAll(projectsToAdd.stream().map(ShareLatexProjectViewModel::new).collect(Collectors.toList())); + } + + public SimpleListProperty projectsProperty() { + return this.projects; + } + + @Subscribe + public void listenToSharelatexEntryMessage(ShareLatexEntryMessageEvent event) { + + Path p = stateManager.getActiveDatabase().get().getDatabasePath().get(); + + try (BufferedWriter writer = Files.newBufferedWriter(p, StandardCharsets.UTF_8)) { + writer.write(event.getNewDatabaseContent()); + writer.close(); + + } catch (IOException e) { + LOGGER.error("Problem writing new database content", e); + } + + } + +} diff --git a/src/main/java/org/jabref/gui/sharelatex/ShareLatexProjectViewModel.java b/src/main/java/org/jabref/gui/sharelatex/ShareLatexProjectViewModel.java new file mode 100644 index 00000000000..16e876229ff --- /dev/null +++ b/src/main/java/org/jabref/gui/sharelatex/ShareLatexProjectViewModel.java @@ -0,0 +1,63 @@ +package org.jabref.gui.sharelatex; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +import org.jabref.model.sharelatex.ShareLatexProject; + +/** + * Data class + * @author CS + * + */ +public class ShareLatexProjectViewModel { + + private final SimpleBooleanProperty active = new SimpleBooleanProperty(false); + private final String projectId; + private final StringProperty projectTitle; + private final StringProperty firstName; + private final StringProperty lastName; + private final StringProperty lastUpdated; + + public ShareLatexProjectViewModel(ShareLatexProject project) { + this.projectId = project.getProjectId(); + this.projectTitle = new SimpleStringProperty(project.getProjectTitle()); + this.firstName = new SimpleStringProperty(project.getFirstName()); + this.lastName = new SimpleStringProperty(project.getLastName()); + this.lastUpdated = new SimpleStringProperty(project.getLastUpdated()); + } + + public String getProjectId() { + return projectId; + } + + public StringProperty getProjectTitle() { + return projectTitle; + } + + public StringProperty getFirstName() { + return firstName; + } + + public StringProperty getLastName() { + return lastName; + } + + public StringProperty getLastUpdated() { + return lastUpdated; + } + + public Boolean isActive() { + return active.getValue(); + } + + public BooleanProperty isActiveProperty() { + return active; + } + + public void setActive(boolean active) { + this.active.set(active); + } +} diff --git a/src/main/java/org/jabref/logic/sharelatex/MyCustomClientEndpointConfigurator.java b/src/main/java/org/jabref/logic/sharelatex/MyCustomClientEndpointConfigurator.java new file mode 100644 index 00000000000..bc88b2540a4 --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/MyCustomClientEndpointConfigurator.java @@ -0,0 +1,43 @@ +package org.jabref.logic.sharelatex; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.websocket.ClientEndpointConfig; +import javax.websocket.HandshakeResponse; + +public class MyCustomClientEndpointConfigurator extends ClientEndpointConfig.Configurator { + + private final String userAgent = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0"; + private final String serverOrigin; + private final Map cookies; + + public MyCustomClientEndpointConfigurator(String serverOrigin, Map cookies) { + super(); + this.serverOrigin = serverOrigin; + this.cookies = cookies; + } + + @Override + public void beforeRequest(Map> headers) { + + headers.put("User-Agent", Arrays.asList(userAgent)); + headers.put("Origin", Arrays.asList(serverOrigin)); + + String result = cookies.entrySet() + .stream() + .map(entry -> entry.getKey() + "=" + entry.getValue()) + .collect(Collectors.joining("; ")); + headers.put("Cookie", Arrays.asList(result)); + } + + @Override + public void afterResponse(HandshakeResponse handshakeResponse) { + final Map> headers = handshakeResponse.getHeaders(); + + System.out.println("headers " + headers); + + } +} diff --git a/src/main/java/org/jabref/logic/sharelatex/ShareLatexJsonMessage.java b/src/main/java/org/jabref/logic/sharelatex/ShareLatexJsonMessage.java new file mode 100644 index 00000000000..b93b6f261ce --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/ShareLatexJsonMessage.java @@ -0,0 +1,66 @@ +package org.jabref.logic.sharelatex; + +import java.util.List; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +public class ShareLatexJsonMessage { + + public String createDeleteInsertMessage(String docId, int position, int version, String oldContent, String newContent) { + + JsonObject insertContent = new JsonObject(); + insertContent.addProperty("p", position); + insertContent.addProperty("d", oldContent); + + JsonObject deleteContent = new JsonObject(); + deleteContent.addProperty("p", position); + deleteContent.addProperty("i", newContent); + + JsonArray opArray = new JsonArray(); + opArray.add(insertContent); + opArray.add(deleteContent); + + JsonObject docIdOp = new JsonObject(); + docIdOp.addProperty("doc", docId); + docIdOp.add("op", opArray); + docIdOp.addProperty("v", version); + + JsonArray argsArray = new JsonArray(); + argsArray.add(docId); + argsArray.add(docIdOp); + + JsonObject obj = new JsonObject(); + obj.addProperty("name", "applyOtUpdate"); + obj.add("args", argsArray); + + return obj.toString(); + + } + + public String createUpdateMessageAsInsertOrDelete(String docId, int version, List docs) { + JsonArray opArray = new JsonArray(); + for (SharelatexDoc doc : docs) { + JsonObject deleteOrInsertContent = new JsonObject(); + deleteOrInsertContent.addProperty("p", doc.getPosition()); + deleteOrInsertContent.addProperty(doc.getOperation(), doc.getContent()); + opArray.add(deleteOrInsertContent); + } + + JsonObject docIdOp = new JsonObject(); + docIdOp.addProperty("doc", docId); + docIdOp.add("op", opArray); + docIdOp.addProperty("v", version); + + JsonArray argsArray = new JsonArray(); + argsArray.add(docId); + argsArray.add(docIdOp); + + JsonObject obj = new JsonObject(); + obj.addProperty("name", "applyOtUpdate"); + obj.add("args", argsArray); + + return obj.toString(); + } + +} diff --git a/src/main/java/org/jabref/logic/sharelatex/ShareLatexManager.java b/src/main/java/org/jabref/logic/sharelatex/ShareLatexManager.java new file mode 100644 index 00000000000..3ef2802efad --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/ShareLatexManager.java @@ -0,0 +1,76 @@ +package org.jabref.logic.sharelatex; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; + +import org.jabref.JabRefExecutorService; +import org.jabref.logic.exporter.BibtexDatabaseWriter; +import org.jabref.logic.exporter.SaveException; +import org.jabref.logic.exporter.SavePreferences; +import org.jabref.logic.exporter.StringSaveSession; +import org.jabref.logic.importer.ImportFormatPreferences; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.sharelatex.ShareLatexProject; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +public class ShareLatexManager { + + private static final Log LOGGER = LogFactory.getLog(ShareLatexManager.class); + + private final SharelatexConnector connector = new SharelatexConnector(); + private final ShareLatexParser parser = new ShareLatexParser(); + + public String login(String server, String username, String password) throws IOException { + return connector.connectToServer(server, username, password); + } + + public List getProjects() throws IOException { + if (connector.getProjects().isPresent()) { + return parser.getProjectFromJson(connector.getProjects().get()); + } + return Collections.emptyList(); + } + + public void startWebSocketHandler(String projectID, BibDatabaseContext database, ImportFormatPreferences preferences) { + JabRefExecutorService.INSTANCE.executeAndWait(() -> { + + try { + connector.startWebsocketListener(projectID, database, preferences); + } catch (URISyntaxException e) { + LOGGER.error(e); + } + registerListener(ShareLatexManager.this); + + }); + } + + public void sendNewDatabaseContent(BibDatabaseContext database) { + try { + BibtexDatabaseWriter databaseWriter = new BibtexDatabaseWriter<>(StringSaveSession::new); + SavePreferences preferences = new SavePreferences().withEncoding(StandardCharsets.UTF_8).withSaveInOriginalOrder(true); + StringSaveSession saveSession = databaseWriter.saveDatabase(database, preferences); + String updatedcontent = saveSession.getStringValue().replace("\r\n", "\n"); + + connector.sendNewDatabaseContent(updatedcontent); + } catch (InterruptedException | SaveException e) { + LOGGER.error("Could not prepare databse for saving ", e); + } + } + + public void registerListener(Object listener) { + connector.registerListener(listener); + } + + public void unregisterListener(Object listener) { + connector.unregisterListener(listener); + } + + public void disconnectAndCloseConnection() { + connector.disconnectAndCloseConn(); + } +} diff --git a/src/main/java/org/jabref/logic/sharelatex/ShareLatexParser.java b/src/main/java/org/jabref/logic/sharelatex/ShareLatexParser.java new file mode 100644 index 00000000000..040d02946a0 --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/ShareLatexParser.java @@ -0,0 +1,175 @@ +package org.jabref.logic.sharelatex; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.StringJoiner; + +import org.jabref.logic.importer.ImportFormatPreferences; +import org.jabref.logic.importer.ParseException; +import org.jabref.logic.importer.fileformat.BibtexParser; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.sharelatex.ShareLatexProject; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.bitbucket.cowwoc.diffmatchpatch.DiffMatchPatch; +import org.bitbucket.cowwoc.diffmatchpatch.DiffMatchPatch.Diff; +import org.bitbucket.cowwoc.diffmatchpatch.DiffMatchPatch.Operation; + +public class ShareLatexParser { + + private final JsonParser parser = new JsonParser(); + + public int getVersionFromBibTexJsonString(String content) { + JsonArray array = parseFirstPartOfMessageAsArray(content); + return array.get(2).getAsInt(); + } + + public List parseBibEntryFromJsonMessageString(String message, ImportFormatPreferences prefs) + throws ParseException { + return parseBibEntryFromJsonArray(parseFirstPartOfMessageAsArray(message), prefs); + } + + public String getBibTexStringFromJsonMessage(String message) { + return getBibTexStringFromJsonArray(parseFirstPartOfMessageAsArray(message)); + } + + public String getOtErrorMessageContent(String otUpdateError) { + + JsonObject obj = parseFirstPartOfMessageAsObject(otUpdateError); + return obj.get("args").getAsJsonArray().get(0).getAsString(); + } + + public String getFirstBibTexDatabaseId(String json) { + + JsonObject obj = parseFirstPartOfMessageAsArray(json).get(1).getAsJsonObject(); + JsonArray arr = obj.get("rootFolder").getAsJsonArray(); + + Optional docs = arr.get(0).getAsJsonObject().entrySet().stream() + .filter(entry -> entry.getKey().equals("docs")).map(v -> v.getValue().getAsJsonArray()).findFirst(); + + if (docs.isPresent()) { + + JsonArray jsonArray = docs.get(); + for (JsonElement doc : jsonArray) { + String name = doc.getAsJsonObject().get("name").getAsString(); + String id = doc.getAsJsonObject().get("_id").getAsString(); + + if (name.endsWith(".bib")) { + return id; + } + + } + } + return ""; + } + + public List generateDiffs(String before, String after) { + DiffMatchPatch patch = new DiffMatchPatch(); + + LinkedList diffs = patch.diffMain(before, after); + patch.diffCleanupSemantic(diffs); + + int pos = 0; + + List docsWithChanges = new ArrayList<>(); + + for (Diff d : diffs) { + + if (d.operation == Operation.INSERT) { + SharelatexDoc doc = new SharelatexDoc(); + doc.setPosition(pos); + doc.setContent(d.text); + doc.setOperation("i"); + docsWithChanges.add(doc); + pos += d.text.length(); + } else if (d.operation == Operation.DELETE) { + SharelatexDoc doc = new SharelatexDoc(); + doc.setPosition(pos); + doc.setContent(d.text); + doc.setOperation("d"); + + docsWithChanges.add(doc); + + } else if (d.operation == Operation.EQUAL) { + pos += d.text.length(); + } + + } + return docsWithChanges; + + } + + public List getProjectFromJson(JsonObject json) { + + List projects = new ArrayList<>(); + if (json.has("projects")) { + JsonArray projectArray = json.get("projects").getAsJsonArray(); + for (JsonElement elem : projectArray) { + + String id = elem.getAsJsonObject().get("id").getAsString(); + String name = elem.getAsJsonObject().get("name").getAsString(); + String lastUpdated = elem.getAsJsonObject().get("lastUpdated").getAsString(); + //String owner = elem.getAsJsonObject().get("owner_ref").getAsString(); + + JsonObject owner = elem.getAsJsonObject().get("owner").getAsJsonObject(); + String firstName = owner.get("first_name").getAsString(); + String lastName = owner.get("last_name").getAsString(); + + ShareLatexProject project = new ShareLatexProject(id, name, firstName, lastName, lastUpdated); + projects.add(project); + } + } + return projects; + + } + + private List parseBibEntryFromJsonArray(JsonArray arr, ImportFormatPreferences prefs) + throws ParseException { + + String bibtexString = getBibTexStringFromJsonArray(arr); + BibtexParser parser = new BibtexParser(prefs); + return parser.parseEntries(bibtexString); + } + + private JsonArray parseFirstPartOfMessageAsArray(String documentToParse) { + String jsonToRead = documentToParse.substring(documentToParse.indexOf("+") + 1, documentToParse.length()); + JsonArray arr = parser.parse(jsonToRead).getAsJsonArray(); + return arr; + } + + private JsonObject parseFirstPartOfMessageAsObject(String documentToParse) { + String jsonToRead = documentToParse.substring(documentToParse.indexOf("{"), documentToParse.length()); + return parser.parse(jsonToRead).getAsJsonObject(); + } + + private String getBibTexStringFromJsonArray(JsonArray arr) { + + JsonArray stringArr = arr.get(1).getAsJsonArray(); + + StringJoiner joiner = new StringJoiner("\n"); + + for (JsonElement elem : stringArr) { + joiner.add(elem.getAsString()); + } + + return joiner.toString(); + } + + /** + * Fixes wrongly encoded UTF-8 strings which were encoded into ISO-8859-1 + * Workaround for server side bug + * @param wrongEncoded The wrongly encoded string + * @return The correct UTF-8 string + */ + public String fixUTF8Strings(String wrongEncoded) { + String str = new String(wrongEncoded.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8); + return new String(str.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8); + } + +} diff --git a/src/main/java/org/jabref/logic/sharelatex/SharelatexConnector.java b/src/main/java/org/jabref/logic/sharelatex/SharelatexConnector.java new file mode 100644 index 00000000000..6bbc2f7c619 --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/SharelatexConnector.java @@ -0,0 +1,171 @@ +package org.jabref.logic.sharelatex; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.jabref.logic.importer.ImportFormatPreferences; +import org.jabref.model.database.BibDatabaseContext; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.http.client.utils.URIBuilder; +import org.json.JSONObject; +import org.jsoup.Connection; +import org.jsoup.Connection.Method; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; + +public class SharelatexConnector { + + private static final Log LOGGER = LogFactory.getLog(SharelatexConnector.class); + + private final String contentType = "application/json; charset=utf-8"; + private final JsonParser parser = new JsonParser(); + private final String userAgent = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0"; + private Map loginCookies = new HashMap<>(); + private String server; + private String loginUrl; + private String csrfToken; + private String projectUrl; + private final WebSocketClientWrapper client = new WebSocketClientWrapper(); + + public String connectToServer(String serverUri, String user, String password) throws IOException { + + this.server = serverUri; + this.loginUrl = server + "/login"; + Connection.Response crsfResponse; + + crsfResponse = Jsoup.connect(loginUrl).method(Method.GET) + .execute(); + + Document welcomePage = crsfResponse.parse(); + Map welcomCookies = crsfResponse.cookies(); + + csrfToken = welcomePage.select("input[name=_csrf]").attr("value"); + + String json = "{\"_csrf\":" + JSONObject.quote(csrfToken) + + ",\"email\":" + JSONObject.quote(user) + ",\"password\":" + JSONObject.quote(password) + "}"; + + Connection.Response loginResponse = Jsoup.connect(loginUrl) + .header("Content-Type", contentType) + .header("Accept", "application/json, text/plain, */*") + .cookies(welcomCookies) + .method(Method.POST) + .requestBody(json) + .followRedirects(true) + .ignoreContentType(true) + .userAgent(userAgent) + .execute(); + + System.out.println(loginResponse.body()); + ///Error handling block + if (contentType.equals(loginResponse.contentType())) { + + if (loginResponse.body().contains("message")) { + JsonElement jsonTree = parser.parse(loginResponse.body()); + JsonObject obj = jsonTree.getAsJsonObject(); + JsonObject message = obj.get("message").getAsJsonObject(); + String errorMessage = message.get("text").getAsString(); + System.out.println(errorMessage); + + return errorMessage; + } + + } + + loginCookies = loginResponse.cookies(); + if (loginCookies.isEmpty()) { + loginCookies = welcomCookies; + } + + return ""; + } + + public Optional getProjects() throws IOException { + projectUrl = server + "/project"; + Connection.Response projectsResponse = Jsoup.connect(projectUrl) + .referrer(loginUrl).cookies(loginCookies).method(Method.GET).userAgent(userAgent).execute(); + + Optional scriptContent = Optional + .of(projectsResponse.parse().select("script#data").first()); + + if (scriptContent.isPresent()) { + + String data = scriptContent.get().data(); + JsonElement jsonTree = parser.parse(data); + + JsonObject obj = jsonTree.getAsJsonObject(); + + return Optional.of(obj); + + } + return Optional.empty(); + } + + public void startWebsocketListener(String projectId, BibDatabaseContext database, ImportFormatPreferences prefs) + throws URISyntaxException { + long millis = System.currentTimeMillis(); + System.out.println(millis); + String socketioUrl = server + "/socket.io/1"; + String scheme = server.contains("https://") ? "wss" : "ws"; + try { + Connection.Response webSocketresponse = Jsoup.connect(socketioUrl) + .cookies(loginCookies) + .data("t", String.valueOf(millis)).method(Method.GET).execute(); + + System.out.println(webSocketresponse.body()); + + String resp = webSocketresponse.body(); + String channel = resp.substring(0, resp.indexOf(":")); + + URI webSocketchannelUri = new URIBuilder(socketioUrl + "/websocket/" + channel).setScheme(scheme).build(); + System.out.println("WebSocketChannelUrl " + webSocketchannelUri); + client.setImportFormatPrefs(prefs); + client.setServerNameOrigin(server); + client.setCookies(loginCookies); + client.createAndConnect(webSocketchannelUri, projectId, database); + + setDatabaseName(database); + + } catch (IOException e) { + LOGGER.error("Problem starting websocket", e); + } + } + + public void sendNewDatabaseContent(String newContent) throws InterruptedException { + client.sendNewDatabaseContent(newContent); + } + + public void registerListener(Object listener) { + client.registerListener(listener); + + } + + public void unregisterListener(Object listener) { + client.unregisterListener(listener); + } + + public void disconnectAndCloseConn() { + try { + client.leaveDocAndCloseConn(); + } catch (IOException e) { + LOGGER.error("Problem leaving document and closing websocket", e); + } + + } + + private void setDatabaseName(BibDatabaseContext database) { + String dbName = database.getDatabasePath().map(Path::getFileName).map(Path::toString).orElse(""); + client.setDatabaseName(dbName); + } + +} diff --git a/src/main/java/org/jabref/logic/sharelatex/SharelatexDoc.java b/src/main/java/org/jabref/logic/sharelatex/SharelatexDoc.java new file mode 100644 index 00000000000..ccd8622dd27 --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/SharelatexDoc.java @@ -0,0 +1,61 @@ +package org.jabref.logic.sharelatex; + +import java.util.Objects; + +public class SharelatexDoc { + + private int position; + private String content; + private String operation; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public int getPosition() { + return position; + } + + public void setPosition(int position) { + this.position = position; + } + + public String getOperation() { + return operation; + } + + public void setOperation(String opType) { + this.operation = opType; + } + + @Override + public String toString() { + return operation + " " + position + " " + content; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + SharelatexDoc other = (SharelatexDoc) obj; + + return Objects.equals(content, other.content) && Objects.equals(position, other.position) && Objects.equals(operation, other.operation); + } + + @Override + public int hashCode() { + return Objects.hash(content, position, operation); + } + +} diff --git a/src/main/java/org/jabref/logic/sharelatex/WebSocketClientWrapper.java b/src/main/java/org/jabref/logic/sharelatex/WebSocketClientWrapper.java new file mode 100644 index 00000000000..950de7e3bd6 --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/WebSocketClientWrapper.java @@ -0,0 +1,337 @@ +package org.jabref.logic.sharelatex; + +import java.io.IOException; +import java.net.URI; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.websocket.ClientEndpointConfig; +import javax.websocket.CloseReason; +import javax.websocket.Endpoint; +import javax.websocket.EndpointConfig; +import javax.websocket.MessageHandler.Whole; +import javax.websocket.Session; + +import org.jabref.JabRefExecutorService; +import org.jabref.logic.importer.ImportFormatPreferences; +import org.jabref.logic.importer.ParseException; +import org.jabref.logic.sharelatex.events.ShareLatexContinueMessageEvent; +import org.jabref.logic.sharelatex.events.ShareLatexEntryMessageEvent; +import org.jabref.logic.sharelatex.events.ShareLatexErrorMessageEvent; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; + +import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.glassfish.tyrus.client.ClientManager; +import org.glassfish.tyrus.client.ClientProperties; +import org.glassfish.tyrus.ext.extension.deflate.PerMessageDeflateExtension; + +public class WebSocketClientWrapper { + + private static final Log LOGGER = LogFactory.getLog(WebSocketClientWrapper.class); + private final BlockingQueue queue = new LinkedBlockingQueue<>(); + private final ShareLatexParser parser = new ShareLatexParser(); + + private Session session; + private String oldContent; + private int version; + private int commandCounter; + private ImportFormatPreferences prefs; + private String docId; + private String projectId; + private String databaseName; + private final EventBus eventBus = new EventBus("SharelatexEventBus"); + private boolean leftDoc = false; + private boolean errorReceived = false; + + private String serverOrigin; + private Map cookies; + + public WebSocketClientWrapper() { + this.eventBus.register(this); + } + + public void setImportFormatPrefs(ImportFormatPreferences prefs) { + this.prefs = prefs; + } + + public void createAndConnect(URI webSocketchannelUri, String projectId, BibDatabaseContext database) { + + try { + this.projectId = projectId; + + ClientEndpointConfig.Configurator configurator = new MyCustomClientEndpointConfigurator(serverOrigin, cookies); + final ClientEndpointConfig cec = ClientEndpointConfig.Builder.create().extensions(Arrays.asList(new PerMessageDeflateExtension())) + .configurator(configurator).build(); + final CountDownLatch messageLatch = new CountDownLatch(1); + + ClientManager client = ClientManager.createClient(); + client.getProperties().put(ClientProperties.REDIRECT_ENABLED, true); + client.getProperties().put(ClientProperties.LOG_HTTP_UPGRADE, true); + + ClientManager.ReconnectHandler reconnectHandler = new ClientManager.ReconnectHandler() { + + private final AtomicInteger counter = new AtomicInteger(0); + + @Override + public boolean onConnectFailure(Exception exception) { + final int i = counter.incrementAndGet(); + if (i <= 3) { + LOGGER.debug( + "### Reconnecting... (reconnect count: " + i + ")", exception); + return true; + } else { + messageLatch.countDown(); + return false; + } + } + + @Override + public long getDelay() { + return 0; + } + + }; + client.getProperties().put(ClientProperties.RECONNECT_HANDLER, reconnectHandler); + + this.session = client.connectToServer(new Endpoint() { + + @Override + public void onOpen(Session session, EndpointConfig config) { + + session.addMessageHandler(String.class, (Whole) message -> { + message = parser.fixUTF8Strings(message); + LOGGER.debug("Received new message " + message); + parseContents(message); + }); + } + + @Override + public void onError(Session session, Throwable t) { + LOGGER.error(t); + } + + @Override + public void onClose(Session session, CloseReason closeReason) { + if (errorReceived) { + LOGGER.debug("Error received in close session"); + } + + } + }, cec, webSocketchannelUri); + + //TODO: Change Dialog + //TODO: On database change event or on save event send new version + //TODO: When new db content arrived run merge dialog + //TODO: Identfiy active database/Name of database/doc Id (partly done) + + } catch (Exception e) { + e.printStackTrace(); + } + + } + + public void joinProject(String projectId) throws IOException { + incrementCommandCounter(); + String text = "5:" + commandCounter + "+::{\"name\":\"joinProject\",\"args\":[{\"project_id\":\"" + projectId + + "\"}]}"; + session.getBasicRemote().sendText(text); + } + + public void joinDoc(String documentId) throws IOException { + incrementCommandCounter(); + String text = "5:" + commandCounter + "+::{\"name\":\"joinDoc\",\"args\":[\"" + documentId + "\"]}"; + session.getBasicRemote().sendText(text); + } + + public void leaveDocument(String documentId) throws IOException { + incrementCommandCounter(); + String text = "5:" + commandCounter + "+::{\"name\":\"leaveDoc\",\"args\":[\"" + documentId + "\"]}"; + if (session != null) { + session.getBasicRemote().sendText(text); + } + + } + + private void sendHeartBeat() throws IOException { + session.getBasicRemote().sendText("2::"); + } + + public void sendNewDatabaseContent(String newContent) throws InterruptedException { + queue.put(newContent); + } + + private void sendUpdateAsDeleteAndInsert(String docId, int position, int version, String oldContent, String newContent) throws IOException { + ShareLatexJsonMessage message = new ShareLatexJsonMessage(); + + List diffDocs = parser.generateDiffs(oldContent, newContent); + String str = message.createUpdateMessageAsInsertOrDelete(docId, version, diffDocs); + + LOGGER.debug("Send new update Message"); + + session.getBasicRemote().sendText("5:::" + str); + } + + @Subscribe + public synchronized void listenToSharelatexEntryMessage(ShareLatexContinueMessageEvent event) { + + JabRefExecutorService.INSTANCE.executeInterruptableTask(() -> { + try { + String updatedContent = queue.take(); + if (!leftDoc) { + LOGGER.debug("Taken from queue"); + sendUpdateAsDeleteAndInsert(docId, 0, version, oldContent, updatedContent); + + } + } catch (IOException | InterruptedException e) { + Thread.currentThread().interrupt(); + LOGGER.debug("Exception in taking from queue", e); + } + }); + + } + + //Actual response handling + private void parseContents(String message) { + try { + + if (message.contains(":::1")) { + + Thread.currentThread().sleep(300); + LOGGER.debug("Got :::1. Joining project"); + + } + if (message.contains("2::")) { + setLeftDoc(false); + eventBus.post(new ShareLatexContinueMessageEvent()); + sendHeartBeat(); + + } + + if (message.endsWith("[null]")) { + LOGGER.debug("Received null-> Rejoining doc"); + joinDoc(docId); + } + + if (message.startsWith("[null,{", message.indexOf("+") + 1)) { + LOGGER.debug("We get a list with all files"); + //We get a list with all files + + String docIdOfFirstBibtex = parser.getFirstBibTexDatabaseId(message); + + LOGGER.debug("DBs with ID " + docIdOfFirstBibtex); + setDocID(docIdOfFirstBibtex); + joinDoc(docId); + + } + if (message.contains("{\"name\":\"connectionAccepted\"}") && (projectId != null)) { + + LOGGER.debug("Joining project"); + Thread.sleep(200); + joinProject(projectId); + + } + + if (message.contains("[null,[")) { + System.out.println("Message could be an entry "); + + int version = parser.getVersionFromBibTexJsonString(message); + setVersion(version); + + String bibtexString = parser.getBibTexStringFromJsonMessage(message); + setBibTexString(bibtexString); + List entries = parser.parseBibEntryFromJsonMessageString(message, prefs); + + LOGGER.debug("Got new entries"); + setLeftDoc(false); + + eventBus.post(new ShareLatexEntryMessageEvent(entries, bibtexString)); + eventBus.post(new ShareLatexContinueMessageEvent()); + + } + + if (message.contains("otUpdateApplied")) { + LOGGER.debug("We got an update"); + + leaveDocument(docId); + setLeftDoc(true); + } + if (message.contains("otUpdateError")) { + String error = parser.getOtErrorMessageContent(message); + eventBus.post(new ShareLatexErrorMessageEvent(error)); + } + if (message.contains("0::")) { + leaveDocAndCloseConn(); + } + + } catch (IOException | ParseException e) { + LOGGER.error("Error in parsing", e); + } catch (InterruptedException e) { + LOGGER.debug(e); + } + } + + public void setDatabaseName(String bibFileName) { + this.databaseName = bibFileName; + } + + public void leaveDocAndCloseConn() throws IOException { + leaveDocument(docId); + queue.clear(); + if (session != null) { + session.close(); + } + + } + + public void setServerNameOrigin(String serverOrigin) { + this.serverOrigin = serverOrigin; + + } + + public void setCookies(Map cookies) { + this.cookies = cookies; + + } + + public void registerListener(Object listener) { + eventBus.register(listener); + } + + public void unregisterListener(Object listener) { + eventBus.unregister(listener); + } + + private synchronized void setDocID(String docId) { + this.docId = docId; + } + + private synchronized void setVersion(int version) { + this.version = version; + } + + private synchronized void setBibTexString(String bibtex) { + this.oldContent = bibtex; + } + + private synchronized void incrementCommandCounter() { + this.commandCounter = commandCounter + 1; + } + + private synchronized void setLeftDoc(boolean leftDoc) { + this.leftDoc = leftDoc; + } + + private synchronized void setErrorReceived(boolean errorReceived) { + this.errorReceived = errorReceived; + } + +} diff --git a/src/main/java/org/jabref/logic/sharelatex/events/ShareLatexContinueMessageEvent.java b/src/main/java/org/jabref/logic/sharelatex/events/ShareLatexContinueMessageEvent.java new file mode 100644 index 00000000000..3f0df281ab7 --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/events/ShareLatexContinueMessageEvent.java @@ -0,0 +1,5 @@ +package org.jabref.logic.sharelatex.events; + +public class ShareLatexContinueMessageEvent { + //empty +} diff --git a/src/main/java/org/jabref/logic/sharelatex/events/ShareLatexEntryMessageEvent.java b/src/main/java/org/jabref/logic/sharelatex/events/ShareLatexEntryMessageEvent.java new file mode 100644 index 00000000000..6b6a4dc1a89 --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/events/ShareLatexEntryMessageEvent.java @@ -0,0 +1,27 @@ +package org.jabref.logic.sharelatex.events; + +import java.util.ArrayList; +import java.util.List; + +import org.jabref.model.entry.BibEntry; + +public class ShareLatexEntryMessageEvent { + + private List entries = new ArrayList<>(); + + private final String database; + + public ShareLatexEntryMessageEvent(List entries, String database) { + this.entries = entries; + this.database = database; + } + + public List getEntries() { + return this.entries; + } + + public String getNewDatabaseContent() { + return this.database; + } + +} diff --git a/src/main/java/org/jabref/logic/sharelatex/events/ShareLatexErrorMessageEvent.java b/src/main/java/org/jabref/logic/sharelatex/events/ShareLatexErrorMessageEvent.java new file mode 100644 index 00000000000..14242fb150d --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/events/ShareLatexErrorMessageEvent.java @@ -0,0 +1,10 @@ +package org.jabref.logic.sharelatex.events; + +public class ShareLatexErrorMessageEvent { + + String errorMessage; + + public ShareLatexErrorMessageEvent(String errorMessage) { + this.errorMessage = errorMessage; + } +} diff --git a/src/main/java/org/jabref/model/sharelatex/ShareLatexProject.java b/src/main/java/org/jabref/model/sharelatex/ShareLatexProject.java new file mode 100644 index 00000000000..94eada33ecb --- /dev/null +++ b/src/main/java/org/jabref/model/sharelatex/ShareLatexProject.java @@ -0,0 +1,71 @@ +package org.jabref.model.sharelatex; + +import java.util.Objects; + +public class ShareLatexProject { + + private final String projectId; + private final String projectTitle; + private final String firstName; + private final String lastName; + private final String lastUpdated; + + public ShareLatexProject(String projectId, String projectTitle, String firstName, String lastName, String lastUpdated) { + this.projectId = projectId; + this.projectTitle = projectTitle; + this.lastUpdated = lastUpdated; + this.firstName = firstName; + this.lastName = lastName; + } + + public String getLastUpdated() { + return lastUpdated; + } + + public String getProjectId() { + return projectId; + } + + public String getProjectTitle() { + return projectTitle; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + + @Override + public String toString() { + return "[projectId =" + projectId + ", " + + "projectTitle = " + projectTitle + ", " + + "firstName = " + firstName + ", " + + "lastName = " + lastName + ", " + + "lastUpdated = " + lastUpdated + ""; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ShareLatexProject other = (ShareLatexProject) obj; + + return Objects.equals(projectId, other.projectId) && Objects.equals(projectTitle, other.projectTitle) && Objects.equals(lastUpdated, other.lastUpdated) && Objects.equals(lastName, other.lastName) && Objects.equals(firstName, other.firstName); + } + + @Override + public int hashCode() { + return Objects.hash(projectId, projectTitle, lastUpdated, lastName, firstName); + } + +} diff --git a/src/main/java/org/jabref/preferences/JabRefPreferences.java b/src/main/java/org/jabref/preferences/JabRefPreferences.java index 16caeb95a31..d2831f3371e 100644 --- a/src/main/java/org/jabref/preferences/JabRefPreferences.java +++ b/src/main/java/org/jabref/preferences/JabRefPreferences.java @@ -377,7 +377,7 @@ public class JabRefPreferences implements PreferencesService { private static final String COLLECT_TELEMETRY = "collectTelemetry"; private static final String ALREADY_ASKED_TO_COLLECT_TELEMETRY = "askedCollectTelemetry"; private static final Log LOGGER = LogFactory.getLog(JabRefPreferences.class); - private static final Class PREFS_BASE_CLASS = JabRefMain.class; + private static final Class PREFS_BASE_CLASS = JabRefMain.class; private static final String DB_CONNECT_USERNAME = "dbConnectUsername"; private static final String DB_CONNECT_DATABASE = "dbConnectDatabase"; private static final String DB_CONNECT_HOSTNAME = "dbConnectHostname"; @@ -1388,6 +1388,7 @@ public boolean isKeywordSyncEnabled() { && getBoolean(JabRefPreferences.AUTOSYNCSPECIALFIELDSTOKEYWORDS); } + @Override public ImportFormatPreferences getImportFormatPreferences() { return new ImportFormatPreferences(customImports, getDefaultEncoding(), getKeywordDelimiter(), getBibtexKeyPatternPreferences(), getFieldContentParserPreferences(), diff --git a/src/main/java/org/jabref/preferences/PreferencesService.java b/src/main/java/org/jabref/preferences/PreferencesService.java index 6ea1a9d56dc..04f87af27f8 100644 --- a/src/main/java/org/jabref/preferences/PreferencesService.java +++ b/src/main/java/org/jabref/preferences/PreferencesService.java @@ -1,9 +1,11 @@ package org.jabref.preferences; import org.jabref.gui.keyboard.KeyBindingRepository; +import org.jabref.logic.importer.ImportFormatPreferences; import org.jabref.logic.journals.JournalAbbreviationPreferences; public interface PreferencesService { + JournalAbbreviationPreferences getJournalAbbreviationPreferences(); void storeKeyBindingRepository(KeyBindingRepository keyBindingRepository); @@ -11,4 +13,6 @@ public interface PreferencesService { KeyBindingRepository getKeyBindingRepository(); void storeJournalAbbreviationPreferences(JournalAbbreviationPreferences abbreviationsPreferences); + + ImportFormatPreferences getImportFormatPreferences(); } diff --git a/src/main/resources/images/Icons.properties b/src/main/resources/images/Icons.properties index 3618d8fb943..85318f9c6e8 100644 --- a/src/main/resources/images/Icons.properties +++ b/src/main/resources/images/Icons.properties @@ -22,3 +22,4 @@ emacs=emacs.png mdl=mdl-icon.png mdlloading=mdlloading.gif mdlListIcon=mdlListIcon.png +sharelatex=lion.png diff --git a/src/main/resources/images/external/lion.png b/src/main/resources/images/external/lion.png new file mode 100644 index 0000000000000000000000000000000000000000..0b12a47a9591e0bc9eeb1643a00f58dff8fa3f9e GIT binary patch literal 804 zcmV+<1Ka$GP)-aT6+nY_Hsp6b2t-22_{e&^gH zV$lVzHJ^SfV+}AWcmwnr8~}CzO92qqLIu4?pFaN_-LL3RF`I3&RCpaJgRw4>`VJ&x zXFoHs(gkDb!jq4+InYH{_(tmn?T?YM2pPa88Eb61=}Z)?6BgUD8OcrqlStiw@&93% zbpey0pNrXS(_A7k_YsESY9|H2DBcu z!3L007)`V84i<(g+Z9u4pkhA#2xHqkgn6@F?ZdAgs(6UN-nW)P8>2eBeB0DSveJ;8 zI$0=HE&>#D>7ORPFS*i?4A&ce7ziSJUjFS5P$KaH)Oh^>D|?NHrk5GlvI+ zcREH!s)f>vXBd}bzGhasYG*>8<%lw{xn3!A*Jk~&KDkVPF8w3Glx$G&7Qp1L3V$j> z_}QZtm$JqSh~@xHiz!?sv&S<<$+r$*I=Hao-JU|}#RGtw z`3+kF$qG+jm1n%n?l%1W7-3m!D{T(ILiNP^&f|~Oy|ctLJ_r}f&BnONohS?t)}LN~ zE^`{}Vr0=DKCrC{wO3*}fEi;J*gY5O3D8o%jW|#ks@z`C*a@cZP80M34YN>-ygHAR zDrcio`ts@Dqh!F>@l@^pqQjvtm+2PvYXq9a>?qPdGu5(Yq#N~#GZViYE>(W}&(i~_ zEvxpV#v2b~`h;vJuoUSQk-J6WFQAt4sq>2t2Y}-b-%l)&{a~_Nz!>1c{sek~cd_|! iH$k{_U%6b3qW=O*xC3+*hwH%r0000 + + + + + + + + + + + + + + + + + + + + + + + + + + +