Desyn c проблемы между ObservableList, ListView и SelectionModel в JavaFX - PullRequest
0 голосов
/ 29 апреля 2020

У меня есть почтовое приложение, в котором пользователь вручную получает почту с сервера, каждый раз, когда это происходит, почта сохраняется в ObservableArrayList. Этот список связан с ListView через конструктор ListView (ObservableList). SelectionModel ListView настроен для отображения электронной почты, которая в данный момент выбрана на панели справа, с подробной информацией выше.

Проблема возникает, если пользователь когда-либо пытается получить второй набор электронных писем. Основная модель выбора, по-видимому, не соответствует обновленному списку, и нажатие любого элемента в списке заполнит зрителя информацией из сообщения, которое ранее занимало эту позицию. Это не проблема при извлечении сообщения, поскольку предварительный просмотр ячейки отражает изменение. Я пробовал почти каждую комбинацию методов для сброса базовой модели выбора, но на данный момент у меня ничего не вышло.

Возможно, связанная с этим ошибка (хотя и сомневаюсь в этом) заключается в том, что при попытке удалить сообщение электронной почты выбрасываются десятки идентичных исключения и причины такого десин c.

Минимальный воспроизводимый пример (Требуется javax.JavaMail):

артефакт Maven для javamail

<dependency>
    <groupId>com.sun.mail</groupId>
    <artifactId>javax.mail</artifactId>
    <version>1.6.2</version>
</dependency>

Основной класс

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.web.WebView;
import javafx.stage.Stage;

import javax.mail.*;
import java.util.HashMap;
import java.util.Properties;

public class TagMailMain extends Application {

    public static final HashMap<String, Account> accounts = new HashMap<>();

    public static void main(final String[] args) {
        launch(args);
    }

    // Add elements
    private final Pane root = new Pane();
    private final
    Scene primaryScene = new Scene(root);

    private Account                         account;

    private boolean                         hasMailBeenChecked  = false;

    private Store  store;
    private Folder emailFolder;
    private final ObservableList<Message> messages    = FXCollections.observableArrayList();
    private final ListView<Message>       messageList = new ListView<>(messages);

    private final WebView messageView = new WebView();

    private void fetch(final String host, final String storeType, final String user, final String password) {
        final Task<Message[]> task = new Task<Message[]>() {

            @Override
            public Message[] call() {
                try {
                    if (hasMailBeenChecked) {
                        // close the store and folder objects
                        emailFolder.close(true);
                        store.close();
                    }

                    // create properties field
                    final Properties properties = new Properties();

                    properties.put("mail.store.protocol", storeType);
                    properties.put("mail." + storeType + ".host", host);
                    if (storeType.contains("pop"))
                        properties.put("mail." + storeType + ".port", "995");
                    else if (storeType.contains("imap")) properties.put("mail." + storeType + ".port", "993");
                    properties.put("mail." + storeType + ".ssl.enable", "true");
                    final Session emailSession = Session.getDefaultInstance(properties);

                    // create the store object and connect with the storeType server
                    store = emailSession.getStore(storeType);

                    store.connect(host, user, password);

                    // create the folder object and open it
                    emailFolder = store.getFolder("INBOX");
                    emailFolder.open(Folder.READ_WRITE);

                    return emailFolder.getMessages();

                } catch (final Exception e) {
                    e.printStackTrace();
                }
                return null;
            }
        };

        task.setOnSucceeded(e -> {
            messages.clear();
            messageList.getItems().clear();
            messages.setAll(task.getValue());
            messages.sort((o1, o2) -> {
                try {
                    if (o1.getSentDate() == (o2.getSentDate()))
                        return 0;
                    else if (o1.getSentDate().getTime() < o2.getSentDate().getTime())
                        return 1;
                    else
                        return -1;
                } catch (final MessagingException e1) {
                    e1.printStackTrace();
                    return 0;
                }
            });
            hasMailBeenChecked = true;
            messageList.refresh();
        });

        final Thread thread = new Thread(task);
        thread.start();
    }

