OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

Blame | Last modification | View Log | RSS feed

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
 * 
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each file.
 */
 
 package org.openconcerto.ui.login;

import org.openconcerto.ui.DefaultGridBagConstraints;
import org.openconcerto.ui.ReloadPanel;
import org.openconcerto.ui.SwingThrottle;
import org.openconcerto.ui.component.combo.ISearchableCombo;
import org.openconcerto.ui.valuewrapper.EmptyValueWrapper;
import org.openconcerto.ui.valuewrapper.ValueWrapperFactory;
import org.openconcerto.utils.Base64;
import org.openconcerto.utils.JImage;
import org.openconcerto.utils.i18n.TM;
import org.openconcerto.utils.i18n.TranslationManager;
import org.openconcerto.utils.jsonrpc.JSONRPCClient;
import org.openconcerto.utils.text.SimpleDocumentListener;

import java.awt.Color;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JPopupMenu;
import javax.swing.JSeparator;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;

import net.minidev.json.JSONArray;
import net.minidev.json.JSONObject;

public class LoginFrame extends JFrame implements ActionListener, ConnectionStateListener {

    public static final int CONNECTION_NOT_INITIATED = -1;
    public static final int CONNECTION_INPROGRESS = 0;
    public static final int CONNECTION_OK = 1;
    public static final int CONNECTION_TIMEOUT = 2;
    public static final int CONNECTION_REFUSED = 3;
    public static final int CONNECTION_BROKEN = 4;
    private final LoginProperties loginProperties;

    // Login
    private EmptyValueWrapper<String> textLogin;
    // Password
    private JPasswordField textPassWord;
    private String encryptedPassword;
    private String clearPassword;
    private boolean allowStoredPass = true;

    // Company
    private ISearchableCombo<Company> comboCompany;

    private ReloadPanel reloadPanel;

    private CompanyIListModel model;

    // 1/ open an empty interface, rotate reloadpanel
    // 2/ fill with stored login/password/ defaultcompany (id+name)
    // 3a/ when connection to server is ready, stop reloadpanel
    // 3b/ when login+pass are entered, retrieve allowed companies and enable
    // connect when
    // connection to server is ready

    private final JCheckBox saveCheckBox = new JCheckBox(getTM().translate("loginPanel.storePass"));
    private final JButton buttonConnect = new JButton(getTM().translate("loginPanel.loginAction"));

    private final JLabel loginLabel = new JLabel(getTM().translate("loginPanel.loginLabel"));
    private final JLabel passwordLabel = new JLabel(getTM().translate("loginPanel.passLabel"));
    private final JLabel companyLabel = new JLabel(getTM().translate("loginPanel.companyLabel"));
    private String localeBaseName = null;
    private final List<Locale> localesToDisplay = new ArrayList<Locale>();
    private final JButton langButton = new JButton(Locale.ROOT.getLanguage());
    private String company;
    private final JSONRPCClient client;
    private int connectionState = CONNECTION_NOT_INITIATED;
    private final ExecutorService executorService = Executors.newFixedThreadPool(1);
    private final String serverUrl;
    private String context;
    private int idCompany;
    private SwingThrottle tCheckValidity;

    public LoginFrame(String serverUrl, final JSONRPCClient client, String context) throws IOException {
        this.client = client;
        this.serverUrl = serverUrl;
        this.context = context;
        this.setContentPane(createLoginPanel());
        // load stored info
        System.err.println("LoginFrame.LoginFrame()" + serverUrl);
        this.loginProperties = new LoginProperties(serverUrl);
        this.loginProperties.load();

        final Company storedDefaultCompany = new Company(this.loginProperties.getLastCompanyId(), this.loginProperties.getLastCompanyName());
        this.init(this.loginProperties.getLastLoginName(), this.loginProperties.getEncryptedStoredPassword(), storedDefaultCompany);

    }

    public synchronized int getConnectionState() {
        return this.connectionState;
    }

    public synchronized void setConnectionState(int connectionState) {
        this.connectionState = connectionState;
    }

    private TM getTM() {
        return org.openconcerto.utils.i18n.TM.getInstance();
    }

    private void init(String storedLogin, String storedEncryptedPassword, Company storedDefaultCompany) {
        this.textLogin.setValue(storedLogin);

        if (this.saveCheckBox != null && storedEncryptedPassword != null && storedEncryptedPassword.length() > 0) {
            this.saveCheckBox.setSelected(true);
        }
        // to show the user its password has been retrieved
        if (storedEncryptedPassword != null) {
            final char[] s = new char[8];
            Arrays.fill(s, ' ');
            this.textPassWord.setText(new String(s));
            this.clearPassword = null;
        } else
            this.clearPassword = "";

        this.model = new CompanyIListModel();
        this.comboCompany.initCache(this.model);

        this.tCheckValidity = new SwingThrottle(800, new Runnable() {

            @Override
            public void run() {
                checkValidity();
            }

        });
    }

