2
0

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:
Maarten Billemont 2018-07-09 01:13:25 -04:00
parent 529f1feace
commit 954c4f8d63
20 changed files with 446 additions and 151 deletions

View File

@ -45,8 +45,6 @@ public class MPAlgorithmV0 extends MPAlgorithm {
Native.load( MPAlgorithmV0.class, "mpw" ); Native.load( MPAlgorithmV0.class, "mpw" );
} }
public final Version version = MPAlgorithm.Version.V0;
protected final Logger logger = Logger.get( getClass() ); protected final Logger logger = Logger.get( getClass() );
@Nullable @Nullable

View File

@ -22,7 +22,6 @@ import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
import com.google.common.base.Charsets; import com.google.common.base.Charsets;
import com.google.common.io.ByteSource; 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.logging.Logger;
import com.lyndir.lhunath.opal.system.util.TypeUtils; import com.lyndir.lhunath.opal.system.util.TypeUtils;
import com.lyndir.masterpassword.gui.view.PasswordFrame; import com.lyndir.masterpassword.gui.view.PasswordFrame;
@ -51,6 +50,9 @@ public class GUI implements UnlockFrame.SignInCallback {
private PasswordFrame<?, ?> passwordFrame; private PasswordFrame<?, ?> passwordFrame;
public static void main(final String... args) { public static void main(final String... args) {
Thread.setDefaultUncaughtExceptionHandler(
(t, e) -> logger.err( e, "Uncaught: %s", e.getLocalizedMessage() ) );
if (Config.get().checkForUpdates()) if (Config.get().checkForUpdates())
checkUpdate(); checkUpdate();

View File

@ -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.ObjectUtils.*;
import static com.lyndir.lhunath.opal.system.util.StringUtils.*; 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.collect.Maps;
import com.google.common.io.Resources; import com.google.common.io.Resources;
import com.google.common.util.concurrent.JdkFutureAdapters; import com.google.common.util.concurrent.JdkFutureAdapters;
@ -51,16 +50,18 @@ import org.jetbrains.annotations.NonNls;
public abstract class Res { public abstract class Res {
private static final int AVATAR_COUNT = 19; private static final int AVATAR_COUNT = 19;
private static final Map<Window, ScheduledExecutorService> executorByWindow = new WeakHashMap<>(); 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 Logger logger = Logger.get( Res.class );
private static final Colors colors = new Colors(); private static final Colors colors = new Colors();
public static Future<?> execute(final Window host, final Runnable job) { public static Future<?> job(final Window host, final Runnable job) {
return schedule( host, job, 0, TimeUnit.MILLISECONDS ); return job( host, job, 0, TimeUnit.MILLISECONDS );
} }
public static Future<?> schedule(final Window host, final Runnable job, final long delay, final TimeUnit timeUnit) { public static Future<?> job(final Window host, final Runnable job, final long delay, final TimeUnit timeUnit) {
return getExecutor( host ).schedule( () -> { return jobExecutor( host ).schedule( () -> {
try { try {
job.run(); job.run();
} }
@ -70,33 +71,29 @@ public abstract class Res {
}, delay, timeUnit ); }, delay, timeUnit );
} }
public static <V> ListenableFuture<V> execute(final Window host, final Callable<V> job) { public static <V> ListenableFuture<V> job(final Window host, final Callable<V> job) {
return schedule( host, job, 0, TimeUnit.MILLISECONDS ); 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) { public static <V> ListenableFuture<V> job(final Window host, final Callable<V> job, final long delay, final TimeUnit timeUnit) {
ScheduledExecutorService executor = getExecutor( host ); ScheduledExecutorService executor = jobExecutor( host );
return JdkFutureAdapters.listenInPoolThread( executor.schedule( () -> { return JdkFutureAdapters.listenInPoolThread( executor.schedule( job::call, delay, timeUnit ), executor );
try {
return job.call();
}
catch (final Throwable t) {
logger.err( t, "Unexpected: %s", t.getLocalizedMessage() );
throw Throwables.propagate( t );
}
}, delay, timeUnit ), executor );
} }
private static ScheduledExecutorService getExecutor(final Window host) { public static Executor uiExecutor(final boolean immediate) {
ScheduledExecutorService executor = executorByWindow.get( host ); return immediate? immediateUiExecutor: laterUiExecutor;
}
public static ScheduledExecutorService jobExecutor(final Window host) {
ScheduledExecutorService executor = jobExecutorByWindow.get( host );
if (executor == null) { if (executor == null) {
executorByWindow.put( host, executor = Executors.newSingleThreadScheduledExecutor() ); jobExecutorByWindow.put( host, executor = Executors.newSingleThreadScheduledExecutor() );
host.addWindowListener( new WindowAdapter() { host.addWindowListener( new WindowAdapter() {
@Override @Override
public void windowClosed(final WindowEvent e) { public void windowClosed(final WindowEvent e) {
ExecutorService executor = executorByWindow.remove( host ); ExecutorService executor = jobExecutorByWindow.remove( host );
if (executor != null) if (executor != null)
executor.shutdownNow(); executor.shutdownNow();
} }
@ -204,7 +201,7 @@ public abstract class Res {
font = Font.createFont( Font.TRUETYPE_FONT, Resources.getResource( fontResourceName ).openStream() ) ) ); font = Font.createFont( Font.TRUETYPE_FONT, Resources.getResource( fontResourceName ).openStream() ) ) );
} }
catch (final FontFormatException | IOException e) { catch (final FontFormatException | IOException e) {
throw Throwables.propagate( e ); throw logger.bug( e );
} }
return font; return font;

View File

@ -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 );
}
}
}

View File

@ -22,6 +22,7 @@ import com.google.common.primitives.UnsignedInteger;
import com.lyndir.masterpassword.MPAlgorithm; import com.lyndir.masterpassword.MPAlgorithm;
import com.lyndir.masterpassword.MPResultType; import com.lyndir.masterpassword.MPResultType;
import com.lyndir.masterpassword.model.impl.MPBasicSite; import com.lyndir.masterpassword.model.impl.MPBasicSite;
import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -44,6 +45,7 @@ public class MPIncognitoSite extends MPBasicSite<MPIncognitoQuestion> {
this.user = user; this.user = user;
} }
@Nonnull
@Override @Override
public MPIncognitoUser getUser() { public MPIncognitoUser getUser() {
return user; return user;

View File

@ -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 );
}
}

View File

@ -23,11 +23,12 @@ import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.primitives.UnsignedInteger; 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.*;
import com.lyndir.masterpassword.gui.Res; import com.lyndir.masterpassword.gui.Res;
import com.lyndir.masterpassword.gui.util.Components; import com.lyndir.masterpassword.gui.util.*;
import com.lyndir.masterpassword.gui.util.UnsignedIntegerModel;
import com.lyndir.masterpassword.model.MPSite; import com.lyndir.masterpassword.model.MPSite;
import com.lyndir.masterpassword.model.MPUser; import com.lyndir.masterpassword.model.MPUser;
import com.lyndir.masterpassword.model.impl.MPFileSite; import com.lyndir.masterpassword.model.impl.MPFileSite;
@ -35,7 +36,7 @@ import com.lyndir.masterpassword.model.impl.MPFileUser;
import java.awt.*; import java.awt.*;
import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable; import java.awt.datatransfer.Transferable;
import java.awt.event.*; import java.awt.event.WindowEvent;
import java.util.Collection; import java.util.Collection;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.Nonnull; 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 { 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") @SuppressWarnings("FieldCanBeLocal")
private final Components.GradientPanel root; private final Components.GradientPanel root;
private final JTextField siteNameField; private final JTextField siteNameField;
@ -96,30 +99,23 @@ public abstract class PasswordFrame<U extends MPUser<S>, S extends MPSite<?>> ex
siteNameField = Components.textField(), Components.stud(), siteNameField = Components.textField(), Components.stud(),
siteActionButton = Components.button( "Add Site" ) ); siteActionButton = Components.button( "Add Site" ) );
siteNameField.getDocument().addDocumentListener( this ); siteNameField.getDocument().addDocumentListener( this );
siteNameField.addActionListener( new ActionListener() { siteNameField.addActionListener(
@Override e -> Futures.addCallback( updatePassword( true ), new FailableCallback<String>( logger ) {
public void actionPerformed(final ActionEvent e) {
Futures.addCallback( updatePassword( true ), new FutureCallback<String>() {
@Override @Override
public void onSuccess(@Nullable final String sitePassword) { public void onSuccess(@Nullable final String sitePassword) {
if (sitePassword == null)
return;
if (currentSite instanceof MPFileSite)
((MPFileSite) currentSite).use();
Transferable clipboardContents = new StringSelection( sitePassword ); Transferable clipboardContents = new StringSelection( sitePassword );
Toolkit.getDefaultToolkit().getSystemClipboard().setContents( clipboardContents, null ); 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 ) ) );
@Override siteActionButton.addActionListener(
public void onFailure(@Nonnull final Throwable t) { e -> {
}
} );
}
} );
siteActionButton.addActionListener( e -> {
if (currentSite == null) if (currentSite == null)
return; return;
if (currentSite instanceof MPFileSite) if (currentSite instanceof MPFileSite)
@ -229,10 +225,9 @@ public abstract class PasswordFrame<U extends MPUser<S>, S extends MPSite<?>> ex
site.setCounter( siteCounter ); site.setCounter( siteCounter );
} }
ListenableFuture<String> passwordFuture = Res.execute( this, () -> site.getResult( MPKeyPurpose.Authentication, null, null ) ); ListenableFuture<String> passwordFuture = Res.job( this, () ->
Futures.addCallback( passwordFuture, new FutureCallback<String>() { site.getResult( MPKeyPurpose.Authentication, null, null ) );
@Override
public void onSuccess(@Nullable final String sitePassword) {
SwingUtilities.invokeLater( () -> { SwingUtilities.invokeLater( () -> {
updatingUI = true; updatingUI = true;
currentSite = site; currentSite = site;
@ -242,21 +237,24 @@ public abstract class PasswordFrame<U extends MPUser<S>, S extends MPSite<?>> ex
else else
siteActionButton.setText( "Add Site" ); siteActionButton.setText( "Add Site" );
resultTypeField.setSelectedItem( currentSite.getResultType() ); resultTypeField.setSelectedItem( currentSite.getResultType() );
siteVersionField.setSelectedItem( currentSite.getAlgorithm() ); siteVersionField.setSelectedItem( currentSite.getAlgorithm().version() );
siteCounterField.setValue( currentSite.getCounter() ); siteCounterField.setValue( currentSite.getCounter() );
siteNameField.setText( currentSite.getName() ); siteNameField.setText( currentSite.getName() );
if (siteNameField.getText().startsWith( siteNameQuery )) if (siteNameField.getText().startsWith( siteNameQuery ))
siteNameField.select( siteNameQuery.length(), siteNameField.getText().length() ); 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 ); passwordField.setText( sitePassword );
tipLabel.setText( "Press [Enter] to copy the password. Then paste it into the password field." );
updatingUI = false; updatingUI = false;
} );
}
@Override
public void onFailure(@Nonnull final Throwable t) {
} }
}, Res.uiExecutor( true ) );
} ); } );
return passwordFuture; return passwordFuture;

View File

@ -153,7 +153,7 @@ public class UnlockFrame extends JFrame {
boolean checkSignIn() { boolean checkSignIn() {
if (identiconFuture != null) if (identiconFuture != null)
identiconFuture.cancel( false ); identiconFuture.cancel( false );
identiconFuture = Res.schedule( this, () -> SwingUtilities.invokeLater( () -> { identiconFuture = Res.job( this, () -> SwingUtilities.invokeLater( () -> {
String fullName = (user == null)? "": user.getFullName(); String fullName = (user == null)? "": user.getFullName();
char[] masterPassword = authenticationPanel.getMasterPassword(); char[] masterPassword = authenticationPanel.getMasterPassword();
@ -186,7 +186,7 @@ public class UnlockFrame extends JFrame {
signInButton.setEnabled( false ); signInButton.setEnabled( false );
signInButton.setText( "Signing In..." ); signInButton.setText( "Signing In..." );
Res.execute( this, () -> { Res.job( this, () -> {
try { try {
user.authenticate( authenticationPanel.getMasterPassword() ); user.authenticate( authenticationPanel.getMasterPassword() );

View File

@ -21,6 +21,7 @@ package com.lyndir.masterpassword.model;
import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedInteger;
import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.*;
import java.util.Collection; import java.util.Collection;
import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -31,41 +32,50 @@ public interface MPSite<Q extends MPQuestion> extends Comparable<MPSite<?>> {
// - Meta // - Meta
@Nonnull
String getName(); String getName();
void setName(String name); void setName(String name);
// - Algorithm // - Algorithm
@Nonnull
MPAlgorithm getAlgorithm(); MPAlgorithm getAlgorithm();
void setAlgorithm(MPAlgorithm algorithm); void setAlgorithm(MPAlgorithm algorithm);
@Nonnull
UnsignedInteger getCounter(); UnsignedInteger getCounter();
void setCounter(UnsignedInteger counter); void setCounter(UnsignedInteger counter);
@Nonnull
MPResultType getResultType(); MPResultType getResultType();
void setResultType(MPResultType resultType); void setResultType(MPResultType resultType);
@Nonnull
MPResultType getLoginType(); MPResultType getLoginType();
void setLoginType(@Nullable MPResultType loginType); void setLoginType(@Nullable MPResultType loginType);
@Nonnull
String getResult(MPKeyPurpose keyPurpose, @Nullable String keyContext, @Nullable String state) String getResult(MPKeyPurpose keyPurpose, @Nullable String keyContext, @Nullable String state)
throws MPKeyUnavailableException, MPAlgorithmException; throws MPKeyUnavailableException, MPAlgorithmException;
@Nonnull
String getLogin(@Nullable String state) String getLogin(@Nullable String state)
throws MPKeyUnavailableException, MPAlgorithmException; throws MPKeyUnavailableException, MPAlgorithmException;
// - Relations // - Relations
MPUser<? extends MPSite<?>> getUser(); @Nonnull
MPUser<?> getUser();
void addQuestion(Q question); void addQuestion(Q question);
void deleteQuestion(Q question); void deleteQuestion(Q question);
@Nonnull
Collection<Q> getQuestions(); Collection<Q> getQuestions();
} }

View File

@ -18,8 +18,7 @@
package com.lyndir.masterpassword.model; package com.lyndir.masterpassword.model;
import com.google.common.collect.ImmutableList; import com.google.common.collect.*;
import com.google.common.collect.Maps;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
@ -37,7 +36,7 @@ public abstract class MPUserManager<U extends MPUser<?>> {
} }
public Collection<U> getUsers() { public Collection<U> getUsers() {
return ImmutableList.copyOf( usersByName.values() ); return ImmutableSortedSet.copyOf( usersByName.values() );
} }
public U getUserNamed(final String fullName) { public U getUserNamed(final String fullName) {

View File

@ -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;
}
}
}

View File

@ -31,7 +31,7 @@ import org.jetbrains.annotations.NotNull;
/** /**
* @author lhunath, 2018-05-14 * @author lhunath, 2018-05-14
*/ */
public abstract class MPBasicQuestion implements MPQuestion { public abstract class MPBasicQuestion extends Changeable implements MPQuestion {
private final String keyword; private final String keyword;
private MPResultType type; private MPResultType type;
@ -56,6 +56,8 @@ public abstract class MPBasicQuestion implements MPQuestion {
@Override @Override
public void setType(final MPResultType type) { public void setType(final MPResultType type) {
this.type = type; this.type = type;
setChanged();
} }
@Nonnull @Nonnull
@ -70,6 +72,13 @@ public abstract class MPBasicQuestion implements MPQuestion {
@Override @Override
public abstract MPBasicSite<?> getSite(); public abstract MPBasicSite<?> getSite();
@Override
protected void onChanged() {
super.onChanged();
getSite().setChanged();
}
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hashCode( getKeyword() ); return Objects.hashCode( getKeyword() );

View File

@ -26,6 +26,7 @@ import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.model.MPQuestion; import com.lyndir.masterpassword.model.MPQuestion;
import com.lyndir.masterpassword.model.MPSite; import com.lyndir.masterpassword.model.MPSite;
import java.util.*; import java.util.*;
import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -33,7 +34,7 @@ import org.jetbrains.annotations.NotNull;
/** /**
* @author lhunath, 14-12-16 * @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 String name;
private MPAlgorithm algorithm; 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; this.loginType = (loginType == null)? algorithm.mpw_default_login_type(): loginType;
} }
@Nonnull
@Override @Override
public String getName() { public String getName() {
return name; return name;
@ -64,8 +66,11 @@ public abstract class MPBasicSite<Q extends MPQuestion> implements MPSite<Q> {
@Override @Override
public void setName(final String name) { public void setName(final String name) {
this.name = name; this.name = name;
setChanged();
} }
@Nonnull
@Override @Override
public MPAlgorithm getAlgorithm() { public MPAlgorithm getAlgorithm() {
return algorithm; return algorithm;
@ -74,8 +79,11 @@ public abstract class MPBasicSite<Q extends MPQuestion> implements MPSite<Q> {
@Override @Override
public void setAlgorithm(final MPAlgorithm algorithm) { public void setAlgorithm(final MPAlgorithm algorithm) {
this.algorithm = algorithm; this.algorithm = algorithm;
setChanged();
} }
@Nonnull
@Override @Override
public UnsignedInteger getCounter() { public UnsignedInteger getCounter() {
return counter; return counter;
@ -84,8 +92,11 @@ public abstract class MPBasicSite<Q extends MPQuestion> implements MPSite<Q> {
@Override @Override
public void setCounter(final UnsignedInteger counter) { public void setCounter(final UnsignedInteger counter) {
this.counter = counter; this.counter = counter;
setChanged();
} }
@Nonnull
@Override @Override
public MPResultType getResultType() { public MPResultType getResultType() {
return resultType; return resultType;
@ -94,8 +105,11 @@ public abstract class MPBasicSite<Q extends MPQuestion> implements MPSite<Q> {
@Override @Override
public void setResultType(final MPResultType resultType) { public void setResultType(final MPResultType resultType) {
this.resultType = resultType; this.resultType = resultType;
setChanged();
} }
@Nonnull
@Override @Override
public MPResultType getLoginType() { public MPResultType getLoginType() {
return loginType; return loginType;
@ -104,8 +118,11 @@ public abstract class MPBasicSite<Q extends MPQuestion> implements MPSite<Q> {
@Override @Override
public void setLoginType(@Nullable final MPResultType loginType) { public void setLoginType(@Nullable final MPResultType loginType) {
this.loginType = ifNotNullElse( loginType, getAlgorithm().mpw_default_login_type() ); this.loginType = ifNotNullElse( loginType, getAlgorithm().mpw_default_login_type() );
setChanged();
} }
@Nonnull
@Override @Override
public String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext, @Nullable final String state) public String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext, @Nullable final String state)
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
@ -131,6 +148,7 @@ public abstract class MPBasicSite<Q extends MPQuestion> implements MPSite<Q> {
keyPurpose, keyContext, type, state ); keyPurpose, keyContext, type, state );
} }
@Nonnull
@Override @Override
public String getLogin(@Nullable final String state) public String getLogin(@Nullable final String state)
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
@ -141,18 +159,34 @@ public abstract class MPBasicSite<Q extends MPQuestion> implements MPSite<Q> {
@Override @Override
public void addQuestion(final Q question) { public void addQuestion(final Q question) {
questions.add( question ); questions.add( question );
setChanged();
} }
@Override @Override
public void deleteQuestion(final Q question) { public void deleteQuestion(final Q question) {
questions.remove( question ); questions.remove( question );
setChanged();
} }
@Nonnull
@Override @Override
public Collection<Q> getQuestions() { public Collection<Q> getQuestions() {
return Collections.unmodifiableCollection( questions ); return Collections.unmodifiableCollection( questions );
} }
@Nonnull
@Override
public abstract MPBasicUser<?> getUser();
@Override
protected void onChanged() {
super.onChanged();
getUser().setChanged();
}
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hashCode( getName() ); return Objects.hashCode( getName() );

View File

@ -20,7 +20,7 @@ package com.lyndir.masterpassword.model.impl;
import static com.lyndir.lhunath.opal.system.util.StringUtils.*; 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.CodeUtils;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.*;
@ -34,7 +34,7 @@ import javax.annotation.Nullable;
/** /**
* @author lhunath, 2014-06-08 * @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() ); protected final Logger logger = Logger.get( getClass() );
@ -64,6 +64,8 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> implements MPUser<S>
@Override @Override
public void setAvatar(final int avatar) { public void setAvatar(final int avatar) {
this.avatar = avatar; this.avatar = avatar;
setChanged();
} }
@Nonnull @Nonnull
@ -81,6 +83,8 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> implements MPUser<S>
@Override @Override
public void setAlgorithm(final MPAlgorithm algorithm) { public void setAlgorithm(final MPAlgorithm algorithm) {
this.algorithm = algorithm; this.algorithm = algorithm;
setChanged();
} }
@Nullable @Nullable
@ -136,7 +140,7 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> implements MPUser<S>
public MPMasterKey getMasterKey() public MPMasterKey getMasterKey()
throws MPKeyUnavailableException { throws MPKeyUnavailableException {
if (masterKey == null) 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; return masterKey;
} }
@ -144,11 +148,15 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> implements MPUser<S>
@Override @Override
public void addSite(final S site) { public void addSite(final S site) {
sites.add( site ); sites.add( site );
setChanged();
} }
@Override @Override
public void deleteSite(final S site) { public void deleteSite(final S site) {
sites.remove( site ); sites.remove( site );
setChanged();
} }
@Nonnull @Nonnull
@ -160,7 +168,7 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> implements MPUser<S>
@Nonnull @Nonnull
@Override @Override
public Collection<S> findSites(final String query) { public Collection<S> findSites(final String query) {
ImmutableList.Builder<S> results = ImmutableList.builder(); ImmutableSortedSet.Builder<S> results = ImmutableSortedSet.naturalOrder();
for (final S site : getSites()) for (final S site : getSites())
if (site.getName().startsWith( query )) if (site.getName().startsWith( query ))
results.add( site ); results.add( site );

View File

@ -33,19 +33,24 @@ public class MPFileQuestion extends MPBasicQuestion {
private final MPFileSite site; private final MPFileSite site;
@Nullable @Nullable
private String state; private String answerState;
public MPFileQuestion(final MPFileSite site, final String keyword, 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() ) ); super( keyword, ifNotNullElse( type, site.getAlgorithm().mpw_default_answer_type() ) );
this.site = site; this.site = site;
this.state = state; this.answerState = answerState;
}
@Nullable
public String getAnswerState() {
return answerState;
} }
public String getAnswer() public String getAnswer()
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
return getAnswer( state ); return getAnswer( answerState );
} }
public void setAnswer(final MPResultType type, @Nullable final String answer) public void setAnswer(final MPResultType type, @Nullable final String answer)
@ -53,10 +58,12 @@ public class MPFileQuestion extends MPBasicQuestion {
setType( type ); setType( type );
if (answer == null) if (answer == null)
this.state = null; this.answerState = null;
else else
this.state = getSite().getState( this.answerState = getSite().getState(
MPKeyPurpose.Recovery, getKeyword(), null, getType(), answer ); MPKeyPurpose.Recovery, getKeyword(), null, getType(), answer );
setChanged();
} }
@Nonnull @Nonnull

View File

@ -20,6 +20,7 @@ package com.lyndir.masterpassword.model.impl;
import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedInteger;
import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.model.MPSite;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import org.joda.time.Instant; import org.joda.time.Instant;
@ -29,6 +30,7 @@ import org.joda.time.ReadableInstant;
/** /**
* @author lhunath, 14-12-05 * @author lhunath, 14-12-05
*/ */
@SuppressWarnings("ComparableImplementedButEqualsNotOverridden")
public class MPFileSite extends MPBasicSite<MPFileQuestion> { public class MPFileSite extends MPBasicSite<MPFileQuestion> {
private final MPFileUser user; private final MPFileUser user;
@ -77,6 +79,8 @@ public class MPFileSite extends MPBasicSite<MPFileQuestion> {
public void setUrl(@Nullable final String url) { public void setUrl(@Nullable final String url) {
this.url = url; this.url = url;
setChanged();
} }
public int getUses() { public int getUses() {
@ -125,6 +129,8 @@ public class MPFileSite extends MPBasicSite<MPFileQuestion> {
else else
this.resultState = getState( this.resultState = getState(
MPKeyPurpose.Authentication, null, getCounter(), getResultType(), password ); MPKeyPurpose.Authentication, null, getCounter(), getResultType(), password );
setChanged();
} }
@Nullable @Nullable
@ -141,10 +147,22 @@ public class MPFileSite extends MPBasicSite<MPFileQuestion> {
else else
this.loginState = getState( this.loginState = getState(
MPKeyPurpose.Identification, null, null, getLoginType(), loginName ); MPKeyPurpose.Identification, null, null, getLoginType(), loginName );
setChanged();
} }
@Nonnull
@Override @Override
public MPFileUser getUser() { public MPFileUser getUser() {
return user; 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 );
}
} }

View File

@ -18,7 +18,6 @@
package com.lyndir.masterpassword.model.impl; package com.lyndir.masterpassword.model.impl;
import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException; import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
import com.lyndir.masterpassword.model.MPUser; import com.lyndir.masterpassword.model.MPUser;
@ -34,9 +33,6 @@ import org.joda.time.ReadableInstant;
@SuppressWarnings("ComparableImplementedButEqualsNotOverridden") @SuppressWarnings("ComparableImplementedButEqualsNotOverridden")
public class MPFileUser extends MPBasicUser<MPFileSite> { public class MPFileUser extends MPBasicUser<MPFileSite> {
@SuppressWarnings("UnusedDeclaration")
private static final Logger logger = Logger.get( MPFileUser.class );
@Nullable @Nullable
private byte[] keyID; private byte[] keyID;
private MPMarshalFormat format; private MPMarshalFormat format;
@ -101,6 +97,8 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
public void setFormat(final MPMarshalFormat format) { public void setFormat(final MPMarshalFormat format) {
this.format = format; this.format = format;
setChanged();
} }
public MPMarshaller.ContentMode getContentMode() { public MPMarshaller.ContentMode getContentMode() {
@ -109,6 +107,8 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
public void setContentMode(final MPMarshaller.ContentMode contentMode) { public void setContentMode(final MPMarshaller.ContentMode contentMode) {
this.contentMode = contentMode; this.contentMode = contentMode;
setChanged();
} }
public MPResultType getDefaultType() { public MPResultType getDefaultType() {
@ -117,6 +117,8 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
public void setDefaultType(final MPResultType defaultType) { public void setDefaultType(final MPResultType defaultType) {
this.defaultType = defaultType; this.defaultType = defaultType;
setChanged();
} }
public ReadableInstant getLastUsed() { public ReadableInstant getLastUsed() {
@ -125,6 +127,8 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
public void use() { public void use() {
lastUsed = new Instant(); lastUsed = new Instant();
setChanged();
} }
public void setJSON(final MPJSONFile json) { public void setJSON(final MPJSONFile json) {
@ -141,8 +145,23 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
throws MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException { throws MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException {
super.authenticate( masterKey ); super.authenticate( masterKey );
if (keyID == null) if (keyID == null) {
keyID = masterKey.getKeyID( getAlgorithm() ); 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() void save()
@ -152,7 +171,7 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
@Override @Override
public int compareTo(final MPUser<?> o) { 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) if (comparison != 0)
return comparison; return comparison;

View File

@ -18,14 +18,14 @@
package com.lyndir.masterpassword.model.impl; package com.lyndir.masterpassword.model.impl;
import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import java.util.*; import java.util.*;
/** /**
* @author lhunath, 2018-05-14 * @author lhunath, 2018-05-14
*/ */
@JsonInclude(value = JsonInclude.Include.CUSTOM, valueFilter = MPJSONAnyObject.MPJSONEmptyValue.class)
class MPJSONAnyObject { class MPJSONAnyObject {
@JsonAnySetter @JsonAnySetter
@ -35,4 +35,21 @@ class MPJSONAnyObject {
public Map<String, Object> getAny() { public Map<String, Object> getAny() {
return Collections.unmodifiableMap( any ); 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;
}
}
} }

View File

@ -20,7 +20,10 @@ package com.lyndir.masterpassword.model.impl;
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*; 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.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedInteger;
import com.lyndir.lhunath.opal.system.CodeUtils; import com.lyndir.lhunath.opal.system.CodeUtils;
@ -37,18 +40,28 @@ import org.joda.time.Instant;
/** /**
* @author lhunath, 2018-04-27 * @author lhunath, 2018-04-27
*/ */
@SuppressFBWarnings( "URF_UNREAD_FIELD" ) @SuppressFBWarnings("URF_UNREAD_FIELD")
public class MPJSONFile extends MPJSONAnyObject { public class MPJSONFile extends MPJSONAnyObject {
protected static final ObjectMapper objectMapper = new ObjectMapper(); protected static final ObjectMapper objectMapper = new ObjectMapper();
static { 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 ); objectMapper.setVisibility( PropertyAccessor.FIELD, JsonAutoDetect.Visibility.NON_PRIVATE );
} }
public MPJSONFile write(final MPFileUser modelUser) public MPJSONFile write(final MPFileUser modelUser)
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
// Section: "export" // Section: "export"
if (export == null) if (export == null)
export = new Export(); export = new Export();
@ -98,38 +111,27 @@ public class MPJSONFile extends MPJSONAnyObject {
site.uses = modelSite.getUses(); site.uses = modelSite.getUses();
site.last_used = MPConstants.dateTimeFormatter.print( modelSite.getLastUsed() ); 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) if (site._ext_mpw == null)
site._ext_mpw = new Site.Ext(); site._ext_mpw = new Site.Ext();
site._ext_mpw.url = modelSite.getUrl(); 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; return this;
@ -143,6 +145,7 @@ public class MPJSONFile extends MPJSONAnyObject {
(user.default_type != null)? user.default_type: algorithm.mpw_default_result_type(), (user.default_type != null)? user.default_type: algorithm.mpw_default_result_type(),
(user.last_used != null)? MPConstants.dateTimeFormatter.parseDateTime( user.last_used ): new Instant(), (user.last_used != null)? MPConstants.dateTimeFormatter.parseDateTime( user.last_used ): new Instant(),
MPMarshalFormat.JSON, export.redacted? MPMarshaller.ContentMode.PROTECTED: MPMarshaller.ContentMode.VISIBLE ); MPMarshalFormat.JSON, export.redacted? MPMarshaller.ContentMode.PROTECTED: MPMarshaller.ContentMode.VISIBLE );
model.beginChanges();
model.setJSON( this ); model.setJSON( this );
if (masterPassword != null) if (masterPassword != null)
model.authenticate( masterPassword ); model.authenticate( masterPassword );
@ -167,6 +170,7 @@ public class MPJSONFile extends MPJSONAnyObject {
model.addSite( site ); model.addSite( site );
} }
model.endChanges();
return model; return model;
} }
@ -193,26 +197,26 @@ public class MPJSONFile extends MPJSONAnyObject {
String full_name; String full_name;
String last_used; String last_used;
@Nullable @Nullable
MPAlgorithm.Version algorithm;
@Nullable
String key_id; String key_id;
@Nullable @Nullable
MPAlgorithm.Version algorithm;
@Nullable
MPResultType default_type; MPResultType default_type;
} }
public static class Site extends MPJSONAnyObject { public static class Site extends MPJSONAnyObject {
@Nullable
MPResultType type;
long counter; long counter;
MPAlgorithm.Version algorithm; MPAlgorithm.Version algorithm;
@Nullable @Nullable
MPResultType type;
@Nullable
String password; String password;
@Nullable @Nullable
MPResultType login_type;
@Nullable
String login_name; String login_name;
@Nullable
MPResultType login_type;
int uses; int uses;
@Nullable @Nullable

View File

@ -37,7 +37,7 @@ public class MPJSONMarshaller implements MPMarshaller {
throws MPKeyUnavailableException, MPMarshalException, MPAlgorithmException { throws MPKeyUnavailableException, MPMarshalException, MPAlgorithmException {
try { try {
return objectMapper.writeValueAsString( user.getJSON().write( user ) ); return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString( user.getJSON().write( user ) );
} }
catch (final JsonProcessingException e) { catch (final JsonProcessingException e) {
throw new MPMarshalException( "Couldn't compose JSON for: " + user, e ); throw new MPMarshalException( "Couldn't compose JSON for: " + user, e );