update author list in license for (JAL-826)
[jalview.git] / src / jalview / gui / WebserviceInfo.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.7)
3  * Copyright (C) 2011 J Procter, AM Waterhouse, J Engelhardt, LM Lui, G Barton, M Clamp, S Searle
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
10  * 
11  * Jalview is distributed in the hope that it will be useful, but 
12  * WITHOUT ANY WARRANTY; without even the implied warranty 
13  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
14  * PURPOSE.  See the GNU General Public License for more details.
15  * 
16  * You should have received a copy of the GNU General Public License along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 package jalview.gui;
19
20 import java.util.*;
21
22 import java.awt.*;
23 import java.awt.event.*;
24 import java.awt.image.*;
25 import javax.swing.*;
26 import javax.swing.event.HyperlinkEvent;
27 import javax.swing.event.HyperlinkListener;
28 import javax.swing.event.HyperlinkEvent.EventType;
29 import javax.swing.text.html.HTMLEditorKit;
30 import javax.swing.text.html.StyleSheet;
31
32 import jalview.bin.Cache;
33 import jalview.jbgui.*;
34 import jalview.ws.WSClientI;
35
36 /**
37  * Base class for web service client thread and gui TODO: create StAX parser to
38  * extract html body content reliably when preparing html formatted job statuses
39  * 
40  * @author $author$
41  * @version $Revision$
42  */
43 public class WebserviceInfo extends GWebserviceInfo implements
44         HyperlinkListener
45 {
46
47   /** Job is Queued */
48   public static final int STATE_QUEUING = 0;
49
50   /** Job is Running */
51   public static final int STATE_RUNNING = 1;
52
53   /** Job has finished with no errors */
54   public static final int STATE_STOPPED_OK = 2;
55
56   /** Job has been cancelled with no errors */
57   public static final int STATE_CANCELLED_OK = 3;
58
59   /** job has stopped because of some error */
60   public static final int STATE_STOPPED_ERROR = 4;
61
62   /** job has failed because of some unavoidable service interruption */
63   public static final int STATE_STOPPED_SERVERERROR = 5;
64
65   int currentStatus = STATE_QUEUING;
66
67   Image image;
68
69   int angle = 0;
70
71   String title = "";
72
73   jalview.ws.WSClientI thisService;
74
75   boolean serviceIsCancellable;
76
77   JInternalFrame frame;
78
79   JTabbedPane subjobs = null;
80
81   java.util.Vector jobPanes = null;
82
83   private boolean serviceCanMergeResults = false;
84
85   private boolean viewResultsImmediatly = true;
86
87   /**
88    * Get
89    * 
90    * @param flag
91    *          to indicate if results will be shown in a new window as soon as
92    *          they are available.
93    */
94   public boolean isViewResultsImmediatly()
95   {
96     return viewResultsImmediatly;
97   }
98
99   /**
100    * Set
101    * 
102    * @param flag
103    *          to indicate if results will be shown in a new window as soon as
104    *          they are available.
105    */
106   public void setViewResultsImmediatly(boolean viewResultsImmediatly)
107   {
108     this.viewResultsImmediatly = viewResultsImmediatly;
109   }
110
111   private StyleSheet getStyleSheet(HTMLEditorKit editorKit)
112   {
113
114     // Copied blatantly from
115     // http://www.velocityreviews.com/forums/t132265-string-into-htmldocument.html
116     StyleSheet myStyleSheet = new StyleSheet();
117
118     myStyleSheet.addStyleSheet(editorKit.getStyleSheet());
119
120     editorKit.setStyleSheet(myStyleSheet);
121
122     /*
123      * Set the style sheet rules here by reading them from the constants
124      * interface.
125      */
126     /*
127      * for (int ix=0; ix<CSS_RULES.length; ix++) {
128      * 
129      * myStyleSheet.addRule(CSS_RULES[ix]);
130      * 
131      * }
132      */
133     return myStyleSheet;
134
135   }
136
137   // tabbed or not
138   public synchronized int addJobPane()
139   {
140     JScrollPane jobpane = new JScrollPane();
141     JComponent _progressText;
142     if (renderAsHtml)
143     {
144       JEditorPane progressText = new JEditorPane("text/html", "");
145       progressText.addHyperlinkListener(this);
146       _progressText = progressText;
147       // progressText.setFont(new java.awt.Font("Verdana", 0, 10));
148       // progressText.setBorder(null);
149       progressText.setEditable(false);
150       /*
151        * HTMLEditorKit myEditorKit = new HTMLEditorKit();
152        * 
153        * StyleSheet myStyleSheet = getStyleSheet(myEditorKit);
154        * 
155        * HTMLDocument tipDocument = (HTMLDocument)
156        * (myEditorKit.createDefaultDocument());
157        * 
158        * progressText.setDocument(tipDocument);
159        */progressText.setText("<html><h1>WS Job</h1></html>");
160     }
161     else
162     {
163       JTextArea progressText = new JTextArea();
164       _progressText = progressText;
165
166       progressText.setFont(new java.awt.Font("Verdana", 0, 10));
167       progressText.setBorder(null);
168       progressText.setEditable(false);
169       progressText.setText("WS Job");
170       progressText.setLineWrap(true);
171       progressText.setWrapStyleWord(true);
172     }
173     jobpane.setName("JobPane");
174     jobpane.getViewport().add(_progressText, null);
175     jobpane.setBorder(null);
176     if (jobPanes == null)
177     {
178       jobPanes = new Vector();
179     }
180     int newpane = jobPanes.size();
181     jobPanes.add(jobpane);
182
183     if (newpane == 0)
184     {
185       this.add(jobpane, BorderLayout.CENTER);
186     }
187     else
188     {
189       if (newpane == 1)
190       {
191         // revert to a tabbed pane.
192         JScrollPane firstpane;
193         this.remove(firstpane = (JScrollPane) jobPanes.get(0));
194         subjobs = new JTabbedPane();
195         this.add(subjobs, BorderLayout.CENTER);
196         subjobs.add(firstpane);
197         subjobs.setTitleAt(0, firstpane.getName());
198       }
199       subjobs.add(jobpane);
200     }
201     return newpane; // index for accessor methods below
202   }
203
204   /**
205    * Creates a new WebserviceInfo object.
206    * 
207    * @param title
208    *          short name and job type
209    * @param info
210    *          reference or other human readable description
211    */
212   public WebserviceInfo(String title, String info)
213   {
214     init(title, info, 520, 500);
215   }
216
217   /**
218    * Creates a new WebserviceInfo object.
219    * 
220    * @param title
221    *          DOCUMENT ME!
222    * @param info
223    *          DOCUMENT ME!
224    * @param width
225    *          DOCUMENT ME!
226    * @param height
227    *          DOCUMENT ME!
228    */
229   public WebserviceInfo(String title, String info, int width, int height)
230   {
231     init(title, info, width, height);
232   }
233
234   /**
235    * DOCUMENT ME!
236    * 
237    * @return DOCUMENT ME!
238    */
239   public jalview.ws.WSClientI getthisService()
240   {
241     return thisService;
242   }
243
244   /**
245    * Update state of GUI based on client capabilities (like whether the job is
246    * cancellable, whether the 'merge results' button is shown.
247    * 
248    * @param newservice
249    *          service client to query for capabilities
250    */
251   public void setthisService(jalview.ws.WSClientI newservice)
252   {
253     thisService = newservice;
254     serviceIsCancellable = newservice.isCancellable();
255     frame.setClosable(!serviceIsCancellable);
256     serviceCanMergeResults = newservice.canMergeResults();
257     rebuildButtonPanel();
258   }
259
260   private void rebuildButtonPanel()
261   {
262     if (buttonPanel != null)
263     {
264       buttonPanel.removeAll();
265       if (serviceIsCancellable)
266       {
267         buttonPanel.add(cancel);
268         frame.setClosable(false);
269       }
270       else
271       {
272         frame.setClosable(true);
273       }
274     }
275   }
276
277   /**
278    * DOCUMENT ME!
279    * 
280    * @param title
281    *          DOCUMENT ME!
282    * @param info
283    *          DOCUMENT ME!
284    * @param width
285    *          DOCUMENT ME!
286    * @param height
287    *          DOCUMENT ME!
288    */
289   void init(String title, String info, int width, int height)
290   {
291     frame = new JInternalFrame();
292     frame.setContentPane(this);
293     Desktop.addInternalFrame(frame, title, width, height);
294     frame.setClosable(false);
295
296     this.title = title;
297     setInfoText(info);
298
299     java.net.URL url = getClass().getResource("/images/logo.gif");
300     image = java.awt.Toolkit.getDefaultToolkit().createImage(url);
301
302     MediaTracker mt = new MediaTracker(this);
303     mt.addImage(image, 0);
304
305     try
306     {
307       mt.waitForID(0);
308     } catch (Exception ex)
309     {
310     }
311
312     AnimatedPanel ap = new AnimatedPanel();
313     titlePanel.add(ap, BorderLayout.CENTER);
314
315     Thread thread = new Thread(ap);
316     thread.start();
317     final WebserviceInfo thisinfo = this;
318     frame.addInternalFrameListener(new javax.swing.event.InternalFrameAdapter()
319     {
320       public void internalFrameClosed(
321               javax.swing.event.InternalFrameEvent evt)
322       {
323         // System.out.println("Shutting down webservice client");
324         WSClientI service = thisinfo.getthisService();
325         if (service != null && service.isCancellable())
326         {
327           service.cancelJob();
328         }
329       };
330     });
331     frame.validate();
332
333   }
334
335   /**
336    * DOCUMENT ME!
337    * 
338    * @param status
339    *          integer status from state constants
340    */
341   public void setStatus(int status)
342   {
343     currentStatus = status;
344   }
345
346   /**
347    * subjob status indicator
348    * 
349    * @param jobpane
350    * @param status
351    */
352   public void setStatus(int jobpane, int status)
353   {
354     if (jobpane < 0 || jobpane >= jobPanes.size())
355     {
356       throw new Error("setStatus called for non-existent job pane."
357               + jobpane);
358     }
359     switch (status)
360     {
361     case STATE_QUEUING:
362       setProgressName(jobpane + " - QUEUED", jobpane);
363       break;
364     case STATE_RUNNING:
365       setProgressName(jobpane + " - RUNNING", jobpane);
366       break;
367     case STATE_STOPPED_OK:
368       setProgressName(jobpane + " - FINISHED", jobpane);
369       break;
370     case STATE_CANCELLED_OK:
371       setProgressName(jobpane + " - CANCELLED", jobpane);
372       break;
373     case STATE_STOPPED_ERROR:
374       setProgressName(jobpane + " - BROKEN", jobpane);
375       break;
376     case STATE_STOPPED_SERVERERROR:
377       setProgressName(jobpane + " - ALERT", jobpane);
378       break;
379     default:
380       setProgressName(jobpane + " - UNKNOWN STATE", jobpane);
381     }
382   }
383
384   /**
385    * DOCUMENT ME!
386    * 
387    * @return DOCUMENT ME!
388    */
389   public String getInfoText()
390   {
391     return infoText.getText();
392   }
393
394   /**
395    * DOCUMENT ME!
396    * 
397    * @param text
398    *          DOCUMENT ME!
399    */
400   public void setInfoText(String text)
401   {
402     infoText.setText(text);
403   }
404
405   /**
406    * DOCUMENT ME!
407    * 
408    * @param text
409    *          DOCUMENT ME!
410    */
411   public void appendInfoText(String text)
412   {
413     infoText.append(text);
414   }
415
416   /**
417    * DOCUMENT ME!
418    * 
419    * @return DOCUMENT ME!
420    */
421   public String getProgressText(int which)
422   {
423     if (jobPanes == null)
424     {
425       addJobPane();
426     }
427     if (renderAsHtml)
428     {
429       return ((JEditorPane) ((JScrollPane) jobPanes.get(which))
430               .getViewport().getComponent(0)).getText();
431     }
432     else
433     {
434       return ((JTextArea) ((JScrollPane) jobPanes.get(which)).getViewport()
435               .getComponent(0)).getText();
436     }
437   }
438
439   /**
440    * DOCUMENT ME!
441    * 
442    * @param text
443    *          DOCUMENT ME!
444    */
445   public void setProgressText(int which, String text)
446   {
447     if (jobPanes == null)
448     {
449       addJobPane();
450     }
451     if (renderAsHtml)
452     {
453       ((JEditorPane) ((JScrollPane) jobPanes.get(which)).getViewport()
454               .getComponent(0)).setText(ensureHtmlTagged(text));
455     }
456     else
457     {
458       ((JTextArea) ((JScrollPane) jobPanes.get(which)).getViewport()
459               .getComponent(0)).setText(text);
460     }
461   }
462
463   /**
464    * extract content from &lt;body&gt; content &lt;/body&gt;
465    * 
466    * @param text
467    * @param leaveFirst
468    *          - set to leave the initial html tag intact
469    * @param leaveLast
470    *          - set to leave the final html tag intact
471    * @return
472    */
473   private String getHtmlFragment(String text, boolean leaveFirst,
474           boolean leaveLast)
475   {
476     if (text == null)
477     {
478       return null;
479     }
480     String lowertxt = text.toLowerCase();
481     int htmlpos = leaveFirst ? -1 : lowertxt.indexOf("<body");
482
483     int htmlend = leaveLast ? -1 : lowertxt.indexOf("</body");
484     int htmlpose = lowertxt.indexOf(">", htmlpos), htmlende = lowertxt
485             .indexOf(">", htmlend);
486     if (htmlend == -1 && htmlpos == -1)
487     {
488       return text;
489     }
490     if (htmlend > -1)
491     {
492       return text.substring((htmlpos == -1 ? 0 : htmlpose + 1), htmlend);
493     }
494     return text.substring(htmlpos == -1 ? 0 : htmlpose + 1);
495   }
496
497   /**
498    * very simple routine for adding/ensuring html tags are present in text.
499    * 
500    * @param text
501    * @return properly html tag enclosed text
502    */
503   private String ensureHtmlTagged(String text)
504   {
505     if (text == null)
506     {
507       return "";
508     }
509     String lowertxt = text.toLowerCase();
510     int htmlpos = lowertxt.indexOf("<body");
511     int htmlend = lowertxt.indexOf("</body");
512     int doctype = lowertxt.indexOf("<!doctype");
513     int xmltype = lowertxt.indexOf("<?xml");
514     if (htmlend == -1)
515     {
516       text = text + "</body></html>";
517     }
518     if (htmlpos > -1)
519     {
520       if ((doctype > -1 && htmlpos > doctype)
521               || (xmltype > -1 && htmlpos > xmltype))
522       {
523         text = "<html><head></head><body>\n" + text.substring(htmlpos - 1);
524       }
525     }
526     else
527     {
528       text = "<html><head></head><body>\n" + text;
529     }
530     if (text.indexOf("<meta") > -1)
531     {
532       System.err.println("HTML COntent: \n" + text
533               + "<< END HTML CONTENT\n");
534
535     }
536     return text;
537   }
538
539   /**
540    * DOCUMENT ME!
541    * 
542    * @param text
543    *          DOCUMENT ME!
544    */
545   public void appendProgressText(int which, String text)
546   {
547     if (jobPanes == null)
548     {
549       addJobPane();
550     }
551     if (renderAsHtml)
552     {
553       String txt = getHtmlFragment(
554               ((JEditorPane) ((JScrollPane) jobPanes.get(which))
555                       .getViewport().getComponent(0)).getText(), true,
556               false);
557       ((JEditorPane) ((JScrollPane) jobPanes.get(which)).getViewport()
558               .getComponent(0)).setText(ensureHtmlTagged(txt
559               + getHtmlFragment(text, false, true)));
560     }
561     else
562     {
563       ((JTextArea) ((JScrollPane) jobPanes.get(which)).getViewport()
564               .getComponent(0)).append(text);
565     }
566   }
567
568   /**
569    * setProgressText(0, text)
570    */
571   public void setProgressText(String text)
572   {
573     setProgressText(0, text);
574   }
575
576   /**
577    * appendProgressText(0, text)
578    */
579   public void appendProgressText(String text)
580   {
581     appendProgressText(0, text);
582   }
583
584   /**
585    * getProgressText(0)
586    */
587   public String getProgressText()
588   {
589     return getProgressText(0);
590   }
591
592   /**
593    * get the tab title for a subjob
594    * 
595    * @param which
596    *          int
597    * @return String
598    */
599   public String getProgressName(int which)
600   {
601     if (jobPanes == null)
602     {
603       addJobPane();
604     }
605     if (subjobs != null)
606     {
607       return subjobs.getTitleAt(which);
608     }
609     else
610     {
611       return ((JScrollPane) jobPanes.get(which)).getViewport()
612               .getComponent(0).getName();
613     }
614   }
615
616   /**
617    * set the tab title for a subjob
618    * 
619    * @param name
620    *          String
621    * @param which
622    *          int
623    */
624   public void setProgressName(String name, int which)
625   {
626     if (subjobs != null)
627     {
628       subjobs.setTitleAt(which, name);
629       subjobs.revalidate();
630       subjobs.repaint();
631     }
632     JScrollPane c = (JScrollPane) jobPanes.get(which);
633     c.getViewport().getComponent(0).setName(name);
634     c.repaint();
635   }
636
637   /**
638    * Gui action for cancelling the current job, if possible.
639    * 
640    * @param e
641    *          DOCUMENT ME!
642    */
643   protected void cancel_actionPerformed(ActionEvent e)
644   {
645     if (!serviceIsCancellable)
646     {
647       // JBPNote : TODO: We should REALLY just tell the WSClientI to cancel
648       // anyhow - it has to stop threads and clean up
649       // JBPNote : TODO: Instead of a warning, we should have an optional 'Are
650       // you sure?' prompt
651       warnUser("This job cannot be cancelled.\nJust close the window.",
652               "Cancel job");
653     }
654     else
655     {
656       thisService.cancelJob();
657     }
658     frame.setClosable(true);
659   }
660
661   /**
662    * Spawns a thread that pops up a warning dialog box with the given message
663    * and title.
664    * 
665    * @param message
666    * @param title
667    */
668   public void warnUser(final String message, final String title)
669   {
670     javax.swing.SwingUtilities.invokeLater(new Runnable()
671     {
672       public void run()
673       {
674         JOptionPane.showInternalMessageDialog(Desktop.desktop, message,
675                 title, JOptionPane.WARNING_MESSAGE);
676
677       }
678     });
679   }
680
681   /**
682    * Set up GUI for user to get at results - and possibly automatically display
683    * them if viewResultsImmediatly is set.
684    */
685   public void setResultsReady()
686   {
687     frame.setClosable(true);
688     buttonPanel.remove(cancel);
689     buttonPanel.add(showResultsNewFrame);
690     if (serviceCanMergeResults)
691     {
692       buttonPanel.add(mergeResults);
693       buttonPanel.setLayout(new GridLayout(2, 1, 5, 5));
694     }
695     buttonPanel.validate();
696     validate();
697     if (viewResultsImmediatly)
698     {
699       showResultsNewFrame.doClick();
700     }
701   }
702
703   /**
704    * called when job has finished but no result objects can be passed back to
705    * user
706    */
707   public void setFinishedNoResults()
708   {
709     frame.setClosable(true);
710     buttonPanel.remove(cancel);
711     buttonPanel.validate();
712     validate();
713   }
714
715   class AnimatedPanel extends JPanel implements Runnable
716   {
717     long startTime = 0;
718
719     BufferedImage offscreen;
720
721     public void run()
722     {
723       startTime = System.currentTimeMillis();
724
725       while (currentStatus < STATE_STOPPED_OK)
726       {
727         try
728         {
729           Thread.sleep(50);
730
731           int units = (int) ((System.currentTimeMillis() - startTime) / 10f);
732           angle += units;
733           angle %= 360;
734           startTime = System.currentTimeMillis();
735
736           if (currentStatus >= STATE_STOPPED_OK)
737           {
738             angle = 0;
739           }
740
741           repaint();
742         } catch (Exception ex)
743         {
744         }
745       }
746
747       cancel.setEnabled(false);
748     }
749
750     void drawPanel()
751     {
752       if (offscreen == null || offscreen.getWidth(this) != getWidth()
753               || offscreen.getHeight(this) != getHeight())
754       {
755         offscreen = new BufferedImage(getWidth(), getHeight(),
756                 BufferedImage.TYPE_INT_ARGB);
757       }
758
759       Graphics2D g = (Graphics2D) offscreen.getGraphics();
760
761       g.setColor(Color.white);
762       g.fillRect(0, 0, getWidth(), getHeight());
763
764       g.setFont(new Font("Arial", Font.BOLD, 12));
765       g.setColor(Color.black);
766
767       switch (currentStatus)
768       {
769       case STATE_QUEUING:
770         g.drawString(title.concat(" - queuing"), 60, 30);
771
772         break;
773
774       case STATE_RUNNING:
775         g.drawString(title.concat(" - running"), 60, 30);
776
777         break;
778
779       case STATE_STOPPED_OK:
780         g.drawString(title.concat(" - complete"), 60, 30);
781
782         break;
783
784       case STATE_CANCELLED_OK:
785         g.drawString(title.concat(" - job cancelled!"), 60, 30);
786
787         break;
788
789       case STATE_STOPPED_ERROR:
790         g.drawString(title.concat(" - job error!"), 60, 30);
791
792         break;
793
794       case STATE_STOPPED_SERVERERROR:
795         g.drawString(title.concat(" - Server Error! (try later)"), 60, 30);
796
797         break;
798       }
799
800       if (image != null)
801       {
802         g.rotate(Math.toRadians(angle), 28, 28);
803         g.drawImage(image, 10, 10, this);
804         g.rotate(-Math.toRadians(angle), 28, 28);
805       }
806     }
807
808     public void paintComponent(Graphics g1)
809     {
810       drawPanel();
811
812       g1.drawImage(offscreen, 0, 0, this);
813     }
814   }
815
816   boolean renderAsHtml = false;
817
818   public void setRenderAsHtml(boolean b)
819   {
820     renderAsHtml = b;
821   }
822
823   public void hyperlinkUpdate(HyperlinkEvent e)
824   {
825     if (e.getEventType() == EventType.ACTIVATED)
826     {
827       String url=null;
828       try
829       {
830         url = e.getURL().toString();
831         Desktop.showUrl(url);
832       } catch (Exception x)
833       {
834         if (url!=null) { 
835           Cache.log.error("Couldn't handle string "+url+" as a URL.");
836         }
837         // ignore any exceptions due to dud links.
838       }
839
840     }
841   }
842 }