View Javadoc
1   package com.github.celldynamics.quimp.registration;
2   
3   import java.awt.BorderLayout;
4   import java.awt.Desktop;
5   import java.awt.Dimension;
6   import java.awt.FlowLayout;
7   import java.awt.GridLayout;
8   import java.awt.Window;
9   import java.awt.event.ActionEvent;
10  import java.awt.event.ActionListener;
11  import java.awt.event.MouseAdapter;
12  import java.awt.event.MouseEvent;
13  import java.io.IOException;
14  import java.net.URISyntaxException;
15  import java.nio.charset.StandardCharsets;
16  import java.security.MessageDigest;
17  import java.security.NoSuchAlgorithmException;
18  import java.util.List;
19  
20  import javax.swing.JButton;
21  import javax.swing.JDialog;
22  import javax.swing.JEditorPane;
23  import javax.swing.JFrame;
24  import javax.swing.JLabel;
25  import javax.swing.JMenuItem;
26  import javax.swing.JOptionPane;
27  import javax.swing.JPanel;
28  import javax.swing.JPopupMenu;
29  import javax.swing.JScrollPane;
30  import javax.swing.JTextField;
31  import javax.swing.SwingWorker;
32  import javax.swing.event.HyperlinkEvent;
33  import javax.swing.event.HyperlinkListener;
34  
35  import org.slf4j.Logger;
36  import org.slf4j.LoggerFactory;
37  
38  import com.github.celldynamics.quimp.PropertyReader;
39  import com.github.celldynamics.quimp.QuimP;
40  
41  import ij.Prefs;
42  
43  /*
44   * !>
45   * @startuml doc-files/Registration_1_UML.png
46   * actor User
47   * activate Module
48   * Module-->Registration : <<create>>
49   * activate Registration
50   * Registration->Registration : check if registered
51   * Registration->UI : not registered, show UI
52   * activate UI
53   * == Cancelled ==
54   * User -> UI : Cancel
55   * UI->UI : wait
56   * ... Wait ...
57   * UI-->Registration
58   * deactivate UI
59   * Registration-->Module
60   * destroy Registration
61   * ... Continue with module...
62   * Module->Module : Run()
63   * deactivate Module
64   * @enduml
65   * 
66   * @startuml doc-files/Registration_2_UML.png
67   * actor User
68   * activate Module
69   * Module-->Registration : <<create>>
70   * activate Registration
71   * Registration->Registration : check if registered
72   * Registration->UI : not registered, show UI
73   * activate UI
74   * == Apply ==
75   * User -> UI : Apply
76   * UI->UI : compare hashes
77   * UI-->Registration
78   * deactivate UI
79   * Registration-->Module
80   * destroy Registration
81   * ... Continue with module...
82   * Module->Module : Run()
83   * deactivate Module
84   * @enduml
85   * 
86   * @startuml doc-files/Registration_3_UML.png
87   * start
88   * :read reg details;
89   * note right
90   * From IJ_Prefs.txt file
91   * end note
92   * :compute hash from email;
93   * note left
94   * Validate whether hash and email
95   * from prefs matches
96   * end note
97   * if(hash the same as in IJ_Prefs?) then (no)
98   * 
99   * :show registration UI;
100  * if(Button) then (cancel)
101  * :wait;
102  * else (apply)
103  * :Check registration;
104  * if(registration correct) then (yes)
105  * :Store in IJ_Prefs.txt;
106  * else (no)
107  * start
108  * endif
109  * endif
110  * endif
111  * stop
112  * @enduml
113  * 
114  * @startuml doc-files/Registration_4_UML.png
115  * [*] --> DisplayUI
116  * DisplayUI : Cancel
117  * DisplayUI : Apply
118  * 
119  * DisplayUI->Wait : Cancel
120  * Wait -> [*]
121  * 
122  * DisplayUI->Register : Apply
123  * Register : compute hashes\nand compare
124  * Register-> DisplayUI : hash not valid
125  * Register ->[*] : hash valid
126  * @enduml
127  * 
128  * !<
129  */
130 /**
131  * Support registration checking.
132  * 
133  * <p>Registration component is self running. It should be located at very beginning of Module code.
134  * It
135  * blocks Module from execution until driving from Registration is returned to it.
136  * 
137  * <p>Registration process follows the scheme for cancel action: <br>
138  * <img src="doc-files/Registration_1_UML.png"/><br>
139  * And for register action: <br>
140  * <img src="doc-files/Registration_2_UML.png"/><br>
141  * Tests on first run: <br>
142  * <img src="doc-files/Registration_3_UML.png"/><br>
143  * UI transactions: <br>
144  * <img src="doc-files/Registration_4_UML.png"/><br>
145  * 
146  * @author p.baniukiewicz
147  *
148  */
149 public class Registration extends JDialog implements ActionListener {
150 
151   /**
152    * The Constant LOGGER.
153    */
154   static final Logger LOGGER = LoggerFactory.getLogger(Registration.class.getName());
155 
156   private JButton bnOk;
157   private JButton bnCancel;
158   /**
159    * Flag that indicates that user has waited already.
160    */
161   public boolean waited = false;
162   /**
163    * How long to wait in sec.
164    */
165   private final int secondsToWait = 5;
166   private JTextField tfEmail;
167   private JTextField tfKey;
168   private Window owner;
169 
170   private JPopupMenu popup;
171   private JEditorPane helpArea;
172 
173   private static final long serialVersionUID = -3889439366816085913L;
174 
175   /**
176    * Create and display registration window if necessary.
177    * 
178    * @param owner owner
179    * @param title title
180    */
181   public Registration(Window owner, String title) {
182     super(owner, title, ModalityType.APPLICATION_MODAL);
183     this.owner = owner;
184     // skip if debug mode
185     String skipReg = new PropertyReader().readProperty("quimpconfig.properties", "noRegWindow");
186     if (Boolean.parseBoolean(skipReg) == true) {
187       LOGGER.info("Skipping reg window. Hope ony for tests... ");
188       return;
189     }
190     // display reg window if not registered
191     if (checkRegistration() == false) {
192       build(title, true);
193     }
194   }
195 
196   /**
197    * Construct object but does not display window.
198    * 
199    * <p>Allow to set some parameters before window construction.
200    * 
201    * @param owner owner
202    */
203   public Registration(Window owner) {
204     this.owner = owner;
205   }
206 
207   /**
208    * Construct and display window.
209    * 
210    * @param title title
211    * @param show indicate whether show window on screen
212    */
213   public void build(String title, boolean show) {
214     buildMenu();
215     buildWindow();
216     setVisible(show);
217   }
218 
219   /**
220    * Build popup menu.
221    */
222   private void buildMenu() {
223     popup = new JPopupMenu();
224     // changing the name must follow with actionPerformed
225     JMenuItem selectall = new JMenuItem("Select All");
226     // changing the name must follow with actionPerformed
227     JMenuItem copy = new JMenuItem("Copy");
228     selectall.addActionListener(this);
229     copy.addActionListener(this);
230     popup.add(selectall);
231     popup.add(copy);
232 
233   }
234 
235   /**
236    * Build main window.
237    */
238   private void buildWindow() {
239     JPanel wndpanel = new JPanel();
240     wndpanel.setLayout(new BorderLayout());
241 
242     // ok, cancel row
243     JPanel caButtons = new JPanel();
244     caButtons.setLayout(new FlowLayout(FlowLayout.CENTER));
245     bnOk = new JButton("Apply");
246     bnOk.addActionListener(this);
247     bnCancel = new JButton("Cancel");
248     bnCancel.addActionListener(this);
249     caButtons.add(bnOk);
250     caButtons.add(bnCancel);
251     wndpanel.add(caButtons, BorderLayout.SOUTH);
252     // registration area and key and email
253     JPanel centerarea = new JPanel();
254     centerarea.setLayout(new BorderLayout());
255     wndpanel.add(centerarea, BorderLayout.CENTER);
256     // html info
257     try {
258       helpArea = new JEditorPane(getClass().getResource("reg.html").toURI().toURL());
259 
260       helpArea.setContentType("text/html");
261       helpArea.setEditable(false);
262       JScrollPane helpPanel = new JScrollPane(helpArea);
263       helpPanel.setPreferredSize(new Dimension(500, 600));
264       centerarea.add(helpPanel, BorderLayout.CENTER);
265       // add mouse listeners
266       helpArea.addMouseListener(new MouseAdapter() {
267         @Override
268         public void mousePressed(MouseEvent e) {
269           maybeShowPopup(e);
270         }
271 
272         @Override
273         public void mouseReleased(MouseEvent e) {
274           maybeShowPopup(e);
275         }
276 
277         private void maybeShowPopup(MouseEvent e) {
278           if (e.isPopupTrigger()) {
279             popup.show(e.getComponent(), e.getX(), e.getY());
280           }
281         }
282       });
283       // add link listener
284       helpArea.addHyperlinkListener(new HyperlinkListener() {
285         public void hyperlinkUpdate(HyperlinkEvent e) {
286           if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
287             try {
288               Desktop.getDesktop().browse(e.getURL().toURI());
289             } catch (Exception e1) {
290               LOGGER.error("Can not start browser: " + e1.getMessage());
291               LOGGER.debug(e1.getMessage(), e1);
292             }
293           }
294         }
295       });
296     } catch (IOException | URISyntaxException e2) {
297       LOGGER.error("Can not read resource registration page: " + e2.getMessage());
298       LOGGER.debug(e2.getMessage(), e2);
299     }
300     // email and key fields
301     JPanel regarea = new JPanel();
302     // regarea.setBackground(Color.YELLOW);
303     regarea.setLayout(new GridLayout(3, 2));
304     ((GridLayout) regarea.getLayout()).setHgap(10);
305     ((GridLayout) regarea.getLayout()).setVgap(2);
306     centerarea.add(regarea, BorderLayout.SOUTH);
307     tfEmail = new JTextField(16);
308     tfKey = new JTextField(16);
309     regarea.add(new JLabel(""));
310     regarea.add(new JLabel(""));
311 
312     regarea.add(tfEmail);
313     regarea.add(new JLabel("Registration email"));
314 
315     regarea.add(tfKey);
316     regarea.add(new JLabel("Your key"));
317 
318     add(wndpanel);
319     if (owner != null) {
320       setLocation(owner.getLocation());
321     }
322     setAlwaysOnTop(true);
323     pack();
324     setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
325   }
326 
327   /*
328    * (non-Javadoc)
329    * 
330    * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
331    */
332   @Override
333   public void actionPerformed(ActionEvent e) {
334     // clicked Cancel and user has not not waited yet
335     if (e.getSource() == bnCancel && waited == false) {
336       bnOk.setEnabled(false);
337       bnCancel.setEnabled(false);
338       Worker w = new Worker(secondsToWait); // will change button text
339       w.execute();
340       return;
341     }
342     // Cancel and waited already - quit
343     if (e.getSource() == bnCancel && waited == true) {
344       dispose();
345       return;
346     }
347     // apply - get code, say thank you and exit
348     if (e.getSource() == bnOk) {
349       boolean ret = validateRegInfo(tfEmail.getText(), tfKey.getText());
350       if (ret) {
351         waited = true;
352         registerUser(tfEmail.getText(), tfKey.getText());
353         JOptionPane.showMessageDialog(this, "Thank you for registering our product.", "OK!",
354                 JOptionPane.INFORMATION_MESSAGE);
355         dispose();
356       } else {
357         JOptionPane.showMessageDialog(this, "The key you provided does not match to the email.",
358                 "Error", JOptionPane.WARNING_MESSAGE);
359       }
360       return;
361     }
362     // support for action events
363     switch (e.getActionCommand()) {
364       case "Copy":
365         helpArea.copy();
366         break;
367       case "Select All":
368         helpArea.selectAll();
369         break;
370       default:
371         throw new IllegalArgumentException("Unknown action.");
372     }
373   }
374 
375   /**
376    * Add registration info to the IJ configuration.
377    * 
378    * @param email email
379    * @param key key
380    */
381   private void registerUser(final String email, final String key) {
382     Prefs.set("registration" + QuimP.QUIMP_PREFS_SUFFIX + ".mail", email);
383     Prefs.set("registration" + QuimP.QUIMP_PREFS_SUFFIX + ".key", key);
384 
385   }
386 
387   /**
388    * Read info from IJ container.
389    * 
390    * @return Array of [0] reg email, [1] key
391    */
392   public String[] readRegInfo() {
393     String[] ret = new String[2];
394     ret[0] = Prefs.get("registration" + QuimP.QUIMP_PREFS_SUFFIX + ".mail", "");
395     ret[1] = Prefs.get("registration" + QuimP.QUIMP_PREFS_SUFFIX + ".key", "");
396     return ret;
397   }
398 
399   /**
400    * Read registration details from IJ and fills the registration window.
401    */
402   public void fillRegForm() {
403     String[] reg = readRegInfo();
404     tfEmail.setText(reg[0]);
405     tfKey.setText(reg[1]);
406   }
407 
408   /**
409    * Check if user is registered.
410    * 
411    * @return True if user registration info is available in IJ_Prefs.txt and data are valid.
412    */
413   private boolean checkRegistration() {
414     String[] reginfo = readRegInfo();
415     boolean ret = validateRegInfo(reginfo[0], reginfo[1]);
416     return ret;
417   }
418 
419   /**
420    * Validate whether key matches to email.
421    * 
422    * @param email email
423    * @param key key
424    * @return <tt>true</tt> is key matches to email.
425    */
426   private boolean validateRegInfo(final String email, final String key) {
427     if (email == null || key == null) {
428       return false;
429     }
430     String emails = email.toLowerCase();
431     String keys = key.toLowerCase();
432     // remove all white chars
433     emails = emails.replaceAll("\\s+", "");
434     keys = keys.replaceAll("\\s+", "");
435     if (email.isEmpty() || key.isEmpty()) {
436       return false;
437     }
438 
439     String salt = "secret"; // :)
440     // add secret word
441     emails = emails + salt;
442     // compute hash
443     try {
444       MessageDigest md = MessageDigest.getInstance("MD5");
445       md.update(emails.getBytes(StandardCharsets.UTF_8));
446       StringBuilder sb = new StringBuilder();
447       for (byte b : md.digest()) { // convert to char
448         // dealing with conversion to int from byte and leading 0 for values smaller than 16
449         String ss = Integer.toHexString(0x100 | (b & 0xff)).substring(1);
450         sb.append(ss);
451       }
452       String digest = sb.toString().toLowerCase();
453       LOGGER.trace("email: " + email + " Digest: " + digest);
454       if (digest.equals(keys)) {
455         return true;
456       }
457     } catch (NoSuchAlgorithmException e) {
458       e.printStackTrace();
459     }
460     return false;
461   }
462 
463   /**
464    * Do counting and waiting in background updating UI in the same time.
465    * 
466    * @author p.baniukiewicz
467    * @see <a href=
468    *      "link">http://stackoverflow.com/questions/11817688/how-to-update-swing-gui-from-inside-a-long-method</a>
469    */
470   class Worker extends SwingWorker<String, String> {
471     private int wait;
472     private Dimension dc;
473 
474     /**
475      * Instantiates a new worker.
476      *
477      * @param wait the wait
478      */
479     public Worker(int wait) {
480       this.wait = wait;
481     }
482 
483     /*
484      * (non-Javadoc)
485      * 
486      * @see javax.swing.SwingWorker#doInBackground()
487      */
488     @Override
489     protected String doInBackground() throws Exception {
490       dc = bnCancel.getSize(); // remember size of button
491       // This is what's called in the .execute method
492       for (int i = 0; i < wait; i++) {
493         // This sends the results to the .process method
494         publish(String.valueOf(wait - i));
495         Thread.sleep(1000);
496       }
497       // at the end of job reenable everything
498       waited = true; // set flag on the end of wait
499       bnOk.setEnabled(true);
500       bnCancel.setEnabled(true);
501       bnCancel.setText("Cancel");
502       return null;
503     }
504 
505     /*
506      * (non-Javadoc)
507      * 
508      * @see javax.swing.SwingWorker#process(java.util.List)
509      */
510     @Override
511     protected void process(List<String> item) {
512       // This updates the UI
513       bnCancel.setText(item.get(0));
514       bnCancel.setPreferredSize(dc); // keep size as original
515     }
516   }
517 
518   /**
519    * Support for popupmenu.
520    * 
521    * @author p.baniukiewicz
522    *
523    */
524   class PopupListener extends MouseAdapter {
525 
526     /*
527      * (non-Javadoc)
528      * 
529      * @see java.awt.event.MouseAdapter#mousePressed(java.awt.event.MouseEvent)
530      */
531     @Override
532     public void mousePressed(MouseEvent e) {
533       maybeShowPopup(e);
534     }
535 
536     /*
537      * (non-Javadoc)
538      * 
539      * @see java.awt.event.MouseAdapter#mouseReleased(java.awt.event.MouseEvent)
540      */
541     @Override
542     public void mouseReleased(MouseEvent e) {
543       maybeShowPopup(e);
544     }
545 
546     private void maybeShowPopup(MouseEvent e) {
547       if (e.isPopupTrigger()) {
548         popup.show(e.getComponent(), e.getX(), e.getY());
549       }
550     }
551   }
552 
553 }