Registration.java

package com.github.celldynamics.quimp.registration;

import java.awt.BorderLayout;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;

import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JEditorPane;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.SwingWorker;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.github.celldynamics.quimp.PropertyReader;
import com.github.celldynamics.quimp.QuimP;

import ij.Prefs;

/*
 * !>
 * @startuml doc-files/Registration_1_UML.png
 * actor User
 * activate Module
 * Module-->Registration : <<create>>
 * activate Registration
 * Registration->Registration : check if registered
 * Registration->UI : not registered, show UI
 * activate UI
 * == Cancelled ==
 * User -> UI : Cancel
 * UI->UI : wait
 * ... Wait ...
 * UI-->Registration
 * deactivate UI
 * Registration-->Module
 * destroy Registration
 * ... Continue with module...
 * Module->Module : Run()
 * deactivate Module
 * @enduml
 * 
 * @startuml doc-files/Registration_2_UML.png
 * actor User
 * activate Module
 * Module-->Registration : <<create>>
 * activate Registration
 * Registration->Registration : check if registered
 * Registration->UI : not registered, show UI
 * activate UI
 * == Apply ==
 * User -> UI : Apply
 * UI->UI : compare hashes
 * UI-->Registration
 * deactivate UI
 * Registration-->Module
 * destroy Registration
 * ... Continue with module...
 * Module->Module : Run()
 * deactivate Module
 * @enduml
 * 
 * @startuml doc-files/Registration_3_UML.png
 * start
 * :read reg details;
 * note right
 * From IJ_Prefs.txt file
 * end note
 * :compute hash from email;
 * note left
 * Validate whether hash and email
 * from prefs matches
 * end note
 * if(hash the same as in IJ_Prefs?) then (no)
 * 
 * :show registration UI;
 * if(Button) then (cancel)
 * :wait;
 * else (apply)
 * :Check registration;
 * if(registration correct) then (yes)
 * :Store in IJ_Prefs.txt;
 * else (no)
 * start
 * endif
 * endif
 * endif
 * stop
 * @enduml
 * 
 * @startuml doc-files/Registration_4_UML.png
 * [*] --> DisplayUI
 * DisplayUI : Cancel
 * DisplayUI : Apply
 * 
 * DisplayUI->Wait : Cancel
 * Wait -> [*]
 * 
 * DisplayUI->Register : Apply
 * Register : compute hashes\nand compare
 * Register-> DisplayUI : hash not valid
 * Register ->[*] : hash valid
 * @enduml
 * 
 * !<
 */
/**
 * Support registration checking.
 * 
 * <p>Registration component is self running. It should be located at very beginning of Module code.
 * It
 * blocks Module from execution until driving from Registration is returned to it.
 * 
 * <p>Registration process follows the scheme for cancel action: <br>
 * <img src="doc-files/Registration_1_UML.png"/><br>
 * And for register action: <br>
 * <img src="doc-files/Registration_2_UML.png"/><br>
 * Tests on first run: <br>
 * <img src="doc-files/Registration_3_UML.png"/><br>
 * UI transactions: <br>
 * <img src="doc-files/Registration_4_UML.png"/><br>
 * 
 * @author p.baniukiewicz
 *
 */
public class Registration extends JDialog implements ActionListener {

  /**
   * The Constant LOGGER.
   */
  static final Logger LOGGER = LoggerFactory.getLogger(Registration.class.getName());

  private JButton bnOk;
  private JButton bnCancel;
  /**
   * Flag that indicates that user has waited already.
   */
  public boolean waited = false;
  /**
   * How long to wait in sec.
   */
  private final int secondsToWait = 5;
  private JTextField tfEmail;
  private JTextField tfKey;
  private Window owner;

  private JPopupMenu popup;
  private JEditorPane helpArea;

  private static final long serialVersionUID = -3889439366816085913L;

  /**
   * Create and display registration window if necessary.
   * 
   * @param owner owner
   * @param title title
   */
  public Registration(Window owner, String title) {
    super(owner, title, ModalityType.APPLICATION_MODAL);
    this.owner = owner;
    // skip if debug mode
    String skipReg = new PropertyReader().readProperty("quimpconfig.properties", "noRegWindow");
    if (Boolean.parseBoolean(skipReg) == true) {
      LOGGER.info("Skipping reg window. Hope ony for tests... ");
      return;
    }
    // display reg window if not registered
    if (checkRegistration() == false) {
      build(title, true);
    }
  }

  /**
   * Construct object but does not display window.
   * 
   * <p>Allow to set some parameters before window construction.
   * 
   * @param owner owner
   */
  public Registration(Window owner) {
    this.owner = owner;
  }

  /**
   * Construct and display window.
   * 
   * @param title title
   * @param show indicate whether show window on screen
   */
  public void build(String title, boolean show) {
    buildMenu();
    buildWindow();
    setVisible(show);
  }