    private JPanel createLoginPanel() {
        JPanel panel = new JPanel();

        panel.setLayout(new GridBagLayout());

        final GridBagConstraints c = new GridBagConstraints();
        c.gridheight = 1;
        c.gridwidth = 2;
        c.gridx = 0;
        c.gridy = 0;
        c.weightx = 1;
        c.weighty = 0;
        c.insets = new Insets(0, 0, 0, 0);
        c.fill = GridBagConstraints.HORIZONTAL;

        // Logo

        JImage imageLogo = new JImage(LoginFrame.class.getResource("OpenConcerto_login.png"));

        imageLogo.setBackground(Color.WHITE);
        imageLogo.check();
        panel.add(imageLogo, c);
        c.gridy++;
        c.gridwidth = GridBagConstraints.REMAINDER;
        panel.add(new JSeparator(SwingConstants.HORIZONTAL), c);

        // Login
        c.insets = new Insets(2, 2, 1, 2);
        c.gridy++;
        c.gridwidth = 1;
        c.weightx = 0;

        this.loginLabel.setHorizontalAlignment(SwingConstants.RIGHT);
        panel.add(this.loginLabel, c);

        this.textLogin = new EmptyValueWrapper<String>(ValueWrapperFactory.create(new JTextField(), String.class));

        c.gridx++;
        c.weightx = 1;
        panel.add(this.textLogin.getComp(), c);
        ((JTextField) this.textLogin.getComp()).addActionListener(this);
        this.textLogin.addValueListener(new PropertyChangeListener() {
            public void propertyChange(final PropertyChangeEvent evt) {
                LoginFrame.this.tCheckValidity.execute();
            }
        });

        // Password
        c.gridy++;
        c.gridx = 0;
        c.weightx = 0;
        System.err.println("LoginFrame.createLoginPanel()22c");
        this.passwordLabel.setHorizontalAlignment(SwingConstants.RIGHT);
        panel.add(this.passwordLabel, c);

        this.textPassWord = new JPasswordField();

        c.gridx++;
        c.weightx = 1;
        panel.add(this.textPassWord, c);
        this.textPassWord.getDocument().addDocumentListener(new SimpleDocumentListener() {
            public void update(final DocumentEvent e) {
                LoginFrame.this.clearPassword = String.valueOf(LoginFrame.this.textPassWord.getPassword());
                LoginFrame.this.tCheckValidity.execute();
            }
        });
        this.textPassWord.addActionListener(this);
        System.err.println("LoginFrame.createLoginPanel()22d");
        // Societe

        c.gridy++;
        c.gridx = 0;
        c.weightx = 0;

        this.companyLabel.setHorizontalAlignment(SwingConstants.RIGHT);
        panel.add(this.companyLabel, c);

        this.comboCompany = new ISearchableCombo<Company>(true);
        this.comboCompany.setEnabled(false);

        c.gridx++;
        panel.add(this.comboCompany, c);

        // Button
        final JPanel panelButton = new JPanel();
        panelButton.setOpaque(false);
        panelButton.setLayout(new GridBagLayout());
        final GridBagConstraints c2 = new DefaultGridBagConstraints();
        c2.weightx = 1;
        if (this.allowStoredPass) {

            this.saveCheckBox.setOpaque(false);
            panelButton.add(this.saveCheckBox, c2);
            c2.weightx = 0;

        }
        c2.gridx++;

        // Language selector
        //
        this.langButton.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                final JPopupMenu menu = new JPopupMenu();
                final Locale locale = Locale.getDefault();
                for (final Locale l : LoginFrame.this.localesToDisplay) {
                    final JCheckBoxMenuItem menuItem = new JCheckBoxMenuItem(l.getDisplayName(l));
                    if (l.equals(locale)) {
                        menuItem.setSelected(true);
                    }
                    menu.add(menuItem);
                    menuItem.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
                            setUILanguage(l);
                        }
                    });
                }

                menu.show(LoginFrame.this.langButton, 0, 0);

            }
        });
        this.langButton.setOpaque(false);
        this.langButton.setBorderPainted(false);
        this.langButton.setContentAreaFilled(false);
        this.langButton.setBorder(null);
        this.langButton.setFocusable(false);
        this.langButton.setVisible(false);
        panelButton.add(this.langButton, c2);
        c2.gridx++;
        this.reloadPanel = new ReloadPanel();
        this.reloadPanel.setOpaque(false);

        this.reloadPanel.setMode(ReloadPanel.MODE_EMPTY);
        panelButton.add(this.reloadPanel, c2);
        c2.gridx++;
        c2.weightx = 0;
        this.buttonConnect.setEnabled(false);
        this.buttonConnect.setOpaque(false);
        panelButton.add(this.buttonConnect, c2);

        c.gridy++;
        c.gridx = 0;
        c.weightx = 0;
        c.gridwidth = GridBagConstraints.REMAINDER;
        c.weighty = 1;
        panel.add(panelButton, c);

        this.buttonConnect.addActionListener(this);

        initLocalization("org.openconcerto.ui.login.Messages",
                Arrays.asList(Locale.FRANCE, Locale.CANADA_FRENCH, new Locale("fr", "CH"), new Locale("fr", "BE"), Locale.UK, Locale.CANADA, Locale.US, Locale.GERMANY, new Locale("de", "CH")));

        setUILanguage(Locale.getDefault());

        return panel;
    }

    private void checkValidity() {
        String login = this.textLogin.getValue();
        this.buttonConnect.setEnabled(login != null && !login.trim().isEmpty() && this.comboCompany.getSelectedItem() != null);

        // this.buttonConnect.setEnabled(this.connectionAllowed == null &&
        // this.areFieldsValidated());
        // this.buttonConnect.setToolTipText(this.connectionAllowed);
        System.err.println("LoginFrame.checkValidity()");
        String ePassword = this.encryptedPassword;
        if (ePassword == null) {
            if (this.clearPassword == null) {
                this.clearPassword = "";
            }
            ePassword = encodePassword(this.clearPassword);
        }
        // Empty login is not allowed
        if (login == null || login.trim().isEmpty()) {
            return;
        }
        this.reloadPanel.setMode(ReloadPanel.MODE_ROTATE);
        try {
            sendAllowedCompanyRequest(login, ePassword);
        } catch (Exception e) {
            // Connection to server impossible
            this.reloadPanel.setMode(ReloadPanel.MODE_BLINK);
            e.printStackTrace();
        }
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        if (this.comboCompany.getSelectedItem() == null) {
            return;
        }

        // Connect
        this.setEnabled(false);
        this.reloadPanel.setMode(ReloadPanel.MODE_ROTATE);
        String ePassword = this.encryptedPassword;
        if (ePassword == null) {
            if (this.clearPassword == null) {
                this.clearPassword = "";
            }
            ePassword = encodePassword(this.clearPassword);
        }
        this.company = this.comboCompany.getSelectedItem().getName();
        sendLoginRequest(this.textLogin.getValue(), ePassword, this.comboCompany.getSelectedItem().getId(), this.context);

    }

    @Override
    public void stateChanged(final int oldState, final int newState) {
        if (SwingUtilities.isEventDispatchThread()) {
            throw new IllegalAccessError("Must not be called in EventDispatchThread");
        }

        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                if (newState == CONNECTION_REFUSED || newState == CONNECTION_TIMEOUT) {
                    LoginFrame.this.reloadPanel.setMode(ReloadPanel.MODE_BLINK);
                } else if (newState == CONNECTION_OK) {
                    LoginFrame.this.reloadPanel.setMode(ReloadPanel.MODE_EMPTY);
                } else {
                    LoginFrame.this.reloadPanel.setMode(ReloadPanel.MODE_BLINK);
                    // Connection to server lost
                    // FIXME: autoreconnect after 10s
                }

            }
        });

    }

    @Override
    public void setAllowedCompanies(final List<Company> list) {
        if (SwingUtilities.isEventDispatchThread()) {
            throw new IllegalAccessError("Must not be called in EventDispatchThread");
        }

        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {

                LoginFrame.this.model.updateFrom(list);
                if (!list.isEmpty()) {
                    LoginFrame.this.comboCompany.setEnabled(true);
                    for (Company company : list) {
                        // Reselect from saved company
                        if (company.getId() == LoginFrame.this.loginProperties.getLastCompanyId()) {
                            LoginFrame.this.comboCompany.setValue(company);
                            break;
                        }
                    }

                    if (LoginFrame.this.comboCompany.getSelectedItem() == null) {
                        LoginFrame.this.comboCompany.setSelectedIndex(0);
                    }
                    LoginFrame.this.buttonConnect.setEnabled(!LoginFrame.this.textLogin.getValue().trim().isEmpty() && LoginFrame.this.comboCompany.getSelectedItem() != null);
                } else {
                    LoginFrame.this.buttonConnect.setEnabled(false);
                }
                LoginFrame.this.reloadPanel.setMode(ReloadPanel.MODE_EMPTY);
            }
        });
    }

    @Override
    public void setEnabled(boolean b) {
        if (b) {
            this.reloadPanel.setMode(ReloadPanel.MODE_EMPTY);
        }
        this.textLogin.getComp().setEnabled(b);
        this.textPassWord.setEnabled(b);
        this.comboCompany.setEnabled(b);
        this.saveCheckBox.setEnabled(b);
        this.buttonConnect.setEnabled(b);
    }

    private final void initLocalization(final String baseName, final List<Locale> toDisplay) {
        if (baseName == null)
            throw new NullPointerException("Null baseName");
        if (this.localeBaseName != null)
            throw new IllegalStateException("Already inited to " + this.localeBaseName);
        this.localeBaseName = baseName;
        this.localesToDisplay.addAll(toDisplay);
        // this.setUILanguage(UserProps.getInstance().getLocale());
        TM.getInstance();
    }

    private void setUILanguage(Locale locale) {
        System.err.println("LoginFrame.setUILanguage():" + this.localeBaseName + " locale:" + locale);
        final ResourceBundle bundle = ResourceBundle.getBundle(this.localeBaseName, locale, TranslationManager.getControl());
        this.loginLabel.setText(bundle.getString("loginLabel"));
        this.passwordLabel.setText(bundle.getString("passwordLabel"));
        this.companyLabel.setText(bundle.getString("companyLabel"));
        this.saveCheckBox.setText(bundle.getString("saveCheckBox"));
        this.buttonConnect.setText(bundle.getString("buttonConnect"));
        this.langButton.setText(locale.getLanguage());
        this.langButton.setVisible(true);
        Locale.setDefault(locale);
    }

    private static MessageDigest md;

    private static synchronized MessageDigest getMessageDigest() {
        if (md == null)
            try {
                md = MessageDigest.getInstance("SHA-1");
            } catch (NoSuchAlgorithmException e) {
                // should be standard
                throw new IllegalStateException("no SHA1", e);
            }
        return md;
    }

    public static String encodePassword(String clearPassword) {
        final byte[] s;
        synchronized (getMessageDigest()) {
            getMessageDigest().reset();
            getMessageDigest().update(clearPassword.getBytes());
            s = getMessageDigest().digest();
        }
        return Base64.encodeBytes(s);
    }

    public String getCompany() {
        return this.company;
    }

    public void saveAndDispose() throws IOException {
        this.loginProperties.setLastCompanyId(this.comboCompany.getSelectedItem().getId());
        this.loginProperties.setLastCompanyName(this.comboCompany.getSelectedItem().getName());
        this.loginProperties.setLastLoginName(this.textLogin.getValue());
        this.loginProperties.store();
        dispose();
    }

    private void sendAllowedCompanyRequest(String login, String passwordHash) {
        this.executorService.execute(new Runnable() {

            @Override
            public void run() {
                LoginFrame.this.client.setCredentials(login, passwordHash);
                final JSONObject params = new JSONObject();
                params.put("login", login);
                try {
                    JSONObject result = (JSONObject) LoginFrame.this.client.rpcCall(LoginFrame.this.serverUrl, "getAllowedCompanies", params);
                    JSONArray array = (JSONArray) result.get("companies");
                    List<Company> list = new ArrayList<>();
                    for (Object o : array) {
                        final Company c = new Company();
                        c.fromJSON((JSONObject) o);
                        list.add(c);
                    }
                    setAllowedCompanies(list);

                } catch (IOException e) {
                    setAllowedCompanies(Collections.emptyList());
                    e.printStackTrace();
                }

            }
        });
    }

    /**
     * @param encodedPassword : Base64 encodded hash of the password
     */
    private void sendLoginRequest(String login, String encodedPassword, int idCompany, final String context) {
        this.executorService.execute(new Runnable() {

            @Override
            public void run() {
                stateChanged(getConnectionState(), CONNECTION_INPROGRESS);
                LoginFrame.this.client.setCredentials(login, encodedPassword);
                final JSONObject params = new JSONObject();
                params.put("company-id", idCompany);
                params.put("context", context);
                try {
                    JSONObject result = (JSONObject) LoginFrame.this.client.rpcCall(LoginFrame.this.serverUrl, "login", params);
                    if (result.getAsString("status").equals("granted")) {
                        LoginFrame.this.idCompany = idCompany;
                        stateChanged(getConnectionState(), CONNECTION_OK);
                    } else {
                        stateChanged(getConnectionState(), CONNECTION_REFUSED);
                    }
                } catch (IOException e) {
                    stateChanged(getConnectionState(), CONNECTION_BROKEN);
                }

            }
        });

    }

    public int getCompanyId() {
        return this.idCompany;
    }
}