    @Override
    public void init() throws Exception {
        Button editAccount = new Button("Accounts");
        editAccount.setOnAction((e) -> {
            final TagMailAccountsEdit accountsWindow = new TagMailAccountsEdit();
            accountsWindow.init();
        });

        Button checkMail = new Button("Check mail");
        checkMail.setOnAction(e -> {
            messageList.getSelectionModel().clearSelection();
            final ChoiceDialog<Account> accountChooser = new ChoiceDialog<>(null, accounts.values());
            accountChooser.setHeaderText("Select an account to check mail");
            final Stage dialogStage = (Stage) accountChooser.getDialogPane().getScene().getWindow();

            if (showAccountChooser(accountChooser))
                return;
            if (account.getRetrievalProtocol() == Protocol.IMAP)
                fetch(account.getServerIMAP(), "imap", account.getUsername(), account.getPassword());
            else if (account.getRetrievalProtocol() == Protocol.POP3)
                fetch(account.getServerPOP3(), "pop3", account.getUsername(), account.getPassword());
        });
        // Center
        final Label toLabel = new Label("To:  ");
        final Label fromLabel = new Label("From:  ");
        final Label subjectLabel = new Label("Subject:  ");
        final Label dateLabel = new Label("Received:  ");

        final VBox messageViewBox = new VBox(toLabel, fromLabel, subjectLabel, dateLabel, messageView);

        final HBox center = new HBox(messageList, messageViewBox);

        final BorderPane mainPane = new BorderPane(center, editAccount, null, checkMail, null);
        root.getChildren().add(mainPane);

        messageList.setCellFactory(list -> new MessageCell());
        messageList.getSelectionModel().selectedItemProperty().addListener(e -> {
            final Message m = messageList.getSelectionModel().getSelectedItem();
            if (m != null) {
                try {
                    m.setFlag(Flags.Flag.SEEN, true);

                    StringBuilder recipients = new StringBuilder();
                    for (final Address i : m.getAllRecipients()) recipients.append(i.toString());
                    toLabel.setText("To:  " + recipients);

                    StringBuilder from = new StringBuilder();
                    for (final Address i : m.getFrom()) from.append(i.toString());
                    fromLabel.setText("From:  " + from);

                    subjectLabel.setText("Subject:  " + m.getSubject());
                    dateLabel.setText("Received:  " + m.getSentDate().toString());

                    messageView.getEngine().loadContent(mimeToHTML(m));
                } catch (MessagingException e1) {
                    e1.printStackTrace();
                }
            }
        });
    }

    private boolean showAccountChooser(ChoiceDialog<Account> accountChooser) {
        accountChooser.showAndWait();
        account = accountChooser.getResult();
        if (account == null)
            return true;
        if (account.getPassword() == null) {
            Alert alert = new Alert(AlertType.ERROR, "No password, please log in", ButtonType.CLOSE);
            alert.showAndWait();
            return true;
        }
        return false;
    }

    private String mimeToHTML(final Part m) {
        return"";
    }
    @Override
    public void start(final Stage primaryStage) throws Exception {
        primaryStage.setTitle("TagMail");
        primaryStage.setResizable(false);
        primaryStage.setScene(primaryScene);
        primaryStage.show();
    }
}

Пользовательская ячейка для отображения объекта Message в ListView

import javafx.scene.control.ListCell;

import javax.mail.Message;
import javax.mail.MessagingException;

public class MessageCell extends ListCell<Message> {

    public MessageCell() {
    }

