Implement security answers & immediate site lookup.
This commit is contained in:
		@@ -36,7 +36,7 @@ _needs() {
 | 
			
		||||
        IFS=: read pkg tools <<< "$spec"
 | 
			
		||||
        IFS=, read -a tools <<< "${tools:-$pkg}"
 | 
			
		||||
        for tool in "${tools[@]}"; do
 | 
			
		||||
            hash "$tool" && continue 2
 | 
			
		||||
            hash "$tool" 2>/dev/null && continue 2
 | 
			
		||||
        done
 | 
			
		||||
 | 
			
		||||
        echo >&2 "Missing: $pkg.  Please install this package."
 | 
			
		||||
 
 | 
			
		||||
@@ -31,17 +31,7 @@ import javax.annotation.Nullable;
 | 
			
		||||
 */
 | 
			
		||||
public class MPIncognitoQuestion extends MPBasicQuestion {
 | 
			
		||||
 | 
			
		||||
    private final MPIncognitoSite site;
 | 
			
		||||
 | 
			
		||||
    public MPIncognitoQuestion(final MPIncognitoSite site, final String keyword, @Nullable final MPResultType type) {
 | 
			
		||||
        super( keyword, ifNotNullElse( type, site.getAlgorithm().mpw_default_answer_type() ) );
 | 
			
		||||
 | 
			
		||||
        this.site = site;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public MPIncognitoSite getSite() {
 | 
			
		||||
        return site;
 | 
			
		||||
        super( site, keyword, ifNotNullElse( type, site.getAlgorithm().mpw_default_answer_type() ) );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -40,4 +40,10 @@ public class MPIncognitoSite extends MPBasicSite<MPIncognitoUser, MPIncognitoQue
 | 
			
		||||
                           @Nullable final MPResultType resultType, @Nullable final MPResultType loginType) {
 | 
			
		||||
        super( user, siteName, (algorithm == null)? user.getAlgorithm(): algorithm, counter, resultType, loginType );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public MPIncognitoQuestion addQuestion(final String keyword) {
 | 
			
		||||
        return addQuestion( new MPIncognitoQuestion( this, keyword, null ) );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ package com.lyndir.masterpassword.gui.model;
 | 
			
		||||
 | 
			
		||||
import com.lyndir.masterpassword.MPAlgorithm;
 | 
			
		||||
import com.lyndir.masterpassword.model.impl.MPBasicUser;
 | 
			
		||||
import javax.annotation.Nonnull;
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -38,6 +39,7 @@ public class MPIncognitoUser extends MPBasicUser<MPIncognitoSite> {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public MPIncognitoSite addSite(final String siteName) {
 | 
			
		||||
        return addSite( new MPIncognitoSite( this, siteName ) );
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,15 @@
 | 
			
		||||
package com.lyndir.masterpassword.gui.model;
 | 
			
		||||
 | 
			
		||||
import com.lyndir.masterpassword.model.MPSite;
 | 
			
		||||
import com.lyndir.masterpassword.model.impl.MPBasicQuestion;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @author lhunath, 2018-07-27
 | 
			
		||||
 */
 | 
			
		||||
public class MPNewQuestion extends MPBasicQuestion {
 | 
			
		||||
 | 
			
		||||
    public MPNewQuestion(final MPSite<?> site, final String keyword) {
 | 
			
		||||
        super( site, keyword, site.getAlgorithm().mpw_default_answer_type() );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -2,6 +2,7 @@ package com.lyndir.masterpassword.gui.model;
 | 
			
		||||
 | 
			
		||||
import com.lyndir.masterpassword.model.*;
 | 
			
		||||
import com.lyndir.masterpassword.model.impl.*;
 | 
			
		||||
import javax.annotation.Nonnull;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -12,4 +13,10 @@ public class MPNewSite extends MPBasicSite<MPUser<?>, MPQuestion> {
 | 
			
		||||
    public MPNewSite(final MPUser<?> user, final String siteName) {
 | 
			
		||||
        super( user, siteName );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public MPQuestion addQuestion(final String keyword) {
 | 
			
		||||
        throw new UnsupportedOperationException( "Cannot add a question to a site that hasn't been created yet." );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -100,6 +100,7 @@ public abstract class Components {
 | 
			
		||||
 | 
			
		||||
    public static int showDialog(@Nullable final Component owner, @Nullable final String title, final JOptionPane pane) {
 | 
			
		||||
        JDialog dialog = pane.createDialog( owner, title );
 | 
			
		||||
        dialog.setMinimumSize( new Dimension( 520, 0 ) );
 | 
			
		||||
        dialog.setModalityType( Dialog.ModalityType.DOCUMENT_MODAL );
 | 
			
		||||
        showDialog( dialog );
 | 
			
		||||
 | 
			
		||||
@@ -143,7 +144,6 @@ public abstract class Components {
 | 
			
		||||
                                      title, Dialog.ModalityType.DOCUMENT_MODAL );
 | 
			
		||||
        dialog.setMinimumSize( new Dimension( 320, 0 ) );
 | 
			
		||||
        dialog.setLocationRelativeTo( owner );
 | 
			
		||||
        dialog.setLocationByPlatform( true );
 | 
			
		||||
        dialog.setContentPane( content );
 | 
			
		||||
 | 
			
		||||
        return showDialog( dialog );
 | 
			
		||||
@@ -155,6 +155,7 @@ public abstract class Components {
 | 
			
		||||
        dialog.getRootPane().putClientProperty( "Window.style", "small" );
 | 
			
		||||
        dialog.pack();
 | 
			
		||||
 | 
			
		||||
        dialog.setLocationByPlatform( true );
 | 
			
		||||
        dialog.setVisible( true );
 | 
			
		||||
 | 
			
		||||
        return dialog;
 | 
			
		||||
@@ -176,7 +177,7 @@ public abstract class Components {
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static JTextField textField(@Nullable final String text, @Nullable final Consumer<String> selection) {
 | 
			
		||||
    public static JTextField textField(@Nullable final String text, @Nullable final Consumer<String> change) {
 | 
			
		||||
        return new JTextField( text ) {
 | 
			
		||||
            {
 | 
			
		||||
                setBorder( BorderFactory.createCompoundBorder( BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ),
 | 
			
		||||
@@ -184,23 +185,25 @@ public abstract class Components {
 | 
			
		||||
                setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) );
 | 
			
		||||
                setAlignmentX( LEFT_ALIGNMENT );
 | 
			
		||||
 | 
			
		||||
                if (selection != null)
 | 
			
		||||
                if (change != null) {
 | 
			
		||||
                    getDocument().addDocumentListener( new DocumentListener() {
 | 
			
		||||
                        @Override
 | 
			
		||||
                        public void insertUpdate(final DocumentEvent e) {
 | 
			
		||||
                            selection.accept( getText() );
 | 
			
		||||
                            change.accept( getText() );
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        @Override
 | 
			
		||||
                        public void removeUpdate(final DocumentEvent e) {
 | 
			
		||||
                            selection.accept( getText() );
 | 
			
		||||
                            change.accept( getText() );
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        @Override
 | 
			
		||||
                        public void changedUpdate(final DocumentEvent e) {
 | 
			
		||||
                            selection.accept( getText() );
 | 
			
		||||
                            change.accept( getText() );
 | 
			
		||||
                        }
 | 
			
		||||
                    } );
 | 
			
		||||
                    change.accept( getText() );
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            @Override
 | 
			
		||||
@@ -228,6 +231,7 @@ public abstract class Components {
 | 
			
		||||
    public static <E> JList<E> list(final ListModel<E> model, final Function<E, String> valueTransformer) {
 | 
			
		||||
        return new JList<E>( model ) {
 | 
			
		||||
            {
 | 
			
		||||
                setAlignmentX( LEFT_ALIGNMENT );
 | 
			
		||||
                setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) );
 | 
			
		||||
                setCellRenderer( new DefaultListCellRenderer() {
 | 
			
		||||
                    @Override
 | 
			
		||||
@@ -241,7 +245,9 @@ public abstract class Components {
 | 
			
		||||
                        return this;
 | 
			
		||||
                    }
 | 
			
		||||
                } );
 | 
			
		||||
                setAlignmentX( LEFT_ALIGNMENT );
 | 
			
		||||
 | 
			
		||||
                if (model instanceof CollectionListModel)
 | 
			
		||||
                    ((CollectionListModel<E>) model).registerList( this );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            @Override
 | 
			
		||||
 
 | 
			
		||||
@@ -9,8 +9,6 @@ import com.lyndir.masterpassword.model.MPUser;
 | 
			
		||||
import com.lyndir.masterpassword.model.impl.MPFileUser;
 | 
			
		||||
import com.lyndir.masterpassword.model.impl.MPFileUserManager;
 | 
			
		||||
import java.awt.*;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.concurrent.CopyOnWriteArraySet;
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import javax.swing.*;
 | 
			
		||||
 | 
			
		||||
@@ -26,8 +24,6 @@ public class FilesPanel extends JPanel implements MPFileUserManager.Listener, Ma
 | 
			
		||||
 | 
			
		||||
    private final CollectionListModel<MPUser<?>> usersModel =
 | 
			
		||||
            CollectionListModel.<MPUser<?>>copy( MPFileUserManager.get().getFiles() ).selection( MasterPassword.get()::activateUser );
 | 
			
		||||
    private final JComboBox<? extends MPUser<?>> userField  =
 | 
			
		||||
            Components.comboBox( usersModel, user -> ifNotNull( user, MPUser::getFullName ) );
 | 
			
		||||
 | 
			
		||||
    protected FilesPanel() {
 | 
			
		||||
        setOpaque( false );
 | 
			
		||||
@@ -46,7 +42,7 @@ public class FilesPanel extends JPanel implements MPFileUserManager.Listener, Ma
 | 
			
		||||
        add( Components.strut( Components.margin() ) );
 | 
			
		||||
 | 
			
		||||
        // User Selection
 | 
			
		||||
        add( userField );
 | 
			
		||||
        add( Components.comboBox( usersModel, user -> ifNotNull( user, MPUser::getFullName ) ) );
 | 
			
		||||
 | 
			
		||||
        MPFileUserManager.get().addListener( this );
 | 
			
		||||
        MasterPassword.get().addListener( this );
 | 
			
		||||
 
 | 
			
		||||
@@ -3,12 +3,10 @@ package com.lyndir.masterpassword.gui.view;
 | 
			
		||||
import com.lyndir.lhunath.opal.system.logging.Logger;
 | 
			
		||||
import com.lyndir.masterpassword.gui.util.Components;
 | 
			
		||||
import com.lyndir.masterpassword.gui.util.Res;
 | 
			
		||||
import com.lyndir.masterpassword.model.MPUser;
 | 
			
		||||
import com.lyndir.masterpassword.model.impl.MPFileUserManager;
 | 
			
		||||
import java.awt.*;
 | 
			
		||||
import java.awt.event.ComponentAdapter;
 | 
			
		||||
import java.awt.event.ComponentEvent;
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import javax.swing.*;
 | 
			
		||||
import javax.swing.border.BevelBorder;
 | 
			
		||||
 | 
			
		||||
@@ -23,8 +21,6 @@ public class MasterPasswordFrame extends JFrame {
 | 
			
		||||
 | 
			
		||||
    @SuppressWarnings("FieldCanBeLocal")
 | 
			
		||||
    private final Components.GradientPanel root        = Components.borderPanel( Res.colors().frameBg(), BoxLayout.PAGE_AXIS );
 | 
			
		||||
    private final FilesPanel               filesPanel  = new FilesPanel();
 | 
			
		||||
    private final JPanel                   userPanel   = Components.panel( new BorderLayout( 0, 0 ) );
 | 
			
		||||
    private final UserContentPanel         userContent = new UserContentPanel();
 | 
			
		||||
 | 
			
		||||
    @SuppressWarnings("MagicNumber")
 | 
			
		||||
@@ -32,15 +28,16 @@ public class MasterPasswordFrame extends JFrame {
 | 
			
		||||
        super( "Master Password" );
 | 
			
		||||
 | 
			
		||||
        setContentPane( root );
 | 
			
		||||
        root.add( filesPanel );
 | 
			
		||||
        root.add( new FilesPanel() );
 | 
			
		||||
        root.add( Components.strut() );
 | 
			
		||||
        root.add( userPanel );
 | 
			
		||||
 | 
			
		||||
        JPanel userPanel = Components.panel( new BorderLayout( 0, 0 ) );
 | 
			
		||||
        userPanel.add( userContent.getUserToolbar(), BorderLayout.LINE_START );
 | 
			
		||||
        userPanel.add( userContent.getSiteToolbar(), BorderLayout.LINE_END );
 | 
			
		||||
        userPanel.add( Components.borderPanel(
 | 
			
		||||
                BorderFactory.createBevelBorder( BevelBorder.RAISED, Res.colors().controlBorder(), Res.colors().frameBg() ),
 | 
			
		||||
                Res.colors().controlBg(), BoxLayout.PAGE_AXIS, userContent ), BorderLayout.CENTER );
 | 
			
		||||
        root.add( userPanel );
 | 
			
		||||
 | 
			
		||||
        addComponentListener( new ComponentHandler() );
 | 
			
		||||
        setPreferredSize( new Dimension( 800, 560 ) );
 | 
			
		||||
 
 | 
			
		||||
@@ -10,8 +10,7 @@ import com.lyndir.lhunath.opal.system.util.ObjectUtils;
 | 
			
		||||
import com.lyndir.masterpassword.*;
 | 
			
		||||
import com.lyndir.masterpassword.gui.MPGuiConstants;
 | 
			
		||||
import com.lyndir.masterpassword.gui.MasterPassword;
 | 
			
		||||
import com.lyndir.masterpassword.gui.model.MPIncognitoUser;
 | 
			
		||||
import com.lyndir.masterpassword.gui.model.MPNewSite;
 | 
			
		||||
import com.lyndir.masterpassword.gui.model.*;
 | 
			
		||||
import com.lyndir.masterpassword.gui.util.*;
 | 
			
		||||
import com.lyndir.masterpassword.gui.util.Platform;
 | 
			
		||||
import com.lyndir.masterpassword.model.*;
 | 
			
		||||
@@ -290,9 +289,9 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
 | 
			
		||||
        private final JButton resetButton  = Components.button( Res.icons().reset(), event -> resetUser(),
 | 
			
		||||
                                                                "Change the master password for this user." );
 | 
			
		||||
 | 
			
		||||
        private final JPasswordField masterPasswordField = Components.passwordField();
 | 
			
		||||
        private final JLabel         errorLabel          = Components.label();
 | 
			
		||||
        private final JLabel         identiconLabel      = Components.label( SwingConstants.CENTER );
 | 
			
		||||
        private final JPasswordField masterPasswordField;
 | 
			
		||||
        private final JLabel         errorLabel;
 | 
			
		||||
        private final JLabel         identiconLabel;
 | 
			
		||||
 | 
			
		||||
        private Future<?> identiconJob;
 | 
			
		||||
 | 
			
		||||
@@ -312,16 +311,16 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
 | 
			
		||||
            add( Components.heading( user.getFullName(), SwingConstants.CENTER ) );
 | 
			
		||||
            add( Components.strut() );
 | 
			
		||||
 | 
			
		||||
            add( identiconLabel );
 | 
			
		||||
            add( identiconLabel = Components.label( SwingConstants.CENTER ) );
 | 
			
		||||
            identiconLabel.setFont( Res.fonts().emoticonsFont( Components.TEXT_SIZE_CONTROL ) );
 | 
			
		||||
            add( Box.createGlue() );
 | 
			
		||||
 | 
			
		||||
            add( Components.label( "Master Password:" ) );
 | 
			
		||||
            add( Components.strut() );
 | 
			
		||||
            add( masterPasswordField );
 | 
			
		||||
            add( masterPasswordField = Components.passwordField() );
 | 
			
		||||
            masterPasswordField.addActionListener( this );
 | 
			
		||||
            masterPasswordField.getDocument().addDocumentListener( this );
 | 
			
		||||
            add( errorLabel );
 | 
			
		||||
            add( errorLabel = Components.label() );
 | 
			
		||||
            errorLabel.setForeground( Res.colors().errorFg() );
 | 
			
		||||
            add( Box.createGlue() );
 | 
			
		||||
        }
 | 
			
		||||
@@ -468,21 +467,20 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
 | 
			
		||||
                                                                   "Sign out and lock user." );
 | 
			
		||||
        private final JButton settingsButton  = Components.button( Res.icons().settings(), event -> showSiteSettings(),
 | 
			
		||||
                                                                   "Show site settings." );
 | 
			
		||||
        private final JButton questionsButton = Components.button( Res.icons().question(), null,
 | 
			
		||||
        private final JButton questionsButton = Components.button( Res.icons().question(), event -> showSiteQuestions(),
 | 
			
		||||
                                                                   "Show site recovery questions." );
 | 
			
		||||
        private final JButton deleteButton    = Components.button( Res.icons().delete(), event -> deleteSite(),
 | 
			
		||||
                                                                   "Delete the site from the user." );
 | 
			
		||||
 | 
			
		||||
        @Nonnull
 | 
			
		||||
        private final MPUser<?>                      user;
 | 
			
		||||
        private final JLabel                         passwordLabel = Components.label( SwingConstants.CENTER );
 | 
			
		||||
        private final JLabel                         passwordField = Components.heading( SwingConstants.CENTER );
 | 
			
		||||
        private final JLabel                         queryLabel    = Components.label();
 | 
			
		||||
        private final JTextField                     queryField    = Components.textField( null, this::updateSites );
 | 
			
		||||
        private final CollectionListModel<MPSite<?>> sitesModel    =
 | 
			
		||||
                new CollectionListModel<MPSite<?>>().selection( this::showSiteResult );
 | 
			
		||||
        private final JList<MPSite<?>>               sitesList     =
 | 
			
		||||
                Components.list( sitesModel, this::getSiteDescription );
 | 
			
		||||
        private final JLabel                         passwordLabel;
 | 
			
		||||
        private final JLabel                         passwordField;
 | 
			
		||||
        private final JLabel                         answerField;
 | 
			
		||||
        private final JLabel                         queryLabel;
 | 
			
		||||
        private final JTextField                     queryField;
 | 
			
		||||
        private final CollectionListModel<MPSite<?>> sitesModel;
 | 
			
		||||
        private final JList<MPSite<?>>               sitesList;
 | 
			
		||||
 | 
			
		||||
        private Future<?> updateSitesJob;
 | 
			
		||||
 | 
			
		||||
@@ -501,27 +499,32 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
 | 
			
		||||
            siteToolbar.add( questionsButton );
 | 
			
		||||
            siteToolbar.add( deleteButton );
 | 
			
		||||
            settingsButton.setEnabled( false );
 | 
			
		||||
            questionsButton.setEnabled( false );
 | 
			
		||||
            deleteButton.setEnabled( false );
 | 
			
		||||
 | 
			
		||||
            add( Components.heading( user.getFullName(), SwingConstants.CENTER ) );
 | 
			
		||||
 | 
			
		||||
            add( passwordLabel );
 | 
			
		||||
            add( passwordField );
 | 
			
		||||
            add( passwordLabel = Components.label( SwingConstants.CENTER ) );
 | 
			
		||||
            add( passwordField = Components.heading( SwingConstants.CENTER ) );
 | 
			
		||||
            passwordField.setForeground( Res.colors().highlightFg() );
 | 
			
		||||
            passwordField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) );
 | 
			
		||||
            answerField = Components.heading( SwingConstants.CENTER );
 | 
			
		||||
            answerField.setForeground( Res.colors().highlightFg() );
 | 
			
		||||
            answerField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) );
 | 
			
		||||
            add( Box.createGlue() );
 | 
			
		||||
            add( Components.strut() );
 | 
			
		||||
 | 
			
		||||
            add( queryLabel );
 | 
			
		||||
            add( queryLabel = Components.label() );
 | 
			
		||||
            queryLabel.setText( strf( "%s's password for:", user.getFullName() ) );
 | 
			
		||||
            add( queryField );
 | 
			
		||||
            add( queryField = Components.textField( null, this::updateSites ) );
 | 
			
		||||
            queryField.putClientProperty( "JTextField.variant", "search" );
 | 
			
		||||
            queryField.addActionListener( event -> useSite() );
 | 
			
		||||
            queryField.addKeyListener( this );
 | 
			
		||||
            queryField.requestFocusInWindow();
 | 
			
		||||
            add( Components.strut() );
 | 
			
		||||
            add( Components.scrollPane( sitesList ) );
 | 
			
		||||
            sitesModel.registerList( sitesList );
 | 
			
		||||
            add( Components.scrollPane( sitesList = Components.list(
 | 
			
		||||
                    sitesModel = new CollectionListModel<MPSite<?>>().selection( this::showSiteResult ),
 | 
			
		||||
                    this::getSiteDescription ) ) );
 | 
			
		||||
            add( Box.createGlue() );
 | 
			
		||||
 | 
			
		||||
            addHierarchyListener( e -> {
 | 
			
		||||
@@ -587,10 +590,56 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
 | 
			
		||||
                                Components.textField( fileSite.getUrl(), fileSite::setUrl ),
 | 
			
		||||
                                Components.strut() );
 | 
			
		||||
 | 
			
		||||
            Components.showDialog( this, site.getSiteName(), new JOptionPane( Components.panel(
 | 
			
		||||
            Components.showDialog( this, strf( "Settings for %s", site.getSiteName() ), new JOptionPane( Components.panel(
 | 
			
		||||
                    BoxLayout.PAGE_AXIS, components.build().toArray( new Component[0] ) ) ) );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void showSiteQuestions() {
 | 
			
		||||
            MPSite<?> site = sitesModel.getSelectedItem();
 | 
			
		||||
            if (site == null)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            CollectionListModel<MPQuestion> questionsModel = new CollectionListModel<MPQuestion>().selection( this::showQuestionResult );
 | 
			
		||||
            JList<MPQuestion>               questionsList  = Components.list( questionsModel, MPQuestion::getKeyword );
 | 
			
		||||
            JTextField queryField = Components.textField( null, query -> Res.job( () -> {
 | 
			
		||||
                Collection<MPQuestion> questions = new LinkedList<>( site.findQuestions( query ) );
 | 
			
		||||
 | 
			
		||||
                if (!Strings.isNullOrEmpty( query ))
 | 
			
		||||
                    if (questions.stream().noneMatch( question -> question.getKeyword().equalsIgnoreCase( query ) ))
 | 
			
		||||
                        questions.add( new MPNewQuestion( site, query ) );
 | 
			
		||||
 | 
			
		||||
                Res.ui( () -> questionsModel.set( questions ) );
 | 
			
		||||
            } ) );
 | 
			
		||||
            queryField.putClientProperty( "JTextField.variant", "search" );
 | 
			
		||||
            queryField.addActionListener( event -> useQuestion( questionsModel.getSelectedItem() ) );
 | 
			
		||||
            queryField.addKeyListener( new KeyAdapter() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void keyPressed(final KeyEvent event) {
 | 
			
		||||
                    if ((event.getKeyCode() == KeyEvent.VK_UP) || (event.getKeyCode() == KeyEvent.VK_DOWN))
 | 
			
		||||
                        questionsList.dispatchEvent( event );
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void keyReleased(final KeyEvent event) {
 | 
			
		||||
                    if ((event.getKeyCode() == KeyEvent.VK_UP) || (event.getKeyCode() == KeyEvent.VK_DOWN))
 | 
			
		||||
                        questionsList.dispatchEvent( event );
 | 
			
		||||
                }
 | 
			
		||||
            } );
 | 
			
		||||
 | 
			
		||||
            Components.showDialog( this, strf( "Recovery answers for %s", site.getSiteName() ), new JOptionPane( Components.panel(
 | 
			
		||||
                    BoxLayout.PAGE_AXIS,
 | 
			
		||||
                    Components.label( "Security Question Keyword:" ), queryField,
 | 
			
		||||
                    Components.strut(),
 | 
			
		||||
                    Components.label( "Answer:" ), answerField,
 | 
			
		||||
                    Components.strut(),
 | 
			
		||||
                    Components.scrollPane( questionsList ) ) ) {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void selectInitialValue() {
 | 
			
		||||
                    queryField.requestFocusInWindow();
 | 
			
		||||
                }
 | 
			
		||||
            } );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void deleteSite() {
 | 
			
		||||
            MPSite<?> site = sitesModel.getSelectedItem();
 | 
			
		||||
            if (site == null)
 | 
			
		||||
@@ -668,6 +717,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
 | 
			
		||||
                    passwordLabel.setText( " " );
 | 
			
		||||
                    passwordField.setText( " " );
 | 
			
		||||
                    settingsButton.setEnabled( false );
 | 
			
		||||
                    questionsButton.setEnabled( false );
 | 
			
		||||
                    deleteButton.setEnabled( false );
 | 
			
		||||
                } );
 | 
			
		||||
                return;
 | 
			
		||||
@@ -683,6 +733,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
 | 
			
		||||
                        passwordLabel.setText( strf( "Your password for %s:", site.getSiteName() ) );
 | 
			
		||||
                        passwordField.setText( result );
 | 
			
		||||
                        settingsButton.setEnabled( true );
 | 
			
		||||
                        questionsButton.setEnabled( true );
 | 
			
		||||
                        deleteButton.setEnabled( true );
 | 
			
		||||
                    } );
 | 
			
		||||
                }
 | 
			
		||||
@@ -692,6 +743,66 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
 | 
			
		||||
            } );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void useQuestion(@Nullable final MPQuestion question) {
 | 
			
		||||
            if (question instanceof MPNewQuestion) {
 | 
			
		||||
                if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(
 | 
			
		||||
                        this,
 | 
			
		||||
                        strf( "<html>Remember the answer for the security question with keyword <strong>%s</strong>?</html>",
 | 
			
		||||
                              question.getKeyword() ),
 | 
			
		||||
                        "New Question", JOptionPane.YES_NO_OPTION )) {
 | 
			
		||||
                    useQuestion( question.getSite().addQuestion( question.getKeyword() ) );
 | 
			
		||||
                }
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            showQuestionResult( question, result -> {
 | 
			
		||||
                if (result == null)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                if (question instanceof MPFileQuestion)
 | 
			
		||||
                    ((MPFileQuestion) question).use();
 | 
			
		||||
 | 
			
		||||
                Transferable clipboardContents = new StringSelection( result );
 | 
			
		||||
                Toolkit.getDefaultToolkit().getSystemClipboard().setContents( clipboardContents, null );
 | 
			
		||||
 | 
			
		||||
                Res.ui( () -> {
 | 
			
		||||
                    Window answerDialog = SwingUtilities.windowForComponent( answerField );
 | 
			
		||||
                    if (answerDialog instanceof Dialog)
 | 
			
		||||
                        answerDialog.setVisible( false );
 | 
			
		||||
 | 
			
		||||
                    Window window = SwingUtilities.windowForComponent( UserContentPanel.this );
 | 
			
		||||
                    if (window instanceof Frame)
 | 
			
		||||
                        ((Frame) window).setExtendedState( Frame.ICONIFIED );
 | 
			
		||||
                } );
 | 
			
		||||
            } );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void showQuestionResult(@Nullable final MPQuestion question) {
 | 
			
		||||
            showQuestionResult( question, null );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void showQuestionResult(@Nullable final MPQuestion question, @Nullable final Consumer<String> resultCallback) {
 | 
			
		||||
            if (question == null) {
 | 
			
		||||
                if (resultCallback != null)
 | 
			
		||||
                    resultCallback.accept( null );
 | 
			
		||||
                Res.ui( () -> answerField.setText( " " ) );
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Res.job( () -> {
 | 
			
		||||
                try {
 | 
			
		||||
                    String answer = question.getAnswer();
 | 
			
		||||
                    if (resultCallback != null)
 | 
			
		||||
                        resultCallback.accept( answer );
 | 
			
		||||
 | 
			
		||||
                    Res.ui( () -> answerField.setText( answer ) );
 | 
			
		||||
                }
 | 
			
		||||
                catch (final MPKeyUnavailableException | MPAlgorithmException e) {
 | 
			
		||||
                    logger.err( e, "While resolving answer for: %s", question );
 | 
			
		||||
                }
 | 
			
		||||
            } );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void keyTyped(final KeyEvent event) {
 | 
			
		||||
        }
 | 
			
		||||
@@ -713,13 +824,11 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
 | 
			
		||||
                updateSitesJob.cancel( true );
 | 
			
		||||
 | 
			
		||||
            updateSitesJob = Res.job( () -> {
 | 
			
		||||
                Collection<MPSite<?>> sites = new LinkedList<>();
 | 
			
		||||
                if (!Strings.isNullOrEmpty( query )) {
 | 
			
		||||
                    sites.addAll( new LinkedList<>( user.findSites( query ) ) );
 | 
			
		||||
                Collection<MPSite<?>> sites = new LinkedList<>( user.findSites( query ) );
 | 
			
		||||
 | 
			
		||||
                if (!Strings.isNullOrEmpty( query ))
 | 
			
		||||
                    if (sites.stream().noneMatch( site -> site.getSiteName().equalsIgnoreCase( query ) ))
 | 
			
		||||
                        sites.add( new MPNewSite( user, query ) );
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Res.ui( () -> sitesModel.set( sites ) );
 | 
			
		||||
            } );
 | 
			
		||||
 
 | 
			
		||||
@@ -40,6 +40,13 @@ public interface MPQuestion extends Comparable<MPQuestion> {
 | 
			
		||||
 | 
			
		||||
    void setType(MPResultType type);
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    default String getAnswer()
 | 
			
		||||
            throws MPKeyUnavailableException, MPAlgorithmException {
 | 
			
		||||
 | 
			
		||||
        return getAnswer( null );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    String getAnswer(@Nullable String state)
 | 
			
		||||
            throws MPKeyUnavailableException, MPAlgorithmException;
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@
 | 
			
		||||
 | 
			
		||||
package com.lyndir.masterpassword.model;
 | 
			
		||||
 | 
			
		||||
import com.google.common.collect.ImmutableCollection;
 | 
			
		||||
import com.google.common.primitives.UnsignedInteger;
 | 
			
		||||
import com.lyndir.masterpassword.*;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
@@ -57,6 +58,7 @@ public interface MPSite<Q extends MPQuestion> extends Comparable<MPSite<?>> {
 | 
			
		||||
 | 
			
		||||
    void setLoginType(@Nullable MPResultType loginType);
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    default String getResult()
 | 
			
		||||
            throws MPKeyUnavailableException, MPAlgorithmException {
 | 
			
		||||
 | 
			
		||||
@@ -79,6 +81,16 @@ public interface MPSite<Q extends MPQuestion> extends Comparable<MPSite<?>> {
 | 
			
		||||
    String getResult(MPKeyPurpose keyPurpose, @Nullable String keyContext, @Nullable String state)
 | 
			
		||||
            throws MPKeyUnavailableException, MPAlgorithmException;
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    String getResult(MPKeyPurpose keyPurpose, @Nullable String keyContext,
 | 
			
		||||
                     @Nullable UnsignedInteger counter, MPResultType type, @Nullable String state)
 | 
			
		||||
            throws MPKeyUnavailableException, MPAlgorithmException;
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    String getState(MPKeyPurpose keyPurpose, @Nullable String keyContext,
 | 
			
		||||
                    @Nullable UnsignedInteger counter, MPResultType type, String state)
 | 
			
		||||
            throws MPKeyUnavailableException, MPAlgorithmException;
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    default String getLogin()
 | 
			
		||||
            throws MPKeyUnavailableException, MPAlgorithmException {
 | 
			
		||||
@@ -94,10 +106,17 @@ public interface MPSite<Q extends MPQuestion> extends Comparable<MPSite<?>> {
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    MPUser<?> getUser();
 | 
			
		||||
 | 
			
		||||
    boolean addQuestion(Q question);
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    Q addQuestion(String keyword);
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    Q addQuestion(Q question);
 | 
			
		||||
 | 
			
		||||
    boolean deleteQuestion(Q question);
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    Collection<Q> getQuestions();
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    ImmutableCollection<Q> findQuestions(@Nullable String query);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -100,6 +100,7 @@ public interface MPUser<S extends MPSite<?>> extends Comparable<MPUser<?>> {
 | 
			
		||||
 | 
			
		||||
    // - Relations
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    S addSite(String siteName);
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,7 @@ import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
 | 
			
		||||
 | 
			
		||||
import com.lyndir.masterpassword.*;
 | 
			
		||||
import com.lyndir.masterpassword.model.MPQuestion;
 | 
			
		||||
import com.lyndir.masterpassword.model.MPSite;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
import javax.annotation.Nonnull;
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
@@ -33,14 +34,23 @@ import org.jetbrains.annotations.NotNull;
 | 
			
		||||
 */
 | 
			
		||||
public abstract class MPBasicQuestion extends Changeable implements MPQuestion {
 | 
			
		||||
 | 
			
		||||
    private final MPSite<?> site;
 | 
			
		||||
    private final String       keyword;
 | 
			
		||||
 | 
			
		||||
    private MPResultType type;
 | 
			
		||||
 | 
			
		||||
    protected MPBasicQuestion(final String keyword, final MPResultType type) {
 | 
			
		||||
    protected MPBasicQuestion(final MPSite<?> site, final String keyword, final MPResultType type) {
 | 
			
		||||
        this.site = site;
 | 
			
		||||
        this.keyword = keyword;
 | 
			
		||||
        this.type = type;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public MPSite<?> getSite() {
 | 
			
		||||
        return site;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getKeyword() {
 | 
			
		||||
@@ -55,7 +65,7 @@ public abstract class MPBasicQuestion extends Changeable implements MPQuestion {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void setType(final MPResultType type) {
 | 
			
		||||
        if (Objects.equals(this.type, type))
 | 
			
		||||
        if (this.type == type)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        this.type = type;
 | 
			
		||||
@@ -70,15 +80,12 @@ public abstract class MPBasicQuestion extends Changeable implements MPQuestion {
 | 
			
		||||
        return getSite().getResult( MPKeyPurpose.Recovery, getKeyword(), null, getType(), state );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public abstract MPBasicSite<?, ?> getSite();
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onChanged() {
 | 
			
		||||
        super.onChanged();
 | 
			
		||||
 | 
			
		||||
        getSite().setChanged();
 | 
			
		||||
        if (site instanceof Changeable)
 | 
			
		||||
            ((Changeable) site).setChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,9 @@ package com.lyndir.masterpassword.model.impl;
 | 
			
		||||
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
 | 
			
		||||
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
 | 
			
		||||
 | 
			
		||||
import com.google.common.base.Strings;
 | 
			
		||||
import com.google.common.collect.ImmutableCollection;
 | 
			
		||||
import com.google.common.collect.ImmutableSortedSet;
 | 
			
		||||
import com.google.common.primitives.UnsignedInteger;
 | 
			
		||||
import com.lyndir.masterpassword.*;
 | 
			
		||||
import com.lyndir.masterpassword.model.*;
 | 
			
		||||
@@ -35,9 +38,9 @@ import javax.annotation.Nullable;
 | 
			
		||||
public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> extends Changeable
 | 
			
		||||
        implements MPSite<Q> {
 | 
			
		||||
 | 
			
		||||
    private final Collection<Q> questions = new LinkedHashSet<>();
 | 
			
		||||
    private final U             user;
 | 
			
		||||
    private final String        siteName;
 | 
			
		||||
    private final Collection<Q> questions = new LinkedHashSet<>();
 | 
			
		||||
 | 
			
		||||
    private MPAlgorithm     algorithm;
 | 
			
		||||
    private UnsignedInteger counter;
 | 
			
		||||
@@ -104,7 +107,7 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void setResultType(final MPResultType resultType) {
 | 
			
		||||
        if (Objects.equals( this.resultType, resultType ))
 | 
			
		||||
        if (this.resultType == resultType)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        this.resultType = resultType;
 | 
			
		||||
@@ -119,7 +122,7 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void setLoginType(@Nullable final MPResultType loginType) {
 | 
			
		||||
        if (Objects.equals( this.loginType, loginType ))
 | 
			
		||||
        if (this.loginType == loginType)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        this.loginType = ifNotNullElse( loginType, getAlgorithm().mpw_default_login_type() );
 | 
			
		||||
@@ -134,7 +137,9 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
 | 
			
		||||
        return getResult( keyPurpose, keyContext, getCounter(), getResultType(), state );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext,
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext,
 | 
			
		||||
                            @Nullable final UnsignedInteger counter, final MPResultType type, @Nullable final String state)
 | 
			
		||||
            throws MPKeyUnavailableException, MPAlgorithmException {
 | 
			
		||||
 | 
			
		||||
@@ -143,7 +148,9 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
 | 
			
		||||
                keyPurpose, keyContext, type, state );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected String getState(final MPKeyPurpose keyPurpose, @Nullable final String keyContext,
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getState(final MPKeyPurpose keyPurpose, @Nullable final String keyContext,
 | 
			
		||||
                           @Nullable final UnsignedInteger counter, final MPResultType type, final String state)
 | 
			
		||||
            throws MPKeyUnavailableException, MPAlgorithmException {
 | 
			
		||||
 | 
			
		||||
@@ -160,13 +167,13 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
 | 
			
		||||
        return getResult( MPKeyPurpose.Identification, null, null, getLoginType(), state );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean addQuestion(final Q question) {
 | 
			
		||||
        if (!questions.add( question ))
 | 
			
		||||
            return false;
 | 
			
		||||
    public Q addQuestion(final Q question) {
 | 
			
		||||
        questions.add( question );
 | 
			
		||||
 | 
			
		||||
        setChanged();
 | 
			
		||||
        return true;
 | 
			
		||||
        return question;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
@@ -184,6 +191,17 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
 | 
			
		||||
        return Collections.unmodifiableCollection( questions );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public ImmutableCollection<Q> findQuestions(@Nullable final String query) {
 | 
			
		||||
        ImmutableSortedSet.Builder<Q> results = ImmutableSortedSet.naturalOrder();
 | 
			
		||||
        for (final Q question : getQuestions())
 | 
			
		||||
            if (Strings.isNullOrEmpty( query ) || question.getKeyword().startsWith( query ))
 | 
			
		||||
                results.add( question );
 | 
			
		||||
 | 
			
		||||
        return results.build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public U getUser() {
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ package com.lyndir.masterpassword.model.impl;
 | 
			
		||||
 | 
			
		||||
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
 | 
			
		||||
 | 
			
		||||
import com.google.common.base.Strings;
 | 
			
		||||
import com.google.common.collect.ImmutableCollection;
 | 
			
		||||
import com.google.common.collect.ImmutableSortedSet;
 | 
			
		||||
import com.lyndir.lhunath.opal.system.CodeUtils;
 | 
			
		||||
@@ -202,9 +203,8 @@ public abstract class MPBasicUser<S extends MPBasicSite<?, ?>> extends Changeabl
 | 
			
		||||
    @Override
 | 
			
		||||
    public ImmutableCollection<S> findSites(@Nullable final String query) {
 | 
			
		||||
        ImmutableSortedSet.Builder<S> results = ImmutableSortedSet.naturalOrder();
 | 
			
		||||
        if (query != null)
 | 
			
		||||
        for (final S site : getSites())
 | 
			
		||||
                if (site.getSiteName().startsWith( query ))
 | 
			
		||||
            if (Strings.isNullOrEmpty( query ) || site.getSiteName().startsWith( query ))
 | 
			
		||||
                results.add( site );
 | 
			
		||||
 | 
			
		||||
        return results.build();
 | 
			
		||||
 
 | 
			
		||||
@@ -30,16 +30,13 @@ import javax.annotation.Nullable;
 | 
			
		||||
 */
 | 
			
		||||
public class MPFileQuestion extends MPBasicQuestion {
 | 
			
		||||
 | 
			
		||||
    private final MPFileSite site;
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    private String answerState;
 | 
			
		||||
 | 
			
		||||
    public MPFileQuestion(final MPFileSite site, final String keyword,
 | 
			
		||||
                          @Nullable final MPResultType type, @Nullable final String answerState) {
 | 
			
		||||
        super( keyword, ifNotNullElse( type, site.getAlgorithm().mpw_default_answer_type() ) );
 | 
			
		||||
        super( site, keyword, ifNotNullElse( type, site.getAlgorithm().mpw_default_answer_type() ) );
 | 
			
		||||
 | 
			
		||||
        this.site = site;
 | 
			
		||||
        this.answerState = answerState;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -48,6 +45,8 @@ public class MPFileQuestion extends MPBasicQuestion {
 | 
			
		||||
        return answerState;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getAnswer()
 | 
			
		||||
            throws MPKeyUnavailableException, MPAlgorithmException {
 | 
			
		||||
        return getAnswer( answerState );
 | 
			
		||||
@@ -66,9 +65,8 @@ public class MPFileQuestion extends MPBasicQuestion {
 | 
			
		||||
        setChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public MPFileSite getSite() {
 | 
			
		||||
        return site;
 | 
			
		||||
    public void use() {
 | 
			
		||||
        if (getSite() instanceof MPFileSite)
 | 
			
		||||
            ((MPFileSite) getSite()).use();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -145,6 +145,12 @@ public class MPFileSite extends MPBasicSite<MPFileUser, MPFileQuestion> {
 | 
			
		||||
        setChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public MPFileQuestion addQuestion(final String keyword) {
 | 
			
		||||
        return addQuestion( new MPFileQuestion( this, keyword, null, null ) );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int compareTo(@Nonnull final MPSite<?> o) {
 | 
			
		||||
        int comparison = (o instanceof MPFileSite)? ((MPFileSite) o).getLastUsed().compareTo( getLastUsed() ): 0;
 | 
			
		||||
 
 | 
			
		||||
@@ -207,6 +207,7 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
 | 
			
		||||
        super.reset();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public MPFileSite addSite(final String siteName) {
 | 
			
		||||
        return addSite( new MPFileSite( this, siteName ) );
 | 
			
		||||
 
 | 
			
		||||
 Submodule public/site updated: b190b45756...0623231ad5
									
								
							
		Reference in New Issue
	
	Block a user