  /**
   * Build popup menu.
   */
  private void buildMenu() {
    popup = new JPopupMenu();
    // changing the name must follow with actionPerformed
    JMenuItem selectall = new JMenuItem("Select All");
    // changing the name must follow with actionPerformed
    JMenuItem copy = new JMenuItem("Copy");
    selectall.addActionListener(this);
    copy.addActionListener(this);
    popup.add(selectall);
    popup.add(copy);

  }

  /**
   * Build main window.
   */
  private void buildWindow() {
    JPanel wndpanel = new JPanel();
    wndpanel.setLayout(new BorderLayout());

    // ok, cancel row
    JPanel caButtons = new JPanel();
    caButtons.setLayout(new FlowLayout(FlowLayout.CENTER));
    bnOk = new JButton("Apply");
    bnOk.addActionListener(this);
    bnCancel = new JButton("Cancel");
    bnCancel.addActionListener(this);
    caButtons.add(bnOk);
    caButtons.add(bnCancel);
    wndpanel.add(caButtons, BorderLayout.SOUTH);
    // registration area and key and email
    JPanel centerarea = new JPanel();
    centerarea.setLayout(new BorderLayout());
    wndpanel.add(centerarea, BorderLayout.CENTER);
    // html info
    try {
      helpArea = new JEditorPane(getClass().getResource("reg.html").toURI().toURL());

      helpArea.setContentType("text/html");
      helpArea.setEditable(false);
      JScrollPane helpPanel = new JScrollPane(helpArea);
      helpPanel.setPreferredSize(new Dimension(500, 600));
      centerarea.add(helpPanel, BorderLayout.CENTER);
      // add mouse listeners
      helpArea.addMouseListener(new MouseAdapter() {
        @Override
        public void mousePressed(MouseEvent e) {
          maybeShowPopup(e);
        }

        @Override
        public void mouseReleased(MouseEvent e) {
          maybeShowPopup(e);
        }

        private void maybeShowPopup(MouseEvent e) {
          if (e.isPopupTrigger()) {
            popup.show(e.getComponent(), e.getX(), e.getY());
          }
        }
      });
      // add link listener
      helpArea.addHyperlinkListener(new HyperlinkListener() {
        public void hyperlinkUpdate(HyperlinkEvent e) {
          if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
            try {
              Desktop.getDesktop().browse(e.getURL().toURI());
            } catch (Exception e1) {
              LOGGER.error("Can not start browser: " + e1.getMessage());
              LOGGER.debug(e1.getMessage(), e1);
            }
          }
        }
      });
    } catch (IOException | URISyntaxException e2) {
      LOGGER.error("Can not read resource registration page: " + e2.getMessage());
      LOGGER.debug(e2.getMessage(), e2);
    }
    // email and key fields
    JPanel regarea = new JPanel();
    // regarea.setBackground(Color.YELLOW);
    regarea.setLayout(new GridLayout(3, 2));
    ((GridLayout) regarea.getLayout()).setHgap(10);
    ((GridLayout) regarea.getLayout()).setVgap(2);
    centerarea.add(regarea, BorderLayout.SOUTH);
    tfEmail = new JTextField(16);
    tfKey = new JTextField(16);
    regarea.add(new JLabel(""));
    regarea.add(new JLabel(""));

    regarea.add(tfEmail);
    regarea.add(new JLabel("Registration email"));

    regarea.add(tfKey);
    regarea.add(new JLabel("Your key"));

