Java improvements.
UI threading improvements. Save user/site changes to file. Ordering of user / site fixes. Add questions to JSON output. Bring JSON output format in line with C.
This commit is contained in:
		@@ -45,8 +45,6 @@ public class MPAlgorithmV0 extends MPAlgorithm {
 | 
			
		||||
        Native.load( MPAlgorithmV0.class, "mpw" );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public final Version version = MPAlgorithm.Version.V0;
 | 
			
		||||
 | 
			
		||||
    protected final Logger logger = Logger.get( getClass() );
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,6 @@ import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
 | 
			
		||||
 | 
			
		||||
import com.google.common.base.Charsets;
 | 
			
		||||
import com.google.common.io.ByteSource;
 | 
			
		||||
import com.google.common.io.CharSource;
 | 
			
		||||
import com.lyndir.lhunath.opal.system.logging.Logger;
 | 
			
		||||
import com.lyndir.lhunath.opal.system.util.TypeUtils;
 | 
			
		||||
import com.lyndir.masterpassword.gui.view.PasswordFrame;
 | 
			
		||||
@@ -51,6 +50,9 @@ public class GUI implements UnlockFrame.SignInCallback {
 | 
			
		||||
    private       PasswordFrame<?, ?> passwordFrame;
 | 
			
		||||
 | 
			
		||||
    public static void main(final String... args) {
 | 
			
		||||
        Thread.setDefaultUncaughtExceptionHandler(
 | 
			
		||||
                (t, e) -> logger.err( e, "Uncaught: %s", e.getLocalizedMessage() ) );
 | 
			
		||||
 | 
			
		||||
        if (Config.get().checkForUpdates())
 | 
			
		||||
            checkUpdate();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,6 @@ package com.lyndir.masterpassword.gui;
 | 
			
		||||
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
 | 
			
		||||
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
 | 
			
		||||
 | 
			
		||||
import com.google.common.base.Throwables;
 | 
			
		||||
import com.google.common.collect.Maps;
 | 
			
		||||
import com.google.common.io.Resources;
 | 
			
		||||
import com.google.common.util.concurrent.JdkFutureAdapters;
 | 
			
		||||
@@ -50,17 +49,19 @@ import org.jetbrains.annotations.NonNls;
 | 
			
		||||
@SuppressWarnings({ "HardcodedFileSeparator", "MethodReturnAlwaysConstant", "SpellCheckingInspection" })
 | 
			
		||||
public abstract class Res {
 | 
			
		||||
 | 
			
		||||
    private static final int                                   AVATAR_COUNT     = 19;
 | 
			
		||||
    private static final Map<Window, ScheduledExecutorService> executorByWindow = new WeakHashMap<>();
 | 
			
		||||
    private static final Logger                                logger           = Logger.get( Res.class );
 | 
			
		||||
    private static final Colors                                colors           = new Colors();
 | 
			
		||||
    private static final int                                   AVATAR_COUNT        = 19;
 | 
			
		||||
    private static final Map<Window, ScheduledExecutorService> jobExecutorByWindow = new WeakHashMap<>();
 | 
			
		||||
    private static final Executor                              immediateUiExecutor = new SwingExecutorService( true );
 | 
			
		||||
    private static final Executor                              laterUiExecutor     = new SwingExecutorService( false );
 | 
			
		||||
    private static final Logger                                logger              = Logger.get( Res.class );
 | 
			
		||||
    private static final Colors                                colors              = new Colors();
 | 
			
		||||
 | 
			
		||||
    public static Future<?> execute(final Window host, final Runnable job) {
 | 
			
		||||
        return schedule( host, job, 0, TimeUnit.MILLISECONDS );
 | 
			
		||||
    public static Future<?> job(final Window host, final Runnable job) {
 | 
			
		||||
        return job( host, job, 0, TimeUnit.MILLISECONDS );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static Future<?> schedule(final Window host, final Runnable job, final long delay, final TimeUnit timeUnit) {
 | 
			
		||||
        return getExecutor( host ).schedule( () -> {
 | 
			
		||||
    public static Future<?> job(final Window host, final Runnable job, final long delay, final TimeUnit timeUnit) {
 | 
			
		||||
        return jobExecutor( host ).schedule( () -> {
 | 
			
		||||
            try {
 | 
			
		||||
                job.run();
 | 
			
		||||
            }
 | 
			
		||||
@@ -70,33 +71,29 @@ public abstract class Res {
 | 
			
		||||
        }, delay, timeUnit );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static <V> ListenableFuture<V> execute(final Window host, final Callable<V> job) {
 | 
			
		||||
        return schedule( host, job, 0, TimeUnit.MILLISECONDS );
 | 
			
		||||
    public static <V> ListenableFuture<V> job(final Window host, final Callable<V> job) {
 | 
			
		||||
        return job( host, job, 0, TimeUnit.MILLISECONDS );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static <V> ListenableFuture<V> schedule(final Window host, final Callable<V> job, final long delay, final TimeUnit timeUnit) {
 | 
			
		||||
        ScheduledExecutorService executor = getExecutor( host );
 | 
			
		||||
        return JdkFutureAdapters.listenInPoolThread( executor.schedule( () -> {
 | 
			
		||||
            try {
 | 
			
		||||
                return job.call();
 | 
			
		||||
            }
 | 
			
		||||
            catch (final Throwable t) {
 | 
			
		||||
                logger.err( t, "Unexpected: %s", t.getLocalizedMessage() );
 | 
			
		||||
                throw Throwables.propagate( t );
 | 
			
		||||
            }
 | 
			
		||||
        }, delay, timeUnit ), executor );
 | 
			
		||||
    public static <V> ListenableFuture<V> job(final Window host, final Callable<V> job, final long delay, final TimeUnit timeUnit) {
 | 
			
		||||
        ScheduledExecutorService executor = jobExecutor( host );
 | 
			
		||||
        return JdkFutureAdapters.listenInPoolThread( executor.schedule( job::call, delay, timeUnit ), executor );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static ScheduledExecutorService getExecutor(final Window host) {
 | 
			
		||||
        ScheduledExecutorService executor = executorByWindow.get( host );
 | 
			
		||||
    public static Executor uiExecutor(final boolean immediate) {
 | 
			
		||||
        return immediate? immediateUiExecutor: laterUiExecutor;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static ScheduledExecutorService jobExecutor(final Window host) {
 | 
			
		||||
        ScheduledExecutorService executor = jobExecutorByWindow.get( host );
 | 
			
		||||
 | 
			
		||||
        if (executor == null) {
 | 
			
		||||
            executorByWindow.put( host, executor = Executors.newSingleThreadScheduledExecutor() );
 | 
			
		||||
            jobExecutorByWindow.put( host, executor = Executors.newSingleThreadScheduledExecutor() );
 | 
			
		||||
 | 
			
		||||
            host.addWindowListener( new WindowAdapter() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void windowClosed(final WindowEvent e) {
 | 
			
		||||
                    ExecutorService executor = executorByWindow.remove( host );
 | 
			
		||||
                    ExecutorService executor = jobExecutorByWindow.remove( host );
 | 
			
		||||
                    if (executor != null)
 | 
			
		||||
                        executor.shutdownNow();
 | 
			
		||||
                }
 | 
			
		||||
@@ -204,7 +201,7 @@ public abstract class Res {
 | 
			
		||||
                        font = Font.createFont( Font.TRUETYPE_FONT, Resources.getResource( fontResourceName ).openStream() ) ) );
 | 
			
		||||
            }
 | 
			
		||||
            catch (final FontFormatException | IOException e) {
 | 
			
		||||
                throw Throwables.propagate( e );
 | 
			
		||||
                throw logger.bug( e );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        return font;
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,91 @@
 | 
			
		||||
package com.lyndir.masterpassword.gui;
 | 
			
		||||
 | 
			
		||||
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
 | 
			
		||||
 | 
			
		||||
import com.google.common.collect.*;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.concurrent.*;
 | 
			
		||||
import javax.swing.*;
 | 
			
		||||
import org.jetbrains.annotations.NotNull;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @author lhunath, 2018-07-08
 | 
			
		||||
 */
 | 
			
		||||
public class SwingExecutorService extends AbstractExecutorService {
 | 
			
		||||
 | 
			
		||||
    private final List<Runnable>         pendingCommands = Lists.newLinkedList();
 | 
			
		||||
    private final BlockingQueue<Boolean> terminated      = Queues.newLinkedBlockingDeque( 1 );
 | 
			
		||||
    private final boolean                immediate;
 | 
			
		||||
    private       boolean                shutdown;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param immediate Allow immediate execution of the job in {@link #execute(Runnable)} if already on the right thread.
 | 
			
		||||
     *                  If {@code false}, jobs are always posted for later execution on the event thread.
 | 
			
		||||
     */
 | 
			
		||||
    public SwingExecutorService(final boolean immediate) {
 | 
			
		||||
        this.immediate = immediate;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void shutdown() {
 | 
			
		||||
        shutdown = true;
 | 
			
		||||
 | 
			
		||||
        synchronized (pendingCommands) {
 | 
			
		||||
            if (pendingCommands.isEmpty())
 | 
			
		||||
                terminated.offer( true );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @NotNull
 | 
			
		||||
    @Override
 | 
			
		||||
    public List<Runnable> shutdownNow() {
 | 
			
		||||
        shutdown();
 | 
			
		||||
 | 
			
		||||
        synchronized (pendingCommands) {
 | 
			
		||||
            return ImmutableList.copyOf( pendingCommands );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean isShutdown() {
 | 
			
		||||
        return shutdown;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean isTerminated() {
 | 
			
		||||
        return ifNotNullElse( terminated.peek(), false );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean awaitTermination(final long timeout, @NotNull final TimeUnit unit)
 | 
			
		||||
            throws InterruptedException {
 | 
			
		||||
        return ifNotNullElse( terminated.poll( timeout, unit ), false );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void execute(@NotNull final Runnable command) {
 | 
			
		||||
        if (shutdown)
 | 
			
		||||
            throw new RejectedExecutionException( "Executor is shut down." );
 | 
			
		||||
 | 
			
		||||
        synchronized (pendingCommands) {
 | 
			
		||||
            pendingCommands.add( command );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (immediate && SwingUtilities.isEventDispatchThread())
 | 
			
		||||
            run( command );
 | 
			
		||||
        else
 | 
			
		||||
            SwingUtilities.invokeLater( () -> run( command ) );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void run(final Runnable command) {
 | 
			
		||||
        command.run();
 | 
			
		||||
 | 
			
		||||
        synchronized (pendingCommands) {
 | 
			
		||||
            pendingCommands.remove( command );
 | 
			
		||||
 | 
			
		||||
            if (shutdown && pendingCommands.isEmpty())
 | 
			
		||||
                terminated.offer( true );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -22,6 +22,7 @@ import com.google.common.primitives.UnsignedInteger;
 | 
			
		||||
import com.lyndir.masterpassword.MPAlgorithm;
 | 
			
		||||
import com.lyndir.masterpassword.MPResultType;
 | 
			
		||||
import com.lyndir.masterpassword.model.impl.MPBasicSite;
 | 
			
		||||
import javax.annotation.Nonnull;
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -44,6 +45,7 @@ public class MPIncognitoSite extends MPBasicSite<MPIncognitoQuestion> {
 | 
			
		||||
        this.user = user;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public MPIncognitoUser getUser() {
 | 
			
		||||
        return user;
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,23 @@
 | 
			
		||||
package com.lyndir.masterpassword.gui.util;
 | 
			
		||||
 | 
			
		||||
import com.google.common.util.concurrent.FutureCallback;
 | 
			
		||||
import com.lyndir.lhunath.opal.system.logging.Logger;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @author lhunath, 2018-07-08
 | 
			
		||||
 */
 | 
			
		||||
public abstract class FailableCallback<T> implements FutureCallback<T> {
 | 
			
		||||
 | 
			
		||||
    private final Logger logger;
 | 
			
		||||
 | 
			
		||||
    protected FailableCallback(final Logger logger) {
 | 
			
		||||
        this.logger = logger;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onFailure(final Throwable t) {
 | 
			
		||||
        logger.err( t, "Future failed." );
 | 
			
		||||
        onSuccess( null );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -23,11 +23,12 @@ import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
 | 
			
		||||
 | 
			
		||||
import com.google.common.collect.Iterables;
 | 
			
		||||
import com.google.common.primitives.UnsignedInteger;
 | 
			
		||||
import com.google.common.util.concurrent.*;
 | 
			
		||||
import com.google.common.util.concurrent.Futures;
 | 
			
		||||
import com.google.common.util.concurrent.ListenableFuture;
 | 
			
		||||
import com.lyndir.lhunath.opal.system.logging.Logger;
 | 
			
		||||
import com.lyndir.masterpassword.*;
 | 
			
		||||
import com.lyndir.masterpassword.gui.Res;
 | 
			
		||||
import com.lyndir.masterpassword.gui.util.Components;
 | 
			
		||||
import com.lyndir.masterpassword.gui.util.UnsignedIntegerModel;
 | 
			
		||||
import com.lyndir.masterpassword.gui.util.*;
 | 
			
		||||
import com.lyndir.masterpassword.model.MPSite;
 | 
			
		||||
import com.lyndir.masterpassword.model.MPUser;
 | 
			
		||||
import com.lyndir.masterpassword.model.impl.MPFileSite;
 | 
			
		||||
@@ -35,7 +36,7 @@ import com.lyndir.masterpassword.model.impl.MPFileUser;
 | 
			
		||||
import java.awt.*;
 | 
			
		||||
import java.awt.datatransfer.StringSelection;
 | 
			
		||||
import java.awt.datatransfer.Transferable;
 | 
			
		||||
import java.awt.event.*;
 | 
			
		||||
import java.awt.event.WindowEvent;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
import javax.annotation.Nonnull;
 | 
			
		||||
@@ -50,6 +51,8 @@ import javax.swing.event.DocumentListener;
 | 
			
		||||
 */
 | 
			
		||||
public abstract class PasswordFrame<U extends MPUser<S>, S extends MPSite<?>> extends JFrame implements DocumentListener {
 | 
			
		||||
 | 
			
		||||
    private static final Logger logger = Logger.get( PasswordFrame.class );
 | 
			
		||||
 | 
			
		||||
    @SuppressWarnings("FieldCanBeLocal")
 | 
			
		||||
    private final Components.GradientPanel       root;
 | 
			
		||||
    private final JTextField                     siteNameField;
 | 
			
		||||
@@ -96,40 +99,33 @@ public abstract class PasswordFrame<U extends MPUser<S>, S extends MPSite<?>> ex
 | 
			
		||||
                                                        siteNameField = Components.textField(), Components.stud(),
 | 
			
		||||
                                                        siteActionButton = Components.button( "Add Site" ) );
 | 
			
		||||
        siteNameField.getDocument().addDocumentListener( this );
 | 
			
		||||
        siteNameField.addActionListener( new ActionListener() {
 | 
			
		||||
            @Override
 | 
			
		||||
            public void actionPerformed(final ActionEvent e) {
 | 
			
		||||
                Futures.addCallback( updatePassword( true ), new FutureCallback<String>() {
 | 
			
		||||
        siteNameField.addActionListener(
 | 
			
		||||
                e -> Futures.addCallback( updatePassword( true ), new FailableCallback<String>( logger ) {
 | 
			
		||||
                    @Override
 | 
			
		||||
                    public void onSuccess(@Nullable final String sitePassword) {
 | 
			
		||||
                        if (sitePassword == null)
 | 
			
		||||
                            return;
 | 
			
		||||
 | 
			
		||||
                        if (currentSite instanceof MPFileSite)
 | 
			
		||||
                            ((MPFileSite) currentSite).use();
 | 
			
		||||
 | 
			
		||||
                        Transferable clipboardContents = new StringSelection( sitePassword );
 | 
			
		||||
                        Toolkit.getDefaultToolkit().getSystemClipboard().setContents( clipboardContents, null );
 | 
			
		||||
 | 
			
		||||
                        SwingUtilities.invokeLater( () -> {
 | 
			
		||||
                            passwordField.setText( null );
 | 
			
		||||
                            siteNameField.setText( null );
 | 
			
		||||
 | 
			
		||||
                            dispatchEvent( new WindowEvent( PasswordFrame.this, WindowEvent.WINDOW_CLOSING ) );
 | 
			
		||||
                        } );
 | 
			
		||||
                        dispatchEvent( new WindowEvent( PasswordFrame.this, WindowEvent.WINDOW_CLOSING ) );
 | 
			
		||||
                    }
 | 
			
		||||
                }, Res.uiExecutor( false ) ) );
 | 
			
		||||
        siteActionButton.addActionListener(
 | 
			
		||||
                e -> {
 | 
			
		||||
                    if (currentSite == null)
 | 
			
		||||
                        return;
 | 
			
		||||
                    if (currentSite instanceof MPFileSite)
 | 
			
		||||
                        this.user.deleteSite( currentSite );
 | 
			
		||||
                    else
 | 
			
		||||
                        this.user.addSite( currentSite );
 | 
			
		||||
                    siteNameField.requestFocus();
 | 
			
		||||
 | 
			
		||||
                    @Override
 | 
			
		||||
                    public void onFailure(@Nonnull final Throwable t) {
 | 
			
		||||
                    }
 | 
			
		||||
                    updatePassword( true );
 | 
			
		||||
                } );
 | 
			
		||||
            }
 | 
			
		||||
        } );
 | 
			
		||||
        siteActionButton.addActionListener( e -> {
 | 
			
		||||
            if (currentSite == null)
 | 
			
		||||
                return;
 | 
			
		||||
            if (currentSite instanceof MPFileSite)
 | 
			
		||||
                this.user.deleteSite( currentSite );
 | 
			
		||||
            else
 | 
			
		||||
                this.user.addSite( currentSite );
 | 
			
		||||
            siteNameField.requestFocus();
 | 
			
		||||
 | 
			
		||||
            updatePassword( true );
 | 
			
		||||
        } );
 | 
			
		||||
        sitePanel.add( siteControls );
 | 
			
		||||
        sitePanel.add( Components.stud() );
 | 
			
		||||
 | 
			
		||||
@@ -229,34 +225,36 @@ public abstract class PasswordFrame<U extends MPUser<S>, S extends MPSite<?>> ex
 | 
			
		||||
            site.setCounter( siteCounter );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ListenableFuture<String> passwordFuture = Res.execute( this, () -> site.getResult( MPKeyPurpose.Authentication, null, null ) );
 | 
			
		||||
        Futures.addCallback( passwordFuture, new FutureCallback<String>() {
 | 
			
		||||
            @Override
 | 
			
		||||
            public void onSuccess(@Nullable final String sitePassword) {
 | 
			
		||||
                SwingUtilities.invokeLater( () -> {
 | 
			
		||||
                    updatingUI = true;
 | 
			
		||||
                    currentSite = site;
 | 
			
		||||
                    siteActionButton.setVisible( user instanceof MPFileUser );
 | 
			
		||||
                    if (currentSite instanceof MPFileSite)
 | 
			
		||||
                        siteActionButton.setText( "Delete Site" );
 | 
			
		||||
                    else
 | 
			
		||||
                        siteActionButton.setText( "Add Site" );
 | 
			
		||||
                    resultTypeField.setSelectedItem( currentSite.getResultType() );
 | 
			
		||||
                    siteVersionField.setSelectedItem( currentSite.getAlgorithm() );
 | 
			
		||||
                    siteCounterField.setValue( currentSite.getCounter() );
 | 
			
		||||
                    siteNameField.setText( currentSite.getName() );
 | 
			
		||||
                    if (siteNameField.getText().startsWith( siteNameQuery ))
 | 
			
		||||
                        siteNameField.select( siteNameQuery.length(), siteNameField.getText().length() );
 | 
			
		||||
        ListenableFuture<String> passwordFuture = Res.job( this, () ->
 | 
			
		||||
                site.getResult( MPKeyPurpose.Authentication, null, null ) );
 | 
			
		||||
 | 
			
		||||
        SwingUtilities.invokeLater( () -> {
 | 
			
		||||
            updatingUI = true;
 | 
			
		||||
            currentSite = site;
 | 
			
		||||
            siteActionButton.setVisible( user instanceof MPFileUser );
 | 
			
		||||
            if (currentSite instanceof MPFileSite)
 | 
			
		||||
                siteActionButton.setText( "Delete Site" );
 | 
			
		||||
            else
 | 
			
		||||
                siteActionButton.setText( "Add Site" );
 | 
			
		||||
            resultTypeField.setSelectedItem( currentSite.getResultType() );
 | 
			
		||||
            siteVersionField.setSelectedItem( currentSite.getAlgorithm().version() );
 | 
			
		||||
            siteCounterField.setValue( currentSite.getCounter() );
 | 
			
		||||
            siteNameField.setText( currentSite.getName() );
 | 
			
		||||
            if (siteNameField.getText().startsWith( siteNameQuery ))
 | 
			
		||||
                siteNameField.select( siteNameQuery.length(), siteNameField.getText().length() );
 | 
			
		||||
            passwordField.setText( null );
 | 
			
		||||
            tipLabel.setText( "Getting password..." );
 | 
			
		||||
 | 
			
		||||
            Futures.addCallback( passwordFuture, new FailableCallback<String>( logger ) {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSuccess(@Nullable final String sitePassword) {
 | 
			
		||||
                    if (sitePassword != null)
 | 
			
		||||
                        tipLabel.setText( "Press [Enter] to copy the password.  Then paste it into the password field." );
 | 
			
		||||
 | 
			
		||||
                    passwordField.setText( sitePassword );
 | 
			
		||||
                    tipLabel.setText( "Press [Enter] to copy the password.  Then paste it into the password field." );
 | 
			
		||||
                    updatingUI = false;
 | 
			
		||||
                } );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            @Override
 | 
			
		||||
            public void onFailure(@Nonnull final Throwable t) {
 | 
			
		||||
            }
 | 
			
		||||
                }
 | 
			
		||||
            }, Res.uiExecutor( true ) );
 | 
			
		||||
        } );
 | 
			
		||||
 | 
			
		||||
        return passwordFuture;
 | 
			
		||||
 
 | 
			
		||||
@@ -153,7 +153,7 @@ public class UnlockFrame extends JFrame {
 | 
			
		||||
    boolean checkSignIn() {
 | 
			
		||||
        if (identiconFuture != null)
 | 
			
		||||
            identiconFuture.cancel( false );
 | 
			
		||||
        identiconFuture = Res.schedule( this, () -> SwingUtilities.invokeLater( () -> {
 | 
			
		||||
        identiconFuture = Res.job( this, () -> SwingUtilities.invokeLater( () -> {
 | 
			
		||||
            String fullName       = (user == null)? "": user.getFullName();
 | 
			
		||||
            char[] masterPassword = authenticationPanel.getMasterPassword();
 | 
			
		||||
 | 
			
		||||
@@ -186,7 +186,7 @@ public class UnlockFrame extends JFrame {
 | 
			
		||||
        signInButton.setEnabled( false );
 | 
			
		||||
        signInButton.setText( "Signing In..." );
 | 
			
		||||
 | 
			
		||||
        Res.execute( this, () -> {
 | 
			
		||||
        Res.job( this, () -> {
 | 
			
		||||
            try {
 | 
			
		||||
                user.authenticate( authenticationPanel.getMasterPassword() );
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@ package com.lyndir.masterpassword.model;
 | 
			
		||||
import com.google.common.primitives.UnsignedInteger;
 | 
			
		||||
import com.lyndir.masterpassword.*;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import javax.annotation.Nonnull;
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -31,41 +32,50 @@ public interface MPSite<Q extends MPQuestion> extends Comparable<MPSite<?>> {
 | 
			
		||||
 | 
			
		||||
    // - Meta
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    String getName();
 | 
			
		||||
 | 
			
		||||
    void setName(String name);
 | 
			
		||||
 | 
			
		||||
    // - Algorithm
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    MPAlgorithm getAlgorithm();
 | 
			
		||||
 | 
			
		||||
    void setAlgorithm(MPAlgorithm algorithm);
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    UnsignedInteger getCounter();
 | 
			
		||||
 | 
			
		||||
    void setCounter(UnsignedInteger counter);
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    MPResultType getResultType();
 | 
			
		||||
 | 
			
		||||
    void setResultType(MPResultType resultType);
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    MPResultType getLoginType();
 | 
			
		||||
 | 
			
		||||
    void setLoginType(@Nullable MPResultType loginType);
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    String getResult(MPKeyPurpose keyPurpose, @Nullable String keyContext, @Nullable String state)
 | 
			
		||||
            throws MPKeyUnavailableException, MPAlgorithmException;
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    String getLogin(@Nullable String state)
 | 
			
		||||
            throws MPKeyUnavailableException, MPAlgorithmException;
 | 
			
		||||
 | 
			
		||||
    // - Relations
 | 
			
		||||
 | 
			
		||||
    MPUser<? extends MPSite<?>> getUser();
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    MPUser<?> getUser();
 | 
			
		||||
 | 
			
		||||
    void addQuestion(Q question);
 | 
			
		||||
 | 
			
		||||
    void deleteQuestion(Q question);
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    Collection<Q> getQuestions();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -18,8 +18,7 @@
 | 
			
		||||
 | 
			
		||||
package com.lyndir.masterpassword.model;
 | 
			
		||||
 | 
			
		||||
import com.google.common.collect.ImmutableList;
 | 
			
		||||
import com.google.common.collect.Maps;
 | 
			
		||||
import com.google.common.collect.*;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
@@ -37,7 +36,7 @@ public abstract class MPUserManager<U extends MPUser<?>> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Collection<U> getUsers() {
 | 
			
		||||
        return ImmutableList.copyOf( usersByName.values() );
 | 
			
		||||
        return ImmutableSortedSet.copyOf( usersByName.values() );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public U getUserNamed(final String fullName) {
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,59 @@
 | 
			
		||||
package com.lyndir.masterpassword.model.impl;
 | 
			
		||||
 | 
			
		||||
import java.util.concurrent.ExecutorService;
 | 
			
		||||
import java.util.concurrent.Executors;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @author lhunath, 2018-07-08
 | 
			
		||||
 */
 | 
			
		||||
public class Changeable {
 | 
			
		||||
 | 
			
		||||
    private static final ExecutorService changeExecutor = Executors.newSingleThreadExecutor();
 | 
			
		||||
 | 
			
		||||
    private boolean changed;
 | 
			
		||||
    private boolean batchingChanges;
 | 
			
		||||
 | 
			
		||||
    void setChanged() {
 | 
			
		||||
        synchronized (changeExecutor) {
 | 
			
		||||
            if (changed)
 | 
			
		||||
                return;
 | 
			
		||||
            changed = true;
 | 
			
		||||
 | 
			
		||||
            if (batchingChanges)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            changeExecutor.submit( () -> {
 | 
			
		||||
                synchronized (changeExecutor) {
 | 
			
		||||
                    if (batchingChanges)
 | 
			
		||||
                        return;
 | 
			
		||||
                    changed = false;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                onChanged();
 | 
			
		||||
            } );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected void onChanged() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void beginChanges() {
 | 
			
		||||
        synchronized (changeExecutor) {
 | 
			
		||||
            batchingChanges = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public boolean endChanges() {
 | 
			
		||||
        synchronized (changeExecutor) {
 | 
			
		||||
            batchingChanges = false;
 | 
			
		||||
 | 
			
		||||
            if (changed) {
 | 
			
		||||
                this.changed = false;
 | 
			
		||||
                setChanged();
 | 
			
		||||
                return true;
 | 
			
		||||
            } else
 | 
			
		||||
                return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -31,7 +31,7 @@ import org.jetbrains.annotations.NotNull;
 | 
			
		||||
/**
 | 
			
		||||
 * @author lhunath, 2018-05-14
 | 
			
		||||
 */
 | 
			
		||||
public abstract class MPBasicQuestion implements MPQuestion {
 | 
			
		||||
public abstract class MPBasicQuestion extends Changeable implements MPQuestion {
 | 
			
		||||
 | 
			
		||||
    private final String       keyword;
 | 
			
		||||
    private       MPResultType type;
 | 
			
		||||
@@ -56,6 +56,8 @@ public abstract class MPBasicQuestion implements MPQuestion {
 | 
			
		||||
    @Override
 | 
			
		||||
    public void setType(final MPResultType type) {
 | 
			
		||||
        this.type = type;
 | 
			
		||||
 | 
			
		||||
        setChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
@@ -70,6 +72,13 @@ public abstract class MPBasicQuestion implements MPQuestion {
 | 
			
		||||
    @Override
 | 
			
		||||
    public abstract MPBasicSite<?> getSite();
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onChanged() {
 | 
			
		||||
        super.onChanged();
 | 
			
		||||
 | 
			
		||||
        getSite().setChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int hashCode() {
 | 
			
		||||
        return Objects.hashCode( getKeyword() );
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,7 @@ import com.lyndir.masterpassword.*;
 | 
			
		||||
import com.lyndir.masterpassword.model.MPQuestion;
 | 
			
		||||
import com.lyndir.masterpassword.model.MPSite;
 | 
			
		||||
import java.util.*;
 | 
			
		||||
import javax.annotation.Nonnull;
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import org.jetbrains.annotations.NotNull;
 | 
			
		||||
 | 
			
		||||
@@ -33,7 +34,7 @@ import org.jetbrains.annotations.NotNull;
 | 
			
		||||
/**
 | 
			
		||||
 * @author lhunath, 14-12-16
 | 
			
		||||
 */
 | 
			
		||||
public abstract class MPBasicSite<Q extends MPQuestion> implements MPSite<Q> {
 | 
			
		||||
public abstract class MPBasicSite<Q extends MPQuestion> extends Changeable implements MPSite<Q> {
 | 
			
		||||
 | 
			
		||||
    private String          name;
 | 
			
		||||
    private MPAlgorithm     algorithm;
 | 
			
		||||
@@ -56,6 +57,7 @@ public abstract class MPBasicSite<Q extends MPQuestion> implements MPSite<Q> {
 | 
			
		||||
        this.loginType = (loginType == null)? algorithm.mpw_default_login_type(): loginType;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getName() {
 | 
			
		||||
        return name;
 | 
			
		||||
@@ -64,8 +66,11 @@ public abstract class MPBasicSite<Q extends MPQuestion> implements MPSite<Q> {
 | 
			
		||||
    @Override
 | 
			
		||||
    public void setName(final String name) {
 | 
			
		||||
        this.name = name;
 | 
			
		||||
 | 
			
		||||
        setChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public MPAlgorithm getAlgorithm() {
 | 
			
		||||
        return algorithm;
 | 
			
		||||
@@ -74,8 +79,11 @@ public abstract class MPBasicSite<Q extends MPQuestion> implements MPSite<Q> {
 | 
			
		||||
    @Override
 | 
			
		||||
    public void setAlgorithm(final MPAlgorithm algorithm) {
 | 
			
		||||
        this.algorithm = algorithm;
 | 
			
		||||
 | 
			
		||||
        setChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public UnsignedInteger getCounter() {
 | 
			
		||||
        return counter;
 | 
			
		||||
@@ -84,8 +92,11 @@ public abstract class MPBasicSite<Q extends MPQuestion> implements MPSite<Q> {
 | 
			
		||||
    @Override
 | 
			
		||||
    public void setCounter(final UnsignedInteger counter) {
 | 
			
		||||
        this.counter = counter;
 | 
			
		||||
 | 
			
		||||
        setChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public MPResultType getResultType() {
 | 
			
		||||
        return resultType;
 | 
			
		||||
@@ -94,8 +105,11 @@ public abstract class MPBasicSite<Q extends MPQuestion> implements MPSite<Q> {
 | 
			
		||||
    @Override
 | 
			
		||||
    public void setResultType(final MPResultType resultType) {
 | 
			
		||||
        this.resultType = resultType;
 | 
			
		||||
 | 
			
		||||
        setChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public MPResultType getLoginType() {
 | 
			
		||||
        return loginType;
 | 
			
		||||
@@ -104,8 +118,11 @@ public abstract class MPBasicSite<Q extends MPQuestion> implements MPSite<Q> {
 | 
			
		||||
    @Override
 | 
			
		||||
    public void setLoginType(@Nullable final MPResultType loginType) {
 | 
			
		||||
        this.loginType = ifNotNullElse( loginType, getAlgorithm().mpw_default_login_type() );
 | 
			
		||||
 | 
			
		||||
        setChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext, @Nullable final String state)
 | 
			
		||||
            throws MPKeyUnavailableException, MPAlgorithmException {
 | 
			
		||||
@@ -131,6 +148,7 @@ public abstract class MPBasicSite<Q extends MPQuestion> implements MPSite<Q> {
 | 
			
		||||
                keyPurpose, keyContext, type, state );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getLogin(@Nullable final String state)
 | 
			
		||||
            throws MPKeyUnavailableException, MPAlgorithmException {
 | 
			
		||||
@@ -141,18 +159,34 @@ public abstract class MPBasicSite<Q extends MPQuestion> implements MPSite<Q> {
 | 
			
		||||
    @Override
 | 
			
		||||
    public void addQuestion(final Q question) {
 | 
			
		||||
        questions.add( question );
 | 
			
		||||
 | 
			
		||||
        setChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void deleteQuestion(final Q question) {
 | 
			
		||||
        questions.remove( question );
 | 
			
		||||
 | 
			
		||||
        setChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public Collection<Q> getQuestions() {
 | 
			
		||||
        return Collections.unmodifiableCollection( questions );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public abstract MPBasicUser<?> getUser();
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onChanged() {
 | 
			
		||||
        super.onChanged();
 | 
			
		||||
 | 
			
		||||
        getUser().setChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int hashCode() {
 | 
			
		||||
        return Objects.hashCode( getName() );
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ package com.lyndir.masterpassword.model.impl;
 | 
			
		||||
 | 
			
		||||
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
 | 
			
		||||
 | 
			
		||||
import com.google.common.collect.ImmutableList;
 | 
			
		||||
import com.google.common.collect.ImmutableSortedSet;
 | 
			
		||||
import com.lyndir.lhunath.opal.system.CodeUtils;
 | 
			
		||||
import com.lyndir.lhunath.opal.system.logging.Logger;
 | 
			
		||||
import com.lyndir.masterpassword.*;
 | 
			
		||||
@@ -34,7 +34,7 @@ import javax.annotation.Nullable;
 | 
			
		||||
/**
 | 
			
		||||
 * @author lhunath, 2014-06-08
 | 
			
		||||
 */
 | 
			
		||||
public abstract class MPBasicUser<S extends MPBasicSite<?>> implements MPUser<S> {
 | 
			
		||||
public abstract class MPBasicUser<S extends MPBasicSite<?>> extends Changeable implements MPUser<S> {
 | 
			
		||||
 | 
			
		||||
    protected final Logger logger = Logger.get( getClass() );
 | 
			
		||||
 | 
			
		||||
@@ -64,6 +64,8 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> implements MPUser<S>
 | 
			
		||||
    @Override
 | 
			
		||||
    public void setAvatar(final int avatar) {
 | 
			
		||||
        this.avatar = avatar;
 | 
			
		||||
 | 
			
		||||
        setChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
@@ -81,6 +83,8 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> implements MPUser<S>
 | 
			
		||||
    @Override
 | 
			
		||||
    public void setAlgorithm(final MPAlgorithm algorithm) {
 | 
			
		||||
        this.algorithm = algorithm;
 | 
			
		||||
 | 
			
		||||
        setChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
@@ -136,7 +140,7 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> implements MPUser<S>
 | 
			
		||||
    public MPMasterKey getMasterKey()
 | 
			
		||||
            throws MPKeyUnavailableException {
 | 
			
		||||
        if (masterKey == null)
 | 
			
		||||
            throw new MPKeyUnavailableException( "Master key was not yet set." );
 | 
			
		||||
            throw new MPKeyUnavailableException( "Master key was not yet set for: " + this );
 | 
			
		||||
 | 
			
		||||
        return masterKey;
 | 
			
		||||
    }
 | 
			
		||||
@@ -144,11 +148,15 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> implements MPUser<S>
 | 
			
		||||
    @Override
 | 
			
		||||
    public void addSite(final S site) {
 | 
			
		||||
        sites.add( site );
 | 
			
		||||
 | 
			
		||||
        setChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void deleteSite(final S site) {
 | 
			
		||||
        sites.remove( site );
 | 
			
		||||
 | 
			
		||||
        setChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
@@ -160,7 +168,7 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> implements MPUser<S>
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public Collection<S> findSites(final String query) {
 | 
			
		||||
        ImmutableList.Builder<S> results = ImmutableList.builder();
 | 
			
		||||
        ImmutableSortedSet.Builder<S> results = ImmutableSortedSet.naturalOrder();
 | 
			
		||||
        for (final S site : getSites())
 | 
			
		||||
            if (site.getName().startsWith( query ))
 | 
			
		||||
                results.add( site );
 | 
			
		||||
 
 | 
			
		||||
@@ -33,19 +33,24 @@ public class MPFileQuestion extends MPBasicQuestion {
 | 
			
		||||
    private final MPFileSite site;
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    private String state;
 | 
			
		||||
    private String answerState;
 | 
			
		||||
 | 
			
		||||
    public MPFileQuestion(final MPFileSite site, final String keyword,
 | 
			
		||||
                          @Nullable final MPResultType type, @Nullable final String state) {
 | 
			
		||||
                          @Nullable final MPResultType type, @Nullable final String answerState) {
 | 
			
		||||
        super( keyword, ifNotNullElse( type, site.getAlgorithm().mpw_default_answer_type() ) );
 | 
			
		||||
 | 
			
		||||
        this.site = site;
 | 
			
		||||
        this.state = state;
 | 
			
		||||
        this.answerState = answerState;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public String getAnswerState() {
 | 
			
		||||
        return answerState;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public String getAnswer()
 | 
			
		||||
            throws MPKeyUnavailableException, MPAlgorithmException {
 | 
			
		||||
        return getAnswer( state );
 | 
			
		||||
        return getAnswer( answerState );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setAnswer(final MPResultType type, @Nullable final String answer)
 | 
			
		||||
@@ -53,10 +58,12 @@ public class MPFileQuestion extends MPBasicQuestion {
 | 
			
		||||
        setType( type );
 | 
			
		||||
 | 
			
		||||
        if (answer == null)
 | 
			
		||||
            this.state = null;
 | 
			
		||||
            this.answerState = null;
 | 
			
		||||
        else
 | 
			
		||||
            this.state = getSite().getState(
 | 
			
		||||
            this.answerState = getSite().getState(
 | 
			
		||||
                    MPKeyPurpose.Recovery, getKeyword(), null, getType(), answer );
 | 
			
		||||
 | 
			
		||||
        setChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ package com.lyndir.masterpassword.model.impl;
 | 
			
		||||
 | 
			
		||||
import com.google.common.primitives.UnsignedInteger;
 | 
			
		||||
import com.lyndir.masterpassword.*;
 | 
			
		||||
import com.lyndir.masterpassword.model.MPSite;
 | 
			
		||||
import javax.annotation.Nonnull;
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import org.joda.time.Instant;
 | 
			
		||||
@@ -29,6 +30,7 @@ import org.joda.time.ReadableInstant;
 | 
			
		||||
/**
 | 
			
		||||
 * @author lhunath, 14-12-05
 | 
			
		||||
 */
 | 
			
		||||
@SuppressWarnings("ComparableImplementedButEqualsNotOverridden")
 | 
			
		||||
public class MPFileSite extends MPBasicSite<MPFileQuestion> {
 | 
			
		||||
 | 
			
		||||
    private final MPFileUser user;
 | 
			
		||||
@@ -77,6 +79,8 @@ public class MPFileSite extends MPBasicSite<MPFileQuestion> {
 | 
			
		||||
 | 
			
		||||
    public void setUrl(@Nullable final String url) {
 | 
			
		||||
        this.url = url;
 | 
			
		||||
 | 
			
		||||
        setChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public int getUses() {
 | 
			
		||||
@@ -125,6 +129,8 @@ public class MPFileSite extends MPBasicSite<MPFileQuestion> {
 | 
			
		||||
        else
 | 
			
		||||
            this.resultState = getState(
 | 
			
		||||
                    MPKeyPurpose.Authentication, null, getCounter(), getResultType(), password );
 | 
			
		||||
 | 
			
		||||
        setChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
@@ -141,10 +147,22 @@ public class MPFileSite extends MPBasicSite<MPFileQuestion> {
 | 
			
		||||
        else
 | 
			
		||||
            this.loginState = getState(
 | 
			
		||||
                    MPKeyPurpose.Identification, null, null, getLoginType(), loginName );
 | 
			
		||||
 | 
			
		||||
        setChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nonnull
 | 
			
		||||
    @Override
 | 
			
		||||
    public MPFileUser getUser() {
 | 
			
		||||
        return user;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int compareTo(final MPSite<?> o) {
 | 
			
		||||
        int comparison = (o instanceof MPFileSite)? -getLastUsed().compareTo( ((MPFileSite) o).getLastUsed() ): 0;
 | 
			
		||||
        if (comparison != 0)
 | 
			
		||||
            return comparison;
 | 
			
		||||
 | 
			
		||||
        return super.compareTo( o );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,6 @@
 | 
			
		||||
 | 
			
		||||
package com.lyndir.masterpassword.model.impl;
 | 
			
		||||
 | 
			
		||||
import com.lyndir.lhunath.opal.system.logging.Logger;
 | 
			
		||||
import com.lyndir.masterpassword.*;
 | 
			
		||||
import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
 | 
			
		||||
import com.lyndir.masterpassword.model.MPUser;
 | 
			
		||||
@@ -34,9 +33,6 @@ import org.joda.time.ReadableInstant;
 | 
			
		||||
@SuppressWarnings("ComparableImplementedButEqualsNotOverridden")
 | 
			
		||||
public class MPFileUser extends MPBasicUser<MPFileSite> {
 | 
			
		||||
 | 
			
		||||
    @SuppressWarnings("UnusedDeclaration")
 | 
			
		||||
    private static final Logger logger = Logger.get( MPFileUser.class );
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    private byte[]                   keyID;
 | 
			
		||||
    private MPMarshalFormat          format;
 | 
			
		||||
@@ -101,6 +97,8 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
 | 
			
		||||
 | 
			
		||||
    public void setFormat(final MPMarshalFormat format) {
 | 
			
		||||
        this.format = format;
 | 
			
		||||
 | 
			
		||||
        setChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public MPMarshaller.ContentMode getContentMode() {
 | 
			
		||||
@@ -109,6 +107,8 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
 | 
			
		||||
 | 
			
		||||
    public void setContentMode(final MPMarshaller.ContentMode contentMode) {
 | 
			
		||||
        this.contentMode = contentMode;
 | 
			
		||||
 | 
			
		||||
        setChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public MPResultType getDefaultType() {
 | 
			
		||||
@@ -117,6 +117,8 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
 | 
			
		||||
 | 
			
		||||
    public void setDefaultType(final MPResultType defaultType) {
 | 
			
		||||
        this.defaultType = defaultType;
 | 
			
		||||
 | 
			
		||||
        setChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public ReadableInstant getLastUsed() {
 | 
			
		||||
@@ -125,6 +127,8 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
 | 
			
		||||
 | 
			
		||||
    public void use() {
 | 
			
		||||
        lastUsed = new Instant();
 | 
			
		||||
 | 
			
		||||
        setChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setJSON(final MPJSONFile json) {
 | 
			
		||||
@@ -141,8 +145,23 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
 | 
			
		||||
            throws MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException {
 | 
			
		||||
        super.authenticate( masterKey );
 | 
			
		||||
 | 
			
		||||
        if (keyID == null)
 | 
			
		||||
        if (keyID == null) {
 | 
			
		||||
            keyID = masterKey.getKeyID( getAlgorithm() );
 | 
			
		||||
 | 
			
		||||
            setChanged();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onChanged() {
 | 
			
		||||
        super.onChanged();
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            save();
 | 
			
		||||
        }
 | 
			
		||||
        catch (final MPKeyUnavailableException | MPAlgorithmException e) {
 | 
			
		||||
            logger.wrn( e, "Couldn't save change." );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void save()
 | 
			
		||||
@@ -152,7 +171,7 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int compareTo(final MPUser<?> o) {
 | 
			
		||||
        int comparison = (o instanceof MPFileUser)? getLastUsed().compareTo( ((MPFileUser) o).getLastUsed() ): 0;
 | 
			
		||||
        int comparison = (o instanceof MPFileUser)? -getLastUsed().compareTo( ((MPFileUser) o).getLastUsed() ): 0;
 | 
			
		||||
        if (comparison != 0)
 | 
			
		||||
            return comparison;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -18,14 +18,14 @@
 | 
			
		||||
 | 
			
		||||
package com.lyndir.masterpassword.model.impl;
 | 
			
		||||
 | 
			
		||||
import com.fasterxml.jackson.annotation.JsonAnyGetter;
 | 
			
		||||
import com.fasterxml.jackson.annotation.JsonAnySetter;
 | 
			
		||||
import com.fasterxml.jackson.annotation.*;
 | 
			
		||||
import java.util.*;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @author lhunath, 2018-05-14
 | 
			
		||||
 */
 | 
			
		||||
@JsonInclude(value = JsonInclude.Include.CUSTOM, valueFilter = MPJSONAnyObject.MPJSONEmptyValue.class)
 | 
			
		||||
class MPJSONAnyObject {
 | 
			
		||||
 | 
			
		||||
    @JsonAnySetter
 | 
			
		||||
@@ -35,4 +35,21 @@ class MPJSONAnyObject {
 | 
			
		||||
    public Map<String, Object> getAny() {
 | 
			
		||||
        return Collections.unmodifiableMap( any );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @SuppressWarnings("EqualsAndHashcode")
 | 
			
		||||
    public static class MPJSONEmptyValue {
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        @SuppressWarnings({ "ChainOfInstanceofChecks", "Contract" })
 | 
			
		||||
        public boolean equals(final Object obj) {
 | 
			
		||||
            if (obj instanceof Collection<?>)
 | 
			
		||||
                return ((Collection<?>) obj).isEmpty();
 | 
			
		||||
            if (obj instanceof Map<?, ?>)
 | 
			
		||||
                return ((Map<?, ?>) obj).isEmpty();
 | 
			
		||||
            if (obj instanceof MPJSONFile.Site.Ext)
 | 
			
		||||
                return ((MPJSONAnyObject) obj).any.isEmpty();
 | 
			
		||||
 | 
			
		||||
            return obj == null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,10 @@ package com.lyndir.masterpassword.model.impl;
 | 
			
		||||
 | 
			
		||||
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
 | 
			
		||||
 | 
			
		||||
import com.fasterxml.jackson.annotation.*;
 | 
			
		||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
 | 
			
		||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
 | 
			
		||||
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
 | 
			
		||||
import com.fasterxml.jackson.core.util.Separators;
 | 
			
		||||
import com.fasterxml.jackson.databind.ObjectMapper;
 | 
			
		||||
import com.google.common.primitives.UnsignedInteger;
 | 
			
		||||
import com.lyndir.lhunath.opal.system.CodeUtils;
 | 
			
		||||
@@ -37,18 +40,28 @@ import org.joda.time.Instant;
 | 
			
		||||
/**
 | 
			
		||||
 * @author lhunath, 2018-04-27
 | 
			
		||||
 */
 | 
			
		||||
@SuppressFBWarnings( "URF_UNREAD_FIELD" )
 | 
			
		||||
@SuppressFBWarnings("URF_UNREAD_FIELD")
 | 
			
		||||
public class MPJSONFile extends MPJSONAnyObject {
 | 
			
		||||
 | 
			
		||||
    protected static final ObjectMapper objectMapper = new ObjectMapper();
 | 
			
		||||
 | 
			
		||||
    static {
 | 
			
		||||
        objectMapper.setSerializationInclusion( JsonInclude.Include.NON_EMPTY );
 | 
			
		||||
        objectMapper.setDefaultPrettyPrinter( new DefaultPrettyPrinter() {
 | 
			
		||||
            private static final long serialVersionUID = 1;
 | 
			
		||||
 | 
			
		||||
            @Override
 | 
			
		||||
            public DefaultPrettyPrinter withSeparators(final Separators separators) {
 | 
			
		||||
                super.withSeparators( separators );
 | 
			
		||||
                _objectFieldValueSeparatorWithSpaces = separators.getObjectFieldValueSeparator() + " ";
 | 
			
		||||
                return this;
 | 
			
		||||
            }
 | 
			
		||||
        } );
 | 
			
		||||
        objectMapper.setVisibility( PropertyAccessor.FIELD, JsonAutoDetect.Visibility.NON_PRIVATE );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public MPJSONFile write(final MPFileUser modelUser)
 | 
			
		||||
            throws MPKeyUnavailableException, MPAlgorithmException {
 | 
			
		||||
 | 
			
		||||
        // Section: "export"
 | 
			
		||||
        if (export == null)
 | 
			
		||||
            export = new Export();
 | 
			
		||||
@@ -98,38 +111,27 @@ public class MPJSONFile extends MPJSONAnyObject {
 | 
			
		||||
            site.uses = modelSite.getUses();
 | 
			
		||||
            site.last_used = MPConstants.dateTimeFormatter.print( modelSite.getLastUsed() );
 | 
			
		||||
 | 
			
		||||
            if (site.questions == null)
 | 
			
		||||
                site.questions = new LinkedHashMap<>();
 | 
			
		||||
            for (final MPFileQuestion question : modelSite.getQuestions())
 | 
			
		||||
                site.questions.put( question.getKeyword(), new Site.Question() {
 | 
			
		||||
                    {
 | 
			
		||||
                        type = question.getType();
 | 
			
		||||
 | 
			
		||||
                        if (!export.redacted) {
 | 
			
		||||
                            // Clear Text
 | 
			
		||||
                            answer = question.getAnswer();
 | 
			
		||||
                        } else {
 | 
			
		||||
                            // Redacted
 | 
			
		||||
                            if (question.getType().supportsTypeFeature( MPSiteFeature.ExportContent ))
 | 
			
		||||
                                answer = question.getAnswerState();
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                } );
 | 
			
		||||
 | 
			
		||||
            if (site._ext_mpw == null)
 | 
			
		||||
                site._ext_mpw = new Site.Ext();
 | 
			
		||||
            site._ext_mpw.url = modelSite.getUrl();
 | 
			
		||||
 | 
			
		||||
            if (site.questions == null)
 | 
			
		||||
                site.questions = new LinkedHashMap<>();
 | 
			
		||||
            //                for (size_t q = 0; q < site.questions_count; ++q) {
 | 
			
		||||
            //                    MPMarshalledQuestion *question = &site.questions[q];
 | 
			
		||||
            //                    if (!question.keyword)
 | 
			
		||||
            //                        continue;
 | 
			
		||||
            //
 | 
			
		||||
            //                    json_object *json_site_question = json_object_new_object();
 | 
			
		||||
            //                    json_object_object_add( json_site_questions, question.keyword, json_site_question );
 | 
			
		||||
            //                    json_object_object_add( json_site_question, "type = question.type;
 | 
			
		||||
            //
 | 
			
		||||
            //                    if (!user.redacted) {
 | 
			
		||||
            //                        // Clear Text
 | 
			
		||||
            //                const char *answerContent = mpw_siteResult( masterKey, site.name, MPCounterValueInitial,
 | 
			
		||||
            //                                                            MPKeyPurposeRecovery, question.keyword, question.type, question.content, site.algorithm );
 | 
			
		||||
            //                        json_object_object_add( json_site_question, "answer = answerContent;
 | 
			
		||||
            //                    }
 | 
			
		||||
            //                    else {
 | 
			
		||||
            //                        // Redacted
 | 
			
		||||
            //                        if (site.type & MPSiteFeatureExportContent && question.content && strlen( question.content ))
 | 
			
		||||
            //                            json_object_object_add( json_site_question, "answer = question.content;
 | 
			
		||||
            //                    }
 | 
			
		||||
            //                }
 | 
			
		||||
 | 
			
		||||
            //                json_object *json_site_mpw = json_object_new_object();
 | 
			
		||||
            //                fileSite._ext_mpw = json_site_mpw;
 | 
			
		||||
            //                if (site.url)
 | 
			
		||||
            //                    json_object_object_add( json_site_mpw, "url", site.url );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this;
 | 
			
		||||
@@ -143,6 +145,7 @@ public class MPJSONFile extends MPJSONAnyObject {
 | 
			
		||||
                (user.default_type != null)? user.default_type: algorithm.mpw_default_result_type(),
 | 
			
		||||
                (user.last_used != null)? MPConstants.dateTimeFormatter.parseDateTime( user.last_used ): new Instant(),
 | 
			
		||||
                MPMarshalFormat.JSON, export.redacted? MPMarshaller.ContentMode.PROTECTED: MPMarshaller.ContentMode.VISIBLE );
 | 
			
		||||
        model.beginChanges();
 | 
			
		||||
        model.setJSON( this );
 | 
			
		||||
        if (masterPassword != null)
 | 
			
		||||
            model.authenticate( masterPassword );
 | 
			
		||||
@@ -167,6 +170,7 @@ public class MPJSONFile extends MPJSONAnyObject {
 | 
			
		||||
 | 
			
		||||
            model.addSite( site );
 | 
			
		||||
        }
 | 
			
		||||
        model.endChanges();
 | 
			
		||||
 | 
			
		||||
        return model;
 | 
			
		||||
    }
 | 
			
		||||
@@ -193,26 +197,26 @@ public class MPJSONFile extends MPJSONAnyObject {
 | 
			
		||||
        String full_name;
 | 
			
		||||
        String last_used;
 | 
			
		||||
        @Nullable
 | 
			
		||||
        MPAlgorithm.Version algorithm;
 | 
			
		||||
        @Nullable
 | 
			
		||||
        String              key_id;
 | 
			
		||||
        @Nullable
 | 
			
		||||
        MPAlgorithm.Version algorithm;
 | 
			
		||||
        @Nullable
 | 
			
		||||
        MPResultType        default_type;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public static class Site extends MPJSONAnyObject {
 | 
			
		||||
 | 
			
		||||
        @Nullable
 | 
			
		||||
        MPResultType type;
 | 
			
		||||
        long                counter;
 | 
			
		||||
        MPAlgorithm.Version algorithm;
 | 
			
		||||
        @Nullable
 | 
			
		||||
        MPResultType type;
 | 
			
		||||
        @Nullable
 | 
			
		||||
        String       password;
 | 
			
		||||
        @Nullable
 | 
			
		||||
        MPResultType login_type;
 | 
			
		||||
        @Nullable
 | 
			
		||||
        String       login_name;
 | 
			
		||||
        @Nullable
 | 
			
		||||
        MPResultType login_type;
 | 
			
		||||
 | 
			
		||||
        int uses;
 | 
			
		||||
        @Nullable
 | 
			
		||||
 
 | 
			
		||||
@@ -37,7 +37,7 @@ public class MPJSONMarshaller implements MPMarshaller {
 | 
			
		||||
            throws MPKeyUnavailableException, MPMarshalException, MPAlgorithmException {
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            return objectMapper.writeValueAsString( user.getJSON().write( user ) );
 | 
			
		||||
            return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString( user.getJSON().write( user ) );
 | 
			
		||||
        }
 | 
			
		||||
        catch (final JsonProcessingException e) {
 | 
			
		||||
            throw new MPMarshalException( "Couldn't compose JSON for: " + user, e );
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user