diff --git a/build.gradle b/build.gradle index 635cac253d4..7a192977654 100644 --- a/build.gradle +++ b/build.gradle @@ -188,6 +188,11 @@ dependencies { compile group: 'com.microsoft.azure', name: 'applicationinsights-core', version: '2.4.1' compile group: 'com.microsoft.azure', name: 'applicationinsights-logging-log4j2', version: '2.4.1' + implementation 'com.google.code.gson:gson:2.8.5' + compile 'org.glassfish.tyrus.bundles:tyrus-standalone-client:1.15' + compile 'org.glassfish.tyrus.ext:tyrus-extension-deflate:1.15' + compile "org.bitbucket.cowwoc.diff-match-patch:diff-match-patch:1.0" + testCompile 'junit:junit:4.12' testImplementation 'org.junit.jupiter:junit-jupiter:5.5.1' testRuntimeOnly 'org.junit.vintage:junit-vintage-engine:5.5.1' diff --git a/external-libraries.txt b/external-libraries.txt index 107ae83f6e4..98e9a2fcbae 100644 --- a/external-libraries.txt +++ b/external-libraries.txt @@ -180,6 +180,16 @@ Project: Flowless URL: https://github.com/TomasMikula/Flowless License: BSD-2-Clause +Id: org.glassfish.tyrus.bundles:tyrus-standalone-client +Projekt: Tyrus +URL: https://tyrus.java.net/ +Licence: CDDL 1.1 and GPL 2 with CPE + +Id: com.google.code.gson:gson +Project: google-gson +URL: https://github.com/google/gson +License: Apache-2.0 + Id: org.fxmisc.richtext:richtextfx Project: RichTextFX URL: https://github.com/TomasMikula/RichTextFX diff --git a/src/main/java/org/jabref/Globals.java b/src/main/java/org/jabref/Globals.java index 71466ee47c7..b04160b9913 100644 --- a/src/main/java/org/jabref/Globals.java +++ b/src/main/java/org/jabref/Globals.java @@ -20,6 +20,7 @@ import org.jabref.logic.journals.JournalAbbreviationLoader; import org.jabref.logic.protectedterms.ProtectedTermsLoader; import org.jabref.logic.remote.server.RemoteListenerServerLifecycle; +import org.jabref.logic.sharelatex.ShareLatexManager; import org.jabref.logic.util.BuildInfo; import org.jabref.model.entry.BibEntryTypesManager; import org.jabref.model.util.FileUpdateMonitor; @@ -60,6 +61,7 @@ public class Globals { public static ClipBoardManager clipboardManager = new ClipBoardManager(); public static StateManager stateManager = new StateManager(); + public static ShareLatexManager shareLatexManager = new ShareLatexManager(); public static ExporterFactory exportFactory; public static CountingUndoManager undoManager = new CountingUndoManager(); public static BibEntryTypesManager entryTypesManager = new BibEntryTypesManager(); diff --git a/src/main/java/org/jabref/gui/DefaultInjector.java b/src/main/java/org/jabref/gui/DefaultInjector.java index ed9963b55c5..49bc24edbfd 100644 --- a/src/main/java/org/jabref/gui/DefaultInjector.java +++ b/src/main/java/org/jabref/gui/DefaultInjector.java @@ -11,6 +11,7 @@ import org.jabref.logic.journals.JournalAbbreviationLoader; import org.jabref.logic.protectedterms.ProtectedTermsLoader; import org.jabref.model.util.FileUpdateMonitor; +import org.jabref.logic.sharelatex.ShareLatexManager; import org.jabref.preferences.PreferencesService; import com.airhacks.afterburner.injection.Injector; @@ -40,6 +41,8 @@ private static Object createDependency(Class clazz) { return Globals.journalAbbreviationLoader; } else if (clazz == StateManager.class) { return Globals.stateManager; + } else if (clazz == ShareLatexManager.class) { + return Globals.shareLatexManager; } else if (clazz == FileUpdateMonitor.class) { return Globals.getFileUpdateMonitor(); } else if (clazz == ProtectedTermsLoader.class) { diff --git a/src/main/java/org/jabref/gui/JabRefFrame.java b/src/main/java/org/jabref/gui/JabRefFrame.java index d8304f472cd..c2d43db0e6c 100644 --- a/src/main/java/org/jabref/gui/JabRefFrame.java +++ b/src/main/java/org/jabref/gui/JabRefFrame.java @@ -1329,4 +1329,10 @@ private void updateTexts(UndoChangeEvent event) { */ } } + + //TODO: FIXME + public Object getSynchronizeWithSharelatexAction() { + // TODO Auto-generated method stub + return null; + } } diff --git a/src/main/java/org/jabref/gui/actions/DisconnectFromSharelatexAction.java b/src/main/java/org/jabref/gui/actions/DisconnectFromSharelatexAction.java new file mode 100644 index 00000000000..0e587ba6902 --- /dev/null +++ b/src/main/java/org/jabref/gui/actions/DisconnectFromSharelatexAction.java @@ -0,0 +1,23 @@ +package org.jabref.gui.actions; + +import java.awt.event.ActionEvent; + +import javax.swing.AbstractAction; +import javax.swing.Action; + +import org.jabref.Globals; + +public class DisconnectFromSharelatexAction extends AbstractAction { + + public DisconnectFromSharelatexAction() { + super(); + putValue(Action.NAME, "Disconnect from ShareLaTeX"); + + } + + @Override + public void actionPerformed(ActionEvent e) { + Globals.shareLatexManager.disconnectAndCloseConnection(); + } + +} diff --git a/src/main/java/org/jabref/gui/actions/SendChangesToShareLatexAction.java b/src/main/java/org/jabref/gui/actions/SendChangesToShareLatexAction.java new file mode 100644 index 00000000000..d919ccbaf91 --- /dev/null +++ b/src/main/java/org/jabref/gui/actions/SendChangesToShareLatexAction.java @@ -0,0 +1,29 @@ +package org.jabref.gui.actions; + +import java.awt.event.ActionEvent; + +import javax.swing.AbstractAction; +import javax.swing.Action; + +import org.jabref.Globals; +import org.jabref.gui.StateManager; +import org.jabref.logic.sharelatex.ShareLatexManager; + +public class SendChangesToShareLatexAction extends AbstractAction { + + public SendChangesToShareLatexAction() { + super(); + putValue(Action.NAME, "Send changes to ShareLaTeX Server"); + + } + + @Override + public void actionPerformed(ActionEvent e) { + + ShareLatexManager manager = Globals.shareLatexManager; + StateManager stateManager = Globals.stateManager; + manager.sendNewDatabaseContent(stateManager.getActiveDatabase().get()); + System.out.println("Send changes"); + } + +} diff --git a/src/main/java/org/jabref/gui/actions/SynchronizeWithShareLatexAction.java b/src/main/java/org/jabref/gui/actions/SynchronizeWithShareLatexAction.java new file mode 100644 index 00000000000..1732f1d142c --- /dev/null +++ b/src/main/java/org/jabref/gui/actions/SynchronizeWithShareLatexAction.java @@ -0,0 +1,16 @@ +package org.jabref.gui.actions; + +import org.jabref.gui.sharelatex.ShareLatexLoginDialogView; + +public class SynchronizeWithShareLatexAction extends SimpleCommand { + + public SynchronizeWithShareLatexAction() { + super(); + } + + @Override + public void execute() { + new ShareLatexLoginDialogView().show(); + + } +} diff --git a/src/main/java/org/jabref/gui/shared/SharedDatabaseLoginDialogViewModel.java b/src/main/java/org/jabref/gui/shared/SharedDatabaseLoginDialogViewModel.java index 78a6f139091..4490ba60fdc 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.nio.file.Paths; @@ -208,7 +207,7 @@ private void setPreferences() { if (rememberPassword.get()) { try { prefs.setPassword(new Password(password.getValue(), password.getValue()).encrypt()); - } catch (GeneralSecurityException | UnsupportedEncodingException e) { + } catch (GeneralSecurityException e) { LOGGER.error("Could not store the password due to encryption problems.", e); } } else { @@ -246,7 +245,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/ShareLatexLoginDialogView.java b/src/main/java/org/jabref/gui/sharelatex/ShareLatexLoginDialogView.java new file mode 100644 index 00000000000..ef4352c4abe --- /dev/null +++ b/src/main/java/org/jabref/gui/sharelatex/ShareLatexLoginDialogView.java @@ -0,0 +1,78 @@ +package org.jabref.gui.sharelatex; + +import javax.inject.Inject; + +import javafx.fxml.FXML; +import javafx.scene.control.Alert.AlertType; +import javafx.scene.control.Button; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; + +import org.jabref.Globals; +import org.jabref.gui.DialogService; +import org.jabref.gui.FXDialog; +import org.jabref.gui.util.BaseDialog; +import org.jabref.logic.sharelatex.ShareLatexManager; +import org.jabref.logic.sharelatex.SharelatexConnectionProperties; + +public class ShareLatexLoginDialogView extends BaseDialog { + + @FXML private TextField tbAddress; + @FXML private TextField tbUsername; + @FXML private PasswordField tbPassword; + @FXML private Button btnLogin; + @Inject private ShareLatexManager manager; + @Inject DialogService dialogService; + + private SharelatexConnectionProperties props; + private ShareLatexLoginDialogViewModel viewModel; + + @FXML + private void initialize() { + viewModel = new ShareLatexLoginDialogViewModel(); + } + + @FXML + private void closeDialog() { + } + + @FXML + private void signIn() { + btnLogin.setText("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: Wait until pdf + injection stuff gets merged + + 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(); + closeDialog(); + + } + } catch (Exception e) { + + dialogService.showErrorDialogAndWait(e); + + } + + } + /* + FXDialog sharelatexProjectDialog = new FXDialog(AlertType.INFORMATION, "Sharelatex Project Dialog"); + sharelatexProjectDialog.setDialogPane((DialogPane) this.getView()); + sharelatexProjectDialog.setResizable(true); + sharelatexProjectDialog.show(); + */ + +} diff --git a/src/main/java/org/jabref/gui/sharelatex/ShareLatexLoginDialogViewModel.java b/src/main/java/org/jabref/gui/sharelatex/ShareLatexLoginDialogViewModel.java new file mode 100644 index 00000000000..a1eba10e4b4 --- /dev/null +++ b/src/main/java/org/jabref/gui/sharelatex/ShareLatexLoginDialogViewModel.java @@ -0,0 +1,7 @@ +package org.jabref.gui.sharelatex; + +import org.jabref.gui.AbstractViewModel; + +public class ShareLatexLoginDialogViewModel extends AbstractViewModel { + //default construtor used +} diff --git a/src/main/java/org/jabref/gui/sharelatex/ShareLatexProjectDialogView.java b/src/main/java/org/jabref/gui/sharelatex/ShareLatexProjectDialogView.java new file mode 100644 index 00000000000..579ac39e5e5 --- /dev/null +++ b/src/main/java/org/jabref/gui/sharelatex/ShareLatexProjectDialogView.java @@ -0,0 +1,85 @@ +package org.jabref.gui.sharelatex; + +import java.io.IOException; +import java.util.Optional; + +import javax.inject.Inject; + +import javafx.fxml.FXML; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.cell.CheckBoxTableCell; + +import org.jabref.gui.StateManager; +import org.jabref.gui.util.BaseDialog; +import org.jabref.gui.util.DefaultFileUpdateMonitor; +import org.jabref.logic.sharelatex.ShareLatexManager; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.preferences.PreferencesService; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +public class ShareLatexProjectDialogView extends BaseDialog { + + private static final Log LOGGER = LogFactory.getLog(ShareLatexProjectDialogViewModel.class); + + @FXML private TableColumn colActive; + @FXML private TableColumn colTitle; + @FXML private TableColumn colFirstName; + @FXML private TableColumn colLastName; + @FXML private TableColumn colLastModified; + @FXML private TableView tblProjects; + @Inject private ShareLatexManager manager; + @Inject private StateManager stateManager; + @Inject private PreferencesService preferences; + + private ShareLatexProjectDialogViewModel viewModel; + + @Inject private DefaultFileUpdateMonitor fileMonitor; + + @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..df9bd1590ea --- /dev/null +++ b/src/main/java/org/jabref/gui/sharelatex/ShareLatexProjectDialogViewModel.java @@ -0,0 +1,114 @@ +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.AbstractViewModel; +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.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +public class ShareLatexProjectDialogViewModel extends AbstractViewModel { + + private static final Log LOGGER = LogFactory.getLog(ShareLatexProjectDialogViewModel.class); + + private final StateManager stateManager; + private final 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 datase for an etry with the cite key + Optional entryFromLocalDatabase = stateManager.getActiveDatabase().get().getDatabase().getEntryByKey(identifedEntry.getCiteKey()); + + 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()); + writer.close(); + + } catch (IOException e) { + LOGGER.error("Problem writing new database content", e); + } + } + System.out.println("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..16e876229ff --- /dev/null +++ b/src/main/java/org/jabref/gui/sharelatex/ShareLatexProjectViewModel.java @@ -0,0 +1,63 @@ +package org.jabref.gui.sharelatex; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +import org.jabref.model.sharelatex.ShareLatexProject; + +/** + * Data class + * @author CS + * + */ +public class ShareLatexProjectViewModel { + + private final SimpleBooleanProperty active = new SimpleBooleanProperty(false); + private final String projectId; + private final StringProperty projectTitle; + private final StringProperty firstName; + private final StringProperty lastName; + private final StringProperty lastUpdated; + + public ShareLatexProjectViewModel(ShareLatexProject project) { + this.projectId = project.getProjectId(); + this.projectTitle = new SimpleStringProperty(project.getProjectTitle()); + this.firstName = new SimpleStringProperty(project.getFirstName()); + this.lastName = new SimpleStringProperty(project.getLastName()); + this.lastUpdated = new SimpleStringProperty(project.getLastUpdated()); + } + + public String getProjectId() { + return projectId; + } + + public StringProperty getProjectTitle() { + return projectTitle; + } + + public StringProperty getFirstName() { + return firstName; + } + + public StringProperty getLastName() { + return lastName; + } + + public StringProperty getLastUpdated() { + return lastUpdated; + } + + public Boolean isActive() { + return active.getValue(); + } + + public BooleanProperty isActiveProperty() { + return active; + } + + public void setActive(boolean active) { + this.active.set(active); + } +} diff --git a/src/main/java/org/jabref/logic/shared/DBMSConnectionProperties.java b/src/main/java/org/jabref/logic/shared/DBMSConnectionProperties.java index f39eb8a41a4..47da6c7f5d6 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; @@ -202,7 +201,7 @@ private void setFromPreferences(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 45346f334a2..538044d149e 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; @@ -155,7 +154,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 50ebab42e01..880884e493b 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; @@ -44,7 +43,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); } @@ -54,7 +53,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..bc88b2540a4 --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/MyCustomClientEndpointConfigurator.java @@ -0,0 +1,43 @@ +package org.jabref.logic.sharelatex; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.websocket.ClientEndpointConfig; +import javax.websocket.HandshakeResponse; + +public class MyCustomClientEndpointConfigurator extends ClientEndpointConfig.Configurator { + + private final String userAgent = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0"; + private final String serverOrigin; + private final Map cookies; + + public MyCustomClientEndpointConfigurator(String serverOrigin, Map cookies) { + super(); + this.serverOrigin = serverOrigin; + this.cookies = cookies; + } + + @Override + public void beforeRequest(Map> headers) { + + headers.put("User-Agent", Arrays.asList(userAgent)); + headers.put("Origin", Arrays.asList(serverOrigin)); + + String result = cookies.entrySet() + .stream() + .map(entry -> entry.getKey() + "=" + entry.getValue()) + .collect(Collectors.joining("; ")); + headers.put("Cookie", Arrays.asList(result)); + } + + @Override + public void afterResponse(HandshakeResponse handshakeResponse) { + final Map> headers = handshakeResponse.getHeaders(); + + System.out.println("headers " + headers); + + } +} diff --git a/src/main/java/org/jabref/logic/sharelatex/ShareLatexJsonMessage.java b/src/main/java/org/jabref/logic/sharelatex/ShareLatexJsonMessage.java new file mode 100644 index 00000000000..b93b6f261ce --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/ShareLatexJsonMessage.java @@ -0,0 +1,66 @@ +package org.jabref.logic.sharelatex; + +import java.util.List; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +public class ShareLatexJsonMessage { + + public String createDeleteInsertMessage(String docId, int position, int version, String oldContent, String newContent) { + + JsonObject insertContent = new JsonObject(); + insertContent.addProperty("p", position); + insertContent.addProperty("d", oldContent); + + JsonObject deleteContent = new JsonObject(); + deleteContent.addProperty("p", position); + deleteContent.addProperty("i", newContent); + + JsonArray opArray = new JsonArray(); + opArray.add(insertContent); + opArray.add(deleteContent); + + JsonObject docIdOp = new JsonObject(); + docIdOp.addProperty("doc", docId); + docIdOp.add("op", opArray); + docIdOp.addProperty("v", version); + + JsonArray argsArray = new JsonArray(); + argsArray.add(docId); + argsArray.add(docIdOp); + + JsonObject obj = new JsonObject(); + obj.addProperty("name", "applyOtUpdate"); + obj.add("args", argsArray); + + return obj.toString(); + + } + + public String createUpdateMessageAsInsertOrDelete(String docId, int version, List docs) { + JsonArray opArray = new JsonArray(); + for (SharelatexDoc doc : docs) { + JsonObject deleteOrInsertContent = new JsonObject(); + deleteOrInsertContent.addProperty("p", doc.getPosition()); + deleteOrInsertContent.addProperty(doc.getOperation(), doc.getContent()); + opArray.add(deleteOrInsertContent); + } + + JsonObject docIdOp = new JsonObject(); + docIdOp.addProperty("doc", docId); + docIdOp.add("op", opArray); + docIdOp.addProperty("v", version); + + JsonArray argsArray = new JsonArray(); + argsArray.add(docId); + argsArray.add(docIdOp); + + JsonObject obj = new JsonObject(); + obj.addProperty("name", "applyOtUpdate"); + obj.add("args", argsArray); + + return obj.toString(); + } + +} diff --git a/src/main/java/org/jabref/logic/sharelatex/ShareLatexManager.java b/src/main/java/org/jabref/logic/sharelatex/ShareLatexManager.java new file mode 100644 index 00000000000..5ad4ea9dd61 --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/ShareLatexManager.java @@ -0,0 +1,98 @@ +package org.jabref.logic.sharelatex; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.List; + +import org.jabref.Globals; +import org.jabref.JabRefExecutorService; +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.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +public class ShareLatexManager { + + private static final Log LOGGER = LogFactory.getLog(ShareLatexManager.class); + private final SavePreferences prefs; + + private final SharelatexConnector connector = new SharelatexConnector(); + private final ShareLatexParser parser = new ShareLatexParser(); + private SharelatexConnectionProperties properties; + + //TODO: FIXME needs to be udpated to the new methods + public ShareLatexManager() { + + prefs = Globals.prefs.loadForSaveFromPreferences(); + + } + + 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(e); + } + registerListener(ShareLatexManager.this); + + }); + } + + public void sendNewDatabaseContent(BibDatabaseContext database) { + + try { + /* + AtomicFileWriter fileWriter = new AtomicFileWriter(Paths.get(""), prefs.getEncoding()); + + StringWriter strWriter = new StringWriter(); + BibtexDatabaseWriter stringdbWriter = new BibtexDatabaseWriter(strWriter, prefs, Globals.entryTypesManager) + + fileWriter.saveDatabase, prefs); + + stringdbWriter.saveDatabase(database); + String updatedcontent = saveSession.getStringValue().replace("\r\n", "\n"); + */ + connector.sendNewDatabaseContent(""); + } catch (InterruptedException e) { + LOGGER.error("Could not prepare databse for saving ", e); + } + } + + public void registerListener(Object listener) { + connector.registerListener(listener); + } + + public void unregisterListener(Object listener) { + connector.unregisterListener(listener); + } + + public void disconnectAndCloseConnection() { + connector.disconnectAndCloseConn(); + } + + 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..f6ee069862e --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/ShareLatexParser.java @@ -0,0 +1,221 @@ +package org.jabref.logic.sharelatex; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.StringJoiner; + +import org.jabref.logic.importer.ImportFormatPreferences; +import org.jabref.logic.importer.ParseException; +import org.jabref.logic.importer.ParserResult; +import org.jabref.logic.importer.fileformat.BibtexParser; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.sharelatex.ShareLatexProject; +import org.jabref.model.sharelatex.SharelatexOtAppliedMessage; +import org.jabref.model.util.FileUpdateMonitor; + +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.bitbucket.cowwoc.diffmatchpatch.DiffMatchPatch; +import org.bitbucket.cowwoc.diffmatchpatch.DiffMatchPatch.Diff; +import org.bitbucket.cowwoc.diffmatchpatch.DiffMatchPatch.Operation; + +public class ShareLatexParser { + + private final JsonParser parser = new JsonParser(); + 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) { + DiffMatchPatch patch = new DiffMatchPatch(); + + LinkedList diffs = patch.diffMain(before, after); + patch.diffCleanupSemantic(diffs); + + int pos = 0; + + List docsWithChanges = new ArrayList<>(); + + for (Diff d : diffs) { + + if (d.operation == Operation.INSERT) { + SharelatexDoc doc = new SharelatexDoc(); + doc.setPosition(pos); + doc.setContent(d.text); + doc.setOperation("i"); + docsWithChanges.add(doc); + pos += d.text.length(); + } else if (d.operation == Operation.DELETE) { + SharelatexDoc doc = new SharelatexDoc(); + doc.setPosition(pos); + doc.setContent(d.text); + doc.setOperation("d"); + + docsWithChanges.add(doc); + } else if (d.operation == Operation.EQUAL) { + pos += d.text.length(); + } + } + return docsWithChanges; + } + + public List getProjectFromJson(JsonObject json) { + + List projects = new ArrayList<>(); + if (json.has("projects")) { + JsonArray projectArray = json.get("projects").getAsJsonArray(); + for (JsonElement elem : projectArray) { + + String id = elem.getAsJsonObject().get("id").getAsString(); + String name = elem.getAsJsonObject().get("name").getAsString(); + String lastUpdated = elem.getAsJsonObject().get("lastUpdated").getAsString(); + //String owner = elem.getAsJsonObject().get("owner_ref").getAsString(); + + JsonObject owner = elem.getAsJsonObject().get("owner").getAsJsonObject(); + String firstName = owner.get("first_name").getAsString(); + String lastName = owner.get("last_name").getAsString(); + + ShareLatexProject project = new ShareLatexProject(id, name, firstName, lastName, lastUpdated); + projects.add(project); + } + } + return projects; + } + + private List parseBibEntryFromJsonArray(JsonArray arr, ImportFormatPreferences prefs, 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..6c4c2dd55da --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/ShareLatexPreferences.java @@ -0,0 +1,81 @@ +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..06befd77a08 --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/SharelatexConnectionProperties.java @@ -0,0 +1,92 @@ +package org.jabref.logic.sharelatex; + +import java.security.GeneralSecurityException; +import java.util.Objects; + +import org.jabref.logic.shared.security.Password; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +public class SharelatexConnectionProperties { + + private static final Log LOGGER = LogFactory.getLog(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..64cdd0dbd08 --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/SharelatexConnector.java @@ -0,0 +1,172 @@ +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.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.http.client.utils.URIBuilder; +import org.json.JSONObject; +import org.jsoup.Connection; +import org.jsoup.Connection.Method; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; + +public class SharelatexConnector { + + private static final Log LOGGER = LogFactory.getLog(SharelatexConnector.class); + + private final String contentType = "application/json; charset=utf-8"; + private final JsonParser parser = new JsonParser(); + private final String userAgent = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0"; + private Map loginCookies = new HashMap<>(); + private String server; + private String loginUrl; + private String csrfToken; + private String projectUrl; + private final WebSocketClientWrapper client = new WebSocketClientWrapper(); + + public String connectToServer(String serverUri, String user, String password) throws IOException { + + this.server = serverUri; + this.loginUrl = server + "/login"; + Connection.Response crsfResponse; + + crsfResponse = Jsoup.connect(loginUrl).method(Method.GET) + .execute(); + + Document welcomePage = crsfResponse.parse(); + Map welcomCookies = crsfResponse.cookies(); + + csrfToken = welcomePage.select("input[name=_csrf]").attr("value"); + + String json = "{\"_csrf\":" + JSONObject.quote(csrfToken) + + ",\"email\":" + JSONObject.quote(user) + ",\"password\":" + JSONObject.quote(password) + "}"; + + Connection.Response loginResponse = Jsoup.connect(loginUrl) + .header("Content-Type", contentType) + .header("Accept", "application/json, text/plain, */*") + .cookies(welcomCookies) + .method(Method.POST) + .requestBody(json) + .followRedirects(true) + .ignoreContentType(true) + .userAgent(userAgent) + .execute(); + + System.out.println(loginResponse.body()); + ///Error handling block + if (contentType.equals(loginResponse.contentType())) { + + if (loginResponse.body().contains("message")) { + JsonElement jsonTree = parser.parse(loginResponse.body()); + JsonObject obj = jsonTree.getAsJsonObject(); + JsonObject message = obj.get("message").getAsJsonObject(); + String errorMessage = message.get("text").getAsString(); + System.out.println(errorMessage); + + return errorMessage; + } + + } + + loginCookies = loginResponse.cookies(); + if (loginCookies.isEmpty()) { + loginCookies = welcomCookies; + } + + return ""; + } + + public Optional getProjects() throws IOException { + projectUrl = server + "/project"; + Connection.Response projectsResponse = Jsoup.connect(projectUrl) + .referrer(loginUrl).cookies(loginCookies).method(Method.GET).userAgent(userAgent).execute(); + + Optional scriptContent = Optional + .of(projectsResponse.parse().select("script#data").first()); + + if (scriptContent.isPresent()) { + + String data = scriptContent.get().data(); + JsonElement jsonTree = parser.parse(data); + + JsonObject obj = jsonTree.getAsJsonObject(); + + return Optional.of(obj); + + } + return Optional.empty(); + } + + public void startWebsocketListener(String projectId, BibDatabaseContext database, ImportFormatPreferences prefs, FileUpdateMonitor fileMonitor) + throws URISyntaxException { + long millis = System.currentTimeMillis(); + System.out.println(millis); + String socketioUrl = server + "/socket.io/1"; + String scheme = server.contains("https://") ? "wss" : "ws"; + try { + Connection.Response webSocketresponse = Jsoup.connect(socketioUrl) + .cookies(loginCookies) + .data("t", String.valueOf(millis)).method(Method.GET).execute(); + + System.out.println(webSocketresponse.body()); + + String resp = webSocketresponse.body(); + String channel = resp.substring(0, resp.indexOf(":")); + + URI webSocketchannelUri = new URIBuilder(socketioUrl + "/websocket/" + channel).setScheme(scheme).build(); + System.out.println("WebSocketChannelUrl " + webSocketchannelUri); + client.setImportFormatPrefs(prefs, 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..ccd8622dd27 --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/SharelatexDoc.java @@ -0,0 +1,61 @@ +package org.jabref.logic.sharelatex; + +import java.util.Objects; + +public class SharelatexDoc { + + private int position; + private String content; + private String operation; + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public int getPosition() { + return position; + } + + public void setPosition(int position) { + this.position = position; + } + + public String getOperation() { + return operation; + } + + public void setOperation(String opType) { + this.operation = opType; + } + + @Override + public String toString() { + return operation + " " + position + " " + content; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + SharelatexDoc other = (SharelatexDoc) obj; + + return Objects.equals(content, other.content) && Objects.equals(position, other.position) && Objects.equals(operation, other.operation); + } + + @Override + public int hashCode() { + return Objects.hash(content, position, operation); + } + +} diff --git a/src/main/java/org/jabref/logic/sharelatex/WebSocketClientWrapper.java b/src/main/java/org/jabref/logic/sharelatex/WebSocketClientWrapper.java new file mode 100644 index 00000000000..585618e2222 --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/WebSocketClientWrapper.java @@ -0,0 +1,347 @@ +package org.jabref.logic.sharelatex; + +import java.io.IOException; +import java.net.URI; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.websocket.ClientEndpointConfig; +import javax.websocket.CloseReason; +import javax.websocket.Endpoint; +import javax.websocket.EndpointConfig; +import javax.websocket.MessageHandler.Whole; +import javax.websocket.Session; + +import org.jabref.JabRefExecutorService; +import org.jabref.logic.importer.ImportFormatPreferences; +import org.jabref.logic.importer.ParseException; +import org.jabref.logic.sharelatex.events.ShareLatexContinueMessageEvent; +import org.jabref.logic.sharelatex.events.ShareLatexEntryMessageEvent; +import org.jabref.logic.sharelatex.events.ShareLatexErrorMessageEvent; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import 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.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.glassfish.tyrus.client.ClientManager; +import org.glassfish.tyrus.client.ClientProperties; +import org.glassfish.tyrus.ext.extension.deflate.PerMessageDeflateExtension; + +public class WebSocketClientWrapper { + + private static final Log LOGGER = LogFactory.getLog(WebSocketClientWrapper.class); + private final BlockingQueue queue = new LinkedBlockingQueue<>(); + private final ShareLatexParser parser = new ShareLatexParser(); + + private Session session; + private String oldContent; + private int version; + private int commandCounter; + private int position; + 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(t); + } + + @Override + public void onClose(Session session, CloseReason closeReason) { + if (errorReceived) { + LOGGER.debug("Error received in close session"); + } + + } + }, cec, webSocketchannelUri); + + //TODO: Change Dialog + //TODO: On database change event or on save event send new version + //TODO: When new db content arrived run merge dialog + //TODO: Identfiy active database/Name of database/doc Id (partly done) + + } catch (Exception e) { + e.printStackTrace(); + } + + } + + public void joinProject(String projectId) throws IOException { + incrementCommandCounter(); + String text = "5:" + commandCounter + "+::{\"name\":\"joinProject\",\"args\":[{\"project_id\":\"" + projectId + + "\"}]}"; + session.getBasicRemote().sendText(text); + } + + public void joinDoc(String documentId) throws IOException { + incrementCommandCounter(); + String text = "5:" + commandCounter + "+::{\"name\":\"joinDoc\",\"args\":[\"" + documentId + "\"]}"; + session.getBasicRemote().sendText(text); + } + + public void leaveDocument(String documentId) throws IOException { + incrementCommandCounter(); + String text = "5:" + commandCounter + "+::{\"name\":\"leaveDoc\",\"args\":[\"" + documentId + "\"]}"; + if (session != null) { + session.getBasicRemote().sendText(text); + } + + } + + private void sendHeartBeat() throws IOException { + session.getBasicRemote().sendText("2::"); + } + + public void sendNewDatabaseContent(String newContent) throws InterruptedException { + queue.put(newContent); + } + + private void sendUpdateAsDeleteAndInsert(String docId, int position, int version, String oldContent, String newContent) throws IOException { + ShareLatexJsonMessage message = new ShareLatexJsonMessage(); + + List diffDocs = parser.generateDiffs(oldContent, newContent); + String str = message.createUpdateMessageAsInsertOrDelete(docId, version, diffDocs); + + LOGGER.debug("Send new update Message"); + + session.getBasicRemote().sendText("5:::" + str); + } + + @Subscribe + public synchronized void listenToSharelatexEntryMessage(ShareLatexContinueMessageEvent event) { + + JabRefExecutorService.INSTANCE.executeInterruptableTask(() -> { + try { + String updatedContent = queue.take(); + if (!leftDoc) { + LOGGER.debug("Taken from queue"); + sendUpdateAsDeleteAndInsert(docId, 0, version, oldContent, updatedContent); + + } + } catch (IOException | InterruptedException e) { + Thread.currentThread().interrupt(); + LOGGER.debug("Exception in taking from queue", e); + } + }, "ListenToShareLatexTask"); + + } + + //Actual response handling + private void parseContents(String message) { + try { + + System.out.println("Got message: " + message); + if (message.contains(":::1")) { + + Thread.currentThread().sleep(300); + LOGGER.debug("Got :::1. Joining project"); + + } + if (message.contains("2::")) { + setLeftDoc(false); + eventBus.post(new ShareLatexContinueMessageEvent()); + sendHeartBeat(); + + } + + if (message.endsWith("[null]")) { + LOGGER.debug("Received null-> Rejoining doc"); + joinDoc(docId); + } + + if (message.startsWith("[null,{", message.indexOf("+") + 1)) { + LOGGER.debug("We get a list with all files"); + //We get a list with all files + + String docIdOfFirstBibtex = parser.getFirstBibTexDatabaseId(message); + + LOGGER.debug("DBs with ID " + docIdOfFirstBibtex); + setDocID(docIdOfFirstBibtex); + joinDoc(docId); + + } + if (message.contains("{\"name\":\"connectionAccepted\"}") && (projectId != null)) { + + LOGGER.debug("Joining project"); + Thread.sleep(200); + joinProject(projectId); + + } + + if (message.contains("[null,[")) { + System.out.println("Message could be an entry "); + + int version = parser.getVersionFromBibTexJsonString(message); + setVersion(version); + + String bibtexString = parser.getBibTexStringFromJsonMessage(message); + setBibTexString(bibtexString); + List entries = parser.parseBibEntryFromJsonMessageString(message, prefs, 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(e); + } + } + + public void setDatabaseName(String bibFileName) { + this.databaseName = bibFileName; + } + + public void leaveDocAndCloseConn() throws IOException { + leaveDocument(docId); + queue.clear(); + if (session != null) { + session.close(); + } + + } + + public void setServerNameOrigin(String serverOrigin) { + this.serverOrigin = serverOrigin; + + } + + public void setCookies(Map cookies) { + this.cookies = cookies; + + } + + public void registerListener(Object listener) { + eventBus.register(listener); + } + + public void unregisterListener(Object listener) { + eventBus.unregister(listener); + } + + private synchronized void setDocID(String docId) { + this.docId = docId; + } + + private synchronized void setVersion(int version) { + this.version = version; + } + + private synchronized void setBibTexString(String bibtex) { + this.oldContent = bibtex; + } + + private synchronized void incrementCommandCounter() { + this.commandCounter = commandCounter + 1; + } + + private synchronized void setLeftDoc(boolean leftDoc) { + this.leftDoc = leftDoc; + } + + private synchronized void setErrorReceived(boolean errorReceived) { + this.errorReceived = errorReceived; + } + +} diff --git a/src/main/java/org/jabref/logic/sharelatex/events/ShareLatexContinueMessageEvent.java b/src/main/java/org/jabref/logic/sharelatex/events/ShareLatexContinueMessageEvent.java new file mode 100644 index 00000000000..3f0df281ab7 --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/events/ShareLatexContinueMessageEvent.java @@ -0,0 +1,5 @@ +package org.jabref.logic.sharelatex.events; + +public class ShareLatexContinueMessageEvent { + //empty +} diff --git a/src/main/java/org/jabref/logic/sharelatex/events/ShareLatexEntryMessageEvent.java b/src/main/java/org/jabref/logic/sharelatex/events/ShareLatexEntryMessageEvent.java new file mode 100644 index 00000000000..f2cf6279d86 --- /dev/null +++ b/src/main/java/org/jabref/logic/sharelatex/events/ShareLatexEntryMessageEvent.java @@ -0,0 +1,61 @@ +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/database/event/CoarseChangeFilter.java b/src/main/java/org/jabref/model/database/event/CoarseChangeFilter.java index af22218cd95..9b5c4258402 100644 --- a/src/main/java/org/jabref/model/database/event/CoarseChangeFilter.java +++ b/src/main/java/org/jabref/model/database/event/CoarseChangeFilter.java @@ -24,7 +24,7 @@ public CoarseChangeFilter(BibDatabaseContext bibDatabaseContext) { } @Subscribe - public synchronized void listen(@SuppressWarnings("unused") BibDatabaseContextChangedEvent event) { + public synchronized void listen(BibDatabaseContextChangedEvent event) { if (!(event instanceof FieldChangedEvent)) { eventBus.post(event); } else { 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..e60ce13163b --- /dev/null +++ b/src/main/java/org/jabref/model/sharelatex/Arg.java @@ -0,0 +1,39 @@ +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; + } + +} \ No newline at end of file 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..f9581d36093 --- /dev/null +++ b/src/main/java/org/jabref/model/sharelatex/Op.java @@ -0,0 +1,27 @@ +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/ShareLatexProject.java b/src/main/java/org/jabref/model/sharelatex/ShareLatexProject.java new file mode 100644 index 00000000000..94eada33ecb --- /dev/null +++ b/src/main/java/org/jabref/model/sharelatex/ShareLatexProject.java @@ -0,0 +1,71 @@ +package org.jabref.model.sharelatex; + +import java.util.Objects; + +public class ShareLatexProject { + + private final String projectId; + private final String projectTitle; + private final String firstName; + private final String lastName; + private final String lastUpdated; + + public ShareLatexProject(String projectId, String projectTitle, String firstName, String lastName, String lastUpdated) { + this.projectId = projectId; + this.projectTitle = projectTitle; + this.lastUpdated = lastUpdated; + this.firstName = firstName; + this.lastName = lastName; + } + + public String getLastUpdated() { + return lastUpdated; + } + + public String getProjectId() { + return projectId; + } + + public String getProjectTitle() { + return projectTitle; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + + @Override + public String toString() { + return "[projectId =" + projectId + ", " + + "projectTitle = " + projectTitle + ", " + + "firstName = " + firstName + ", " + + "lastName = " + lastName + ", " + + "lastUpdated = " + lastUpdated + ""; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ShareLatexProject other = (ShareLatexProject) obj; + + return Objects.equals(projectId, other.projectId) && Objects.equals(projectTitle, other.projectTitle) && Objects.equals(lastUpdated, other.lastUpdated) && Objects.equals(lastName, other.lastName) && Objects.equals(firstName, other.firstName); + } + + @Override + public int hashCode() { + return Objects.hash(projectId, projectTitle, lastUpdated, lastName, firstName); + } + +} diff --git a/src/main/java/org/jabref/model/sharelatex/SharelatexOtAppliedMessage.java b/src/main/java/org/jabref/model/sharelatex/SharelatexOtAppliedMessage.java new file mode 100644 index 00000000000..59f71c37b77 --- /dev/null +++ b/src/main/java/org/jabref/model/sharelatex/SharelatexOtAppliedMessage.java @@ -0,0 +1,30 @@ +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; + } + +} \ No newline at end of file diff --git a/src/main/java/org/jabref/preferences/JabRefPreferences.java b/src/main/java/org/jabref/preferences/JabRefPreferences.java index 5d732e43703..f1e17c07ad6 100644 --- a/src/main/java/org/jabref/preferences/JabRefPreferences.java +++ b/src/main/java/org/jabref/preferences/JabRefPreferences.java @@ -86,6 +86,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.UpdateFieldPreferences; import org.jabref.logic.util.Version; @@ -239,6 +240,15 @@ public class JabRefPreferences implements PreferencesService { public static final String TIME_STAMP_FORMAT = "timeStampFormat"; public static final String OVERWRITE_TIME_STAMP = "overwriteTimeStamp"; + // 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_ALL_FIELDS = "resolveStringsAllFields"; @@ -470,6 +480,11 @@ private JabRefPreferences() { // Set DOI to be the default ID entry generator defaults.put(ID_ENTRY_GENERATOR, DoiFetcher.NAME); + + //Sharelatex + defaults.put(DEFAULT_NODE, "default"); + defaults.put(PARENT_NODE, "jabref-sharelatex"); + if (OS.OS_X) { defaults.put(FONT_FAMILY, "SansSerif"); defaults.put(EMACS_PATH, "emacsclient"); @@ -1042,7 +1057,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) { @@ -1546,6 +1561,26 @@ public TimestampPreferences getTimestampPreferences() { return new TimestampPreferences(getBoolean(USE_TIME_STAMP), getBoolean(UPDATE_TIMESTAMP), FieldFactory.parseField(get(TIME_STAMP_FIELD)), get(TIME_STAMP_FORMAT), getBoolean(OVERWRITE_TIME_STAMP)); } + 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 LayoutFormatterPreferences getLayoutFormatterPreferences(JournalAbbreviationLoader journalAbbreviationLoader) { Objects.requireNonNull(journalAbbreviationLoader); 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/main/resources/org/jabref/gui/sharelatex/ShareLatexLoginDialog.fxml b/src/main/resources/org/jabref/gui/sharelatex/ShareLatexLoginDialog.fxml new file mode 100644 index 00000000000..383b46d8799 --- /dev/null +++ b/src/main/resources/org/jabref/gui/sharelatex/ShareLatexLoginDialog.fxml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ + + +