JAL-3515 Extract core WSDiscoverer methods to a separate interface
[jalview.git] / src / jalview / ws / jws2 / Jws2Discoverer.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
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
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.ws.jws2;
22
23 import jalview.bin.Cache;
24 import jalview.gui.AlignFrame;
25 import jalview.util.MessageManager;
26 import jalview.ws.WSDiscovererI;
27 import jalview.ws.api.ServiceWithParameters;
28 import jalview.ws.jws2.jabaws2.Jws2Instance;
29 import jalview.ws.params.ParamDatastoreI;
30
31 import java.beans.PropertyChangeEvent;
32 import java.beans.PropertyChangeListener;
33 import java.beans.PropertyChangeSupport;
34 import java.net.MalformedURLException;
35 import java.net.URL;
36 import java.util.ArrayList;
37 import java.util.HashSet;
38 import java.util.List;
39 import java.util.Set;
40 import java.util.StringTokenizer;
41 import java.util.Vector;
42
43 import javax.swing.JMenu;
44
45 import compbio.ws.client.Services;
46
47 /**
48  * discoverer for jws2 services. Follows the lightweight service discoverer
49  * pattern (archetyped by EnfinEnvision2OneWay)
50  * 
51  * @author JimP
52  * 
53  */
54 public class Jws2Discoverer implements WSDiscovererI, Runnable
55 {
56   public static final String COMPBIO_JABAWS = "http://www.compbio.dundee.ac.uk/jabaws";
57
58   /*
59    * the .jalview_properties entry for JWS2 URLS
60    */
61   private final static String JWS2HOSTURLS = "JWS2HOSTURLS";
62
63   /*
64    * Singleton instance
65    */
66   private static Jws2Discoverer discoverer;
67
68   /*
69    * Override for testing only
70    */
71   private static List<String> testUrls = null;
72
73   // preferred url has precedence over others
74   private String preferredUrl;
75
76   private PropertyChangeSupport changeSupport = new PropertyChangeSupport(
77           this);
78
79   private Vector<String> invalidServiceUrls = null;
80
81   private Vector<String> urlsWithoutServices = null;
82
83   private Vector<String> validServiceUrls = null;
84
85   private volatile boolean running = false;
86
87   private volatile boolean aborted = false;
88
89   private Thread oldthread = null;
90
91   /**
92    * holds list of services.
93    */
94   protected Vector<Jws2Instance> services;
95
96   /**
97    * Private constructor enforces use of singleton via getDiscoverer()
98    */
99   private Jws2Discoverer()
100   {
101   }
102
103   /**
104    * change listeners are notified of "services" property changes
105    * 
106    * @param listener
107    *          to be added that consumes new services Hashtable object.
108    */
109   public void addPropertyChangeListener(
110           java.beans.PropertyChangeListener listener)
111   {
112     changeSupport.addPropertyChangeListener(listener);
113   }
114
115   /**
116    * 
117    * 
118    * @param listener
119    *          to be removed
120    */
121   public void removePropertyChangeListener(
122           java.beans.PropertyChangeListener listener)
123   {
124     changeSupport.removePropertyChangeListener(listener);
125   }
126
127   /**
128    * @return the aborted
129    */
130   public boolean isAborted()
131   {
132     return aborted;
133   }
134
135   /**
136    * @param aborted
137    *          the aborted to set
138    */
139   public void setAborted(boolean aborted)
140   {
141     this.aborted = aborted;
142   }
143
144   @Override
145   public void run()
146   {
147
148     if (running && oldthread != null && oldthread.isAlive())
149     {
150       if (!aborted)
151       {
152         return;
153       }
154       while (running)
155       {
156         try
157         {
158           Cache.log.debug(
159                   "Waiting around for old discovery thread to finish.");
160           // wait around until old discoverer dies
161           Thread.sleep(100);
162         } catch (Exception e)
163         {
164         }
165       }
166       aborted = false;
167       Cache.log.debug("Old discovery thread has finished.");
168     }
169     running = true;
170
171     // first set up exclusion list if needed
172     final Set<String> ignoredServices = new HashSet<>();
173     for (String ignored : Cache
174             .getDefault("IGNORED_JABAWS_SERVICETYPES", "").split("\\|"))
175     {
176       ignoredServices.add(ignored);
177     }
178
179     changeSupport.firePropertyChange("services", services,
180             new Vector<Jws2Instance>());
181     oldthread = Thread.currentThread();
182     try
183     {
184       Class foo = getClass().getClassLoader()
185               .loadClass("compbio.ws.client.Jws2Client");
186     } catch (ClassNotFoundException e)
187     {
188       System.err.println(
189               "Not enabling JABA Webservices : client jar is not available."
190                       + "\nPlease check that your webstart JNLP file is up to date!");
191       running = false;
192       return;
193     }
194     // reinitialise records of good and bad service URLs
195     if (services != null)
196     {
197       services.removeAllElements();
198     }
199     if (urlsWithoutServices != null)
200     {
201       urlsWithoutServices.removeAllElements();
202     }
203     if (invalidServiceUrls != null)
204     {
205       invalidServiceUrls.removeAllElements();
206     }
207     if (validServiceUrls != null)
208     {
209       validServiceUrls.removeAllElements();
210     }
211     ArrayList<String> svctypes = new ArrayList<>();
212
213     List<JabaWsServerQuery> qrys = new ArrayList<>();
214     for (final String jwsserver : getServiceUrls())
215     {
216       JabaWsServerQuery squery = new JabaWsServerQuery(this, jwsserver);
217       if (svctypes.size() == 0)
218       {
219         // TODO: remove this ugly hack to get Canonical JABA service ordering
220         // for all possible services
221         for (Services sv : squery.JABAWS2SERVERS)
222         {
223           if (!ignoredServices.contains(sv.toString()))
224           {
225             svctypes.add(sv.toString());
226           }
227         }
228
229       }
230       qrys.add(squery);
231       new Thread(squery).start();
232     }
233     boolean finished = true;
234     do
235     {
236       finished = true;
237       try
238       {
239         Thread.sleep(100);
240       } catch (Exception e)
241       {
242       }
243       ;
244       for (JabaWsServerQuery squery : qrys)
245       {
246         if (squery.isRunning())
247         {
248           finished = false;
249         }
250       }
251       if (aborted)
252       {
253         Cache.log.debug(
254                 "Aborting " + qrys.size() + " JABAWS discovery threads.");
255         for (JabaWsServerQuery squery : qrys)
256         {
257           squery.setQuit(true);
258         }
259       }
260     } while (!aborted && !finished);
261     if (!aborted)
262     {
263       // resort services according to order found in jabaws service list
264       // also ensure services for each host are ordered in same way.
265
266       if (services != null && services.size() > 0)
267       {
268         Jws2Instance[] svcs = new Jws2Instance[services.size()];
269         int[] spos = new int[services.size()];
270         int ipos = 0;
271         List<String> svcUrls = getServiceUrls();
272         for (Jws2Instance svc : services)
273         {
274           svcs[ipos] = svc;
275           spos[ipos++] = 1000 * svcUrls.indexOf(svc.getHostURL()) + 1
276                   + svctypes.indexOf(svc.getName());
277         }
278         jalview.util.QuickSort.sort(spos, svcs);
279         services = new Vector<>();
280         for (Jws2Instance svc : svcs)
281         {
282           if (!ignoredServices.contains(svc.getName()))
283           {
284             services.add(svc);
285           }
286         }
287       }
288     }
289     oldthread = null;
290     running = false;
291     changeSupport.firePropertyChange("services", new Vector<Jws2Instance>(),
292             services);
293   }
294
295   /**
296    * record this service endpoint so we can use it
297    * 
298    * @param jwsservers
299    * @param srv
300    * @param service2
301    */
302   synchronized void addService(String jwsservers, Jws2Instance service)
303   {
304     if (services == null)
305     {
306       services = new Vector<>();
307     }
308     System.out.println(
309             "Discovered service: " + jwsservers + " " + service.toString());
310     // Jws2Instance service = new Jws2Instance(jwsservers, srv.toString(),
311     // service2);
312
313     services.add(service);
314     // retrieve the presets and parameter set and cache now
315     ParamDatastoreI pds = service.getParamStore();
316     if (pds != null)
317     {
318       pds.getPresets();
319     }
320     service.hasParameters();
321     if (validServiceUrls == null)
322     {
323       validServiceUrls = new Vector<>();
324     }
325     validServiceUrls.add(jwsservers);
326   }
327
328   /**
329    * attach all available web services to the appropriate submenu in the given
330    * JMenu
331    */
332   @Override
333   public void attachWSMenuEntry(JMenu wsmenu, final AlignFrame alignFrame)
334   {
335     if (running || services == null || services.size() == 0)
336     {
337       return;
338     }
339     // dynamically regenerate service list.
340     populateWSMenuEntry(wsmenu, alignFrame, null);
341   }
342
343   private void populateWSMenuEntry(JMenu jws2al,
344           final AlignFrame alignFrame, String typeFilter)
345   {
346     PreferredServiceRegistry.getRegistry().populateWSMenuEntry(
347             getServices(),
348             changeSupport, jws2al,
349             alignFrame, typeFilter);
350   }
351
352   public static void main(String[] args)
353   {
354     if (args.length > 0)
355     {
356       testUrls = new ArrayList<>();
357       for (String url : args)
358       {
359         testUrls.add(url);
360       }
361       ;
362     }
363     Thread runner = getDiscoverer()
364             .startDiscoverer(new PropertyChangeListener()
365             {
366
367               @Override
368               public void propertyChange(PropertyChangeEvent evt)
369               {
370                 if (getDiscoverer().services != null)
371                 {
372                   System.out.println("Changesupport: There are now "
373                           + getDiscoverer().services.size() + " services");
374                   int i = 1;
375                   for (ServiceWithParameters instance : getDiscoverer().services)
376                   {
377                     System.out.println("Service " + i++ + " "
378                             + instance.getClass() + "@"
379                             + instance.getHostURL()
380                             + ": " + instance.getActionText());
381                   }
382
383                 }
384               }
385             });
386     while (runner.isAlive())
387     {
388       try
389       {
390         Thread.sleep(50);
391       } catch (InterruptedException e)
392       {
393       }
394       ;
395     }
396     try
397     {
398       Thread.sleep(50);
399     } catch (InterruptedException x)
400     {
401     }
402   }
403
404   /**
405    * Returns the singleton instance of this class.
406    * 
407    * @return
408    */
409   public static Jws2Discoverer getDiscoverer()
410   {
411     if (discoverer == null)
412     {
413       discoverer = new Jws2Discoverer();
414     }
415     return discoverer;
416   }
417
418   @Override
419   public boolean hasServices()
420   {
421     return !running && services != null && services.size() > 0;
422   }
423
424   @Override
425   public boolean isRunning()
426   {
427     return running;
428   }
429
430   @Override
431   public void setServiceUrls(List<String> wsUrls)
432   {
433     if (wsUrls != null && !wsUrls.isEmpty())
434     {
435       StringBuilder urls = new StringBuilder(128);
436       String sep = "";
437       for (String url : wsUrls)
438       {
439         urls.append(sep);
440         urls.append(url);
441         sep = ",";
442       }
443       Cache.setProperty(JWS2HOSTURLS, urls.toString());
444     }
445     else
446     {
447       Cache.removeProperty(JWS2HOSTURLS);
448     }
449   }
450
451   /**
452    * Returns web service URLs, in the order in which they should be tried (or an
453    * empty list).
454    * 
455    * @return
456    */
457   @Override
458   public List<String> getServiceUrls()
459   {
460     if (testUrls != null)
461     {
462       // return test urls, if there are any, instead of touching cache
463       return testUrls;
464     }
465     List<String> urls = new ArrayList<>();
466
467     if (this.preferredUrl != null)
468     {
469       urls.add(preferredUrl);
470     }
471
472     String surls = Cache.getDefault(JWS2HOSTURLS, COMPBIO_JABAWS);
473     try
474     {
475       StringTokenizer st = new StringTokenizer(surls, ",");
476       while (st.hasMoreElements())
477       {
478         String url = null;
479         try
480         {
481           url = st.nextToken();
482           new URL(url);
483           if (!urls.contains(url))
484           {
485             urls.add(url);
486           }
487           else
488           {
489             Cache.log.warn("Ignoring duplicate url " + url + " in "
490                     + JWS2HOSTURLS + " list");
491           }
492         } catch (MalformedURLException ex)
493         {
494           Cache.log.warn("Problem whilst trying to make a URL from '"
495                   + ((url != null) ? url : "<null>") + "'");
496           Cache.log.warn(
497                   "This was probably due to a malformed comma separated list"
498                           + " in the " + JWS2HOSTURLS
499                           + " entry of $(HOME)/.jalview_properties)");
500           Cache.log.debug("Exception was ", ex);
501         }
502       }
503     } catch (Exception ex)
504     {
505       Cache.log.warn("Error parsing comma separated list of urls in "
506               + JWS2HOSTURLS + " preference.", ex);
507     }
508     return urls;
509   }
510
511   @Override
512   public Vector<ServiceWithParameters> getServices()
513   {
514     return (services == null) ? new Vector<>()
515             : new Vector<>(services);
516   }
517
518   /**
519    * test the given URL with the JabaWS test code
520    * 
521    * @param foo
522    * @return
523    */
524   @Override
525   public boolean testServiceUrl(URL foo)
526   {
527     try
528     {
529       compbio.ws.client.WSTester
530               .main(new String[]
531               { "-h=" + foo.toString() });
532     } catch (Exception e)
533     {
534       e.printStackTrace();
535       return false;
536     } catch (OutOfMemoryError e)
537     {
538       e.printStackTrace();
539       return false;
540     } catch (Error e)
541     {
542       e.printStackTrace();
543       return false;
544     }
545
546     return true;
547   }
548
549   public boolean restart()
550   {
551     synchronized (this)
552     {
553       if (running)
554       {
555         aborted = true;
556       }
557       else
558       {
559         running = true;
560       }
561       return aborted;
562     }
563   }
564
565   /**
566    * Start a fresh discovery thread and notify the given object when we're
567    * finished. Any known existing threads will be killed before this one is
568    * started.
569    * 
570    * @param changeSupport2
571    * @return new thread
572    */
573   @Override
574   public Thread startDiscoverer(PropertyChangeListener changeSupport2)
575   {
576     /*    if (restart())
577         {
578           return;
579         }
580         else
581         {
582           Thread thr = new Thread(this);
583           thr.start();
584         }
585        */
586     if (isRunning())
587     {
588       setAborted(true);
589     }
590     addPropertyChangeListener(changeSupport2);
591     Thread thr = new Thread(this);
592     thr.start();
593     return thr;
594   }
595
596   /**
597    * @return the invalidServiceUrls
598    */
599   public Vector<String> getInvalidServiceUrls()
600   {
601     return invalidServiceUrls;
602   }
603
604   /**
605    * @return the urlsWithoutServices
606    */
607   public Vector<String> getUrlsWithoutServices()
608   {
609     return urlsWithoutServices;
610   }
611
612   /**
613    * add an 'empty' JABA server to the list. Only servers not already in the
614    * 'bad URL' list will be added to this list.
615    * 
616    * @param jwsservers
617    */
618   public synchronized void addUrlwithnoservices(String jwsservers)
619   {
620     if (urlsWithoutServices == null)
621     {
622       urlsWithoutServices = new Vector<>();
623     }
624
625     if ((invalidServiceUrls == null
626             || !invalidServiceUrls.contains(jwsservers))
627             && !urlsWithoutServices.contains(jwsservers))
628     {
629       urlsWithoutServices.add(jwsservers);
630     }
631   }
632
633   /**
634    * add a bad URL to the list
635    * 
636    * @param jwsservers
637    */
638   public synchronized void addInvalidServiceUrl(String jwsservers)
639   {
640     if (invalidServiceUrls == null)
641     {
642       invalidServiceUrls = new Vector<>();
643     }
644     if (!invalidServiceUrls.contains(jwsservers))
645     {
646       invalidServiceUrls.add(jwsservers);
647     }
648   }
649
650   /**
651    * 
652    * @return a human readable report of any problems with the service URLs used
653    *         for discovery
654    */
655   @Override
656   public String getErrorMessages()
657   {
658     if (!isRunning() && !isAborted())
659     {
660       StringBuffer ermsg = new StringBuffer();
661       boolean list = false;
662       if (getInvalidServiceUrls() != null
663               && getInvalidServiceUrls().size() > 0)
664       {
665         ermsg.append(MessageManager.getString("warn.urls_not_contacted")
666                 + ": \n");
667         for (String svcurl : getInvalidServiceUrls())
668         {
669           if (list)
670           {
671             ermsg.append(", ");
672           }
673           list = true;
674           ermsg.append(svcurl);
675         }
676         ermsg.append("\n\n");
677       }
678       list = false;
679       if (getUrlsWithoutServices() != null
680               && getUrlsWithoutServices().size() > 0)
681       {
682         ermsg.append(
683                 MessageManager.getString("warn.urls_no_jaba") + ": \n");
684         for (String svcurl : getUrlsWithoutServices())
685         {
686           if (list)
687           {
688             ermsg.append(", ");
689           }
690           list = true;
691           ermsg.append(svcurl);
692         }
693         ermsg.append("\n");
694       }
695       if (ermsg.length() > 1)
696       {
697         return ermsg.toString();
698       }
699
700     }
701     return null;
702   }
703
704   @Override
705   public int getServerStatusFor(String url)
706   {
707     if (validServiceUrls != null && validServiceUrls.contains(url))
708     {
709       return STATUS_OK;
710     }
711     if (urlsWithoutServices != null && urlsWithoutServices.contains(url))
712     {
713       return STATUS_NO_SERVICES;
714     }
715     if (invalidServiceUrls != null && invalidServiceUrls.contains(url))
716     {
717       return STATUS_INVALID;
718     }
719     return STATUS_UNKNOWN;
720   }
721
722   /**
723    * Set a URL to try before any others. For use with command-line parameter to
724    * configure a local Jabaws installation without the need to add to property
725    * files.
726    * 
727    * @param value
728    * @throws MalformedURLException
729    */
730   public void setPreferredUrl(String value) throws MalformedURLException
731   {
732     if (value != null && value.trim().length() > 0)
733     {
734       new URL(value);
735       preferredUrl = value;
736     }
737   }
738 }