diff --git a/build.gradle b/build.gradle index 3189714f041..84c7bb07418 100644 --- a/build.gradle +++ b/build.gradle @@ -201,6 +201,11 @@ dependencies { implementation 'com.vladsch.flexmark:flexmark-ext-gfm-strikethrough:0.64.0' implementation 'com.vladsch.flexmark:flexmark-ext-gfm-tasklist:0.64.0' + implementation 'com.google.code.gson:gson:2.8.6' + implementation 'org.glassfish.tyrus:tyrus-client:1.17' + implementation 'org.glassfish.tyrus:tyrus-container-grizzly-client:1.17' + implementation 'org.glassfish.tyrus.ext:tyrus-extension-deflate:1.17' + implementation group: 'net.harawata', name: 'appdirs', version: '1.2.1' testImplementation 'io.github.classgraph:classgraph:4.8.157' diff --git a/docs/code-howtos/overleaf.md b/docs/code-howtos/overleaf.md new file mode 100644 index 00000000000..6095a66e4cc --- /dev/null +++ b/docs/code-howtos/overleaf.md @@ -0,0 +1,20 @@ +# Notes on the overleaf implementation + +Local setup of overleaf: + +1. Download . +2. Adapt `volumes` (). +3. Execute `docker compose up`. +4. Open to create the administrator user. +5. Create the test user . +6. Copy the displayed password reset URL +7. Open your browser in a private session. +8. Go to the URL copied in step 6 and set the password `jabref`. +9. Create a latex project with the example. + +After a successful setup, one can use as "Server Address". + +After a session execute `docker compose down` + +More information on Overleaf is available at . + diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index ca7ed66cc97..67aa1233aae 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -120,4 +120,8 @@ requires org.eclipse.jgit; uses org.eclipse.jgit.transport.SshSessionFactory; uses org.eclipse.jgit.lib.GpgSigner; + + requires tyrus.client; + requires tyrus.extension.deflate; + requires javax.websocket.api; } diff --git a/src/main/java/org/jabref/gui/DefaultInjector.java b/src/main/java/org/jabref/gui/DefaultInjector.java index 5318c3b95dc..9405bb9a099 100644 --- a/src/main/java/org/jabref/gui/DefaultInjector.java +++ b/src/main/java/org/jabref/gui/DefaultInjector.java @@ -10,6 +10,7 @@ import org.jabref.logic.importer.ImportFormatReader; import org.jabref.logic.journals.JournalAbbreviationRepository; import org.jabref.logic.protectedterms.ProtectedTermsLoader; +import org.jabref.logic.sharelatex.ShareLatexManager; import org.jabref.model.entry.BibEntryTypesManager; import org.jabref.model.util.FileUpdateMonitor; import org.jabref.preferences.PreferencesService; @@ -43,6 +44,8 @@ private static Object createDependency(Class clazz) { return Globals.stateManager; } else if (clazz == ThemeManager.class) { return Globals.getThemeManager(); + } else if (clazz == ShareLatexManager.class) { + return Globals.shareLatexManager; } else if (clazz == FileUpdateMonitor.class) { return Globals.getFileUpdateMonitor(); } else if (clazz == ProtectedTermsLoader.class) { diff --git a/src/main/java/org/jabref/gui/Globals.java b/src/main/java/org/jabref/gui/Globals.java index 6070283d1d1..0f4bd6f0b10 100644 --- a/src/main/java/org/jabref/gui/Globals.java +++ b/src/main/java/org/jabref/gui/Globals.java @@ -18,6 +18,7 @@ import org.jabref.logic.protectedterms.ProtectedTermsLoader; import org.jabref.logic.remote.RemotePreferences; import org.jabref.logic.remote.server.RemoteListenerServerManager; +import org.jabref.logic.sharelatex.ShareLatexManager; import org.jabref.logic.util.BuildInfo; import org.jabref.model.entry.BibEntryTypesManager; import org.jabref.model.strings.StringUtil; @@ -70,6 +71,8 @@ public class Globals { */ public static ProtectedTermsLoader protectedTermsLoader; + public static ShareLatexManager shareLatexManager = new ShareLatexManager(); + public static CountingUndoManager undoManager = new CountingUndoManager(); public static BibEntryTypesManager entryTypesManager = new BibEntryTypesManager(); diff --git a/src/main/java/org/jabref/gui/JabRefFrame.java b/src/main/java/org/jabref/gui/JabRefFrame.java index 4769fd425e7..069583c504e 100644 --- a/src/main/java/org/jabref/gui/JabRefFrame.java +++ b/src/main/java/org/jabref/gui/JabRefFrame.java @@ -51,8 +51,10 @@ import org.jabref.gui.actions.ActionFactory; import org.jabref.gui.actions.ActionHelper; +import org.jabref.gui.actions.SendChangesToShareLatexAction; import org.jabref.gui.actions.SimpleCommand; import org.jabref.gui.actions.StandardActions; +import org.jabref.gui.actions.SynchronizeWithShareLatexAction; import org.jabref.gui.auximport.NewSubLibraryAction; import org.jabref.gui.bibtexextractor.ExtractBibtexAction; import org.jabref.gui.citationkeypattern.GenerateCitationKeyAction; @@ -793,7 +795,10 @@ private MenuBar createMenu() { factory.createSubMenu(StandardActions.REMOTE_DB, factory.createMenuItem(StandardActions.CONNECT_TO_SHARED_DB, new ConnectToSharedDatabaseCommand(this)), - factory.createMenuItem(StandardActions.PULL_CHANGES_FROM_SHARED_DB, new PullChangesFromSharedAction(stateManager))), + factory.createMenuItem(StandardActions.PULL_CHANGES_FROM_SHARED_DB, new PullChangesFromSharedAction(stateManager)), + factory.createMenuItem(StandardActions.SYNCHRONIZE_WITH_SHARELATEX, new SynchronizeWithShareLatexAction()), + factory.createMenuItem(StandardActions.SEND_TO_SHARELATEX, new SendChangesToShareLatexAction()) + ), new SeparatorMenuItem(), 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..bf36a4c0ab5 --- /dev/null +++ b/src/main/java/org/jabref/gui/actions/DisconnectFromSharelatexAction.java @@ -0,0 +1,21 @@ +package org.jabref.gui.actions; + +import java.awt.event.ActionEvent; + +import javax.swing.AbstractAction; +import javax.swing.Action; + +import org.jabref.gui.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..6484b585a5e --- /dev/null +++ b/src/main/java/org/jabref/gui/actions/SendChangesToShareLatexAction.java @@ -0,0 +1,15 @@ +package org.jabref.gui.actions; + +import org.jabref.gui.Globals; +import org.jabref.gui.StateManager; +import org.jabref.logic.sharelatex.ShareLatexManager; + +public class SendChangesToShareLatexAction extends SimpleCommand { + + @Override + public void execute() { + ShareLatexManager manager = Globals.shareLatexManager; + StateManager stateManager = Globals.stateManager; + manager.sendNewDatabaseContent(stateManager.getActiveDatabase().get()); + } +} diff --git a/src/main/java/org/jabref/gui/actions/StandardActions.java b/src/main/java/org/jabref/gui/actions/StandardActions.java index c7bf3ecb80d..159fc69657e 100644 --- a/src/main/java/org/jabref/gui/actions/StandardActions.java +++ b/src/main/java/org/jabref/gui/actions/StandardActions.java @@ -70,6 +70,7 @@ public enum StandardActions implements Action { EXPORT_SELECTED(Localization.lang("Export selected entries"), KeyBinding.EXPORT_SELECTED), CONNECT_TO_SHARED_DB(Localization.lang("Connect to shared database"), IconTheme.JabRefIcons.CONNECT_DB), PULL_CHANGES_FROM_SHARED_DB(Localization.lang("Pull changes from shared database"), KeyBinding.PULL_CHANGES_FROM_SHARED_DATABASE), + SYNCHRONIZE_WITH_SHARELATEX(Localization.lang("Connect to Overleaf"), IconTheme.JabRefIcons.CONNECT_DB), CLOSE_LIBRARY(Localization.lang("Close"), Localization.lang("Close the current library"), IconTheme.JabRefIcons.CLOSE, KeyBinding.CLOSE_DATABASE), CLOSE_OTHER_LIBRARIES(Localization.lang("Close others"), Localization.lang("Close other libraries"), IconTheme.JabRefIcons.CLOSE), CLOSE_ALL_LIBRARIES(Localization.lang("Close all"), Localization.lang("Close all libraries"), IconTheme.JabRefIcons.CLOSE), @@ -173,11 +174,11 @@ public enum StandardActions implements Action { ERROR_CONSOLE(Localization.lang("View event log"), Localization.lang("Display all error messages")), SEARCH_FOR_UPDATES(Localization.lang("Check for updates")), ABOUT(Localization.lang("About JabRef"), Localization.lang("About JabRef")), - EDIT_LIST(Localization.lang("Edit"), IconTheme.JabRefIcons.EDIT), VIEW_LIST(Localization.lang("View"), IconTheme.JabRefIcons.FILE), REMOVE_LIST(Localization.lang("Remove"), IconTheme.JabRefIcons.REMOVE), - RELOAD_LIST(Localization.lang("Reload"), IconTheme.JabRefIcons.REFRESH); + RELOAD_LIST(Localization.lang("Reload"), IconTheme.JabRefIcons.REFRESH), + SEND_TO_SHARELATEX("Send content to Overleaf", "Send to overleaf"); private final String text; private final String description; 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..1f780892279 --- /dev/null +++ b/src/main/java/org/jabref/gui/actions/SynchronizeWithShareLatexAction.java @@ -0,0 +1,11 @@ +package org.jabref.gui.actions; + +import org.jabref.gui.sharelatex.ShareLatexLoginDialogView; + +public class SynchronizeWithShareLatexAction extends SimpleCommand { + + @Override + public void execute() { + new ShareLatexLoginDialogView().showAndWait(); + } +} diff --git a/src/main/java/org/jabref/gui/shared/ConnectToSharedDatabaseCommand.java b/src/main/java/org/jabref/gui/shared/ConnectToSharedDatabaseCommand.java index 9a79e714a65..91934fc9676 100644 --- a/src/main/java/org/jabref/gui/shared/ConnectToSharedDatabaseCommand.java +++ b/src/main/java/org/jabref/gui/shared/ConnectToSharedDatabaseCommand.java @@ -7,7 +7,7 @@ import com.airhacks.afterburner.injection.Injector; /** - * Opens a shared database. + * Opens a shared SQL database */ public class ConnectToSharedDatabaseCommand extends SimpleCommand { diff --git a/src/main/java/org/jabref/gui/shared/SharedDatabaseLoginDialogViewModel.java b/src/main/java/org/jabref/gui/shared/SharedDatabaseLoginDialogViewModel.java index 36411223cae..5d62a88f4b9 100644 --- a/src/main/java/org/jabref/gui/shared/SharedDatabaseLoginDialogViewModel.java +++ b/src/main/java/org/jabref/gui/shared/SharedDatabaseLoginDialogViewModel.java @@ -1,6 +1,5 @@ package org.jabref.gui.shared; -import java.io.UnsupportedEncodingException; import java.nio.file.Files; import java.nio.file.Path; import java.security.GeneralSecurityException; @@ -204,7 +203,7 @@ private void setPreferences() { if (rememberPassword.get()) { try { sharedDatabasePreferences.setPassword(new Password(password.getValue(), user.getValue()).encrypt()); - } catch (GeneralSecurityException | UnsupportedEncodingException e) { + } catch (GeneralSecurityException e) { LOGGER.error("Could not store the password due to encryption problems.", e); } } else { @@ -247,7 +246,7 @@ private void applyPreferences() { if (sharedDatabasePassword.isPresent() && sharedDatabaseUser.isPresent()) { try { password.setValue(new Password(sharedDatabasePassword.get().toCharArray(), sharedDatabaseUser.get()).decrypt()); - } catch (GeneralSecurityException | UnsupportedEncodingException e) { + } catch (GeneralSecurityException e) { LOGGER.error("Could not read the password due to decryption problems.", e); } } diff --git a/src/main/java/org/jabref/gui/sharelatex/ShareLatexLoginDialog.fxml b/src/main/java/org/jabref/gui/sharelatex/ShareLatexLoginDialog.fxml new file mode 100644 index 00000000000..467aebc57a9 --- /dev/null +++ b/src/main/java/org/jabref/gui/sharelatex/ShareLatexLoginDialog.fxml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+
+
+ + + + +
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..847d4e94cca --- /dev/null +++ b/src/main/java/org/jabref/gui/sharelatex/ShareLatexLoginDialogView.java @@ -0,0 +1,81 @@ +package org.jabref.gui.sharelatex; + +import javafx.fxml.FXML; +import javafx.scene.control.Alert.AlertType; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonType; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; + +import org.jabref.gui.DialogService; +import org.jabref.gui.FXDialog; +import org.jabref.gui.Globals; +import org.jabref.gui.util.BaseDialog; +import org.jabref.gui.util.ControlHelper; +import org.jabref.logic.l10n.Localization; +import org.jabref.logic.sharelatex.ShareLatexManager; +import org.jabref.logic.sharelatex.SharelatexConnectionProperties; + +import com.airhacks.afterburner.views.ViewLoader; +import jakarta.inject.Inject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ShareLatexLoginDialogView extends BaseDialog { + + private static final Logger LOGGER = LoggerFactory.getLogger(ShareLatexLoginDialogView.class); + + @FXML private TextField tbAddress; + @FXML private TextField tbUsername; + @FXML private PasswordField tbPassword; + @FXML private ButtonType connectButton; + + private final Button btnLogin; + @Inject private ShareLatexManager manager; + @Inject private DialogService dialogService; + + private SharelatexConnectionProperties props; + + public ShareLatexLoginDialogView() { + ViewLoader.view(this) + .load() + .setAsDialogPane(this); + + ControlHelper.setAction(connectButton, this.getDialogPane(), event -> signIn()); + btnLogin = (Button) this.getDialogPane().lookupButton(connectButton); + } + + @FXML + private void initialize() { + } + + @FXML + private void signIn() { + btnLogin.setText(Localization.lang("Logging in...")); + try { + String result = manager.login(tbAddress.getText(), tbUsername.getText(), tbPassword.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 { + // TODO: Replace with PreferencesService later + + props = new SharelatexConnectionProperties(Globals.prefs.getShareLatexPreferences()); + + props.setUrl(tbAddress.getText()); + props.setUser(tbUsername.getText()); + props.setPassword(tbPassword.getText()); + + manager.setConnectionProperties(props); + + ShareLatexProjectDialogView dlgprojects = new ShareLatexProjectDialogView(); + dlgprojects.show(); + this.close(); + } + } catch (Exception e) { + LOGGER.error("Problems connectiong", e); + dialogService.showErrorDialogAndWait(e); + } + } +} diff --git a/src/main/java/org/jabref/gui/sharelatex/ShareLatexProjectDialog.fxml b/src/main/java/org/jabref/gui/sharelatex/ShareLatexProjectDialog.fxml new file mode 100644 index 00000000000..efb1baaca9f --- /dev/null +++ b/src/main/java/org/jabref/gui/sharelatex/ShareLatexProjectDialog.fxml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..e266be4c00a --- /dev/null +++ b/src/main/java/org/jabref/gui/sharelatex/ShareLatexProjectDialogView.java @@ -0,0 +1,97 @@ +package org.jabref.gui.sharelatex; + +import java.io.IOException; +import java.util.Optional; + +import javafx.fxml.FXML; +import javafx.scene.control.ButtonType; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.cell.CheckBoxTableCell; + +import org.jabref.gui.StateManager; +import org.jabref.gui.util.BaseDialog; +import org.jabref.gui.util.ControlHelper; +import org.jabref.gui.util.DefaultFileUpdateMonitor; +import org.jabref.logic.sharelatex.ShareLatexManager; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.preferences.PreferencesService; + +import com.airhacks.afterburner.views.ViewLoader; +import jakarta.inject.Inject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ShareLatexProjectDialogView extends BaseDialog { + + private static final Logger LOGGER = LoggerFactory.getLogger(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; + + @FXML private ButtonType syncButton; + + @Inject private ShareLatexManager manager; + @Inject private StateManager stateManager; + @Inject private PreferencesService preferences; + + private ShareLatexProjectDialogViewModel viewModel; + + @Inject private DefaultFileUpdateMonitor fileMonitor; + + public ShareLatexProjectDialogView() { + this.setTitle("Overleaf projects"); + + ViewLoader.view(this) + .load() + .setAsDialogPane(this); + + ControlHelper.setAction(syncButton, this.getDialogPane(), event -> synchronizeLibrary()); + } + + @FXML + private void initialize() { + viewModel = new ShareLatexProjectDialogViewModel(stateManager, manager, preferences.getImportFormatPreferences(), fileMonitor); + 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(), fileMonitor); + } + } + + @FXML + private void cancelAndClose() { + } +} 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..27932d9e85b --- /dev/null +++ b/src/main/java/org/jabref/gui/sharelatex/ShareLatexProjectDialogViewModel.java @@ -0,0 +1,102 @@ +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.Optional; +import java.util.stream.Collectors; + +import javafx.beans.property.SimpleListProperty; +import javafx.collections.FXCollections; + +import org.jabref.gui.StateManager; +import org.jabref.logic.importer.ImportFormatPreferences; +import org.jabref.logic.importer.ParserResult; +import org.jabref.logic.importer.fileformat.BibtexImporter; +import org.jabref.logic.sharelatex.ShareLatexManager; +import org.jabref.logic.sharelatex.ShareLatexParser; +import org.jabref.logic.sharelatex.events.ShareLatexEntryMessageEvent; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.sharelatex.ShareLatexProject; +import org.jabref.model.util.FileUpdateMonitor; + +import com.google.common.eventbus.Subscribe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ShareLatexProjectDialogViewModel { + + private static final Logger LOGGER = LoggerFactory.getLogger(ShareLatexProjectDialogViewModel.class); + + private final StateManager stateManager; + private final ShareLatexManager manager; + private final SimpleListProperty projects = new SimpleListProperty<>( + FXCollections.observableArrayList()); + private final ImportFormatPreferences prefs; + + private final FileUpdateMonitor fileMonitor; + + public ShareLatexProjectDialogViewModel(StateManager stateManager, ShareLatexManager manager, ImportFormatPreferences prefs, FileUpdateMonitor fileMonitor) { + this.stateManager = stateManager; + this.prefs = prefs; + this.fileMonitor = fileMonitor; + manager.registerListener(this); + this.manager = manager; + } + + 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 actualDbPath = stateManager.getActiveDatabase().get().getDatabasePath().get(); + List entries = event.getEntries(); + + try { + ParserResult result = new BibtexImporter(prefs, fileMonitor).importDatabase(event.getNewDatabaseContent()); + + ShareLatexParser parser = new ShareLatexParser(); + if (event.getPosition() != -1) { + // Was the change on the sharelatex server side actually an entry? + Optional entryFromPosition = parser.getEntryFromPosition(result, event.getPosition()); + + if (entryFromPosition.isPresent()) { + BibEntry identifedEntry = entryFromPosition.get(); + Optional entryFromSharelatex = entries.stream().filter(searchEntry -> searchEntry.equals(identifedEntry)).findFirst(); + + // We search the local database for an entry with the cite key + Optional entryFromLocalDatabase = stateManager.getActiveDatabase().get().getDatabase().getEntryByCitationKey(identifedEntry.getCitationKey().get()); + + if (entryFromSharelatex.isPresent() && entryFromLocalDatabase.isPresent()) { + /* MergeSharedEntryDialog dlg = new MergeSharedEntryDialog(JabRefGUI.getMainFrame(), + entryFromLocalDatabase.get(), + entryFromSharelatex.get(), + stateManager.getActiveDatabase().get().getMode()); + dlg.setMetaData(stateManager.getActiveDatabase().get().getMetaData()); + dlg.showMergeDialog(); + */ + // TODO: After merge we probably need to send the new content to the server + } + } else { + try (BufferedWriter writer = Files.newBufferedWriter(actualDbPath, StandardCharsets.UTF_8)) { + writer.write(event.getNewDatabaseContent()); + } catch (IOException e) { + LOGGER.error("Problem writing new database content", e); + } + } + LOGGER.debug("Changed chars: {}", event.getChars()); + } + } catch (IOException e1) { + LOGGER.error("Problem parsing position new database content", e1); + } + } +} 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..1dc1af551ab --- /dev/null +++ b/src/main/java/org/jabref/gui/sharelatex/ShareLatexProjectViewModel.java @@ -0,0 +1,58 @@ +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; + +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/shared/DBMSConnectionProperties.java b/src/main/java/org/jabref/logic/shared/DBMSConnectionProperties.java index c41c19d6167..8fa0c57a40c 100644 --- a/src/main/java/org/jabref/logic/shared/DBMSConnectionProperties.java +++ b/src/main/java/org/jabref/logic/shared/DBMSConnectionProperties.java @@ -1,6 +1,5 @@ package org.jabref.logic.shared; -import java.io.UnsupportedEncodingException; import java.security.GeneralSecurityException; import java.util.Objects; import java.util.Optional; @@ -55,7 +54,7 @@ public DBMSConnectionProperties(SharedDatabasePreferences prefs) { if (prefs.getPassword().isPresent()) { try { this.password = new Password(prefs.getPassword().get().toCharArray(), prefs.getUser().get()).decrypt(); - } catch (UnsupportedEncodingException | GeneralSecurityException e) { + } catch (GeneralSecurityException e) { LOGGER.error("Could not decrypt password", e); } } diff --git a/src/main/java/org/jabref/logic/shared/prefs/SharedDatabasePreferences.java b/src/main/java/org/jabref/logic/shared/prefs/SharedDatabasePreferences.java index 4b1a14fe1c9..54953d5205c 100644 --- a/src/main/java/org/jabref/logic/shared/prefs/SharedDatabasePreferences.java +++ b/src/main/java/org/jabref/logic/shared/prefs/SharedDatabasePreferences.java @@ -1,6 +1,5 @@ package org.jabref.logic.shared.prefs; -import java.io.UnsupportedEncodingException; import java.security.GeneralSecurityException; import java.util.Optional; import java.util.prefs.BackingStoreException; @@ -169,7 +168,7 @@ public void putAllDBMSConnectionProperties(DatabaseConnectionProperties properti try { setPassword(new Password(properties.getPassword().toCharArray(), properties.getUser()).encrypt()); - } catch (GeneralSecurityException | UnsupportedEncodingException e) { + } catch (GeneralSecurityException e) { LOGGER.error("Could not store the password due to encryption problems.", e); } } diff --git a/src/main/java/org/jabref/logic/shared/security/Password.java b/src/main/java/org/jabref/logic/shared/security/Password.java index 7d858e5b8f6..fbe7d1828de 100644 --- a/src/main/java/org/jabref/logic/shared/security/Password.java +++ b/src/main/java/org/jabref/logic/shared/security/Password.java @@ -1,6 +1,5 @@ package org.jabref.logic.shared.security; -import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.MessageDigest; @@ -43,7 +42,7 @@ public Password(String phrase, String key) throws NoSuchAlgorithmException, NoSu * * @return Encrypted phrase/password */ - public String encrypt() throws GeneralSecurityException, UnsupportedEncodingException { + public String encrypt() throws GeneralSecurityException { cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); return new String(Base64.getEncoder().encode(cipher.doFinal(phrase)), StandardCharsets.UTF_8); } @@ -53,7 +52,7 @@ public String encrypt() throws GeneralSecurityException, UnsupportedEncodingExce * * @return Decrypted phrase/password */ - public String decrypt() throws GeneralSecurityException, UnsupportedEncodingException { + public String decrypt() throws GeneralSecurityException { cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec); return new String(cipher.doFinal(Base64.getDecoder().decode(phrase)), StandardCharsets.UTF_8); } 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..ec6d6ac285d --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/MyCustomClientEndpointConfigurator.java @@ -0,0 +1,44 @@ +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; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MyCustomClientEndpointConfigurator extends ClientEndpointConfig.Configurator { + + private static final Logger LOGGER = LoggerFactory.getLogger(MyCustomClientEndpointConfigurator.class); + private final String userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.16; rv:84.0) Gecko/20100101 Firefox/84.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(); + LOGGER.debug("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..4e18bfe86d1 --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/ShareLatexJsonMessage.java @@ -0,0 +1,63 @@ +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..5de2ffb4317 --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/ShareLatexManager.java @@ -0,0 +1,97 @@ +package org.jabref.logic.sharelatex; + +import java.io.IOException; +import java.io.StringWriter; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.List; + +import org.jabref.gui.Globals; +import org.jabref.gui.JabRefExecutorService; +import org.jabref.logic.exporter.BibtexDatabaseWriter; +import org.jabref.logic.exporter.SavePreferences; +import org.jabref.logic.importer.ImportFormatPreferences; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.sharelatex.ShareLatexProject; +import org.jabref.model.util.FileUpdateMonitor; +import org.jabref.preferences.GeneralPreferences; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ShareLatexManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(ShareLatexManager.class); + + private final SharelatexConnector connector = new SharelatexConnector(); + private final ShareLatexParser parser = new ShareLatexParser(); + private SharelatexConnectionProperties properties; + + public ShareLatexManager() { + } + + 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, FileUpdateMonitor fileMonitor) { + JabRefExecutorService.INSTANCE.executeAndWait(() -> { + + try { + connector.startWebsocketListener(projectID, database, preferences, fileMonitor); + } catch (URISyntaxException e) { + LOGGER.error("Exception {}", e); + } + registerListener(ShareLatexManager.this); + }); + } + + /** + * pushes the database content to overleaf + * + * @param bibDatabaseContext the context of the database to send + */ + public void sendNewDatabaseContent(BibDatabaseContext bibDatabaseContext) { + try { + GeneralPreferences generalPreferences = Globals.prefs.getGeneralPreferences(); + SavePreferences prefs = Globals.prefs.getSavePreferences(); + + StringWriter outputWriter = new StringWriter(); + BibtexDatabaseWriter databaseWriter = new BibtexDatabaseWriter(outputWriter, "\n", generalPreferences, prefs, Globals.entryTypesManager); + databaseWriter.savePartOfDatabase(bibDatabaseContext, bibDatabaseContext.getEntries()); + String content = outputWriter.toString(); + connector.sendNewDatabaseContent(content); + } catch (InterruptedException e) { + LOGGER.error("Could not prepare database for saving ", e); + } catch (IOException e) { + LOGGER.error("General I/O Exception", e); + } + } + + public void registerListener(Object listener) { + connector.registerListener(listener); + } + + public void unregisterListener(Object listener) { + connector.unregisterListener(listener); + } + + public void disconnectAndCloseConnection() { + connector.disconnectAndCloseConn(); + } + + public void setConnectionProperties(SharelatexConnectionProperties props) { + this.properties = props; + } + + public SharelatexConnectionProperties getConnectionProperties() { + return this.properties; + } +} 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..f0ac425958d --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/ShareLatexParser.java @@ -0,0 +1,240 @@ +package org.jabref.logic.sharelatex; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.StringJoiner; +import java.util.stream.Collectors; + +import org.jabref.logic.importer.ImportFormatPreferences; +import org.jabref.logic.importer.ParseException; +import org.jabref.logic.importer.ParserResult; +import org.jabref.logic.importer.fileformat.BibtexParser; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.sharelatex.ShareLatexOtAppliedMessage; +import org.jabref.model.sharelatex.ShareLatexProject; +import org.jabref.model.util.FileUpdateMonitor; + +import com.github.difflib.DiffUtils; +import com.github.difflib.patch.AbstractDelta; +import com.github.difflib.patch.Patch; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ShareLatexParser { + + private static final Logger LOGGER = LoggerFactory.getLogger(ShareLatexParser.class); + + private final JsonParser parser = new JsonParser(); + private final Gson gson = new GsonBuilder().create(); + + public int getVersionFromBibTexJsonString(String content) { + JsonArray array = parseFirstPartOfMessageAsArray(content); + return array.get(2).getAsInt(); + } + + public ShareLatexOtAppliedMessage getOtAppliedMessage(String content) { + String strs = content.substring(content.indexOf("{"), content.length()); + ShareLatexOtAppliedMessage message = gson.fromJson(strs, ShareLatexOtAppliedMessage.class); + return message; + } + + public int getPositionFromBibtexJsonUpdateMessage(String content) { + String strs = content.substring(content.indexOf("{"), content.length()); + + ShareLatexOtAppliedMessage message = gson.fromJson(strs, ShareLatexOtAppliedMessage.class); + return message.getArgs().get(0).getOp().get(0).getPosition(); + + // 5:::{"name":"otUpdateApplied","args":[{"doc":"5a797ca3b42d76683b3ea200","op":[{"p":633,"d":"A. Viterbi"}],"v":71,"meta":{"source":"x3f_9gg_sYE1IC9v_oTa","user_id":"5a797c98b42d76683b3ea1fc","ts":1517997414640}}]} + } + + private JsonObject getFirstEntryOfArrayAsJsonObject(JsonObject obj, String arrName) { + return obj.get(arrName).getAsJsonArray().get(0).getAsJsonObject(); + } + + public List parseBibEntryFromJsonMessageString(String message, ImportFormatPreferences prefs, FileUpdateMonitor fileMonitor) + throws ParseException { + return parseBibEntryFromJsonArray(parseFirstPartOfMessageAsArray(message), prefs, fileMonitor); + } + + 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) { + Patch patches; + // Splits the lines using "\n" - therefore, we can use "\n" later on to join the text again + patches = DiffUtils.diff(before, after, null); + + int pos = 0; + + List docsWithChanges = new ArrayList<>(); + + for (AbstractDelta delta : patches.getDeltas()) { + SharelatexDoc doc = new SharelatexDoc(); + String newText; + String deletedText; + switch (delta.getType()) { + case EQUAL: + pos += delta.getSource().size(); + break; + case INSERT: + doc.setPosition(pos); + newText = delta.getTarget().getLines().stream().collect(Collectors.joining("\n")); + doc.setContent(newText); + doc.setOperation("i"); + docsWithChanges.add(doc); + pos += newText.length(); + break; + case DELETE: + doc.setPosition(pos); + deletedText = delta.getSource().getLines().stream().collect(Collectors.joining("\n")); + doc.setContent(deletedText); + doc.setOperation("d"); + docsWithChanges.add(doc); + break; + case CHANGE: + // CHANGE is delete and insert + + doc.setPosition(pos); + deletedText = delta.getSource().getLines().stream().collect(Collectors.joining("\n")); + doc.setContent(deletedText); + doc.setOperation("d"); + docsWithChanges.add(doc); + + doc.setPosition(pos); + newText = delta.getTarget().getLines().stream().collect(Collectors.joining("\n")); + doc.setContent(newText); + doc.setOperation("i"); + docsWithChanges.add(doc); + pos += newText.length(); + break; + default: + LOGGER.error("Unknown delta type"); + break; + } + } + 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("email").getAsString(); + + ShareLatexProject project = new ShareLatexProject(id, name, firstName, lastName, lastUpdated); + projects.add(project); + } + } + return projects; + } + + private List parseBibEntryFromJsonArray(JsonArray arr, ImportFormatPreferences prefs, FileUpdateMonitor fileMonitor) + throws ParseException { + String bibtexString = getBibTexStringFromJsonArray(arr); + BibtexParser parser = new BibtexParser(prefs, fileMonitor); + 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); + } + + public Optional getEntryFromPosition(ParserResult result, int position) { + Objects.requireNonNull(result); + if (position < 1) { + throw new IllegalArgumentException("Position must be positive"); + } + + int currentStartPos = 0; + for (BibEntry entry : result.getDatabase().getEntries()) { + int endPos = currentStartPos + entry.getParsedSerialization().length(); + boolean isInRange = (currentStartPos <= position) && (position <= endPos); + if (isInRange) { + return Optional.of(entry); + } else { + currentStartPos = endPos; + } + } + + return Optional.empty(); + } +} diff --git a/src/main/java/org/jabref/logic/sharelatex/ShareLatexPreferences.java b/src/main/java/org/jabref/logic/sharelatex/ShareLatexPreferences.java new file mode 100644 index 00000000000..79da6ac1f79 --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/ShareLatexPreferences.java @@ -0,0 +1,80 @@ +package org.jabref.logic.sharelatex; + +import java.util.Optional; +import java.util.prefs.BackingStoreException; + +public class ShareLatexPreferences { + + private final String defaultNode; + private final String parentNode; + private String sharelatexUser; + private String shareLatexUrl; + private String shareLatexPassword; + private boolean shareLatexRememberPassword; + private String shareLatexProject; + + public ShareLatexPreferences(String defaultNode, String parentNode, String getSharelatexUser, String shareLatexUrl, String shareLatexPassword, boolean shareLatexRememberPassword, String shareLatexProject) { + this.defaultNode = defaultNode; + this.parentNode = parentNode; + this.sharelatexUser = getSharelatexUser; + this.shareLatexUrl = shareLatexUrl; + this.shareLatexPassword = shareLatexPassword; + this.shareLatexRememberPassword = shareLatexRememberPassword; + this.shareLatexProject = shareLatexProject; + } + + public String getSharelatexUrl() { + return getOptionalValue(shareLatexUrl).orElse("https://www.sharelatex.com"); + } + + public Optional getUser() { + return getOptionalValue(sharelatexUser); + } + + public Optional getPassword() { + return getOptionalValue(shareLatexPassword); + } + + public Optional getDefaultProject() { + return getOptionalValue(shareLatexProject); + } + + public String getDefaultNode() { + return defaultNode; + } + + public String getParentNode() { + return parentNode; + } + + public boolean getShareLatexRememberPassword() { + return shareLatexRememberPassword; + } + + public void setSharelatexUrl(String url) { + this.shareLatexUrl = url; + } + + public void setSharelatexUser(String user) { + this.sharelatexUser = user; + } + + public void setSharelatexPassword(String pwd) { + this.shareLatexPassword = pwd; + } + + public void setSharelatexProject(String project) { + this.shareLatexProject = project; + } + + public void setRememberPassword(boolean rememberPassword) { + this.shareLatexRememberPassword = rememberPassword; + } + + public void clear() throws BackingStoreException { + } + + private Optional getOptionalValue(String key) { + return Optional.ofNullable(key); + } +} diff --git a/src/main/java/org/jabref/logic/sharelatex/SharelatexConnectionProperties.java b/src/main/java/org/jabref/logic/sharelatex/SharelatexConnectionProperties.java new file mode 100644 index 00000000000..4ea2257b25d --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/SharelatexConnectionProperties.java @@ -0,0 +1,89 @@ +package org.jabref.logic.sharelatex; + +import java.security.GeneralSecurityException; +import java.util.Objects; + +import org.jabref.logic.shared.security.Password; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SharelatexConnectionProperties { + + private static final Logger LOGGER = LoggerFactory.getLogger(SharelatexConnectionProperties.class); + + private String user; + private String password; + private String url; + private String project; + + public SharelatexConnectionProperties() { + // no data + } + + public SharelatexConnectionProperties(ShareLatexPreferences prefs) { + setFromPreferences(prefs); + } + + public SharelatexConnectionProperties(String url, String user, String password, String project) { + this.url = url; + this.user = user; + this.password = password; + this.project = project; + } + + public void setUser(String user) { + this.user = user; + } + + public void setPassword(String password) { + this.password = password; + } + + public void setUrl(String url) { + this.url = url; + } + + public void setProject(String project) { + this.project = project; + } + + public String getUser() { + return user; + } + + public String getPassword() { + return password; + } + + public String getUrl() { + return url; + } + + public String getProject() { + return project; + } + + public boolean isValid() { + return Objects.nonNull(url) + && Objects.nonNull(user) + && Objects.nonNull(password) + && Objects.nonNull(project); + } + + private void setFromPreferences(ShareLatexPreferences prefs) { + this.url = prefs.getSharelatexUrl(); + prefs.getDefaultProject().ifPresent(proj -> this.project = proj); + + if (prefs.getUser().isPresent()) { + this.user = prefs.getUser().get(); + if (prefs.getPassword().isPresent()) { + try { + this.password = new Password(prefs.getPassword().get().toCharArray(), prefs.getUser().get()).decrypt(); + } catch (GeneralSecurityException e) { + LOGGER.error("Could not decrypt password", e); + } + } + } + } +} 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..c149af0b38b --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/SharelatexConnector.java @@ -0,0 +1,180 @@ +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 org.jabref.model.util.FileUpdateMonitor; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.apache.http.client.utils.URIBuilder; +import org.jsoup.Connection; +import org.jsoup.Connection.Method; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SharelatexConnector { + + private static final Logger LOGGER = LoggerFactory.getLogger(SharelatexConnector.class); + + private final String contentType = "application/json; charset=utf-8"; + private final JsonParser parser = new JsonParser(); + private final String userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:80.0) Gecko/20100101 Firefox/80.0"; + private Map loginCookies = new HashMap<>(); + private String server; + private String loginUrl; + private String csrfToken; + private String projectUrl; + private final WebSocketClientWrapper client = new WebSocketClientWrapper(); + + private static final class AccessData { + public final String _csrf; + public final String email; + public final String password; + + private AccessData(String csrfToken, String user, String password) { + _csrf = csrfToken; + email = user; + this.password = password; + } + } + + 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"); + + AccessData accessData = new AccessData(csrfToken, user, password); + Gson gson = new Gson(); + String json = gson.toJson(accessData); + + 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(); + LOGGER.debug("Body: {}", 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(); + LOGGER.error("error {}", 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(); + + var projectCookies = projectsResponse.cookies(); + this.loginCookies = projectCookies; + + Optional scriptContent = Optional + .ofNullable(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, FileUpdateMonitor fileMonitor) + throws URISyntaxException { + long millis = System.currentTimeMillis(); + LOGGER.trace("millis {}", 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(); + + LOGGER.trace("body {}", webSocketresponse.body()); + + String resp = webSocketresponse.body(); + + var cookies = webSocketresponse.cookies(); + String channel = resp.substring(0, resp.indexOf(":")); + + URI webSocketchannelUri = new URIBuilder(socketioUrl + "/websocket/" + channel).setScheme(scheme).build(); + LOGGER.debug("WebSocketChannelUrl {}", webSocketchannelUri); + client.setImportFormatPrefs(prefs, fileMonitor); + 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..7bb0ec8ff23 --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/SharelatexDoc.java @@ -0,0 +1,60 @@ +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..ab61e59769a --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/WebSocketClientWrapper.java @@ -0,0 +1,330 @@ +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.gui.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 org.jabref.model.sharelatex.ShareLatexOtAppliedMessage; +import org.jabref.model.util.FileUpdateMonitor; + +import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; +import org.glassfish.tyrus.client.ClientManager; +import org.glassfish.tyrus.client.ClientProperties; +import org.glassfish.tyrus.ext.extension.deflate.PerMessageDeflateExtension; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class WebSocketClientWrapper { + + private static final Logger LOGGER = LoggerFactory.getLogger(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 ShareLatexOtAppliedMessage otAppliedMessage = new ShareLatexOtAppliedMessage(); + private final EventBus eventBus = new EventBus("SharelatexEventBus"); + private boolean leftDoc = false; + private boolean errorReceived = false; + + private String serverOrigin; + private Map cookies; + private FileUpdateMonitor fileMonitor; + + public WebSocketClientWrapper() { + this.eventBus.register(this); + } + + public void setImportFormatPrefs(ImportFormatPreferences prefs, FileUpdateMonitor fileMonitor) { + this.prefs = prefs; + this.fileMonitor = fileMonitor; + } + + 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("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) { + LOGGER.error("Exception", e); + } + } + + 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); + } + }, "ListenToShareLatexTask"); + } + + /** + * Actual response handling + * + * @param message the message from ShareLaTeX + */ + private void parseContents(String message) { + try { + LOGGER.debug("Got message: {}", message); + if (message.contains(":::1")) { + Thread.currentThread().sleep(400); + LOGGER.debug("After sleep in :::1"); + + // TODO: Does not work reliable we often get 7:::1+0 this is an error somehow + // we need to get connection accepted with 5 + + // joinProject(projectId); + } + 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(" Conn accepted +Joining project"); + Thread.sleep(200); + joinProject(projectId); + } + + if (message.contains("[null,[")) { + LOGGER.debug("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, fileMonitor); + + LOGGER.debug("Got new entries"); + setLeftDoc(false); + + eventBus.post(new ShareLatexEntryMessageEvent(entries, bibtexString, otAppliedMessage)); + eventBus.post(new ShareLatexContinueMessageEvent()); + } + + if (message.contains("otUpdateApplied")) { + LOGGER.debug("We got an update {}", message); + + otAppliedMessage = parser.getOtAppliedMessage(message); + 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("Exception", 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..239ae1961a1 --- /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..434e0266f69 --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/events/ShareLatexEntryMessageEvent.java @@ -0,0 +1,58 @@ +package org.jabref.logic.sharelatex.events; + +import java.util.ArrayList; +import java.util.List; + +import org.jabref.model.entry.BibEntry; +import org.jabref.model.sharelatex.Arg; +import org.jabref.model.sharelatex.Op; +import org.jabref.model.sharelatex.ShareLatexOtAppliedMessage; + +public class ShareLatexEntryMessageEvent { + + private List entries = new ArrayList<>(); + + private final String database; + private final ShareLatexOtAppliedMessage message; + + public ShareLatexEntryMessageEvent(List entries, String database, ShareLatexOtAppliedMessage message) { + this.entries = entries; + this.database = database; + this.message = message; + } + + public List getEntries() { + return this.entries; + } + + public String getNewDatabaseContent() { + return this.database; + } + + public int getPosition() { + int pos = getOpFromAtPosZero(message).getPosition(); + if (pos > 0) { + return pos; + } + return -1; + } + + public String getChars() { + String chars = getOpFromAtPosZero(message).getChars(); + if (chars != null) { + return chars; + } + return ""; + } + + private Op getOpFromAtPosZero(ShareLatexOtAppliedMessage message) { + if (!this.message.getArgs().isEmpty()) { + Arg arg = message.getArgs().get(0); + List ops = arg.getOp(); + if (!ops.isEmpty()) { + return ops.get(0); + } + } + return new Op(); + } +} 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/Arg.java b/src/main/java/org/jabref/model/sharelatex/Arg.java new file mode 100644 index 00000000000..24dbd26419e --- /dev/null +++ b/src/main/java/org/jabref/model/sharelatex/Arg.java @@ -0,0 +1,38 @@ +package org.jabref.model.sharelatex; + +import java.util.ArrayList; +import java.util.List; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class Arg { + + @SerializedName("doc") @Expose private String doc; + @SerializedName("op") @Expose private List op = new ArrayList<>(); + @SerializedName("v") @Expose private int version; + + public String getDoc() { + return doc; + } + + public void setDoc(String doc) { + this.doc = doc; + } + + public List getOp() { + return op; + } + + public void setOp(List op) { + this.op = op; + } + + public int getVersion() { + return version; + } + + public void setVersion(int v) { + this.version = v; + } +} diff --git a/src/main/java/org/jabref/model/sharelatex/Op.java b/src/main/java/org/jabref/model/sharelatex/Op.java new file mode 100644 index 00000000000..9f01131b77f --- /dev/null +++ b/src/main/java/org/jabref/model/sharelatex/Op.java @@ -0,0 +1,26 @@ +package org.jabref.model.sharelatex; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class Op { + + @SerializedName("p") @Expose private int position; + @SerializedName(value = "d", alternate = "i") @Expose private String chars; + + public int getPosition() { + return position; + } + + public void setPosition(int position) { + this.position = position; + } + + public String getChars() { + return chars; + } + + public void setOp(String op) { + this.chars = op; + } +} diff --git a/src/main/java/org/jabref/model/sharelatex/ShareLatexOtAppliedMessage.java b/src/main/java/org/jabref/model/sharelatex/ShareLatexOtAppliedMessage.java new file mode 100644 index 00000000000..9323190d161 --- /dev/null +++ b/src/main/java/org/jabref/model/sharelatex/ShareLatexOtAppliedMessage.java @@ -0,0 +1,29 @@ +package org.jabref.model.sharelatex; + +import java.util.ArrayList; +import java.util.List; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class ShareLatexOtAppliedMessage { + + @SerializedName("name") @Expose private String name; + @SerializedName("args") @Expose private List args = new ArrayList<>(); + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getArgs() { + return args; + } + + public void setArgs(List args) { + this.args = args; + } +} 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..5870132daec --- /dev/null +++ b/src/main/java/org/jabref/model/sharelatex/ShareLatexProject.java @@ -0,0 +1,70 @@ +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 4781e359d7d..148740e7cb0 100644 --- a/src/main/java/org/jabref/preferences/JabRefPreferences.java +++ b/src/main/java/org/jabref/preferences/JabRefPreferences.java @@ -96,6 +96,7 @@ import org.jabref.logic.protectedterms.ProtectedTermsPreferences; import org.jabref.logic.remote.RemotePreferences; import org.jabref.logic.shared.prefs.SharedDatabasePreferences; +import org.jabref.logic.sharelatex.ShareLatexPreferences; import org.jabref.logic.util.OS; import org.jabref.logic.util.Version; import org.jabref.logic.util.io.AutoLinkPreferences; @@ -231,6 +232,15 @@ public class JabRefPreferences implements PreferencesService { public static final String ADD_CREATION_DATE = "addCreationDate"; public static final String ADD_MODIFICATION_DATE = "addModificationDate"; + // Sharelatex preferences + public static final String DEFAULT_NODE = "default"; + public static final String PARENT_NODE = "jabref-sharelatex"; + public static final String SHARELATEX_URL = "sharelatexUrl"; + public static final String SHARELATEX_USER = "sharelatexUser"; + public static final String SHARELATEX_PASSWORD = "sharelatexPassword"; + public static final String SHARELATEX_REMEMBER_PASSWORD = "sharelatexRememberPassword"; + public static final String SHARELATEX_PROJECT = "sharelatexProject"; + public static final String WARN_ABOUT_DUPLICATES_IN_INSPECTION = "warnAboutDuplicatesInInspection"; public static final String NON_WRAPPABLE_FIELDS = "nonWrappableFields"; public static final String RESOLVE_STRINGS_FOR_FIELDS = "resolveStringsForFields"; @@ -502,6 +512,10 @@ private JabRefPreferences() { defaults.put(USE_CUSTOM_DOI_URI, Boolean.FALSE); defaults.put(BASE_DOI_URI, "https://doi.org"); + // Sharelatex + defaults.put(DEFAULT_NODE, "default"); + defaults.put(PARENT_NODE, "jabref-sharelatex"); + if (OS.OS_X) { defaults.put(FONT_FAMILY, "SansSerif"); defaults.put(PUSH_EMACS_PATH, "emacsclient"); @@ -882,7 +896,7 @@ public boolean getBoolean(String key, boolean def) { } private boolean getBooleanDefault(String key) { - return (Boolean) defaults.get(key); + return (Boolean) (defaults.get(key) == null ? false : defaults.get(key)); } public int getInt(String key) { @@ -1118,6 +1132,26 @@ public LayoutFormatterPreferences getLayoutFormatterPreferences(JournalAbbreviat repository); } + public ShareLatexPreferences getShareLatexPreferences() { + return new ShareLatexPreferences(get(DEFAULT_NODE), get(PARENT_NODE), get(SHARELATEX_USER), get(SHARELATEX_URL), get(SHARELATEX_PASSWORD), getBoolean(SHARELATEX_REMEMBER_PASSWORD), get(SHARELATEX_PROJECT)); + } + + public void storeShareLatexPreferences(ShareLatexPreferences sharelatexPreferences) { + put(JabRefPreferences.DEFAULT_NODE, sharelatexPreferences.getDefaultNode()); + put(JabRefPreferences.PARENT_NODE, sharelatexPreferences.getParentNode()); + sharelatexPreferences.getUser().ifPresent( + user -> put(JabRefPreferences.SHARELATEX_USER, user) + ); + put(JabRefPreferences.SHARELATEX_URL, sharelatexPreferences.getSharelatexUrl()); + sharelatexPreferences.getPassword().ifPresent( + password -> put(JabRefPreferences.SHARELATEX_PASSWORD, password) + ); + putBoolean(JabRefPreferences.SHARELATEX_REMEMBER_PASSWORD, sharelatexPreferences.getShareLatexRememberPassword()); + sharelatexPreferences.getDefaultProject().ifPresent( + project -> put(JabRefPreferences.SHARELATEX_PROJECT, project) + ); + } + @Override public JournalAbbreviationPreferences getJournalAbbreviationPreferences() { if (Objects.nonNull(journalAbbreviationPreferences)) { 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 00000000000..0b12a47a959 Binary files /dev/null and b/src/main/resources/images/external/lion.png differ diff --git a/src/test/java/org/jabref/logic/sharelatex/ShareLatexJsonMessageTest.java b/src/test/java/org/jabref/logic/sharelatex/ShareLatexJsonMessageTest.java new file mode 100644 index 00000000000..606cae12a64 --- /dev/null +++ b/src/test/java/org/jabref/logic/sharelatex/ShareLatexJsonMessageTest.java @@ -0,0 +1,42 @@ +package org.jabref.logic.sharelatex; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ShareLatexJsonMessageTest { + + @Test + public void testCreateDeleteInsertMessage() { + String expected = "{\"name\":\"applyOtUpdate\",\"args\":[\"5936d96b1bd5906b0082f53e\",{\"doc\":\"5936d96b1bd5906b0082f53e\",\"op\":[{\"p\":0,\"d\":\"ToDelete \"},{\"p\":0,\"i\":\" To Insert\"}],\"v\":68}]}"; + ShareLatexJsonMessage message = new ShareLatexJsonMessage(); + + String result = message.createDeleteInsertMessage("5936d96b1bd5906b0082f53e", 0, 68, "ToDelete ", " To Insert"); + assertEquals(expected, result); + } + + @Test + public void testCreateUpdateMessageAsInsertOrDelete() { + String expected = "{\"name\":\"applyOtUpdate\",\"args\":[\"5936d96b1bd5906b0082f53e\",{\"doc\":\"5936d96b1bd5906b0082f53e\",\"op\":[{\"p\":183,\"d\":\"Wirtschaftsinformatik\"},{\"p\":183,\"i\":\"Test\"}],\"v\":468}]}"; + + List docsForTest = new ArrayList<>(); + SharelatexDoc testDoc = new SharelatexDoc(); + testDoc.setContent("Wirtschaftsinformatik"); + testDoc.setPosition(183); + testDoc.setOperation("d"); + docsForTest.add(testDoc); + + SharelatexDoc testDoc2 = new SharelatexDoc(); + testDoc2.setContent("Test"); + testDoc2.setPosition(183); + testDoc2.setOperation("i"); + docsForTest.add(testDoc2); + + ShareLatexJsonMessage message = new ShareLatexJsonMessage(); + String result = message.createUpdateMessageAsInsertOrDelete("5936d96b1bd5906b0082f53e", 468, docsForTest); + assertEquals(expected, result); + } +} diff --git a/src/test/java/org/jabref/logic/sharelatex/ShareLatexManagerTest.java b/src/test/java/org/jabref/logic/sharelatex/ShareLatexManagerTest.java new file mode 100644 index 00000000000..df5b0cce2b3 --- /dev/null +++ b/src/test/java/org/jabref/logic/sharelatex/ShareLatexManagerTest.java @@ -0,0 +1,27 @@ +package org.jabref.logic.sharelatex; + +import java.net.URISyntaxException; + +import org.junit.jupiter.api.Test; + +public class ShareLatexManagerTest { + + @Test + public void test() throws URISyntaxException { + /* disabled for the moments + List projects; + try { + ShareLatexManager manager = new ShareLatexManager(); + manager.login("http://192.168.1.248", "joe@example.com", "test"); + + // manager.login("https://www.sharelatex.com", "email ", "password" ); + + projects = manager.getProjects(); + assertFalse(projects.isEmpty()); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + */ + } +} diff --git a/src/test/java/org/jabref/logic/sharelatex/ShareLatexParserTest.java b/src/test/java/org/jabref/logic/sharelatex/ShareLatexParserTest.java new file mode 100644 index 00000000000..a4848881aa7 --- /dev/null +++ b/src/test/java/org/jabref/logic/sharelatex/ShareLatexParserTest.java @@ -0,0 +1,233 @@ +package org.jabref.logic.sharelatex; + +import java.util.ArrayList; +import java.util.List; + +import org.jabref.model.sharelatex.ShareLatexProject; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ShareLatexParserTest { + + private final ShareLatexParser parser = new ShareLatexParser(); + + @Test + public void testGetSharelatexProjects() { + JsonParser jsonParser = new JsonParser(); + String jsonString = "{\"projects\":[{\"id\":\"58df8f5e27aa5281020536ea\",\"name\":\"PLoS one\",\"lastUpdated\":\"2017-06-07T08:42:14.632Z\",\"publicAccessLevel\":\"private\",\"accessLevel\":\"owner\",\"archived\":false,\"owner_ref\":\"58df8f3627aa5281020536e1\",\"owner\":{\"_id\":\"58df8f3627aa5281020536e1\",\"last_name\":\"\",\"first_name\":\"cschwentker\"}},{\"id\":\"5950e47621d8ee3e76616374\",\"name\":\"Example\",\"lastUpdated\":\"2017-07-16T09:49:16.241Z\",\"publicAccessLevel\":\"private\",\"accessLevel\":\"owner\",\"archived\":false,\"owner_ref\":\"58df8f3627aa5281020536e1\",\"owner\":{\"_id\":\"58df8f3627aa5281020536e1\",\"last_name\":\"\",\"first_name\":\"cschwentker\"}}],\"tags\":[{\"_id\":\"59353074a47d9c0eb124ed11\",\"user_id\":\"58df8f3627aa5281020536e1\",\"name\":\"TestFolder\",\"project_ids\":[]}],\"notifications\":[]}"; + JsonElement jsonTree = jsonParser.parse(jsonString); + JsonObject obj = jsonTree.getAsJsonObject(); + + List actual = parser.getProjectFromJson(obj); + + List expected = new ArrayList<>(); + ShareLatexProject project = new ShareLatexProject("58df8f5e27aa5281020536ea", "PLoS one", "cschwentker", "", "2017-06-07T08:42:14.632Z"); + expected.add(project); + project = new ShareLatexProject("5950e47621d8ee3e76616374", "Example", "cschwentker", "", "2017-07-16T09:49:16.241Z"); + expected.add(project); + + assertEquals(expected, actual); + } + + @Test + public void testFixWrongUTF8IsoEncoded() { + String wrongEncoded = "asdf_Wrocławskiej"; + String rightEncoded = "asdf_Wrocławskiej"; + + String actual = parser.fixUTF8Strings(wrongEncoded); + assertEquals(rightEncoded, actual); + } + + @Test + public void parseBibTexString() { + String message = "6:::78988+[null,[\"@book{adams1995hitchhiker, \",\" title={The Hitchhiker's Guide to the Galaxy},\",\" author={Adams, D.},\",\" isbn={9781417642595},\",\" url={http://books.google.com/books?id=W-xMPgAACAAJ},\",\" year={199},\",\" publisher={San Val}\",\"}\",\"\"],74,[],{}]"; + String expected = "@book{adams1995hitchhiker, \n" + + " title={The Hitchhiker's Guide to the Galaxy},\n" + + " author={Adams, D.},\n" + + " isbn={9781417642595},\n" + + " url={http://books.google.com/books?id=W-xMPgAACAAJ},\n" + + " year={199},\n" + + " publisher={San Val}\n" + + "}\n" + + ""; + + String parsed = parser.getBibTexStringFromJsonMessage(message); + assertEquals(expected, parsed); + } + + @Test + public void testgetDatabaseWithId() { + String document = "6:::1+[null,{\"_id\":\"5909edaff31ff96200ef58dd\",\"name\":\"Test\",\"rootDoc_id\":\"5909edaff31ff96200ef58de\",\"rootFolder\":[{\"_id\":\"5909edaff31ff96200ef58dc\",\"name\":\"rootFolder\",\"folders\":[],\"fileRefs\":[{\"_id\":\"5909edb0f31ff96200ef58e0\",\"name\":\"universe.jpg\"},{\"_id\":\"59118cae98ba55690073c2a0\",\"name\":\"all2.ris\"}],\"docs\":[{\"_id\":\"5909edaff31ff96200ef58de\",\"name\":\"main.tex\"},{\"_id\":\"5909edb0f31ff96200ef58df\",\"name\":\"references.bib\"},{\"_id\":\"5911801698ba55690073c29c\",\"name\":\"aaaaaaaaaaaaaa.bib\"},{\"_id\":\"59368d551bd5906b0082f53a\",\"name\":\"aaaaaaaaaaaaaa (copy 1).bib\"}]}],\"publicAccesLevel\":\"private\",\"dropboxEnabled\":false,\"compiler\":\"pdflatex\",\"description\":\"\",\"spellCheckLanguage\":\"en\",\"deletedByExternalDataSource\":false,\"deletedDocs\":[],\"members\":[{\"_id\":\"5912e195a303b468002eaad0\",\"first_name\":\"jim\",\"last_name\":\"\",\"email\":\"jim@example.com\",\"privileges\":\"readAndWrite\",\"signUpDate\":\"2017-05-10T09:47:01.325Z\"}],\"invites\":[],\"owner\":{\"_id\":\"5909ed80761dc10a01f7abc0\",\"first_name\":\"joe\",\"last_name\":\"\",\"email\":\"joe@example.com\",\"privileges\":\"owner\",\"signUpDate\":\"2017-05-03T14:47:28.665Z\"},\"features\":{\"trackChanges\":true,\"references\":true,\"templates\":true,\"compileGroup\":\"standard\",\"compileTimeout\":180,\"github\":false,\"dropbox\":true,\"versioning\":true,\"collaborators\":-1,\"trackChangesVisible\":false}},\"owner\",2]"; + + ShareLatexParser parser = new ShareLatexParser(); + String actual = parser.getFirstBibTexDatabaseId(document); + String expected = "5909edb0f31ff96200ef58df"; + + assertEquals(expected, actual); + } + + @Test + public void testGetPositionFromUpdateMessage() { + String document = "5:::{\"name\":\"otUpdateApplied\",\"args\":[{\"doc\":\"5a797ca3b42d76683b3ea200\",\"op\":[{\"p\":633,\"d\":\"A. Viterbi\"}],\"v\":71,\"meta\":{\"source\":\"x3f_9gg_sYE1IC9v_oTa\",\"user_id\":\"5a797c98b42d76683b3ea1fc\",\"ts\":1517997414640}}]}"; + ShareLatexParser parser = new ShareLatexParser(); + int actual = parser.getPositionFromBibtexJsonUpdateMessage(document); + assertEquals(633, actual); + } + + @Test + public void testgetVersionFromMessage() { + String bibTexString = "6:::7+[null,[\"@book{adams1995hitchhiker,\",\" title={The Hitchhiker's Guide to the Galaxy},\",\" author={Adams, D.},\",\" isbn={9781417642595},\",\" url={http://books.google.com/books?id=W-xMPgAACAAJ},\",\" year={1995}\",\" publisher={San Val}\",\"}\",\"\"],5,[],{}]"; + + int actual = parser.getVersionFromBibTexJsonString(bibTexString); + assertEquals(5, actual); + } + + @Test + public void testGetErrorMessage() { + String expected = "Delete component 'ławskiej' does not match deleted text 'ławskiej}'"; + String errorMessageJson = "5:::{\"name\":\"otUpdateError\",\"args\":[\"Delete component 'ławskiej' does not match deleted text 'ławskiej}'\",{\"project_id\":\"5936d96b1bd5906b0082f53c\",\"doc_id\":\"5936d96b1bd5906b0082f53e\",\"error\":\"Delete component 'ławskiej' does not match deleted text 'ławskiej}'\"}]}"; + + String actual = parser.getOtErrorMessageContent(errorMessageJson); + assertEquals(expected, actual); + } + + @Test + public void testInsertNewText() { + String before = "hello world"; + String after = "hello beautiful world"; + + List docs = parser.generateDiffs(before, after); + + SharelatexDoc testDoc = new SharelatexDoc(); + testDoc.setContent("beautiful "); + testDoc.setPosition(6); + testDoc.setOperation("i"); + + assertEquals(testDoc, docs.get(0)); + } + + @Test + public void testShiftLaterInsertsByPreviousInserts() { + String before = "the boy played with the ball"; + String after = "the tall boy played with the red ball"; + + List expected = new ArrayList<>(); + SharelatexDoc testDoc = new SharelatexDoc(); + testDoc.setContent("tall "); + testDoc.setPosition(4); + testDoc.setOperation("i"); + expected.add(testDoc); + + SharelatexDoc testDoc2 = new SharelatexDoc(); + testDoc2.setContent("red "); + testDoc2.setPosition(29); + testDoc2.setOperation("i"); + + expected.add(testDoc2); + + List docs = parser.generateDiffs(before, after); + + assertEquals(expected, docs); + } + + @Test + public void testDelete() { + String before = "hello beautiful world"; + String after = "hello world"; + + SharelatexDoc testdoc = new SharelatexDoc(); + testdoc.setContent("beautiful "); + testdoc.setPosition(6); + testdoc.setOperation("d"); + + List docs = parser.generateDiffs(before, after); + + assertEquals(testdoc, docs.get(0)); + } + + @Test + public void testShiftLaterDeleteByFirstDeletes() { + String before = "the tall boy played with the red ball"; + String after = "the boy played with the ball"; + + List expected = new ArrayList<>(); + SharelatexDoc testDoc = new SharelatexDoc(); + testDoc.setContent("tall "); + testDoc.setPosition(4); + testDoc.setOperation("d"); + expected.add(testDoc); + + SharelatexDoc testDoc2 = new SharelatexDoc(); + testDoc2.setContent("red "); + testDoc2.setPosition(24); + testDoc2.setOperation("d"); + expected.add(testDoc2); + + List docs = parser.generateDiffs(before, after); + assertEquals(expected, docs); + } + + @Test + public void testBibTexString() { + String bibTexBefore = "\n" + + "@Testcase{Sam2007,\n" + + " year = {2007},\n" + + " author = {Sam And jason},\n" + + " file = {:Huang2001 - Information Extraction from Voicemail.csv:csv},\n" + + " issue = {3},\n" + + " journal = {Wirtschaftsinformatik},\n" + + " keywords = {software development processes; agile software development environments; time-to-market; Extreme Programming; Crystal methods family; Adaptive Software Development},\n" + + " language = {english},\n" + + " mrnumber = {0937-6429},\n" + + " owner = {Christoph Schwentker},\n" + + " pages = {237--248},\n" + + " publisher = {Gabler Verlag},\n" + + " timestamp = {2016.08.20},\n" + + " title = {Agile Entwicklung Web-basierter Systeme},\n" + + " url = {http://dx.doi.org/10.1007/BF03250842},\n" + + " volume = {44},\n" + + "}\n" + + ""; + + String bibtexAfter = "\n" + + "@Testcase{Sam2007,\n" + + " year = {2007},\n" + + " author = {Sam And jason},\n" + + " file = {:Huang2001 - Information Extraction from Voicemail.csv:csv},\n" + + " issue = {3},\n" + + " journal = {Test},\n" + + " keywords = {software development processes; agile software development environments; time-to-market; Extreme Programming; Crystal methods family; Adaptive Software Development},\n" + + " language = {english},\n" + + " mrnumber = {0937-6429},\n" + + " owner = {Christoph Schwentker},\n" + + " pages = {237--248},\n" + + " publisher = {Gabler Verlag},\n" + + " timestamp = {2016.08.20},\n" + + " title = {Agile Entwicklung Web-basierter Systeme},\n" + + " url = {http://dx.doi.org/10.1007/BF03250842},\n" + + " volume = {44},\n" + + "}\n" + + ""; + + List expected = new ArrayList<>(); + SharelatexDoc testDoc = new SharelatexDoc(); + testDoc.setContent("Wirtschaftsinformatik"); + testDoc.setPosition(183); + testDoc.setOperation("d"); + expected.add(testDoc); + + SharelatexDoc testDoc2 = new SharelatexDoc(); + testDoc2.setContent("Test"); + testDoc2.setPosition(183); + testDoc2.setOperation("i"); + expected.add(testDoc2); + + List docs = parser.generateDiffs(bibTexBefore, bibtexAfter); + assertEquals(expected, docs); + } +} diff --git a/src/test/java/org/jabref/logic/sharelatex/SharelatexConnectorTest.java b/src/test/java/org/jabref/logic/sharelatex/SharelatexConnectorTest.java new file mode 100644 index 00000000000..4c06a25dacc --- /dev/null +++ b/src/test/java/org/jabref/logic/sharelatex/SharelatexConnectorTest.java @@ -0,0 +1,35 @@ +package org.jabref.logic.sharelatex; + +import java.io.IOException; +import java.net.URISyntaxException; + +import org.jabref.gui.JabRefExecutorService; +import org.jabref.logic.importer.ImportFormatPreferences; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.util.DummyFileUpdateMonitor; + +import org.junit.jupiter.api.Test; + +import static org.mockito.Mockito.mock; + +public class SharelatexConnectorTest { + + @Test + public void testLogin() throws IOException { + + SharelatexConnector connector = new SharelatexConnector(); + connector.connectToServer("http://overleaf.com", "developers@jabref.org", "jabref"); + // connector.uploadFile("591188ed98ba55690073c29e",Paths.get("X:\\Users\\CS\\Documents\\_JABREFTEMP\\aaaaaaaaaaaaaa.bib")); + // connector.uploadFileWithWebClient("591188ed98ba55690073c29e", + // Paths.get("X:\\Users\\CS\\Documents\\_JABREFTEMP\\aaaaaaaaaaaaaa.bib")); + JabRefExecutorService.INSTANCE.executeAndWait(() -> { + try { + connector.startWebsocketListener("5936d96b1bd5906b0082f53c", new BibDatabaseContext(), + mock(ImportFormatPreferences.class), new DummyFileUpdateMonitor()); + } catch (URISyntaxException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + }); + } +}