Import & export users + improved user state tracking.
This commit is contained in:
		@@ -44,7 +44,8 @@ public class MPMasterKey {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param masterPassword The characters of the user's master password.
 | 
			
		||||
     *                       <b>Note: this method destroys the contents of the array.</b>
 | 
			
		||||
     *
 | 
			
		||||
     * @apiNote This method destroys the contents of the {@code masterPassword} array.
 | 
			
		||||
     */
 | 
			
		||||
    @SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter")
 | 
			
		||||
    public MPMasterKey(final String fullName, final char[] masterPassword) {
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ plugins {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
description = 'Master Password GUI'
 | 
			
		||||
mainClassName = 'com.lyndir.masterpassword.gui.Main'
 | 
			
		||||
mainClassName = 'com.lyndir.masterpassword.gui.MasterPassword'
 | 
			
		||||
 | 
			
		||||
dependencies {
 | 
			
		||||
    implementation group: 'com.lyndir.lhunath.opal', name: 'opal-system', version: '1.7-p2'
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,26 @@
 | 
			
		||||
package com.lyndir.masterpassword.gui;
 | 
			
		||||
 | 
			
		||||
import com.lyndir.lhunath.opal.system.logging.Logger;
 | 
			
		||||
import com.lyndir.masterpassword.gui.util.Platform;
 | 
			
		||||
import com.lyndir.masterpassword.gui.util.Res;
 | 
			
		||||
import com.lyndir.masterpassword.gui.view.MasterPasswordFrame;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @author lhunath, 2018-07-28
 | 
			
		||||
 */
 | 
			
		||||
public class GUI {
 | 
			
		||||
 | 
			
		||||
    private static final Logger logger = Logger.get( GUI.class );
 | 
			
		||||
 | 
			
		||||
    private final MasterPasswordFrame frame = new MasterPasswordFrame();
 | 
			
		||||
 | 
			
		||||
    public GUI() {
 | 
			
		||||
        Platform.get().installAppForegroundHandler( this::open );
 | 
			
		||||
        Platform.get().installAppReopenHandler( this::open );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void open() {
 | 
			
		||||
        Res.ui( () -> frame.setVisible( true ) );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -23,10 +23,14 @@ import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
 | 
			
		||||
import com.google.common.base.Charsets;
 | 
			
		||||
import com.google.common.io.ByteSource;
 | 
			
		||||
import com.lyndir.lhunath.opal.system.logging.Logger;
 | 
			
		||||
import com.lyndir.masterpassword.gui.platform.BaseGUI;
 | 
			
		||||
import com.lyndir.lhunath.opal.system.util.ObjectUtils;
 | 
			
		||||
import com.lyndir.masterpassword.model.MPUser;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.io.InputStream;
 | 
			
		||||
import java.net.*;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.concurrent.CopyOnWriteArraySet;
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import javax.swing.*;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -35,33 +39,42 @@ import javax.swing.*;
 | 
			
		||||
 *
 | 
			
		||||
 * @author mbillemo
 | 
			
		||||
 */
 | 
			
		||||
public final class Main {
 | 
			
		||||
public final class MasterPassword {
 | 
			
		||||
 | 
			
		||||
    @SuppressWarnings("UnusedDeclaration")
 | 
			
		||||
    private static final Logger logger = Logger.get( Main.class );
 | 
			
		||||
    private static final Logger logger = Logger.get( MasterPassword.class );
 | 
			
		||||
 | 
			
		||||
    public static void main(final String... args) {
 | 
			
		||||
//        Thread.setDefaultUncaughtExceptionHandler(
 | 
			
		||||
//                (t, e) -> logger.bug( e, "Uncaught: %s", e.getLocalizedMessage() ) );
 | 
			
		||||
    private static final MasterPassword instance = new MasterPassword();
 | 
			
		||||
 | 
			
		||||
        // Try and set the system look & feel, if available.
 | 
			
		||||
        try {
 | 
			
		||||
            UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
 | 
			
		||||
        }
 | 
			
		||||
        catch (final UnsupportedLookAndFeelException | ClassNotFoundException | InstantiationException | IllegalAccessException ignored) {
 | 
			
		||||
        }
 | 
			
		||||
    private final Collection<Listener> listeners = new CopyOnWriteArraySet<>();
 | 
			
		||||
 | 
			
		||||
        // Check online to see if this version has been superseded.
 | 
			
		||||
        if (Config.get().checkForUpdates())
 | 
			
		||||
            checkUpdate();
 | 
			
		||||
    @Nullable
 | 
			
		||||
    private MPUser<?> activeUser;
 | 
			
		||||
 | 
			
		||||
        // Create a platform-specific GUI and open it.
 | 
			
		||||
        BaseGUI.createPlatformGUI().open();
 | 
			
		||||
    public static MasterPassword get() {
 | 
			
		||||
        return instance;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public boolean addListener(final Listener listener) {
 | 
			
		||||
        return listeners.add( listener );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public boolean removeListener(final Listener listener) {
 | 
			
		||||
        return listeners.remove( listener );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void activateUser(final MPUser<?> user) {
 | 
			
		||||
        if (ObjectUtils.equals( activeUser, user ))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        activeUser = user;
 | 
			
		||||
        for (final Listener listener : listeners)
 | 
			
		||||
            listener.onUserSelected( activeUser );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void checkUpdate() {
 | 
			
		||||
        try {
 | 
			
		||||
            String implementationVersion = Main.class.getPackage().getImplementationVersion();
 | 
			
		||||
            String implementationVersion = MasterPassword.class.getPackage().getImplementationVersion();
 | 
			
		||||
            String latestVersion = new ByteSource() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public InputStream openStream()
 | 
			
		||||
@@ -90,4 +103,27 @@ public final class Main {
 | 
			
		||||
            logger.wrn( e, "Couldn't check for version update." );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void main(final String... args) {
 | 
			
		||||
        //        Thread.setDefaultUncaughtExceptionHandler(
 | 
			
		||||
        //                (t, e) -> logger.bug( e, "Uncaught: %s", e.getLocalizedMessage() ) );
 | 
			
		||||
 | 
			
		||||
        // Try and set the system look & feel, if available.
 | 
			
		||||
        try {
 | 
			
		||||
            UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
 | 
			
		||||
        }
 | 
			
		||||
        catch (final UnsupportedLookAndFeelException | ClassNotFoundException | InstantiationException | IllegalAccessException ignored) {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check online to see if this version has been superseded.
 | 
			
		||||
        if (Config.get().checkForUpdates())
 | 
			
		||||
            checkUpdate();
 | 
			
		||||
 | 
			
		||||
        // Create a platform-specific GUI and open it.
 | 
			
		||||
        new GUI().open();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public interface Listener {
 | 
			
		||||
        void onUserSelected(@Nullable MPUser<?> user);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,62 +0,0 @@
 | 
			
		||||
//==============================================================================
 | 
			
		||||
// This file is part of Master Password.
 | 
			
		||||
// Copyright (c) 2011-2017, Maarten Billemont.
 | 
			
		||||
//
 | 
			
		||||
// Master Password is free software: you can redistribute it and/or modify
 | 
			
		||||
// it under the terms of the GNU General Public License as published by
 | 
			
		||||
// the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
// (at your option) any later version.
 | 
			
		||||
//
 | 
			
		||||
// Master Password is distributed in the hope that it will be useful,
 | 
			
		||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
// GNU General Public License for more details.
 | 
			
		||||
//
 | 
			
		||||
// You can find a copy of the GNU General Public License in the
 | 
			
		||||
// LICENSE file.  Alternatively, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
package com.lyndir.masterpassword.gui.platform;
 | 
			
		||||
 | 
			
		||||
import com.apple.eawt.*;
 | 
			
		||||
import com.google.common.base.Preconditions;
 | 
			
		||||
import com.lyndir.masterpassword.gui.view.MasterPasswordFrame;
 | 
			
		||||
import javax.swing.*;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @author lhunath, 2014-06-10
 | 
			
		||||
 */
 | 
			
		||||
public class AppleGUI extends BaseGUI {
 | 
			
		||||
 | 
			
		||||
    static Application application = Preconditions.checkNotNull(
 | 
			
		||||
            Application.getApplication(), "Not an Apple Java application." );
 | 
			
		||||
 | 
			
		||||
    public AppleGUI() {
 | 
			
		||||
        application.addAppEventListener( new AppEventHandler() );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected MasterPasswordFrame createFrame() {
 | 
			
		||||
        MasterPasswordFrame frame = super.createFrame();
 | 
			
		||||
        frame.setDefaultCloseOperation( WindowConstants.HIDE_ON_CLOSE );
 | 
			
		||||
        return frame;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private class AppEventHandler implements AppForegroundListener, AppReOpenedListener {
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void appMovedToBackground(final AppEvent.AppForegroundEvent arg0) {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void appRaisedToForeground(final AppEvent.AppForegroundEvent arg0) {
 | 
			
		||||
            open();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void appReOpened(final AppEvent.AppReOpenedEvent arg0) {
 | 
			
		||||
            open();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,58 +0,0 @@
 | 
			
		||||
package com.lyndir.masterpassword.gui.platform;
 | 
			
		||||
 | 
			
		||||
import com.lyndir.lhunath.opal.system.logging.Logger;
 | 
			
		||||
import com.lyndir.lhunath.opal.system.util.TypeUtils;
 | 
			
		||||
import com.lyndir.masterpassword.gui.util.Res;
 | 
			
		||||
import com.lyndir.masterpassword.gui.view.MasterPasswordFrame;
 | 
			
		||||
import java.lang.reflect.InvocationTargetException;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @author lhunath, 2018-07-28
 | 
			
		||||
 */
 | 
			
		||||
public class BaseGUI {
 | 
			
		||||
 | 
			
		||||
    private static final Logger logger = Logger.get( BaseGUI.class );
 | 
			
		||||
 | 
			
		||||
    private final MasterPasswordFrame frame = createFrame();
 | 
			
		||||
 | 
			
		||||
    public static BaseGUI createPlatformGUI() {
 | 
			
		||||
        BaseGUI jdk9GUI = construct( "com.lyndir.masterpassword.gui.platform.JDK9GUI" );
 | 
			
		||||
        if (jdk9GUI != null)
 | 
			
		||||
            return jdk9GUI;
 | 
			
		||||
 | 
			
		||||
        BaseGUI appleGUI = construct( "com.lyndir.masterpassword.gui.platform.AppleGUI" );
 | 
			
		||||
        if (appleGUI != null)
 | 
			
		||||
            return appleGUI;
 | 
			
		||||
 | 
			
		||||
        // Use platform-independent GUI.
 | 
			
		||||
        return new BaseGUI();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    private static BaseGUI construct(final String typeName) {
 | 
			
		||||
        try {
 | 
			
		||||
            // AppleGUI adds support for macOS features.
 | 
			
		||||
            Optional<Class<BaseGUI>> gui = TypeUtils.loadClass( typeName );
 | 
			
		||||
            if (gui.isPresent())
 | 
			
		||||
                return gui.get().getConstructor().newInstance();
 | 
			
		||||
        }
 | 
			
		||||
        catch (@SuppressWarnings("ErrorNotRethrown") final LinkageError ignored) {
 | 
			
		||||
        }
 | 
			
		||||
        catch (final IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) {
 | 
			
		||||
            throw logger.bug( e );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected MasterPasswordFrame createFrame() {
 | 
			
		||||
        return new MasterPasswordFrame();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void open() {
 | 
			
		||||
        Res.ui( () -> frame.setVisible( true ) );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,60 +0,0 @@
 | 
			
		||||
//==============================================================================
 | 
			
		||||
// This file is part of Master Password.
 | 
			
		||||
// Copyright (c) 2011-2017, Maarten Billemont.
 | 
			
		||||
//
 | 
			
		||||
// Master Password is free software: you can redistribute it and/or modify
 | 
			
		||||
// it under the terms of the GNU General Public License as published by
 | 
			
		||||
// the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
// (at your option) any later version.
 | 
			
		||||
//
 | 
			
		||||
// Master Password is distributed in the hope that it will be useful,
 | 
			
		||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
// GNU General Public License for more details.
 | 
			
		||||
//
 | 
			
		||||
// You can find a copy of the GNU General Public License in the
 | 
			
		||||
// LICENSE file.  Alternatively, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
//==============================================================================
 | 
			
		||||
 | 
			
		||||
package com.lyndir.masterpassword.gui.platform;
 | 
			
		||||
 | 
			
		||||
import com.lyndir.masterpassword.gui.view.MasterPasswordFrame;
 | 
			
		||||
import java.awt.*;
 | 
			
		||||
import java.awt.desktop.*;
 | 
			
		||||
import javax.swing.*;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @author lhunath, 2014-06-10
 | 
			
		||||
 */
 | 
			
		||||
@SuppressWarnings("Since15")
 | 
			
		||||
public class JDK9GUI extends BaseGUI {
 | 
			
		||||
 | 
			
		||||
    public JDK9GUI() {
 | 
			
		||||
        Desktop.getDesktop().addAppEventListener( new AppEventHandler() );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected MasterPasswordFrame createFrame() {
 | 
			
		||||
        MasterPasswordFrame frame = super.createFrame();
 | 
			
		||||
        frame.setDefaultCloseOperation( WindowConstants.HIDE_ON_CLOSE );
 | 
			
		||||
        return frame;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private class AppEventHandler implements AppForegroundListener, AppReopenedListener {
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void appRaisedToForeground(final AppForegroundEvent e) {
 | 
			
		||||
            open();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void appMovedToBackground(final AppForegroundEvent e) {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void appReopened(final AppReopenedEvent e) {
 | 
			
		||||
            open();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -21,6 +21,7 @@ package com.lyndir.masterpassword.gui.util;
 | 
			
		||||
import java.awt.*;
 | 
			
		||||
import java.awt.event.ActionEvent;
 | 
			
		||||
import java.awt.event.ActionListener;
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.util.Arrays;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
@@ -110,6 +111,29 @@ public abstract class Components {
 | 
			
		||||
        return (option < 0)? JOptionPane.CLOSED_OPTION: option;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public static File showLoadDialog(@Nullable final Component owner, final String title) {
 | 
			
		||||
        return showFileDialog( owner, title, FileDialog.LOAD, null );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public static File showSaveDialog(@Nullable final Component owner, final String title, final String fileName) {
 | 
			
		||||
        return showFileDialog( owner, title, FileDialog.SAVE, fileName );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    private static File showFileDialog(@Nullable final Component owner, final String title,
 | 
			
		||||
                                       final int mode, @Nullable final String fileName) {
 | 
			
		||||
        FileDialog fileDialog = new FileDialog( JOptionPane.getFrameForComponent( owner ), title, mode );
 | 
			
		||||
        fileDialog.setFile( fileName );
 | 
			
		||||
        fileDialog.setLocationRelativeTo( owner );
 | 
			
		||||
        fileDialog.setLocationByPlatform( true );
 | 
			
		||||
        fileDialog.setVisible( true );
 | 
			
		||||
 | 
			
		||||
        File[] selectedFiles = fileDialog.getFiles();
 | 
			
		||||
        return ((selectedFiles != null) && (selectedFiles.length > 0))? selectedFiles[0]: null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static JDialog showDialog(@Nullable final Component owner, @Nullable final String title, final Container content) {
 | 
			
		||||
        JDialog dialog = new JDialog( (owner != null)? SwingUtilities.windowForComponent( owner ): null,
 | 
			
		||||
                                      title, Dialog.ModalityType.DOCUMENT_MODAL );
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,52 @@
 | 
			
		||||
package com.lyndir.masterpassword.gui.util;
 | 
			
		||||
 | 
			
		||||
import com.lyndir.lhunath.opal.system.logging.Logger;
 | 
			
		||||
import com.lyndir.lhunath.opal.system.util.TypeUtils;
 | 
			
		||||
import com.lyndir.masterpassword.gui.util.platform.BasePlatform;
 | 
			
		||||
import com.lyndir.masterpassword.gui.util.platform.IPlatform;
 | 
			
		||||
import java.lang.reflect.InvocationTargetException;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @author lhunath, 2018-07-29
 | 
			
		||||
 */
 | 
			
		||||
public final class Platform {
 | 
			
		||||
 | 
			
		||||
    private static final Logger    logger = Logger.get( Platform.class );
 | 
			
		||||
    private static final IPlatform activePlatform;
 | 
			
		||||
 | 
			
		||||
    static {
 | 
			
		||||
        IPlatform tryPlatform;
 | 
			
		||||
        if (null != (tryPlatform = construct( "com.lyndir.masterpassword.gui.util.platform.JDK9Platform" )))
 | 
			
		||||
            activePlatform = tryPlatform;
 | 
			
		||||
 | 
			
		||||
        else if (null != (tryPlatform = construct( "com.lyndir.masterpassword.gui.util.platform.ApplePlatform" )))
 | 
			
		||||
            activePlatform = tryPlatform;
 | 
			
		||||
 | 
			
		||||
        else
 | 
			
		||||
            activePlatform = new BasePlatform();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    private static <T> T construct(final String typeName) {
 | 
			
		||||
        try {
 | 
			
		||||
            // AppleGUI adds support for macOS features.
 | 
			
		||||
            Optional<Class<T>> gui = TypeUtils.loadClass( typeName );
 | 
			
		||||
            if (gui.isPresent())
 | 
			
		||||
                return gui.get().getConstructor().newInstance();
 | 
			
		||||
        }
 | 
			
		||||
        catch (@SuppressWarnings("ErrorNotRethrown") final LinkageError ignored) {
 | 
			
		||||
        }
 | 
			
		||||
        catch (final IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) {
 | 
			
		||||
            throw logger.bug( e );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static IPlatform get() {
 | 
			
		||||
        return activePlatform;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -136,6 +136,14 @@ public abstract class Res {
 | 
			
		||||
            return icon( "media/icon_reset.png" );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Icon import_() {
 | 
			
		||||
            return icon( "media/icon_import.png" );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Icon export() {
 | 
			
		||||
            return icon( "media/icon_export.png" );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Icon settings() {
 | 
			
		||||
            return icon( "media/icon_settings.png" );
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,49 @@
 | 
			
		||||
package com.lyndir.masterpassword.gui.util.platform;
 | 
			
		||||
 | 
			
		||||
import com.apple.eawt.*;
 | 
			
		||||
import com.apple.eio.FileManager;
 | 
			
		||||
import com.google.common.base.Preconditions;
 | 
			
		||||
import com.google.common.base.Throwables;
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.io.FileNotFoundException;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @author lhunath, 2018-07-29
 | 
			
		||||
 */
 | 
			
		||||
public class ApplePlatform implements IPlatform {
 | 
			
		||||
 | 
			
		||||
    static Application application = Preconditions.checkNotNull(
 | 
			
		||||
            Application.getApplication(), "Not an Apple Java application." );
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean installAppForegroundHandler(final Runnable handler) {
 | 
			
		||||
        application.addAppEventListener( new AppForegroundListener() {
 | 
			
		||||
            @Override
 | 
			
		||||
            public void appMovedToBackground(final AppEvent.AppForegroundEvent e) {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            @Override
 | 
			
		||||
            public void appRaisedToForeground(final AppEvent.AppForegroundEvent e) {
 | 
			
		||||
                handler.run();
 | 
			
		||||
            }
 | 
			
		||||
        } );
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean installAppReopenHandler(final Runnable handler) {
 | 
			
		||||
        application.addAppEventListener( (AppReOpenedListener) e -> handler.run() );
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean show(final File file) {
 | 
			
		||||
        try {
 | 
			
		||||
            return FileManager.revealInFinder( file );
 | 
			
		||||
        }
 | 
			
		||||
        catch (final FileNotFoundException ignored) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,25 @@
 | 
			
		||||
package com.lyndir.masterpassword.gui.util.platform;
 | 
			
		||||
 | 
			
		||||
import java.io.File;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @author lhunath, 2018-07-29
 | 
			
		||||
 */
 | 
			
		||||
public class BasePlatform implements IPlatform {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean installAppForegroundHandler(final Runnable handler) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean installAppReopenHandler(final Runnable handler) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean show(final File file) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,16 @@
 | 
			
		||||
package com.lyndir.masterpassword.gui.util.platform;
 | 
			
		||||
 | 
			
		||||
import java.io.File;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @author lhunath, 2018-07-29
 | 
			
		||||
 */
 | 
			
		||||
public interface IPlatform {
 | 
			
		||||
 | 
			
		||||
    boolean installAppForegroundHandler(Runnable handler);
 | 
			
		||||
 | 
			
		||||
    boolean installAppReopenHandler(Runnable handler);
 | 
			
		||||
 | 
			
		||||
    boolean show(File file);
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,43 @@
 | 
			
		||||
package com.lyndir.masterpassword.gui.util.platform;
 | 
			
		||||
 | 
			
		||||
import java.awt.*;
 | 
			
		||||
import java.awt.desktop.*;
 | 
			
		||||
import java.io.File;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @author lhunath, 2018-07-29
 | 
			
		||||
 */
 | 
			
		||||
@SuppressWarnings("Since15")
 | 
			
		||||
public class JDK9Platform implements IPlatform {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean installAppForegroundHandler(final Runnable handler) {
 | 
			
		||||
        Desktop.getDesktop().addAppEventListener( new AppForegroundListener() {
 | 
			
		||||
            @Override
 | 
			
		||||
            public void appRaisedToForeground(final AppForegroundEvent e) {
 | 
			
		||||
                handler.run();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            @Override
 | 
			
		||||
            public void appMovedToBackground(final AppForegroundEvent e) {
 | 
			
		||||
            }
 | 
			
		||||
        } );
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean installAppReopenHandler(final Runnable handler) {
 | 
			
		||||
        Desktop.getDesktop().addAppEventListener( (AppReopenedListener) e -> handler.run() );
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean show(final File file) {
 | 
			
		||||
        if (!file.exists())
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        Desktop.getDesktop().browseFileDirectory( file );
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -20,6 +20,6 @@
 | 
			
		||||
 * @author lhunath, 2018-04-26
 | 
			
		||||
 */
 | 
			
		||||
@ParametersAreNonnullByDefault
 | 
			
		||||
package com.lyndir.masterpassword.gui.platform;
 | 
			
		||||
package com.lyndir.masterpassword.gui.util.platform;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.ParametersAreNonnullByDefault;
 | 
			
		||||
@@ -3,6 +3,7 @@ package com.lyndir.masterpassword.gui.view;
 | 
			
		||||
import static com.lyndir.masterpassword.util.Utilities.*;
 | 
			
		||||
 | 
			
		||||
import com.google.common.collect.ImmutableSortedSet;
 | 
			
		||||
import com.lyndir.masterpassword.gui.MasterPassword;
 | 
			
		||||
import com.lyndir.masterpassword.gui.util.*;
 | 
			
		||||
import com.lyndir.masterpassword.model.MPUser;
 | 
			
		||||
import com.lyndir.masterpassword.model.impl.MPFileUser;
 | 
			
		||||
@@ -18,15 +19,13 @@ import javax.swing.*;
 | 
			
		||||
 * @author lhunath, 2018-07-14
 | 
			
		||||
 */
 | 
			
		||||
@SuppressWarnings("serial")
 | 
			
		||||
public class FilesPanel extends JPanel implements MPFileUserManager.Listener {
 | 
			
		||||
 | 
			
		||||
    private final Collection<Listener> listeners = new CopyOnWriteArraySet<>();
 | 
			
		||||
public class FilesPanel extends JPanel implements MPFileUserManager.Listener, MasterPassword.Listener {
 | 
			
		||||
 | 
			
		||||
    private final JButton avatarButton = Components.button( Res.icons().avatar( 0 ), event -> setAvatar(),
 | 
			
		||||
                                                            "Click to change the user's avatar." );
 | 
			
		||||
 | 
			
		||||
    private final CollectionListModel<MPUser<?>> usersModel =
 | 
			
		||||
            CollectionListModel.<MPUser<?>>copy( MPFileUserManager.get().getFiles() ).selection( this::setUser );
 | 
			
		||||
            CollectionListModel.<MPUser<?>>copy( MPFileUserManager.get().getFiles() ).selection( MasterPassword.get()::activateUser );
 | 
			
		||||
    private final JComboBox<? extends MPUser<?>> userField  =
 | 
			
		||||
            Components.comboBox( usersModel, user -> ifNotNull( user, MPUser::getFullName ) );
 | 
			
		||||
 | 
			
		||||
@@ -50,10 +49,7 @@ public class FilesPanel extends JPanel implements MPFileUserManager.Listener {
 | 
			
		||||
        add( userField );
 | 
			
		||||
 | 
			
		||||
        MPFileUserManager.get().addListener( this );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public boolean addListener(final Listener listener) {
 | 
			
		||||
        return listeners.add( listener );
 | 
			
		||||
        MasterPassword.get().addListener( this );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void setAvatar() {
 | 
			
		||||
@@ -65,20 +61,14 @@ public class FilesPanel extends JPanel implements MPFileUserManager.Listener {
 | 
			
		||||
        avatarButton.setIcon( Res.icons().avatar( selectedUser.getAvatar() ) );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setUser(@Nullable final MPUser<?> user) {
 | 
			
		||||
        avatarButton.setIcon( Res.icons().avatar( (user == null)? 0: user.getAvatar() ) );
 | 
			
		||||
 | 
			
		||||
        for (final Listener listener : listeners)
 | 
			
		||||
            listener.onUserSelected( user );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onFilesUpdated(final ImmutableSortedSet<MPFileUser> files) {
 | 
			
		||||
        usersModel.set( files );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public interface Listener {
 | 
			
		||||
 | 
			
		||||
        void onUserSelected(@Nullable MPUser<?> user);
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onUserSelected(@Nullable final MPUser<?> user) {
 | 
			
		||||
        usersModel.setSelectedItem( user );
 | 
			
		||||
        avatarButton.setIcon( Res.icons().avatar( (user == null)? 0: user.getAvatar() ) );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,10 +3,12 @@ package com.lyndir.masterpassword.gui.view;
 | 
			
		||||
import com.lyndir.lhunath.opal.system.logging.Logger;
 | 
			
		||||
import com.lyndir.masterpassword.gui.util.Components;
 | 
			
		||||
import com.lyndir.masterpassword.gui.util.Res;
 | 
			
		||||
import com.lyndir.masterpassword.model.MPUser;
 | 
			
		||||
import com.lyndir.masterpassword.model.impl.MPFileUserManager;
 | 
			
		||||
import java.awt.*;
 | 
			
		||||
import java.awt.event.ComponentAdapter;
 | 
			
		||||
import java.awt.event.ComponentEvent;
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import javax.swing.*;
 | 
			
		||||
import javax.swing.border.BevelBorder;
 | 
			
		||||
 | 
			
		||||
@@ -40,8 +42,6 @@ public class MasterPasswordFrame extends JFrame {
 | 
			
		||||
                BorderFactory.createBevelBorder( BevelBorder.RAISED, Res.colors().controlBorder(), Res.colors().frameBg() ),
 | 
			
		||||
                Res.colors().controlBg(), BoxLayout.PAGE_AXIS, userContent ), BorderLayout.CENTER );
 | 
			
		||||
 | 
			
		||||
        filesPanel.addListener( userContent );
 | 
			
		||||
 | 
			
		||||
        addComponentListener( new ComponentHandler() );
 | 
			
		||||
        setPreferredSize( new Dimension( 800, 560 ) );
 | 
			
		||||
        setDefaultCloseOperation( DISPOSE_ON_CLOSE );
 | 
			
		||||
 
 | 
			
		||||
@@ -8,15 +8,22 @@ import com.google.common.primitives.UnsignedInteger;
 | 
			
		||||
import com.lyndir.lhunath.opal.system.logging.Logger;
 | 
			
		||||
import com.lyndir.lhunath.opal.system.util.ObjectUtils;
 | 
			
		||||
import com.lyndir.masterpassword.*;
 | 
			
		||||
import com.lyndir.masterpassword.gui.MasterPassword;
 | 
			
		||||
import com.lyndir.masterpassword.gui.model.MPNewSite;
 | 
			
		||||
import com.lyndir.masterpassword.gui.util.*;
 | 
			
		||||
import com.lyndir.masterpassword.gui.util.Platform;
 | 
			
		||||
import com.lyndir.masterpassword.model.*;
 | 
			
		||||
import com.lyndir.masterpassword.model.impl.*;
 | 
			
		||||
import java.awt.*;
 | 
			
		||||
import java.awt.datatransfer.StringSelection;
 | 
			
		||||
import java.awt.datatransfer.Transferable;
 | 
			
		||||
import java.awt.event.*;
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.nio.file.Files;
 | 
			
		||||
import java.nio.file.StandardCopyOption;
 | 
			
		||||
import java.util.*;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
import java.util.concurrent.Future;
 | 
			
		||||
import java.util.concurrent.TimeUnit;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
@@ -31,20 +38,22 @@ import javax.swing.event.DocumentListener;
 | 
			
		||||
 * @author lhunath, 2018-07-14
 | 
			
		||||
 */
 | 
			
		||||
@SuppressWarnings("SerializableStoresNonSerializable")
 | 
			
		||||
public class UserContentPanel extends JPanel implements FilesPanel.Listener, MPUser.Listener {
 | 
			
		||||
public class UserContentPanel extends JPanel implements MasterPassword.Listener, MPUser.Listener {
 | 
			
		||||
 | 
			
		||||
    private static final Random  random     = new Random();
 | 
			
		||||
    private static final Logger  logger     = Logger.get( UserContentPanel.class );
 | 
			
		||||
    private static final JButton iconButton = Components.button( Res.icons().user(), null, null );
 | 
			
		||||
 | 
			
		||||
    private final JButton addButton = Components.button( Res.icons().add(), event -> addUser(),
 | 
			
		||||
                                                         "Add a new user to Master Password." );
 | 
			
		||||
    private final JButton addButton    = Components.button( Res.icons().add(), event -> addUser(),
 | 
			
		||||
                                                            "Add a new user to Master Password." );
 | 
			
		||||
    private final JButton importButton = Components.button( Res.icons().import_(), event -> importUser(),
 | 
			
		||||
                                                            "Import a user from a backup file into Master Password." );
 | 
			
		||||
 | 
			
		||||
    private final JPanel userToolbar = Components.panel( BoxLayout.PAGE_AXIS );
 | 
			
		||||
    private final JPanel siteToolbar = Components.panel( BoxLayout.PAGE_AXIS );
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    private MPUser<?>   activeUser;
 | 
			
		||||
    private MPUser<?>   showingUser;
 | 
			
		||||
    private ContentMode contentMode;
 | 
			
		||||
 | 
			
		||||
    public UserContentPanel() {
 | 
			
		||||
@@ -53,7 +62,9 @@ public class UserContentPanel extends JPanel implements FilesPanel.Listener, MPU
 | 
			
		||||
 | 
			
		||||
        setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) );
 | 
			
		||||
        setBorder( Components.marginBorder() );
 | 
			
		||||
        setUser( null );
 | 
			
		||||
        showUser( null );
 | 
			
		||||
 | 
			
		||||
        MasterPassword.get().addListener( this );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected JComponent getUserToolbar() {
 | 
			
		||||
@@ -66,52 +77,52 @@ public class UserContentPanel extends JPanel implements FilesPanel.Listener, MPU
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onUserSelected(@Nullable final MPUser<?> user) {
 | 
			
		||||
        setUser( user );
 | 
			
		||||
        showUser( user );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onUserUpdated(final MPUser<?> user) {
 | 
			
		||||
        setUser( user );
 | 
			
		||||
        showUser( user );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onUserAuthenticated(final MPUser<?> user) {
 | 
			
		||||
        setUser( user );
 | 
			
		||||
        showUser( user );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onUserInvalidated(final MPUser<?> user) {
 | 
			
		||||
        setUser( user );
 | 
			
		||||
        showUser( user );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void setUser(@Nullable final MPUser<?> user) {
 | 
			
		||||
    private void showUser(@Nullable final MPUser<?> user) {
 | 
			
		||||
        Res.ui( () -> {
 | 
			
		||||
            if (activeUser != null)
 | 
			
		||||
                activeUser.removeListener( this );
 | 
			
		||||
            if (showingUser != null)
 | 
			
		||||
                showingUser.removeListener( this );
 | 
			
		||||
 | 
			
		||||
            ContentMode newContentMode = ContentMode.getContentMode( user );
 | 
			
		||||
            if ((newContentMode != contentMode) || !ObjectUtils.equals( activeUser, user )) {
 | 
			
		||||
            if ((newContentMode != contentMode) || !ObjectUtils.equals( showingUser, user )) {
 | 
			
		||||
                userToolbar.removeAll();
 | 
			
		||||
                siteToolbar.removeAll();
 | 
			
		||||
                removeAll();
 | 
			
		||||
                activeUser = user;
 | 
			
		||||
                showingUser = user;
 | 
			
		||||
                switch (contentMode = newContentMode) {
 | 
			
		||||
                    case NO_USER:
 | 
			
		||||
                        add( new NoUserPanel() );
 | 
			
		||||
                        break;
 | 
			
		||||
                    case AUTHENTICATE:
 | 
			
		||||
                        add( new AuthenticateUserPanel( Preconditions.checkNotNull( activeUser ) ) );
 | 
			
		||||
                        add( new AuthenticateUserPanel( Preconditions.checkNotNull( showingUser ) ) );
 | 
			
		||||
                        break;
 | 
			
		||||
                    case AUTHENTICATED:
 | 
			
		||||
                        add( new AuthenticatedUserPanel( Preconditions.checkNotNull( activeUser ) ) );
 | 
			
		||||
                        add( new AuthenticatedUserPanel( Preconditions.checkNotNull( showingUser ) ) );
 | 
			
		||||
                        break;
 | 
			
		||||
                }
 | 
			
		||||
                revalidate();
 | 
			
		||||
                transferFocus();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (activeUser != null)
 | 
			
		||||
                activeUser.addListener( this );
 | 
			
		||||
            if (showingUser != null)
 | 
			
		||||
                showingUser.addListener( this );
 | 
			
		||||
        } );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -122,7 +133,70 @@ public class UserContentPanel extends JPanel implements FilesPanel.Listener, MPU
 | 
			
		||||
        if (fullName == null)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        setUser( MPFileUserManager.get().add( fullName.toString() ) );
 | 
			
		||||
        MasterPassword.get().activateUser( MPFileUserManager.get().add( fullName.toString() ) );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void importUser() {
 | 
			
		||||
        File importFile = Components.showLoadDialog( this, "Import User File" );
 | 
			
		||||
        if (importFile == null)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            MPFileUser importUser = MPFileUser.load( importFile );
 | 
			
		||||
            if (importUser == null) {
 | 
			
		||||
                JOptionPane.showMessageDialog(
 | 
			
		||||
                        this, "Not a Master Password file.",
 | 
			
		||||
                        "Import Failed", JOptionPane.ERROR_MESSAGE );
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            JPasswordField passwordField = Components.passwordField();
 | 
			
		||||
            if (JOptionPane.OK_OPTION == Components.showDialog( this, "Import User", new JOptionPane( Components.panel(
 | 
			
		||||
                    BoxLayout.PAGE_AXIS,
 | 
			
		||||
                    Components.label( strf( "<html>Enter the master password to import <strong>%s</strong>:</html>",
 | 
			
		||||
                                            importUser.getFullName() ) ),
 | 
			
		||||
                    Components.strut(),
 | 
			
		||||
                    passwordField ), JOptionPane.QUESTION_MESSAGE, JOptionPane.OK_CANCEL_OPTION ) {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void selectInitialValue() {
 | 
			
		||||
                    passwordField.requestFocusInWindow();
 | 
			
		||||
                }
 | 
			
		||||
            } )) {
 | 
			
		||||
                try {
 | 
			
		||||
                    importUser.authenticate( passwordField.getPassword() );
 | 
			
		||||
                    Optional<MPFileUser> existingUser = MPFileUserManager.get().getFiles().stream().filter(
 | 
			
		||||
                            user -> user.getFullName().equalsIgnoreCase( importUser.getFullName() ) ).findFirst();
 | 
			
		||||
                    if (existingUser.isPresent() && (JOptionPane.YES_OPTION != JOptionPane.showConfirmDialog(
 | 
			
		||||
                            this,
 | 
			
		||||
                            strf( "<html>Importing user <strong>%s</strong> from this file will replace the existing user with the imported one.<br>"
 | 
			
		||||
                                  + "Are you sure?<br><br>"
 | 
			
		||||
                                  + "<em>Existing user last modified: %s<br>Imported user last modified: %s</em></html>",
 | 
			
		||||
                                  importUser.getFullName(),
 | 
			
		||||
                                  Res.format( existingUser.get().getLastUsed() ),
 | 
			
		||||
                                  Res.format( importUser.getLastUsed() ) ) )))
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                    MasterPassword.get().activateUser( MPFileUserManager.get().add( importUser ) );
 | 
			
		||||
                }
 | 
			
		||||
                catch (final MPIncorrectMasterPasswordException | MPAlgorithmException e) {
 | 
			
		||||
                    JOptionPane.showMessageDialog(
 | 
			
		||||
                            this, e.getLocalizedMessage(),
 | 
			
		||||
                            "Import Failed", JOptionPane.ERROR_MESSAGE );
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch (final IOException e) {
 | 
			
		||||
            logger.err( e, "While reading user import file." );
 | 
			
		||||
            JOptionPane.showMessageDialog(
 | 
			
		||||
                    this, strf( "<html>Couldn't read import file:<br><pre>%s</pre></html>.", e.getLocalizedMessage() ),
 | 
			
		||||
                    "Import Failed", JOptionPane.ERROR_MESSAGE );
 | 
			
		||||
        }
 | 
			
		||||
        catch (MPMarshalException e) {
 | 
			
		||||
            logger.err( e, "While parsing user import file." );
 | 
			
		||||
            JOptionPane.showMessageDialog(
 | 
			
		||||
                    this, strf( "<html>Couldn't parse import file:<br><pre>%s</pre></html>.", e.getLocalizedMessage() ),
 | 
			
		||||
                    "Import Failed", JOptionPane.ERROR_MESSAGE );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private enum ContentMode {
 | 
			
		||||
@@ -147,6 +221,7 @@ public class UserContentPanel extends JPanel implements FilesPanel.Listener, MPU
 | 
			
		||||
            setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) );
 | 
			
		||||
 | 
			
		||||
            userToolbar.add( addButton );
 | 
			
		||||
            userToolbar.add( importButton );
 | 
			
		||||
 | 
			
		||||
            add( Box.createGlue() );
 | 
			
		||||
            add( Components.heading( "Select a user to proceed." ) );
 | 
			
		||||
@@ -160,6 +235,8 @@ public class UserContentPanel extends JPanel implements FilesPanel.Listener, MPU
 | 
			
		||||
        @Nonnull
 | 
			
		||||
        private final MPUser<?> user;
 | 
			
		||||
 | 
			
		||||
        private final JButton exportButton = Components.button( Res.icons().export(), event -> exportUser(),
 | 
			
		||||
                                                                "Export this user to a backup file." );
 | 
			
		||||
        private final JButton deleteButton = Components.button( Res.icons().delete(), event -> deleteUser(),
 | 
			
		||||
                                                                "Delete this user from Master Password." );
 | 
			
		||||
        private final JButton resetButton  = Components.button( Res.icons().reset(), event -> resetUser(),
 | 
			
		||||
@@ -177,6 +254,8 @@ public class UserContentPanel extends JPanel implements FilesPanel.Listener, MPU
 | 
			
		||||
            this.user = user;
 | 
			
		||||
 | 
			
		||||
            userToolbar.add( addButton );
 | 
			
		||||
            userToolbar.add( importButton );
 | 
			
		||||
            userToolbar.add( exportButton );
 | 
			
		||||
            userToolbar.add( deleteButton );
 | 
			
		||||
            userToolbar.add( resetButton );
 | 
			
		||||
 | 
			
		||||
@@ -197,6 +276,27 @@ public class UserContentPanel extends JPanel implements FilesPanel.Listener, MPU
 | 
			
		||||
            add( Box.createGlue() );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void exportUser() {
 | 
			
		||||
            MPFileUser fileUser = (user instanceof MPFileUser)? (MPFileUser) user: null;
 | 
			
		||||
            if (fileUser == null)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            File exportFile = Components.showSaveDialog( this, "Export User File", fileUser.getFile().getName() );
 | 
			
		||||
            if (exportFile == null)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                Platform.get().show(
 | 
			
		||||
                        Files.copy( fileUser.getFile().toPath(), exportFile.toPath(),
 | 
			
		||||
                                    StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES ).toFile() );
 | 
			
		||||
            }
 | 
			
		||||
            catch (final IOException e) {
 | 
			
		||||
                JOptionPane.showMessageDialog(
 | 
			
		||||
                        this, e.getLocalizedMessage(),
 | 
			
		||||
                        "Export Failed", JOptionPane.ERROR_MESSAGE );
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void deleteUser() {
 | 
			
		||||
            MPFileUser fileUser = (user instanceof MPFileUser)? (MPFileUser) user: null;
 | 
			
		||||
            if (fileUser == null)
 | 
			
		||||
@@ -372,7 +472,17 @@ public class UserContentPanel extends JPanel implements FilesPanel.Listener, MPU
 | 
			
		||||
            sitesModel.registerList( sitesList );
 | 
			
		||||
            add( Box.createGlue() );
 | 
			
		||||
 | 
			
		||||
            user.addListener( this );
 | 
			
		||||
            addComponentListener( new ComponentAdapter() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void componentShown(final ComponentEvent e) {
 | 
			
		||||
                    user.addListener( AuthenticatedUserPanel.this );
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void componentHidden(final ComponentEvent e) {
 | 
			
		||||
                    user.removeListener( AuthenticatedUserPanel.this );
 | 
			
		||||
                }
 | 
			
		||||
            } );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void showUserPreferences() {
 | 
			
		||||
 
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 1.9 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 3.1 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 1.9 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 3.1 KiB  | 
@@ -55,12 +55,11 @@ public interface MPUser<S extends MPSite<?>> extends Comparable<MPUser<?>> {
 | 
			
		||||
    /**
 | 
			
		||||
     * Performs an authentication attempt against the keyID for this user.
 | 
			
		||||
     *
 | 
			
		||||
     * Note: If a keyID is not set, authentication will always succeed and the keyID will be set to match the given master password.
 | 
			
		||||
     *
 | 
			
		||||
     * @param masterPassword The password to authenticate with.
 | 
			
		||||
     *                       You cannot re-use this array after passing it in, authentication will destroy its contents.
 | 
			
		||||
     *
 | 
			
		||||
     * @throws MPIncorrectMasterPasswordException If authentication fails due to the given master password not matching the user's keyID.
 | 
			
		||||
     * @apiNote If a keyID is not set, authentication will always succeed and the keyID will be set to match the given master password.
 | 
			
		||||
     * <b>This method destroys the contents of the {@code masterPassword} array.</b>
 | 
			
		||||
     */
 | 
			
		||||
    void authenticate(char[] masterPassword)
 | 
			
		||||
            throws MPIncorrectMasterPasswordException, MPAlgorithmException;
 | 
			
		||||
@@ -68,11 +67,10 @@ public interface MPUser<S extends MPSite<?>> extends Comparable<MPUser<?>> {
 | 
			
		||||
    /**
 | 
			
		||||
     * Performs an authentication attempt against the keyID for this user.
 | 
			
		||||
     *
 | 
			
		||||
     * Note: If a keyID is not set, authentication will always succeed and the keyID will be set to match the given key.
 | 
			
		||||
     *
 | 
			
		||||
     * @param masterKey The master key to authenticate with.
 | 
			
		||||
     *
 | 
			
		||||
     * @throws MPIncorrectMasterPasswordException If authentication fails due to the given master password not matching the user's keyID.
 | 
			
		||||
     * @apiNote If a keyID is not set, authentication will always succeed and the keyID will be set to match the given key.
 | 
			
		||||
     */
 | 
			
		||||
    void authenticate(MPMasterKey masterKey)
 | 
			
		||||
            throws MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException;
 | 
			
		||||
 
 | 
			
		||||
@@ -37,8 +37,9 @@ import javax.annotation.Nullable;
 | 
			
		||||
 */
 | 
			
		||||
public abstract class MPBasicUser<S extends MPBasicSite<?, ?>> extends Changeable implements MPUser<S> {
 | 
			
		||||
 | 
			
		||||
    protected final Logger        logger    = Logger.get( getClass() );
 | 
			
		||||
    private final   Set<Listener> listeners = new CopyOnWriteArraySet<>();
 | 
			
		||||
    private static final Logger logger = Logger.get( MPBasicUser.class );
 | 
			
		||||
 | 
			
		||||
    private final Set<Listener> listeners = new CopyOnWriteArraySet<>();
 | 
			
		||||
 | 
			
		||||
    private       int         avatar;
 | 
			
		||||
    private final String      fullName;
 | 
			
		||||
@@ -65,7 +66,7 @@ public abstract class MPBasicUser<S extends MPBasicSite<?, ?>> extends Changeabl
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void setAvatar(final int avatar) {
 | 
			
		||||
        if (Objects.equals(this.avatar, avatar))
 | 
			
		||||
        if (Objects.equals( this.avatar, avatar ))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        this.avatar = avatar;
 | 
			
		||||
@@ -86,7 +87,7 @@ public abstract class MPBasicUser<S extends MPBasicSite<?, ?>> extends Changeabl
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void setAlgorithm(final MPAlgorithm algorithm) {
 | 
			
		||||
        if (Objects.equals(this.algorithm, algorithm))
 | 
			
		||||
        if (Objects.equals( this.algorithm, algorithm ))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        this.algorithm = algorithm;
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
@@ -36,6 +37,8 @@ import org.joda.time.ReadableInstant;
 | 
			
		||||
@SuppressWarnings("ComparableImplementedButEqualsNotOverridden")
 | 
			
		||||
public class MPFileUser extends MPBasicUser<MPFileSite> {
 | 
			
		||||
 | 
			
		||||
    private static final Logger logger = Logger.get( MPFileUser.class );
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    private byte[]                   keyID;
 | 
			
		||||
    private File                     path;
 | 
			
		||||
@@ -46,13 +49,23 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
 | 
			
		||||
    private ReadableInstant lastUsed;
 | 
			
		||||
    private boolean         complete;
 | 
			
		||||
 | 
			
		||||
    public MPFileUser(final String fullName) {
 | 
			
		||||
        this( fullName, null, MPAlgorithm.Version.CURRENT.getAlgorithm() );
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public static MPFileUser load(final File file)
 | 
			
		||||
            throws IOException, MPMarshalException {
 | 
			
		||||
        for (final MPMarshalFormat format : MPMarshalFormat.values())
 | 
			
		||||
            if (file.getName().endsWith( format.fileSuffix() ))
 | 
			
		||||
                    return format.unmarshaller().readUser( file );
 | 
			
		||||
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm) {
 | 
			
		||||
    public MPFileUser(final String fullName, final File path) {
 | 
			
		||||
        this( fullName, null, MPAlgorithm.Version.CURRENT.getAlgorithm(), path );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm, final File path) {
 | 
			
		||||
        this( fullName, keyID, algorithm, 0, null, new Instant(),
 | 
			
		||||
              MPMarshaller.ContentMode.PROTECTED, MPMarshalFormat.DEFAULT, MPFileUserManager.get().getPath() );
 | 
			
		||||
              MPMarshaller.ContentMode.PROTECTED, MPMarshalFormat.DEFAULT, path );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm,
 | 
			
		||||
@@ -74,6 +87,10 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
 | 
			
		||||
        return (keyID == null)? null: keyID.clone();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setPath(final File path) {
 | 
			
		||||
        this.path = path;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void setAlgorithm(final MPAlgorithm algorithm) {
 | 
			
		||||
        if (!algorithm.equals( getAlgorithm() ) && (keyID != null)) {
 | 
			
		||||
@@ -99,7 +116,7 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setFormat(final MPMarshalFormat format) {
 | 
			
		||||
        if (Objects.equals(this.format, format))
 | 
			
		||||
        if (Objects.equals( this.format, format ))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        this.format = format;
 | 
			
		||||
@@ -111,7 +128,7 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setContentMode(final MPMarshaller.ContentMode contentMode) {
 | 
			
		||||
        if (Objects.equals(this.contentMode, contentMode))
 | 
			
		||||
        if (Objects.equals( this.contentMode, contentMode ))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        this.contentMode = contentMode;
 | 
			
		||||
@@ -123,7 +140,7 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setDefaultType(final MPResultType defaultType) {
 | 
			
		||||
        if (Objects.equals(this.defaultType, defaultType))
 | 
			
		||||
        if (Objects.equals( this.defaultType, defaultType ))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        this.defaultType = defaultType;
 | 
			
		||||
@@ -169,6 +186,19 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void save() {
 | 
			
		||||
        try {
 | 
			
		||||
            if (isComplete())
 | 
			
		||||
                getFormat().marshaller().marshall( this );
 | 
			
		||||
        }
 | 
			
		||||
        catch (final MPKeyUnavailableException e) {
 | 
			
		||||
            logger.wrn( e, "Cannot write out changes for unauthenticated user: %s.", this );
 | 
			
		||||
        }
 | 
			
		||||
        catch (final IOException | MPMarshalException | MPAlgorithmException e) {
 | 
			
		||||
            logger.err( e, "Unable to write out changes for user: %s", this );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void reset() {
 | 
			
		||||
        keyID = null;
 | 
			
		||||
@@ -183,16 +213,7 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onChanged() {
 | 
			
		||||
        try {
 | 
			
		||||
            if (isComplete())
 | 
			
		||||
                getFormat().marshaller().marshall( this );
 | 
			
		||||
        }
 | 
			
		||||
        catch (final MPKeyUnavailableException e) {
 | 
			
		||||
            logger.wrn( e, "Cannot write out changes for unauthenticated user: %s.", this );
 | 
			
		||||
        }
 | 
			
		||||
        catch (final IOException | MPMarshalException | MPAlgorithmException e) {
 | 
			
		||||
            logger.err( e, "Unable to write out changes for user: %s", this );
 | 
			
		||||
        }
 | 
			
		||||
        save();
 | 
			
		||||
 | 
			
		||||
        super.onChanged();
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -78,31 +78,40 @@ public class MPFileUserManager {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (final File file : pathFiles)
 | 
			
		||||
            for (final MPMarshalFormat format : MPMarshalFormat.values())
 | 
			
		||||
                if (file.getName().endsWith( format.fileSuffix() ))
 | 
			
		||||
                    try {
 | 
			
		||||
                        MPFileUser user         = format.unmarshaller().readUser( file );
 | 
			
		||||
                        MPFileUser previousUser = userByName.put( user.getFullName(), user );
 | 
			
		||||
                        if ((previousUser != null) && (previousUser.getFormat().ordinal() > user.getFormat().ordinal()))
 | 
			
		||||
                            userByName.put( previousUser.getFullName(), previousUser );
 | 
			
		||||
                        break;
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (final IOException | MPMarshalException e) {
 | 
			
		||||
                        logger.err( e, "Couldn't read user from: %s", file );
 | 
			
		||||
                    }
 | 
			
		||||
            try {
 | 
			
		||||
                MPFileUser user = MPFileUser.load( file );
 | 
			
		||||
                if (user != null) {
 | 
			
		||||
                    MPFileUser previousUser = userByName.put( user.getFullName(), user );
 | 
			
		||||
                    if ((previousUser != null) && (previousUser.getFormat().ordinal() > user.getFormat().ordinal()))
 | 
			
		||||
                        userByName.put( previousUser.getFullName(), previousUser );
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (final IOException | MPMarshalException e) {
 | 
			
		||||
                logger.err( e, "Couldn't read user from: %s", file );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        fireUpdated();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public MPFileUser add(final String fullName) {
 | 
			
		||||
        MPFileUser user = new MPFileUser( fullName );
 | 
			
		||||
        userByName.put( user.getFullName(), user );
 | 
			
		||||
        return add( new MPFileUser( fullName, getPath() ) );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public MPFileUser add(final MPFileUser user) {
 | 
			
		||||
        user.setPath( getPath() );
 | 
			
		||||
        user.save();
 | 
			
		||||
 | 
			
		||||
        MPFileUser oldUser = userByName.put( user.getFullName(), user );
 | 
			
		||||
        if (oldUser != null)
 | 
			
		||||
            oldUser.invalidate();
 | 
			
		||||
        fireUpdated();
 | 
			
		||||
 | 
			
		||||
        return user;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void delete(final MPFileUser user) {
 | 
			
		||||
        user.invalidate();
 | 
			
		||||
 | 
			
		||||
        // Remove deleted users.
 | 
			
		||||
        File userFile = user.getFile();
 | 
			
		||||
        if (userFile.exists() && !userFile.delete())
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user