    @Override
    protected void updateItem(Message message, boolean empty) {
        super.updateItem(message, empty);

        if (getText() == null) {
            getListView().getSelectionModel().selectedItemProperty().addListener(e -> {
                if (getListView().getSelectionModel().getSelectedItem().equals(getItem())) setGraphic(null); //TODO fix this hacky solution to mark mail as read
            });

            if (empty || message == null) {
                setText(null);
                setGraphic(null);
            } else {

                try {
                    setText(message.getFrom()[0].toString() + "\n" + message.getSubject());
                } catch (MessagingException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

Объект, представляющий учетную запись электронной почты

import javafx.util.Pair;

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.io.Serializable;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;

public class Account implements Serializable {
    private static final long       serialVersionUID    = 2;
    private final String            name;
    private Pair<byte[], byte[]>    passwordHash;           // hash, salt
    private transient String        password;
    private final String            username;
    private final String            serverSMTP;
    private final String            serverIMAP;
    private final String   serverPOP3;
    private final Protocol retrievalProtocol;

    public Account(String name, String password, String username, String serverSMTP, String serverIMAP,
            String serverPOP3, Protocol retrievalProtocol) {
        this.name = name;
        setPassword(password);
        this.username = username;
        this.serverSMTP = serverSMTP;
        this.serverIMAP = serverIMAP;
        this.serverPOP3 = serverPOP3;
        this.retrievalProtocol = retrievalProtocol;
    }

    private Pair<byte[], byte[]> generateHash(String toHash) {
        SecureRandom random = new SecureRandom();
        byte[] salt = new byte[16];
        random.nextBytes(salt);
        KeySpec spec = new PBEKeySpec(toHash.toCharArray(), salt, 65536, 128);
        try {
            SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
            byte[] hash = factory.generateSecret(spec).getEncoded();
            return new Pair<>(hash, salt);
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            e.printStackTrace();
        }
        return null;
    }

    public String getName() {
        return name;
    }

    public boolean checkPassword(String passwordToCheck) {
        byte[] hash = passwordHash.getKey();
        byte[] salt = passwordHash.getValue();
        KeySpec spec = new PBEKeySpec(passwordToCheck.toCharArray(), salt, 65536, 128);
        byte[] hashToCheck=null;
        try {
            SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
            hashToCheck=factory.generateSecret(spec).getEncoded();
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            e.printStackTrace();
        }
        return Arrays.equals(hash, hashToCheck);

    }

    public Protocol getRetrievalProtocol() {
        return retrievalProtocol;
    }

    public String getServerIMAP() {
        return serverIMAP;
    }

    public String getServerPOP3() {
        return serverPOP3;
    }

    public String getServerSMTP() {
        return serverSMTP;
    }

    public String getUsername() {
        return username;
    }

    public void setPasswordHash(String password) {
        passwordHash = generateHash(password);
    }

    public Pair<byte[], byte[]> getPasswordHash() {
        return passwordHash;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
        setPasswordHash(password);
    }

    @Override
    public String toString() {
        return getName();
    }
}

enum Protocol {
    IMAP, POP3
}

Просто редактор объекта Account, возможно не имеет отношения к ошибке

import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

import java.util.Optional;

public class TagMailAccountsEdit {

    // Add elements
    private final StackPane root                = new StackPane();

    private final Scene     primaryScene        = new Scene(root);
    private final Stage     primaryStage        = new Stage();

    private final VBox      accountColumn       = new VBox();
    private final VBox      buttonColumn        = new VBox();
    private final HBox      accountList         = new HBox(accountColumn, new Label("\t\t"), buttonColumn);
    private final Button    newAccountButton    = new Button("New");

    private final VBox      mainBox             = new VBox(accountList, newAccountButton);

    public Account editAccount(Account a) {
        Dialog<Account> dialog = new Dialog<>();
        dialog.setTitle("Account Edit Dialog");
        dialog.setHeaderText("Edit any details of your account here");

        // Set the button types.
        dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);

        // Create the username and password labels and fields.
        GridPane grid = new GridPane();
        grid.setHgap(10);
        grid.setVgap(10);
        grid.setPadding(new Insets(20, 150, 10, 10));

        TextField name = new TextField(a.getName());
        name.setPromptText("Name");
        TextField username = new TextField(a.getUsername());
        username.setPromptText("Username");
        PasswordField password = new PasswordField();
        password.setPromptText("Password");
        TextField serverIMAP = new TextField(a.getServerIMAP());
        serverIMAP.setPromptText("IMAP server");
        TextField serverPOP3 = new TextField(a.getServerPOP3());
        serverPOP3.setPromptText("POP3 server");
        TextField serverSMTP = new TextField(a.getServerSMTP());
        serverSMTP.setPromptText("SMTP server");
        ComboBox<Protocol> retrieveProtocolBox = new ComboBox<>();
        retrieveProtocolBox.getItems().add(0, Protocol.IMAP);
        retrieveProtocolBox.getItems().add(1, Protocol.POP3);
        retrieveProtocolBox.getSelectionModel().select(a.getRetrievalProtocol());

        grid.add(new Label("Name:"), 0, 0);
        grid.add(name, 1, 0);
        grid.add(new Label("Username:"), 0, 1);
        grid.add(username, 1, 1);
        grid.add(new Label("Password:"), 0, 2);
        grid.add(password, 1, 2);
        grid.add(new Label("IMAP Server:"), 0, 3);
        grid.add(serverIMAP, 1, 3);
        grid.add(new Label("POP3 Server:"), 0, 4);
        grid.add(serverPOP3, 1, 4);
        grid.add(new Label("SMTP Server:"), 0, 5);
        grid.add(serverSMTP, 1, 5);
        grid.add(new Label("Default:"), 0, 6);
        grid.add(retrieveProtocolBox, 1, 6);

        dialog.getDialogPane().setContent(grid);

        // Request focus on the username field by default.
        Platform.runLater(username::requestFocus);

        // Convert the result to a username-password-pair when the login button is
        // clicked.
        dialog.setResultConverter(dialogButton -> new Account(name.getText(), password.getText(), username.getText(), serverSMTP.getText(),
                serverIMAP.getText(), serverPOP3.getText(),
                retrieveProtocolBox.getSelectionModel().getSelectedItem()));

        Optional<Account> result = dialog.showAndWait();

        return result.orElse(null);
    }

    public void init() {
        // Setup window
        root.setPrefHeight(64);
        root.setPrefWidth(512);
        mainBox.setSpacing(5);
        mainBox.setFillWidth(true);
        root.getChildren().add(mainBox);

        root.setBackground(new Background(new BackgroundFill(Color.LIGHTGRAY, CornerRadii.EMPTY, Insets.EMPTY)));
        accountList.setBackground(new Background(new BackgroundFill(Color.DARKGRAY, CornerRadii.EMPTY, Insets.EMPTY)));

        refresh();

        newAccountButton.setOnAction(event -> {
            TextInputDialog nameOfAccountDialog = new TextInputDialog();
            nameOfAccountDialog.setHeaderText("Enter a name for the account");
            nameOfAccountDialog.setContentText(null);

            nameOfAccountDialog.showAndWait();
            while (TagMailMain.accounts.containsKey(nameOfAccountDialog.getResult())) {
                nameOfAccountDialog
                        .setContentText("An account by that name already exists, please enter a different name");
                nameOfAccountDialog.showAndWait();
            }
            if (nameOfAccountDialog.getResult() == null) {
                System.out.println("Unknown behavior in nameOfAccount Dialog"); //TODO refine
            } else {
                TagMailMain.accounts.put(nameOfAccountDialog.getResult(),
                        editAccount(new Account(nameOfAccountDialog.getResult(), "", "", "", "", "", Protocol.IMAP)));
            }
            refresh();
        });

        start();
    }

    private void refresh() {
        accountColumn.getChildren().clear();
        buttonColumn.getChildren().clear();
        for (String k : TagMailMain.accounts.keySet()) {
            Account a = TagMailMain.accounts.get(k);
            Button edit = new Button("Edit");
            Button delete = new Button("Delete");
            accountColumn.getChildren().add(new HBox(new Label(a.getName() + ": " + a.getUsername())));
            buttonColumn.getChildren().add(new HBox(edit, delete));
            edit.setOnAction(editEvent -> {
                Account replacement = editAccount(a);
                TagMailMain.accounts.remove(k);
                TagMailMain.accounts.put(replacement.getName(), replacement);
                refresh();
            });
            delete.setOnAction(deleteEvent -> {
                TagMailMain.accounts.remove(k);
                refresh();
            });
        }
    }

    public void start() {
        primaryStage.setTitle("Accounts edit");

        primaryStage.setScene(primaryScene);

        primaryStage.show();
    }
}
...