parallelised service endpoint discovery and ensured that old threads are destroyed...
[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    * @param aborted
97    *          the aborted to set
98    */
99   public void setAborted(boolean aborted)
100   {
101     this.aborted = aborted;
102   }
103
104   Thread oldthread = null;
105
106   public void run()
107   {
108     if (running && oldthread != null && oldthread.isAlive())
109     {
110       if (!aborted)
111       {
112         return;
113       }
114       while (running)
115       {
116         try
117         {
118           Cache.log
119                   .debug("Waiting around for old discovery thread to finish.");
120           // wait around until old discoverer dies
121           Thread.sleep(100);
122         } catch (Exception e)
123         {
124         }
125       }
126       Cache.log.debug("Old discovery thread has finished.");
127     }
128     running = true;
129     oldthread = Thread.currentThread();
130     try
131     {
132       Class foo = getClass().getClassLoader().loadClass(
133               "compbio.ws.client.Jws2Client");
134     } catch (ClassNotFoundException e)
135     {
136       System.err
137               .println("Not enabling Jalview Webservices version 2: client jar is not available."
138                       + "\nPlease check that your webstart JNLP file is up to date!");
139       running = false;
140       return;
141     }
142     if (services != null)
143     {
144       services.removeAllElements();
145     }
146     List<JabaWsServerQuery> qrys = new ArrayList<JabaWsServerQuery>();
147     for (final String jwsservers : getServiceUrls())
148     {
149       JabaWsServerQuery squery = new JabaWsServerQuery(this, jwsservers);
150       qrys.add(squery);
151       new Thread(squery).start();
152     }
153     boolean finished = false;
154     do
155     {
156       try
157       {
158         Thread.sleep(100);
159       } catch (Exception e)
160       {
161       }
162       ;
163       for (JabaWsServerQuery squery : qrys)
164       {
165         finished |= !squery.isRunning();
166       }
167       if (aborted)
168       {
169         Cache.log.debug("Aborting " + qrys.size()
170                 + " JABAWS discovery threads.");
171         for (JabaWsServerQuery squery : qrys)
172         {
173           squery.setQuit(true);
174         }
175       }
176     } while (!aborted && !finished);
177     oldthread = null;
178     running = false;
179     if (!aborted)
180     {
181       changeSupport.firePropertyChange("services", new Vector(), services);
182     }
183   }
184
185   /**
186    * record this service endpoint so we can use it
187    * 
188    * @param jwsservers
189    * @param srv
190    * @param service2
191    */
192   synchronized void addService(String jwsservers, Services srv,
193           MsaWS service2)
194   {
195     if (services == null)
196     {
197       services = new Vector<Jws2Instance>();
198     }
199     System.out.println("Discovered service: " + jwsservers + " "
200             + srv.toString());
201     Jws2Instance service = new Jws2Instance(jwsservers, srv.toString(),
202             service2);
203
204     services.add(service);
205     // retrieve the presets and parameter set and cache now
206     service.getParamStore().getPresets();
207     service.hasParameters();
208   }
209
210   public class Jws2Instance
211   {
212     public String hosturl;
213
214     public String serviceType;
215
216     public MsaWS service;
217
218     public Jws2Instance(String hosturl, String serviceType, MsaWS service)
219     {
220       super();
221       this.hosturl = hosturl;
222       this.serviceType = serviceType;
223       this.service = service;
224     }
225
226     PresetManager presets = null;
227
228     public JabaParamStore paramStore = null;
229
230     /**
231      * non thread safe - gets the presets for this service (blocks whilst it
232      * calls the service to get the preset set)
233      * 
234      * @return service presets or null if exceptions were raised.
235      */
236     public PresetManager getPresets()
237     {
238       if (presets == null)
239       {
240         try
241         {
242           presets = service.getPresets();
243         } catch (Exception ex)
244         {
245           System.err
246                   .println("Exception when retrieving presets for service "
247                           + serviceType + " at " + hosturl);
248         }
249       }
250       return presets;
251     }
252
253     public String getHost()
254     {
255       return hosturl;
256       /*
257        * try { URL serviceurl = new URL(hosturl); if (serviceurl.getPort()!=80)
258        * { return serviceurl.getHost()+":"+serviceurl.getPort(); } return
259        * serviceurl.getHost(); } catch (Exception e) {
260        * System.err.println("Failed to parse service URL '" + hosturl +
261        * "' as a valid URL!"); } return null;
262        */
263     }
264
265     /**
266      * @return short description of what the service will do
267      */
268     public String getActionText()
269     {
270       return "Align with " + serviceType;
271     }
272
273     /**
274      * non-thread safe - blocks whilst accessing service to get complete set of
275      * available options and parameters
276      * 
277      * @return
278      */
279     public RunnerConfig getRunnerConfig()
280     {
281       return service.getRunnerOptions();
282     }
283
284     @Override
285     protected void finalize() throws Throwable
286     {
287       if (service != null)
288       {
289         try
290         {
291           Closeable svc = (Closeable) service;
292           service = null;
293           svc.close();
294         } catch (Exception e)
295         {
296         }
297         ;
298       }
299       super.finalize();
300     }
301
302     public ParamDatastoreI getParamStore()
303     {
304       if (paramStore == null)
305       {
306         try
307         {
308           paramStore = new JabaParamStore(this,
309                   (Desktop.instance != null ? Desktop
310                           .getUserParameterStore() : null));
311         } catch (Exception ex)
312         {
313         }
314
315       }
316       return paramStore;
317     }
318
319     public String getUri()
320     {
321       // this is only valid for Jaba 1.0 - this formula might have to change!
322       return hosturl
323               + (hosturl.lastIndexOf("/") == (hosturl.length() - 1) ? "/"
324                       : "") + serviceType;
325     }
326
327     private boolean hasParams = false, lookedForParams = false;
328
329     public boolean hasParameters()
330     {
331       if (!lookedForParams)
332       {
333         lookedForParams = true;
334         try
335         {
336           hasParams = (getRunnerConfig().getArguments().size() > 0);
337         } catch (Exception e)
338         {
339
340         }
341       }
342       return hasParams;
343     }
344   };
345
346   /**
347    * holds list of services.
348    */
349   protected Vector<Jws2Instance> services;
350
351   /**
352    * find or add a submenu with the given title in the given menu
353    * 
354    * @param menu
355    * @param submenu
356    * @return the new or existing submenu
357    */
358   private JMenu findOrCreateMenu(JMenu menu, String submenu)
359   {
360     JMenu submenuinstance = null;
361     for (int i = 0, iSize = menu.getMenuComponentCount(); i < iSize; i++)
362     {
363       if (menu.getMenuComponent(i) instanceof JMenu
364               && ((JMenu) menu.getMenuComponent(i)).getText().equals(
365                       submenu))
366       {
367         submenuinstance = (JMenu) menu.getMenuComponent(i);
368       }
369     }
370     if (submenuinstance == null)
371     {
372       submenuinstance = new JMenu(submenu);
373       menu.add(submenuinstance);
374     }
375     return submenuinstance;
376
377   }
378
379   public void attachWSMenuEntry(JMenu wsmenu, final AlignFrame alignFrame)
380   {
381     // dynamically regenerate service list.
382     final JMenu jws2al = new JMenu("JABAWS Alignment");
383     jws2al.addMenuListener(new MenuListener()
384     {
385       // TODO: future: add menu listener to parent menu - so submenus are
386       // populated *before* they are selected.
387       @Override
388       public void menuSelected(MenuEvent e)
389       {
390         populateWSMenuEntry(jws2al, alignFrame);
391       }
392
393       @Override
394       public void menuDeselected(MenuEvent e)
395       {
396         // TODO Auto-generated method stub
397
398       }
399
400       @Override
401       public void menuCanceled(MenuEvent e)
402       {
403         // TODO Auto-generated method stub
404
405       }
406
407     });
408     wsmenu.add(jws2al);
409
410   }
411
412   private void populateWSMenuEntry(JMenu jws2al, final AlignFrame alignFrame)
413   {
414     if (running || services == null || services.size() == 0)
415     {
416       return;
417     }
418     boolean byhost = Cache.getDefault("WSMENU_BYHOST", true), bytype = Cache
419             .getDefault("WSMENU_BYTYPE", true);
420     /**
421      * eventually, JWS2 services will appear under the same align/etc submenus.
422      * for moment we keep them separate.
423      */
424     JMenu atpoint;
425     MsaWSClient msacl = new MsaWSClient();
426     Vector hostLabels = new Vector();
427     jws2al.removeAll();
428     String lasthost = null;
429     for (final Jws2Instance service : services)
430     {
431       atpoint = jws2al;
432       String host = service.getHost();
433       String type = service.serviceType;
434       if (byhost)
435       {
436         atpoint = findOrCreateMenu(atpoint, host);
437         if (atpoint.getToolTipText() == null)
438         {
439           atpoint.setToolTipText("Services at " + host);
440         }
441       }
442       if (bytype)
443       {
444         atpoint = findOrCreateMenu(atpoint, type);
445         if (atpoint.getToolTipText() == null)
446         {
447           atpoint.setToolTipText(service.getActionText());
448         }
449       }
450       if (!byhost
451               && !hostLabels.contains(host + service.serviceType
452                       + service.getActionText()))
453       // !hostLabels.contains(host + (bytype ?
454       // service.serviceType+service.getActionText() : "")))
455       {
456         // add a marker indicating where this service is hosted
457         // relies on services from the same host being listed in a contiguous
458         // group
459         JMenuItem hitm;
460         atpoint.addSeparator();
461         if (lasthost == null || !lasthost.equals(host))
462         {
463           atpoint.add(hitm = new JMenuItem(host));
464           hitm.setForeground(Color.blue);
465           lasthost = host;
466         }
467         hostLabels.addElement(host + service.serviceType
468                 + service.getActionText());
469         // hostLabels.addElement(host + (bytype ?
470         // service.serviceType+service.getActionText() : ""));
471       }
472       msacl.attachWSMenuEntry(atpoint, service, alignFrame);
473       /*
474        * JMenuItem sitem = new JMenuItem(service.serviceType);
475        * sitem.setToolTipText("Hosted at " + service.hosturl);
476        * sitem.addActionListener(new ActionListener() {
477        * 
478        * @Override public void actionPerformed(ActionEvent e) { AlignmentView
479        * msa = alignFrame.gatherSequencesForAlignment(); MsaWSClient client =
480        * new MsaWSClient(service, "JWS2 Alignment of " + alignFrame.getTitle(),
481        * msa, false, true, alignFrame.getViewport().getAlignment().getDataset(),
482        * alignFrame); } });
483        */
484     }
485   }
486
487   public static void main(String[] args)
488   {
489     Thread runner = getDiscoverer().startDiscoverer(new PropertyChangeListener()
490     {
491
492       public void propertyChange(PropertyChangeEvent evt)
493       {
494         if (getDiscoverer().services!=null)
495           {System.out.println("Changesupport: There are now "
496                 + getDiscoverer().services.size() + " services");
497           
498           }
499       }
500     });
501     while (runner.isAlive())
502     {
503       try
504       {
505         Thread.sleep(50);
506       } catch (InterruptedException e)
507       {
508       }
509       ;
510     }
511   }
512
513   private static Jws2Discoverer discoverer;
514
515   public static Jws2Discoverer getDiscoverer()
516   {
517     if (discoverer == null)
518     {
519       discoverer = new Jws2Discoverer();
520     }
521     return discoverer;
522   }
523
524   public boolean hasServices()
525   {
526     // TODO Auto-generated method stub
527     return !running && services != null && services.size() > 0;
528   }
529
530   public boolean isRunning()
531   {
532     return running;
533   }
534
535   /**
536    * the jalview .properties entry for JWS2 URLS
537    */
538   final static String JWS2HOSTURLS = "JWS2HOSTURLS";
539
540   public static void setServiceUrls(Vector<String> urls)
541   {
542     if (urls != null)
543     {
544       StringBuffer urlbuffer = new StringBuffer();
545       String sep = "";
546       for (String url : urls)
547       {
548         urlbuffer.append(sep);
549         urlbuffer.append(url);
550         sep = ",";
551       }
552       Cache.setProperty(JWS2HOSTURLS, urlbuffer.toString());
553     }
554     else
555     {
556       Cache.removeProperty(JWS2HOSTURLS);
557     }
558   }
559
560   public static Vector<String> getServiceUrls()
561   {
562     String surls = Cache.getDefault(JWS2HOSTURLS,
563             "http://www.compbio.dundee.ac.uk/jabaws");
564     Vector<String> urls = new Vector<String>();
565     try
566     {
567       StringTokenizer st = new StringTokenizer(surls, ",");
568       while (st.hasMoreElements())
569       {
570         String url = null;
571         try
572         {
573           java.net.URL u = new java.net.URL(url = st.nextToken());
574           if (!urls.contains(url))
575           {
576             urls.add(url);
577           }
578           else
579           {
580             jalview.bin.Cache.log.info("Ignoring duplicate url in "
581                     + JWS2HOSTURLS + " list");
582           }
583         } catch (Exception ex)
584         {
585           jalview.bin.Cache.log
586                   .warn("Problem whilst trying to make a URL from '"
587                           + ((url != null) ? url : "<null>") + "'");
588           jalview.bin.Cache.log
589                   .warn("This was probably due to a malformed comma separated list"
590                           + " in the "
591                           + JWS2HOSTURLS
592                           + " entry of $(HOME)/.jalview_properties)");
593           jalview.bin.Cache.log.debug("Exception was ", ex);
594         }
595       }
596     } catch (Exception ex)
597     {
598       jalview.bin.Cache.log.warn(
599               "Error parsing comma separated list of urls in "
600                       + JWS2HOSTURLS + " preference.", ex);
601     }
602     if (urls.size() >= 0)
603     {
604       return urls;
605     }
606     return null;
607   }
608
609   public Vector<Jws2Instance> getServices()
610   {
611     return (services == null) ? new Vector<Jws2Instance>()
612             : new Vector<Jws2Instance>(services);
613   }
614
615   /**
616    * test the given URL with the JabaWS test code
617    * 
618    * @param foo
619    * @return
620    */
621   public static boolean testServiceUrl(URL foo)
622   {
623     try
624     {
625       compbio.ws.client.WSTester.main(new String[]
626       { "-h=" + foo.toString() });
627     } catch (Exception e)
628     {
629       return false;
630     } catch (OutOfMemoryError e)
631     {
632       return false;
633     } catch (Error e)
634     {
635       return false;
636     }
637     return true;
638   }
639
640   /**
641    * Start a fresh discovery thread and notify the given object when we're finished. Any known existing threads will be killed before this one is started. 
642    * @param changeSupport2
643    * @return new thread
644    */
645   public Thread startDiscoverer(PropertyChangeListener changeSupport2)
646   {
647     if (isRunning())
648     {
649       setAborted(true);
650     }
651     addPropertyChangeListener(changeSupport2);
652     Thread thr = new Thread(this);
653     thr.start();
654     return thr;
655   }
656
657 }