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