diff --git a/platform-independent/java/algorithm/src/main/java/com/lyndir/masterpassword/impl/MPAlgorithmV0.java b/platform-independent/java/algorithm/src/main/java/com/lyndir/masterpassword/impl/MPAlgorithmV0.java index 8cc665c5..946b3e74 100644 --- a/platform-independent/java/algorithm/src/main/java/com/lyndir/masterpassword/impl/MPAlgorithmV0.java +++ b/platform-independent/java/algorithm/src/main/java/com/lyndir/masterpassword/impl/MPAlgorithmV0.java @@ -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 diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/GUI.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/GUI.java index 29345acd..a9e4f558 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/GUI.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/GUI.java @@ -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(); diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/Res.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/Res.java index 7e8f9e7d..16917c46 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/Res.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/Res.java @@ -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 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 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 ListenableFuture execute(final Window host, final Callable job) { - return schedule( host, job, 0, TimeUnit.MILLISECONDS ); + public static ListenableFuture job(final Window host, final Callable job) { + return job( host, job, 0, TimeUnit.MILLISECONDS ); } - public static ListenableFuture schedule(final Window host, final Callable 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 ListenableFuture job(final Window host, final Callable 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; diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/SwingExecutorService.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/SwingExecutorService.java new file mode 100644 index 00000000..b273203c --- /dev/null +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/SwingExecutorService.java @@ -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 pendingCommands = Lists.newLinkedList(); + private final BlockingQueue 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 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 ); + } + } +} diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPIncognitoSite.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPIncognitoSite.java index a853b48e..44efed4e 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPIncognitoSite.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/model/MPIncognitoSite.java @@ -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 { this.user = user; } + @Nonnull @Override public MPIncognitoUser getUser() { return user; diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/FailableCallback.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/FailableCallback.java new file mode 100644 index 00000000..596c4cbd --- /dev/null +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/FailableCallback.java @@ -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 implements FutureCallback { + + 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 ); + } +} diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/PasswordFrame.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/PasswordFrame.java index 4bae7167..3bc357a2 100755 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/PasswordFrame.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/PasswordFrame.java @@ -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, 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, 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() { + siteNameField.addActionListener( + e -> Futures.addCallback( updatePassword( true ), new FailableCallback( 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, S extends MPSite> ex site.setCounter( siteCounter ); } - ListenableFuture passwordFuture = Res.execute( this, () -> site.getResult( MPKeyPurpose.Authentication, null, null ) ); - Futures.addCallback( passwordFuture, new FutureCallback() { - @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 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( 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; diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/UnlockFrame.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/UnlockFrame.java index 3354f74c..c8c75998 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/UnlockFrame.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/UnlockFrame.java @@ -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() ); diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPSite.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPSite.java index feb602ac..d5571eb1 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPSite.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPSite.java @@ -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 extends Comparable> { // - 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> getUser(); + @Nonnull + MPUser getUser(); void addQuestion(Q question); void deleteQuestion(Q question); + @Nonnull Collection getQuestions(); } diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPUserManager.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPUserManager.java index 1ab18f54..60cdfb51 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPUserManager.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPUserManager.java @@ -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> { } public Collection getUsers() { - return ImmutableList.copyOf( usersByName.values() ); + return ImmutableSortedSet.copyOf( usersByName.values() ); } public U getUserNamed(final String fullName) { diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/Changeable.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/Changeable.java new file mode 100644 index 00000000..b09f91e4 --- /dev/null +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/Changeable.java @@ -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; + } + } +} diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicQuestion.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicQuestion.java index 9cd90b21..4285032d 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicQuestion.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicQuestion.java @@ -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() ); diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicSite.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicSite.java index b979bde6..b5a63d38 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicSite.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicSite.java @@ -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 implements MPSite { +public abstract class MPBasicSite extends Changeable implements MPSite { private String name; private MPAlgorithm algorithm; @@ -56,6 +57,7 @@ public abstract class MPBasicSite implements MPSite { 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 implements MPSite { @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 implements MPSite { @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 implements MPSite { @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 implements MPSite { @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 implements MPSite { @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 implements MPSite { keyPurpose, keyContext, type, state ); } + @Nonnull @Override public String getLogin(@Nullable final String state) throws MPKeyUnavailableException, MPAlgorithmException { @@ -141,18 +159,34 @@ public abstract class MPBasicSite implements MPSite { @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 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() ); diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicUser.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicUser.java index d0f65cac..45d45016 100755 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicUser.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicUser.java @@ -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> implements MPUser { +public abstract class MPBasicUser> extends Changeable implements MPUser { protected final Logger logger = Logger.get( getClass() ); @@ -64,6 +64,8 @@ public abstract class MPBasicUser> implements MPUser @Override public void setAvatar(final int avatar) { this.avatar = avatar; + + setChanged(); } @Nonnull @@ -81,6 +83,8 @@ public abstract class MPBasicUser> implements MPUser @Override public void setAlgorithm(final MPAlgorithm algorithm) { this.algorithm = algorithm; + + setChanged(); } @Nullable @@ -136,7 +140,7 @@ public abstract class MPBasicUser> implements MPUser 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> implements MPUser @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> implements MPUser @Nonnull @Override public Collection findSites(final String query) { - ImmutableList.Builder results = ImmutableList.builder(); + ImmutableSortedSet.Builder results = ImmutableSortedSet.naturalOrder(); for (final S site : getSites()) if (site.getName().startsWith( query )) results.add( site ); diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileQuestion.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileQuestion.java index 2a656253..60f49bff 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileQuestion.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileQuestion.java @@ -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 diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileSite.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileSite.java index d258d26d..fa118371 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileSite.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileSite.java @@ -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 { private final MPFileUser user; @@ -77,6 +79,8 @@ public class MPFileSite extends MPBasicSite { public void setUrl(@Nullable final String url) { this.url = url; + + setChanged(); } public int getUses() { @@ -125,6 +129,8 @@ public class MPFileSite extends MPBasicSite { else this.resultState = getState( MPKeyPurpose.Authentication, null, getCounter(), getResultType(), password ); + + setChanged(); } @Nullable @@ -141,10 +147,22 @@ public class MPFileSite extends MPBasicSite { 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 ); + } } diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUser.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUser.java index 558fa798..3cb7a3dc 100755 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUser.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUser.java @@ -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 { - @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 { public void setFormat(final MPMarshalFormat format) { this.format = format; + + setChanged(); } public MPMarshaller.ContentMode getContentMode() { @@ -109,6 +107,8 @@ public class MPFileUser extends MPBasicUser { public void setContentMode(final MPMarshaller.ContentMode contentMode) { this.contentMode = contentMode; + + setChanged(); } public MPResultType getDefaultType() { @@ -117,6 +117,8 @@ public class MPFileUser extends MPBasicUser { public void setDefaultType(final MPResultType defaultType) { this.defaultType = defaultType; + + setChanged(); } public ReadableInstant getLastUsed() { @@ -125,6 +127,8 @@ public class MPFileUser extends MPBasicUser { public void use() { lastUsed = new Instant(); + + setChanged(); } public void setJSON(final MPJSONFile json) { @@ -141,8 +145,23 @@ public class MPFileUser extends MPBasicUser { 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 { @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; diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPJSONAnyObject.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPJSONAnyObject.java index 03082051..8c9adf52 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPJSONAnyObject.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPJSONAnyObject.java @@ -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 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; + } + } } diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPJSONFile.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPJSONFile.java index 8aff5609..4e3f9435 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPJSONFile.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPJSONFile.java @@ -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 diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPJSONMarshaller.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPJSONMarshaller.java index e5ce1889..b8563ce7 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPJSONMarshaller.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPJSONMarshaller.java @@ -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 );