198f01d9872690467508996181dcf75d37251414
[jalview.git] / src / jalview / ws / jws2 / Jws2Discoverer.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.ws.jws2;
19
20 import java.awt.Color;
21 import java.awt.event.ActionEvent;
22 import java.awt.event.ActionListener;
23 import java.beans.PropertyChangeEvent;
24 import java.beans.PropertyChangeListener;
25 import java.io.Closeable;
26 import java.net.ConnectException;
27 import java.net.URL;
28 import java.util.ArrayList;
29 import java.util.HashSet;
30 import java.util.Hashtable;
31 import java.util.List;
32 import java.util.StringTokenizer;
33 import java.util.Vector;
34
35 import javax.swing.JMenu;
36 import javax.swing.JMenuItem;
37 import javax.swing.event.MenuEvent;
38 import javax.swing.event.MenuListener;
39
40 import org.apache.log4j.Level;
41
42 import jalview.bin.Cache;
43 import jalview.datamodel.AlignmentView;
44 import jalview.gui.AlignFrame;
45 import jalview.gui.Desktop;
46 import jalview.gui.JalviewChangeSupport;
47 import jalview.ws.WSMenuEntryProviderI;
48 import jalview.ws.params.ParamDatastoreI;
49 import compbio.data.msa.MsaWS;
50 import compbio.metadata.Option;
51 import compbio.metadata.Preset;
52 import compbio.metadata.PresetManager;
53 import compbio.metadata.RunnerConfig;
54 import compbio.ws.client.Jws2Client;
55 import compbio.ws.client.Services;
56
57 /**
58  * discoverer for jws2 services. Follows the lightweight service discoverer
59  * pattern (archetyped by EnfinEnvision2OneWay)
60  * 
61  * @author JimP
62  * 
63  */
64 public class Jws2Discoverer implements Runnable, WSMenuEntryProviderI
65 {
66   private java.beans.PropertyChangeSupport changeSupport = new java.beans.PropertyChangeSupport(
67           this);
68
69   /**
70    * change listeners are notified of "services" property changes
71    * 
72    * @param listener
73    *          to be added that consumes new services Hashtable object.
74    */
75   public void addPropertyChangeListener(
76           java.beans.PropertyChangeListener listener)
77   {
78     changeSupport.addPropertyChangeListener(listener);
79   }
80
81   /**
82    * 
83    * 
84    * @param listener
85    *          to be removed
86    */
87   public void removePropertyChangeListener(
88           java.beans.PropertyChangeListener listener)
89   {
90     changeSupport.removePropertyChangeListener(listener);
91   }
92
93   boolean running = false, aborted = false;
94
95   /**
96    * @return the aborted
97    */
98   public boolean isAborted()
99   {
100     return aborted;
101   }
102
103   /**
104    * @param aborted
105    *          the aborted to set
106    */
107   public void setAborted(boolean aborted)
108   {
109     this.aborted = aborted;
110   }
111
112   Thread oldthread = null;
113
114   public void run()
115   {
116     if (running && oldthread != null && oldthread.isAlive())
117     {
118       if (!aborted)
119       {
120         return;
121       }
122       while (running)
123       {
124         try
125         {
126           Cache.log
127                   .debug("Waiting around for old discovery thread to finish.");
128           // wait around until old discoverer dies
129           Thread.sleep(100);
130         } catch (Exception e)
131         {
132         }
133       }
134       Cache.log.debug("Old discovery thread has finished.");
135     }
136     running = true;
137     oldthread = Thread.currentThread();
138     try
139     {
140       Class foo = getClass().getClassLoader().loadClass(
141               "compbio.ws.client.Jws2Client");
142     } catch (ClassNotFoundException e)
143     {
144       System.err
145               .println("Not enabling JABA Webservices : client jar is not available."
146                       + "\nPlease check that your webstart JNLP file is up to date!");
147       running = false;
148       return;
149     }
150     // reinitialise records of good and bad service URLs
151     if (services != null)
152     {
153       services.removeAllElements();
154     }
155     if (urlsWithoutServices != null)
156     {
157       urlsWithoutServices.removeAllElements();
158     }
159     if (invalidServiceUrls != null)
160     {
161       invalidServiceUrls.removeAllElements();
162     }
163
164     List<JabaWsServerQuery> qrys = new ArrayList<JabaWsServerQuery>();
165     for (final String jwsservers : getServiceUrls())
166     {
167       JabaWsServerQuery squery = new JabaWsServerQuery(this, jwsservers);
168       qrys.add(squery);
169       new Thread(squery).start();
170     }
171     boolean finished = false;
172     do
173     {
174       try
175       {
176         Thread.sleep(100);
177       } catch (Exception e)
178       {
179       }
180       ;
181       for (JabaWsServerQuery squery : qrys)
182       {
183         finished |= !squery.isRunning();
184       }
185       if (aborted)
186       {
187         Cache.log.debug("Aborting " + qrys.size()
188                 + " JABAWS discovery threads.");
189         for (JabaWsServerQuery squery : qrys)
190         {
191           squery.setQuit(true);
192         }
193       }
194     } while (!aborted && !finished);
195     oldthread = null;
196     running = false;
197     if (!aborted)
198     {
199       changeSupport.firePropertyChange("services", new Vector(), services);
200     }
201   }
202
203   /**
204    * record this service endpoint so we can use it
205    * 
206    * @param jwsservers
207    * @param srv
208    * @param service2
209    */
210   synchronized void addService(String jwsservers, Services srv,
211           MsaWS service2)
212   {
213     if (services == null)
214     {
215       services = new Vector<Jws2Instance>();
216     }
217     System.out.println("Discovered service: " + jwsservers + " "
218             + srv.toString());
219     Jws2Instance service = new Jws2Instance(jwsservers, srv.toString(),
220             service2);
221
222     services.add(service);
223     // retrieve the presets and parameter set and cache now
224     service.getParamStore().getPresets();
225     service.hasParameters();
226   }
227
228   public class Jws2Instance
229   {
230     public String hosturl;
231
232     public String serviceType;
233
234     public MsaWS service;
235
236     public Jws2Instance(String hosturl, String serviceType, MsaWS service)
237     {
238       super();
239       this.hosturl = hosturl;
240       this.serviceType = serviceType;
241       this.service = service;
242     }
243
244     PresetManager presets = null;
245
246     public JabaParamStore paramStore = null;
247
248     /**
249      * non thread safe - gets the presets for this service (blocks whilst it
250      * calls the service to get the preset set)
251      * 
252      * @return service presets or null if exceptions were raised.
253      */
254     public PresetManager getPresets()
255     {
256       if (presets == null)
257       {
258         try
259         {
260           presets = service.getPresets();
261         } catch (Exception ex)
262         {
263           System.err
264                   .println("Exception when retrieving presets for service "
265                           + serviceType + " at " + hosturl);
266         }
267       }
268       return presets;
269     }
270
271     public String getHost()
272     {
273       return hosturl;
274       /*
275        * try { URL serviceurl = new URL(hosturl); if (serviceurl.getPort()!=80)
276        * { return serviceurl.getHost()+":"+serviceurl.getPort(); } return
277        * serviceurl.getHost(); } catch (Exception e) {
278        * System.err.println("Failed to parse service URL '" + hosturl +
279        * "' as a valid URL!"); } return null;
280        */
281     }
282
283     /**
284      * @return short description of what the service will do
285      */
286     public String getActionText()
287     {
288       return "Align with " + serviceType;
289     }
290
291     /**
292      * non-thread safe - blocks whilst accessing service to get complete set of
293      * available options and parameters
294      * 
295      * @return
296      */
297     public RunnerConfig getRunnerConfig()
298     {
299       return service.getRunnerOptions();
300     }
301
302     @Override
303     protected void finalize() throws Throwable
304     {
305       if (service != null)
306       {
307         try
308         {
309           Closeable svc = (Closeable) service;
310           service = null;
311           svc.close();
312         } catch (Exception e)
313         {
314         }
315         ;
316       }
317       super.finalize();
318     }
319
320     public ParamDatastoreI getParamStore()
321     {
322       if (paramStore == null)
323       {
324         try
325         {
326           paramStore = new JabaParamStore(this,
327                   (Desktop.instance != null ? Desktop
328                           .getUserParameterStore() : null));
329         } catch (Exception ex)
330         {
331         }
332
333       }
334       return paramStore;
335     }
336
337     public String getUri()
338     {
339       // this is only valid for Jaba 1.0 - this formula might have to change!
340       return hosturl
341               + (hosturl.lastIndexOf("/") == (hosturl.length() - 1) ? "/"
342                       : "") + serviceType;
343     }
344
345     private boolean hasParams = false, lookedForParams = false;
346
347     public boolean hasParameters()
348     {
349       if (!lookedForParams)
350       {
351         lookedForParams = true;
352         try
353         {
354           hasParams = (getRunnerConfig().getArguments().size() > 0);
355         } catch (Exception e)
356         {
357
358         }
359       }
360       return hasParams;
361     }
362   };
363
364   /**
365    * holds list of services.
366    */
367   protected Vector<Jws2Instance> services;
368
369   /**
370    * find or add a submenu with the given title in the given menu
371    * 
372    * @param menu
373    * @param submenu
374    * @return the new or existing submenu
375    */
376   private JMenu findOrCreateMenu(JMenu menu, String submenu)
377   {
378     JMenu submenuinstance = null;
379     for (int i = 0, iSize = menu.getMenuComponentCount(); i < iSize; i++)
380     {
381       if (menu.getMenuComponent(i) instanceof JMenu
382               && ((JMenu) menu.getMenuComponent(i)).getText().equals(
383                       submenu))
384       {
385         submenuinstance = (JMenu) menu.getMenuComponent(i);
386       }
387     }
388     if (submenuinstance == null)
389     {
390       submenuinstance = new JMenu(submenu);
391       menu.add(submenuinstance);
392     }
393     return submenuinstance;
394
395   }
396
397   public void attachWSMenuEntry(JMenu wsmenu, final AlignFrame alignFrame)
398   {
399     // dynamically regenerate service list.
400     final JMenu jws2al = new JMenu("JABAWS Alignment");
401     jws2al.addMenuListener(new MenuListener()
402     {
403       // TODO: future: add menu listener to parent menu - so submenus are
404       // populated *before* they are selected.
405       @Override
406       public void menuSelected(MenuEvent e)
407       {
408         populateWSMenuEntry(jws2al, alignFrame);
409       }
410
411       @Override
412       public void menuDeselected(MenuEvent e)
413       {
414         // TODO Auto-generated method stub
415
416       }
417
418       @Override
419       public void menuCanceled(MenuEvent e)
420       {
421         // TODO Auto-generated method stub
422
423       }
424
425     });
426     wsmenu.add(jws2al);
427
428   }
429
430   private void populateWSMenuEntry(JMenu jws2al, final AlignFrame alignFrame)
431   {
432     if (running || services == null || services.size() == 0)
433     {
434       return;
435     }
436     boolean byhost = Cache.getDefault("WSMENU_BYHOST", true), bytype = Cache
437             .getDefault("WSMENU_BYTYPE", true);
438     /**
439      * eventually, JWS2 services will appear under the same align/etc submenus.
440      * for moment we keep them separate.
441      */
442     JMenu atpoint;
443     MsaWSClient msacl = new MsaWSClient();
444     Vector hostLabels = new Vector();
445     jws2al.removeAll();
446     String lasthost = null;
447     for (final Jws2Instance service : services)
448     {
449       atpoint = jws2al;
450       String host = service.getHost();
451       String type = service.serviceType;
452       if (byhost)
453       {
454         atpoint = findOrCreateMenu(atpoint, host);
455         if (atpoint.getToolTipText() == null)
456         {
457           atpoint.setToolTipText("Services at " + host);
458         }
459       }
460       if (bytype)
461       {
462         atpoint = findOrCreateMenu(atpoint, type);
463         if (atpoint.getToolTipText() == null)
464         {
465           atpoint.setToolTipText(service.getActionText());
466         }
467       }
468       if (!byhost
469               && !hostLabels.contains(host + service.serviceType
470                       + service.getActionText()))
471       // !hostLabels.contains(host + (bytype ?
472       // service.serviceType+service.getActionText() : "")))
473       {
474         // add a marker indicating where this service is hosted
475         // relies on services from the same host being listed in a contiguous
476         // group
477         JMenuItem hitm;
478         atpoint.addSeparator();
479         if (lasthost == null || !lasthost.equals(host))
480         {
481           atpoint.add(hitm = new JMenuItem(host));
482           hitm.setForeground(Color.blue);
483           lasthost = host;
484         }
485         hostLabels.addElement(host + service.serviceType
486                 + service.getActionText());
487         // hostLabels.addElement(host + (bytype ?
488         // service.serviceType+service.getActionText() : ""));
489       }
490       msacl.attachWSMenuEntry(atpoint, service, alignFrame);
491       /*
492        * JMenuItem sitem = new JMenuItem(service.serviceType);
493        * sitem.setToolTipText("Hosted at " + service.hosturl);
494        * sitem.addActionListener(new ActionListener() {
495        * 
496        * @Override public void actionPerformed(ActionEvent e) { AlignmentView
497        * msa = alignFrame.gatherSequencesForAlignment(); MsaWSClient client =
498        * new MsaWSClient(service, "JWS2 Alignment of " + alignFrame.getTitle(),
499        * msa, false, true, alignFrame.getViewport().getAlignment().getDataset(),
500        * alignFrame); } });
501        */
502     }
503   }
504
505   public static void main(String[] args)
506   {
507     Thread runner = getDiscoverer().startDiscoverer(
508             new PropertyChangeListener()
509             {
510
511               public void propertyChange(PropertyChangeEvent evt)
512               {
513                 if (getDiscoverer().services != null)
514                 {
515                   System.out.println("Changesupport: There are now "
516                           + getDiscoverer().services.size() + " services");
517
518                 }
519               }
520             });
521     while (runner.isAlive())
522     {
523       try
524       {
525         Thread.sleep(50);
526       } catch (InterruptedException e)
527       {
528       }
529       ;
530     }
531   }
532
533   private static Jws2Discoverer discoverer;
534
535   public static Jws2Discoverer getDiscoverer()
536   {
537     if (discoverer == null)
538     {
539       discoverer = new Jws2Discoverer();
540     }
541     return discoverer;
542   }
543
544   public boolean hasServices()
545   {
546     // TODO Auto-generated method stub
547     return !running && services != null && services.size() > 0;
548   }
549
550   public boolean isRunning()
551   {
552     return running;
553   }
554
555   /**
556    * the jalview .properties entry for JWS2 URLS
557    */
558   final static String JWS2HOSTURLS = "JWS2HOSTURLS";
559
560   public static void setServiceUrls(Vector<String> urls)
561   {
562     if (urls != null)
563     {
564       StringBuffer urlbuffer = new StringBuffer();
565       String sep = "";
566       for (String url : urls)
567       {
568         urlbuffer.append(sep);
569         urlbuffer.append(url);
570         sep = ",";
571       }
572       Cache.setProperty(JWS2HOSTURLS, urlbuffer.toString());
573     }
574     else
575     {
576       Cache.removeProperty(JWS2HOSTURLS);
577     }
578   }
579
580   public static Vector<String> getServiceUrls()
581   {
582     String surls = Cache.getDefault(JWS2HOSTURLS,
583             "http://www.compbio.dundee.ac.uk/jabaws");
584     Vector<String> urls = new Vector<String>();
585     try
586     {
587       StringTokenizer st = new StringTokenizer(surls, ",");
588       while (st.hasMoreElements())
589       {
590         String url = null;
591         try
592         {
593           java.net.URL u = new java.net.URL(url = st.nextToken());
594           if (!urls.contains(url))
595           {
596             urls.add(url);
597           }
598           else
599           {
600             jalview.bin.Cache.log.info("Ignoring duplicate url in "
601                     + JWS2HOSTURLS + " list");
602           }
603         } catch (Exception ex)
604         {
605           jalview.bin.Cache.log
606                   .warn("Problem whilst trying to make a URL from '"
607                           + ((url != null) ? url : "<null>") + "'");
608           jalview.bin.Cache.log
609                   .warn("This was probably due to a malformed comma separated list"
610                           + " in the "
611                           + JWS2HOSTURLS
612                           + " entry of $(HOME)/.jalview_properties)");
613           jalview.bin.Cache.log.debug("Exception was ", ex);
614         }
615       }
616     } catch (Exception ex)
617     {
618       jalview.bin.Cache.log.warn(
619               "Error parsing comma separated list of urls in "
620                       + JWS2HOSTURLS + " preference.", ex);
621     }
622     if (urls.size() >= 0)
623     {
624       return urls;
625     }
626     return null;
627   }
628
629   public Vector<Jws2Instance> getServices()
630   {
631     return (services == null) ? new Vector<Jws2Instance>()
632             : new Vector<Jws2Instance>(services);
633   }
634
635   /**
636    * test the given URL with the JabaWS test code
637    * 
638    * @param foo
639    * @return
640    */
641   public static boolean testServiceUrl(URL foo)
642   {
643     try
644     {
645       compbio.ws.client.WSTester.main(new String[]
646       { "-h=" + foo.toString() });
647     } catch (Exception e)
648     {
649       e.printStackTrace();
650       return false;
651     } catch (OutOfMemoryError e)
652     {
653       e.printStackTrace();
654       return false;
655     } catch (Error e)
656     {
657       e.printStackTrace();
658       return false;
659     }
660
661     return true;
662   }
663
664   /**
665    * Start a fresh discovery thread and notify the given object when we're
666    * finished. Any known existing threads will be killed before this one is
667    * started.
668    * 
669    * @param changeSupport2
670    * @return new thread
671    */
672   public Thread startDiscoverer(PropertyChangeListener changeSupport2)
673   {
674     if (isRunning())
675     {
676       setAborted(true);
677     }
678     addPropertyChangeListener(changeSupport2);
679     Thread thr = new Thread(this);
680     thr.start();
681     return thr;
682   }
683
684   Vector<String> invalidServiceUrls = null, urlsWithoutServices = null;
685
686   /**
687    * @return the invalidServiceUrls
688    */
689   public Vector<String> getInvalidServiceUrls()
690   {
691     return invalidServiceUrls;
692   }
693
694   /**
695    * @return the urlsWithoutServices
696    */
697   public Vector<String> getUrlsWithoutServices()
698   {
699     return urlsWithoutServices;
700   }
701
702   /**
703    * add an 'empty' JABA server to the list. Only servers not already in the 'bad URL' list will be added to this list.
704    * 
705    * @param jwsservers
706    */
707   public synchronized void addUrlwithnoservices(String jwsservers)
708   {
709     if (urlsWithoutServices == null)
710     {
711       urlsWithoutServices = new Vector<String>();
712     }
713
714     if ((invalidServiceUrls == null || !invalidServiceUrls
715             .contains(jwsservers))
716             && !urlsWithoutServices.contains(jwsservers))
717     {
718       urlsWithoutServices.add(jwsservers);
719     }
720   }
721
722   /**
723    * add a bad URL to the list
724    * 
725    * @param jwsservers
726    */
727   public synchronized void addInvalidServiceUrl(String jwsservers)
728   {
729     if (invalidServiceUrls == null)
730     {
731       invalidServiceUrls = new Vector<String>();
732     }
733     if (!invalidServiceUrls.contains(jwsservers))
734     {
735       invalidServiceUrls.add(jwsservers);
736     }
737   }
738
739   /**
740    * 
741    * @return a human readable report of any problems with the service URLs used
742    *         for discovery
743    */
744   public String getErrorMessages()
745   {
746     if (!isRunning() && !isAborted())
747     {
748       StringBuffer ermsg = new StringBuffer();
749       boolean list = false;
750       if (getInvalidServiceUrls() != null
751               && getInvalidServiceUrls().size() > 0)
752       {
753         ermsg.append("URLs that could not be contacted: \n");
754         for (String svcurl : getInvalidServiceUrls())
755         {
756           if (list)
757           {
758             ermsg.append(", ");
759           }
760           list = true;
761           ermsg.append(svcurl);
762         }
763         ermsg.append("\n\n");
764       }
765       list = false;
766       if (getUrlsWithoutServices() != null
767               && getUrlsWithoutServices().size() > 0)
768       {
769         ermsg.append("URLs without any JABA Services : \n");
770         for (String svcurl : getUrlsWithoutServices())
771         {
772           if (list)
773           {
774             ermsg.append(", ");
775           }
776           list = true;
777           ermsg.append(svcurl);
778         }
779         ermsg.append("\n");
780       }
781       if (ermsg.length() > 1)
782       {
783         return ermsg.toString();
784       }
785
786     }
787     return null;
788   }
789
790 }