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:
@@ -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() );
|
||||
|
||||
|
Reference in New Issue
Block a user