    add(wndpanel);
    if (owner != null) {
      setLocation(owner.getLocation());
    }
    setAlwaysOnTop(true);
    pack();
    setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
  }

  /*
   * (non-Javadoc)
   * 
   * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
   */
  @Override
  public void actionPerformed(ActionEvent e) {
    // clicked Cancel and user has not not waited yet
    if (e.getSource() == bnCancel && waited == false) {
      bnOk.setEnabled(false);
      bnCancel.setEnabled(false);
      Worker w = new Worker(secondsToWait); // will change button text
      w.execute();
      return;
    }
    // Cancel and waited already - quit
    if (e.getSource() == bnCancel && waited == true) {
      dispose();
      return;
    }
    // apply - get code, say thank you and exit
    if (e.getSource() == bnOk) {
      boolean ret = validateRegInfo(tfEmail.getText(), tfKey.getText());
      if (ret) {
        waited = true;
        registerUser(tfEmail.getText(), tfKey.getText());
        JOptionPane.showMessageDialog(this, "Thank you for registering our product.", "OK!",
                JOptionPane.INFORMATION_MESSAGE);
        dispose();
      } else {
        JOptionPane.showMessageDialog(this, "The key you provided does not match to the email.",
                "Error", JOptionPane.WARNING_MESSAGE);
      }
      return;
    }
    // support for action events
    switch (e.getActionCommand()) {
      case "Copy":
        helpArea.copy();
        break;
      case "Select All":
        helpArea.selectAll();
        break;
      default:
        throw new IllegalArgumentException("Unknown action.");
    }
  }

  /**
   * Add registration info to the IJ configuration.
   * 
   * @param email email
   * @param key key
   */
  private void registerUser(final String email, final String key) {
    Prefs.set("registration" + QuimP.QUIMP_PREFS_SUFFIX + ".mail", email);
    Prefs.set("registration" + QuimP.QUIMP_PREFS_SUFFIX + ".key", key);

  }

  /**
   * Read info from IJ container.
   * 
   * @return Array of [0] reg email, [1] key
   */
  public String[] readRegInfo() {
    String[] ret = new String[2];
    ret[0] = Prefs.get("registration" + QuimP.QUIMP_PREFS_SUFFIX + ".mail", "");
    ret[1] = Prefs.get("registration" + QuimP.QUIMP_PREFS_SUFFIX + ".key", "");
    return ret;
  }

  /**
   * Read registration details from IJ and fills the registration window.
   */
  public void fillRegForm() {
    String[] reg = readRegInfo();
    tfEmail.setText(reg[0]);
    tfKey.setText(reg[1]);
  }

  /**
   * Check if user is registered.
   * 
   * @return True if user registration info is available in IJ_Prefs.txt and data are valid.
   */
  private boolean checkRegistration() {
    String[] reginfo = readRegInfo();
    boolean ret = validateRegInfo(reginfo[0], reginfo[1]);
    return ret;
  }

  /**
   * Validate whether key matches to email.
   * 
   * @param email email
   * @param key key
   * @return <tt>true</tt> is key matches to email.
   */
  private boolean validateRegInfo(final String email, final String key) {
    if (email == null || key == null) {
      return false;
    }
    String emails = email.toLowerCase();
    String keys = key.toLowerCase();
    // remove all white chars
    emails = emails.replaceAll("\\s+", "");
    keys = keys.replaceAll("\\s+", "");
    if (email.isEmpty() || key.isEmpty()) {
      return false;
    }

    String salt = "secret"; // :)
    // add secret word
    emails = emails + salt;
    // compute hash
    try {
      MessageDigest md = MessageDigest.getInstance("MD5");
      md.update(emails.getBytes(StandardCharsets.UTF_8));
      StringBuilder sb = new StringBuilder();
      for (byte b : md.digest()) { // convert to char
        // dealing with conversion to int from byte and leading 0 for values smaller than 16
        String ss = Integer.toHexString(0x100 | (b & 0xff)).substring(1);
        sb.append(ss);
      }
      String digest = sb.toString().toLowerCase();
      LOGGER.trace("email: " + email + " Digest: " + digest);
      if (digest.equals(keys)) {
        return true;
      }
    } catch (NoSuchAlgorithmException e) {
      e.printStackTrace();
    }
    return false;
  }

  /**
   * Do counting and waiting in background updating UI in the same time.
   * 
   * @author p.baniukiewicz
   * @see <a href=
   *      "link">http://stackoverflow.com/questions/11817688/how-to-update-swing-gui-from-inside-a-long-method</a>
   */
  class Worker extends SwingWorker<String, String> {
    private int wait;
    private Dimension dc;

    /**
     * Instantiates a new worker.
     *
     * @param wait the wait
     */
    public Worker(int wait) {
      this.wait = wait;
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.swing.SwingWorker#doInBackground()
     */
    @Override
    protected String doInBackground() throws Exception {
      dc = bnCancel.getSize(); // remember size of button
      // This is what's called in the .execute method
      for (int i = 0; i < wait; i++) {
        // This sends the results to the .process method
        publish(String.valueOf(wait - i));
        Thread.sleep(1000);
      }
      // at the end of job reenable everything
      waited = true; // set flag on the end of wait
      bnOk.setEnabled(true);
      bnCancel.setEnabled(true);
      bnCancel.setText("Cancel");
      return null;
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.swing.SwingWorker#process(java.util.List)
     */
    @Override
    protected void process(List<String> item) {
      // This updates the UI
      bnCancel.setText(item.get(0));
      bnCancel.setPreferredSize(dc); // keep size as original
    }
  }

  /**
   * Support for popupmenu.
   * 
   * @author p.baniukiewicz
   *
   */
  class PopupListener extends MouseAdapter {

    /*
     * (non-Javadoc)
     * 
     * @see java.awt.event.MouseAdapter#mousePressed(java.awt.event.MouseEvent)
     */
    @Override
    public void mousePressed(MouseEvent e) {
      maybeShowPopup(e);
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.awt.event.MouseAdapter#mouseReleased(java.awt.event.MouseEvent)
     */
    @Override
    public void mouseReleased(MouseEvent e) {
      maybeShowPopup(e);
    }

    private void maybeShowPopup(MouseEvent e) {
      if (e.isPopupTrigger()) {
        popup.show(e.getComponent(), e.getX(), e.getY());
      }
    }
  }

}